blob: 82b5373c915880f63d3c9877751a7c88cdf8ec01 [file]
/**
* @fileoverview Creates a node_modules directory in the current working directory
* and symlinks in the node modules needed to run a program.
* This replaces the need for custom module resolution logic inside the process.
*/
import * as fs from 'fs';
import * as path from 'path';
// We cannot rely from the linker on the `@bazel/runfiles` package, hence we import from
// the runfile helper through a checked-in file from `internal/runfiles`. In order to still
// have typings we use a type-only import to the `@bazel/runfiles` package that is the source
// of truth for the checked-in file.
const {runfiles: _defaultRunfiles, _BAZEL_OUT_REGEX}:
typeof import('@bazel/runfiles') = require('../runfiles/index.js')
import {Runfiles} from '@bazel/runfiles';
// Run Bazel with --define=VERBOSE_LOGS=1 to enable this logging
const VERBOSE_LOGS = !!process.env['VERBOSE_LOGS'];
function log_verbose(...m: string[]) {
if (VERBOSE_LOGS) console.error('[link_node_modules.js]', ...m);
}
function log_error(error: Error) {
console.error('[link_node_modules.js] An error has been reported:', error, error.stack);
}
/**
* Create a new directory and any necessary subdirectories
* if they do not exist.
*/
async function mkdirp(p: string) {
if (p && !await exists(p)) {
await mkdirp(path.dirname(p));
log_verbose(`creating directory ${p} in ${process.cwd()}`);
try {
await fs.promises.mkdir(p);
} catch (e) {
if (e.code !== 'EEXIST') {
// can happen if path being created exists via a symlink
throw e;
}
}
}
}
/**
* Gets the `lstat` results for a given path. Returns `null` if the path
* does not exist on disk.
*/
async function gracefulLstat(path: string): Promise<fs.Stats|null> {
try {
return await fs.promises.lstat(path);
} catch (e) {
if (e.code === 'ENOENT') {
return null;
}
throw e;
}
}
/**
* Resolves a symlink to its linked path for a given path. Returns `null` if the path
* does not exist on disk.
*/
function gracefulReadlink(path: string): string|null {
try {
return fs.readlinkSync(path);
} catch (e) {
if (e.code === 'ENOENT') {
return null;
}
throw e;
}
}
/**
* Lists the names of files and directories that exist in the given path. Returns an empty
* array if the path does not exist on disk.
*/
async function gracefulReaddir(path: string): Promise<string[]> {
try {
return await fs.promises.readdir(path);
} catch (e) {
if (e.code === 'ENOENT') {
return [];
}
throw e;
}
}
/**
* Deletes the given module name from the current working directory (i.e. symlink root).
* If the module name resolves to a directory, the directory is deleted. Otherwise the
* existing file or junction is unlinked.
*/
async function unlink(moduleName: string) {
const stat = await gracefulLstat(moduleName);
if (stat === null) {
return;
}
log_verbose(`unlink( ${moduleName} )`);
if (stat.isDirectory()) {
await deleteDirectory(moduleName);
} else {
log_verbose("Deleting file: ", moduleName);
await fs.promises.unlink(moduleName);
}
}
/** Asynchronously deletes a given directory (with contents). */
async function deleteDirectory(p: string) {
log_verbose("Deleting children of", p);
for (let entry of await gracefulReaddir(p)) {
const childPath = path.join(p, entry);
const stat = await gracefulLstat(childPath);
if (stat === null) {
log_verbose(`File does not exist, but is listed as directory entry: ${childPath}`);
continue;
}
if (stat.isDirectory()) {
await deleteDirectory(childPath);
} else {
log_verbose("Deleting file", childPath);
await fs.promises.unlink(childPath);
}
}
log_verbose("Cleaning up dir", p);
await fs.promises.rmdir(p);
}
async function symlink(target: string, p: string): Promise<boolean> {
if (!path.isAbsolute(target)) {
target = path.resolve(process.cwd(), target);
}
log_verbose(`creating symlink ${p} -> ${target}`);
// Use junction on Windows since symlinks require elevated permissions.
// We only link to directories so junctions work for us.
try {
await fs.promises.symlink(target, p, 'junction');
return true;
} catch (e) {
if (e.code !== 'EEXIST') {
throw e;
}
// We assume here that the path is already linked to the correct target.
// Could add some logic that asserts it here, but we want to avoid an extra
// filesystem access so we should only do it under some kind of strict mode.
if (VERBOSE_LOGS) {
// Be verbose about creating a bad symlink
// Maybe this should fail in production as well, but again we want to avoid
// any unneeded file I/O
if (!await exists(p)) {
log_verbose(
'ERROR\n***\nLooks like we created a bad symlink:' +
`\n pwd ${process.cwd()}\n target ${target}\n path ${p}\n***`);
}
}
return false;
}
}
/** Determines an absolute path to the given workspace if it contains node modules. */
async function resolveWorkspaceNodeModules(
workspace: string, startCwd: string, isExecroot: boolean, execroot: string|undefined,
runfiles: Runfiles) {
const targetManifestPath = `${workspace}/node_modules`;
if (isExecroot) {
// Under execroot, the npm workspace will be under an external folder from the startCwd
// `execroot/my_wksp`. For example, `execroot/my_wksp/external/npm/node_modules`. If there is no
// npm workspace, which will be the case if there are no third-party modules dependencies for
// this target, npmWorkspace the root to `execroot/my_wksp/node_modules`.
return `${execroot}/external/${targetManifestPath}`;
}
if (!execroot) {
// This can happen if we are inside a nodejs_image or a nodejs_binary is run manually.
// Resolve as if we are in runfiles in a sandbox.
return path.resolve(`${startCwd}/../${targetManifestPath}`)
}
// Under runfiles, the linker should symlink node_modules at `execroot/my_wksp`
// so that when there are no runfiles (default on Windows) and scripts run out of
// `execroot/my_wksp` they can resolve node_modules with standard node_module resolution
// If we got a runfilesManifest map, look through it for a resolution
// This will happen if we are running a binary that had some npm packages
// "statically linked" into its runfiles
const fromManifest = runfiles.resolve(targetManifestPath);
if (fromManifest) {
return fromManifest;
} else {
const maybe = path.resolve(`${execroot}/external/${targetManifestPath}`);
if (await exists(maybe)) {
// Under runfiles, when not in the sandbox we must symlink node_modules down at the execroot
// `execroot/my_wksp/external/npm/node_modules` since `runfiles/npm/node_modules` will be a
// directory and not a symlink back to the root node_modules where we expect
// to resolve from. This case is tested in internal/linker/test/local.
return maybe;
}
// However, when in the sandbox, `execroot/my_wksp/external/npm/node_modules` does not exist,
// so we must symlink into `runfiles/npm/node_modules`. This directory exists whether legacy
// external runfiles are on or off.
return path.resolve(`${startCwd}/../${targetManifestPath}`)
}
}
// TypeScript lib.es5.d.ts has a mistake: JSON.parse does accept Buffer.
declare global {
interface JSON {
parse(b: {toString: () => string}): any;
}
}
// There is no fs.promises.exists function because
// node core is of the opinion that exists is always too racey to rely on.
async function exists(p: string) {
return (await gracefulLstat(p) !== null);
}
function existsSync(p: string|undefined) {
if (!p) {
return false;
}
try {
fs.lstatSync(p);
return true;
} catch (e) {
if (e.code === 'ENOENT') {
return false;
}
throw e;
}
}
/**
* Given a set of module aliases returns an array of recursive `LinkerTreeElement`.
*
* The tree nodes represent the FS links required to represent the module aliases.
* Each node of the tree hierarchy depends on its parent node having been setup first.
* Each sibling node can be processed concurrently.
*
* The number of symlinks is minimized in situations such as:
*
* Shared parent path to lowest common denominator:
* `@foo/b/c => /path/to/a/b/c`
*
* can be represented as
*
* `@foo => /path/to/a`
*
* Shared parent path across multiple module names:
* `@foo/p/a => /path/to/x/a`
* `@foo/p/c => /path/to/x/a`
*
* can be represented as a single parent
*
* `@foo/p => /path/to/x`
*/
export function reduceModules(modules: LinkerAliases): LinkerTreeElement[] {
return buildModuleHierarchy(Object.keys(modules).sort(), modules, '/').children || [];
}
function buildModuleHierarchy(
moduleNames: string[], modules: LinkerAliases, elementPath: string): LinkerTreeElement {
let element: LinkerTreeElement = {
name: elementPath.slice(0, -1),
link: modules[elementPath.slice(0, -1)],
children: [],
};
for (let i = 0; i < moduleNames.length;) {
const moduleName = moduleNames[i];
const next = moduleName.indexOf('/', elementPath.length + 1);
const moduleGroup = (next === -1) ? (moduleName + '/') : moduleName.slice(0, next + 1);
// An exact match (direct child of element) then it is the element parent, skip it
if (next === -1) {
i++;
}
const siblings: string[] = [];
while (i < moduleNames.length && moduleNames[i].startsWith(moduleGroup)) {
siblings.push(moduleNames[i++]);
}
let childElement = buildModuleHierarchy(siblings, modules, moduleGroup);
for (let cur = childElement; (cur = liftElement(childElement)) !== childElement;) {
childElement = cur;
}
element.children!.push(childElement);
}
// Cleanup empty children+link
if (!element.link) {
delete element.link;
}
if (!element.children || element.children.length === 0) {
delete element.children;
}
return element;
}
function liftElement(element: LinkerTreeElement): LinkerTreeElement {
let {name, link, children} = element;
if (!children || !children.length) {
return element;
}
// This element has a link and all the child elements have aligning links
// => this link alone represents that structure
if (link && allElementsAlignUnder(name, link, children)) {
return {name, link};
}
return element;
}
function allElementsAlignUnder(
parentName: string, parentLink: Link, elements: LinkerTreeElement[]) {
for (const {name, link, children} of elements) {
if (!link || children) {
return false;
}
if (!isDirectChildPath(parentName, name)) {
return false;
}
if (!isDirectChildLink(parentLink, link)) {
return false;
}
if (!isNameLinkPathTopAligned(name, link)) {
return false;
}
}
return true;
}
function isDirectChildPath(parent: string, child: string) {
return parent === path.dirname(child);
}
function isDirectChildLink(parentLink: Link, childLink: Link) {
return parentLink === path.dirname(childLink);
}
function isNameLinkPathTopAligned(namePath: string, linkPath: Link) {
return path.basename(namePath) === path.basename(linkPath);
}
async function visitDirectoryPreserveLinks(
dirPath: string, visit: (filePath: string, stat: fs.Stats) => Promise<void>) {
for (const entry of await fs.promises.readdir(dirPath)) {
const childPath = path.join(dirPath, entry);
const stat = await gracefulLstat(childPath);
if (stat === null) {
continue;
}
if (stat.isDirectory()) {
await visitDirectoryPreserveLinks(childPath, visit);
} else {
await visit(childPath, stat);
}
}
}
export type Link = string;
export type LinkerTreeElement = {
name: string,
link?: Link,
children?: LinkerTreeElement[],
};
export type LinkerAliases = {
[name: string]: Link
};
function findExecroot(startCwd: string): string|undefined {
// We can derive if the process is being run in the execroot if there is a bazel-out folder
if (existsSync(`${startCwd}/bazel-out`)) {
return startCwd;
}
// Look for bazel-out which is used to determine the the path to `execroot/my_wksp`. This works in
// all cases including on rbe where the execroot is a path such as `/b/f/w`. For example, when in
// runfiles on rbe, bazel runs the process in a directory such as
// `/b/f/w/bazel-out/k8-fastbuild/bin/path/to/pkg/some_test.sh.runfiles/my_wksp`. From here we can
// determine the execroot `b/f/w` by finding the first instance of bazel-out.
// NB: If we are inside nodejs_image or a nodejs_binary run manually there may be no execroot
// found.
const bazelOutMatch = startCwd.match(_BAZEL_OUT_REGEX);
return bazelOutMatch ? startCwd.slice(0, bazelOutMatch.index) : undefined;
}
export async function main(args: string[], runfiles: Runfiles) {
if (!args || args.length < 1) throw new Error('requires one argument: modulesManifest path');
const [modulesManifest] = args;
log_verbose('manifest file:', modulesManifest);
let {workspace, bin, roots, module_sets} = JSON.parse(fs.readFileSync(modulesManifest));
log_verbose('manifest contents:', JSON.stringify({workspace, bin, roots, module_sets}, null, 2));
roots = roots || {};
module_sets = module_sets || {};
// Bazel starts actions with pwd=execroot/my_wksp when under execroot or pwd=runfiles/my_wksp
// when under runfiles.
// Normalize the slashes in startCwd for easier matching and manipulation.
const startCwd = process.cwd().replace(/\\/g, '/');
log_verbose('startCwd:', startCwd);
const execroot = findExecroot(startCwd);
log_verbose('execroot:', execroot ? execroot : 'not found');
const isExecroot = startCwd == execroot;
log_verbose('isExecroot:', isExecroot.toString());
if (!isExecroot && execroot) {
// If we're not in the execroot and we've found one then change to the execroot
// directory to create the node_modules symlinks
process.chdir(execroot);
log_verbose('changed directory to execroot', execroot);
}
async function symlinkWithUnlink(
target: string, p: string, stats: fs.Stats|null = null): Promise<boolean> {
if (!path.isAbsolute(target)) {
target = path.resolve(process.cwd(), target);
}
if (stats === null) {
stats = await gracefulLstat(p);
}
// Check if this an an old out-of-date symlink
// If we are running without a runfiles manifest (i.e. in sandbox or with symlinked runfiles),
// then this is guaranteed to be not an artifact from a previous linker run. If not we need to
// check.
if (runfiles.manifest && execroot && stats !== null && stats.isSymbolicLink()) {
// Although `stats` suggests that the file exists as a symlink, it may have been deleted by
// another process. Only proceed unlinking if the file actually still exists.
const symlinkPathRaw = gracefulReadlink(p);
if (symlinkPathRaw !== null) {
const symlinkPath = symlinkPathRaw.replace(/\\/g, '/');
if (path.relative(symlinkPath, target) != '' &&
!path.relative(execroot, symlinkPath).startsWith('..')) {
// Left-over out-of-date symlink from previous run. This can happen if switching between
// root configuration options such as `--noenable_runfiles` and/or
// `--spawn_strategy=standalone`. It can also happen if two different targets link the
// same module name to different targets in a non-sandboxed environment. The latter will
// lead to undeterministic behavior.
// TODO: can we detect the latter case and throw an apprioriate error?
log_verbose(`Out-of-date symlink for ${p} to ${symlinkPath} detected. Target should be ${
target}. Unlinking.`);
await unlink(p);
} else {
log_verbose(`The symlink at ${p} no longer exists, so no need to unlink it.`);
}
}
}
return symlink(target, p);
}
// Symlink all node_modules roots defined. These are 3rd party deps in external npm workspaces
// lined to node_modules folders at the root or in sub-directories
for (const packagePath of Object.keys(roots)) {
const workspace = roots[packagePath];
if (workspace) {
const workspaceNodeModules = await resolveWorkspaceNodeModules(
workspace, startCwd, isExecroot, execroot, runfiles);
log_verbose(`resolved ${workspace} workspace node modules path to ${workspaceNodeModules}`);
if (packagePath) {
// sub-directory node_modules
if (await exists(workspaceNodeModules)) {
// in some cases packagePath may not exist in sandbox if there are no source deps
// and only generated file deps. we create it so that we that we can link to
// packagePath/node_modules since packagePathBin/node_modules is a symlink to
// packagePath/node_modules and is unguarded in launcher.sh as we allow symlinks to fall
// through to from output tree to source tree to prevent resolving the same npm package to
// multiple locations on disk
await mkdirp(packagePath);
await symlinkWithUnlink(workspaceNodeModules, `${packagePath}/node_modules`);
if (!isExecroot) {
// Under runfiles, we symlink into the package in runfiles as well.
// When inside the sandbox, the execroot location will not exist to symlink to.
const runfilesPackagePath = `${startCwd}/${packagePath}`;
if (await exists(runfilesPackagePath)) {
await symlinkWithUnlink(
`${packagePath}/node_modules`, `${runfilesPackagePath}/node_modules`);
}
}
const packagePathBin = `${bin}/${packagePath}`;
if (await exists(packagePathBin)) {
// if bin path exists, symlink bin/package/node_modules -> package/node_modules
// NB: this location is unguarded in launcher.sh to allow symlinks to fall-throught
// package/node_modules to prevent resolving the same npm package to multiple locations
// on disk
await symlinkWithUnlink(
`${packagePath}/node_modules`, `${packagePathBin}/node_modules`);
}
} else {
// Special case if there no target to symlink to for the root workspace, create a
// root node_modules folder for 1st party deps
log_verbose(`no npm workspace node_modules folder under ${
packagePath} to link to; creating node_modules directories in ${process.cwd()} for ${
packagePath} 1p deps`);
await mkdirp(`${packagePath}/node_modules`);
if (!isExecroot) {
// Under runfiles, we symlink into the package in runfiles as well.
// When inside the sandbox, the execroot location will not exist to symlink to.
const runfilesPackagePath = `${startCwd}/${packagePath}`;
await mkdirp(`${runfilesPackagePath}/node_modules`);
await symlinkWithUnlink(
`${packagePath}/node_modules`, `${runfilesPackagePath}/node_modules`);
}
const packagePathBin = `${bin}/${packagePath}`;
await mkdirp(`${packagePathBin}/node_modules`);
await symlinkWithUnlink(`${packagePath}/node_modules`, `${packagePathBin}/node_modules`);
}
} else {
// root node_modules
if (await exists(workspaceNodeModules)) {
await symlinkWithUnlink(workspaceNodeModules, `node_modules`);
} else {
// Special case if there no target to symlink to for the root workspace, create a
// root node_modules folder for 1st party deps
log_verbose(
`no root npm workspace node_modules folder to link to; creating node_modules directory in ${
process.cwd()}`);
await mkdirp('node_modules');
}
}
} else {
if (packagePath) {
// Special case if there for first party node_modules at root only
log_verbose(`no 3p deps at ${packagePath}; creating node_modules directories in ${
process.cwd()} for ${packagePath} 1p deps`);
await mkdirp(`${packagePath}/node_modules`);
if (!isExecroot) {
// Under runfiles, we symlink into the package in runfiles as well.
// When inside the sandbox, the execroot location will not exist to symlink to.
const runfilesPackagePath = `${startCwd}/${packagePath}`;
await mkdirp(`${runfilesPackagePath}/node_modules`);
await symlinkWithUnlink(
`${packagePath}/node_modules`, `${runfilesPackagePath}/node_modules`);
}
const packagePathBin = `${bin}/${packagePath}`;
await mkdirp(`${packagePathBin}/node_modules`);
await symlinkWithUnlink(`${packagePath}/node_modules`, `${packagePathBin}/node_modules`);
} else {
// Special case if there for first party node_modules at root only
log_verbose(`no 3p deps at root; creating node_modules directory in ${
process.cwd()} for root 1p deps`);
await mkdirp('node_modules');
}
}
}
/**
* Whether the given module resolves to a directory that has been created by a previous linker
* run purely to make space for deep module links. e.g. consider a mapping for `my-pkg/a11y`.
* The linker will create folders like `node_modules/my-pkg/` so that the `a11y` symbolic
* junction can be created. The `my-pkg` folder is then considered a leftover from a previous
* linker run as it only contains symbolic links and no actual source files.
*/
async function isLeftoverDirectoryFromLinker(stats: fs.Stats, modulePath: string) {
// If we are running without a runfiles manifest (i.e. in sandbox or with symlinked runfiles),
// then this is guaranteed to be not an artifact from a previous linker run.
if (runfiles.manifest === undefined) {
return false;
}
if (!stats.isDirectory()) {
return false;
}
let isLeftoverFromPreviousLink = true;
// If the directory contains actual files, this cannot be a leftover from a previous
// linker run. The linker only creates directories in the node modules that hold
// symbolic links for configured module mappings.
await visitDirectoryPreserveLinks(modulePath, async (childPath, childStats) => {
if (!childStats.isSymbolicLink()) {
isLeftoverFromPreviousLink = false;
}
});
return isLeftoverFromPreviousLink;
}
/**
* Creates a symlink for the given module. Existing child symlinks which are part of
* the module are preserved in order to not cause race conditions in non-sandbox
* environments where multiple actions rely on the same node modules root.
*
* To avoid unexpected resource removal, a new temporary link for the target is created.
* Then all symlinks from the existing module are cloned. Once done, the existing module
* is unlinked while the temporary link takes place for the given module. This ensures
* that the module link is never removed at any time (causing race condition failures).
*/
async function createSymlinkAndPreserveContents(stats: fs.Stats, modulePath: string,
target: string) {
const tmpPath = `${modulePath}__linker_tmp`;
log_verbose(`createSymlinkAndPreserveContents( ${modulePath} )`);
await symlink(target, tmpPath);
await visitDirectoryPreserveLinks(modulePath, async (childPath, stat) => {
if (stat.isSymbolicLink()) {
const targetPath = path.join(tmpPath, path.relative(modulePath, childPath));
log_verbose(`Cloning symlink into temporary created link ( ${childPath} )`);
await mkdirp(path.dirname(targetPath));
await symlink(targetPath, await fs.promises.realpath(childPath));
}
});
log_verbose(`Removing existing module so that new link can take place ( ${modulePath} )`);
await unlink(modulePath);
await fs.promises.rename(tmpPath, modulePath);
}
async function linkModules(package_path: string, m: LinkerTreeElement) {
const symlinkIn = package_path ? `${package_path}/node_modules` : 'node_modules';
// ensure the parent directory exist
if (path.dirname(m.name)) {
await mkdirp(`${symlinkIn}/${path.dirname(m.name)}`);
}
if (m.link) {
const modulePath = m.link;
let target: string|undefined;
if (isExecroot) {
// If we're running out of the execroot, try the execroot path first.
// If the dependency came in exclusively from a transitive binary target
// then the module won't be at this path but in the runfiles of the binary.
// In that case we'll fallback to resolving via runfiles below.
target = `${startCwd}/${modulePath}`;
}
if (!isExecroot || !existsSync(target)) {
// Transform execroot path to the runfiles manifest path so that
// it can be resolved with runfiles.resolve()
let runfilesPath = modulePath;
if (runfilesPath.startsWith(`${bin}/`)) {
runfilesPath = runfilesPath.slice(bin.length + 1);
} else if (runfilesPath === bin) {
runfilesPath = '';
}
const externalPrefix = 'external/';
if (runfilesPath.startsWith(externalPrefix)) {
runfilesPath = runfilesPath.slice(externalPrefix.length);
} else {
runfilesPath = `${workspace}/${runfilesPath}`;
}
try {
target = runfiles.resolve(runfilesPath);
// if we're resolving from a manifest then make sure we don't resolve
// into the source tree when we are expecting the output tree
if (runfiles.manifest && modulePath.startsWith(`${bin}/`)) {
// Check for BAZEL_OUT_REGEX and not /${bin}/ since resolution
// may be in the `/bazel-out/host` if cfg = "host"
if (!target.match(_BAZEL_OUT_REGEX)) {
const e = new Error(`could not resolve module ${runfilesPath} in output tree`);
(e as any).code = 'MODULE_NOT_FOUND';
throw e;
}
}
} catch (err) {
target = undefined;
log_verbose(`runfiles resolve failed for module '${m.name}': ${err.message}`);
}
}
// Ensure target path absolute for consistency
if (target && !path.isAbsolute(target)) {
target = path.resolve(process.cwd(), target);
}
const symlinkFile = `${symlinkIn}/${m.name}`;
// In environments where runfiles are not symlinked (e.g. Windows), existing linked
// modules are preserved. This could cause issues when a link is created at higher level
// as a conflicting directory is already on disk. e.g. consider in a previous run, we
// linked the modules `my-pkg/overlay`. Later on, in another run, we have a module mapping
// for `my-pkg` itself. The linker cannot create `my-pkg` because the directory `my-pkg`
// already exists. To ensure that the desired link is generated, we create the new desired
// link and move all previous nested links from the old module into the new link. Read more
// about this in the description of `createSymlinkAndPreserveContents`.
const stats = await gracefulLstat(symlinkFile);
const isLeftOver =
(stats !== null && await isLeftoverDirectoryFromLinker(stats, symlinkFile));
// Check if the target exists before creating the symlink.
// This is an extra filesystem access on top of the symlink but
// it is necessary for the time being.
if (target && await exists(target)) {
if (stats !== null && isLeftOver) {
await createSymlinkAndPreserveContents(stats, symlinkFile, target);
} else {
await symlinkWithUnlink(target, symlinkFile, stats);
}
} else {
if (!target) {
log_verbose(`no symlink target found for module ${m.name}`);
} else {
// This can happen if a module mapping is propogated from a dependency
// but the target that generated the mapping in not in the deps. We don't
// want to create symlinks to non-existant targets as this will
// break any nested symlinks that may be created under the module name
// after this.
log_verbose(`potential target ${target} does not exists for module ${m.name}`);
}
if (isLeftOver) {
// Remove left over directory if it exists
await unlink(symlinkFile);
}
}
}
// Process each child branch concurrently
if (m.children) {
await Promise.all(m.children.map(m => linkModules(package_path, m)));
}
}
const links = [];
for (const package_path of Object.keys(module_sets)) {
const modules = module_sets[package_path]
log_verbose(`modules for package path '${package_path}':\n${JSON.stringify(modules, null, 2)}`);
const moduleHierarchy = reduceModules(modules);
log_verbose(`mapping hierarchy for package path '${package_path}':\n${
JSON.stringify(moduleHierarchy)}`);
// Process each root branch concurrently
links.push(...moduleHierarchy.map(m => linkModules(package_path, m)))
}
let code = 0;
await Promise.all(links).catch(e => {
log_error(e);
code = 1;
});
return code;
}
if (require.main === module) {
if (Number(process.versions.node.split('.')[0]) < 10) {
console.error(`ERROR: rules_nodejs linker requires Node v10 or greater, but is running on ${
process.versions.node}`);
console.error('Note that earlier Node versions are no longer in long-term-support, see');
console.error('https://nodejs.org/en/about/releases/');
process.exit(1);
}
(async () => {
try {
process.exitCode = await main(process.argv.slice(2), _defaultRunfiles);
} catch (e) {
log_error(e);
process.exitCode = 1;
}
})();
}