blob: b0aa78697703feeba3567a3c6854fea32ae0400a [file] [log] [blame]
/**
* @license
* Copyright 2017 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Patched NodeJS module loader for bazel. This template is
* expanded to contain module name -> path mappings and then patches the
* NodeJS require() function to substitute the appropriate paths.
*
* @see https://github.com/nodejs/node/blob/master/lib/module.js
*/
'use strict';
var path = require('path');
var fs = require('fs');
// Ensure that node is added to the path for any subprocess calls
const isWindows = /^win/i.test(process.platform);
process.env.PATH = [path.dirname(process.execPath), process.env.PATH].join(isWindows ? ';' : ':');
const VERBOSE_LOGS = !!process.env['VERBOSE_LOGS'];
// If you're really in trouble debugging a module resolution, change this to true
const SILLY_VERBOSE = false;
function log_verbose(...m) {
// This is a template file so we use __filename to output the actual filename
if (VERBOSE_LOGS) console.error(`[${path.basename(__filename)}]`, ...m);
}
/**
* The module roots as pairs of a RegExp to match the require path, and a
* module_root to substitute for the require path.
* Ordered by regex length, longest to smallest.
* @type {!Array<{module_name: RegExp, module_root: string}>}
*/
var MODULE_ROOTS = [TEMPLATED_module_roots].sort(
(a, b) => b.module_name.toString().length - a.module_name.toString().length);
const USER_WORKSPACE_NAME = 'TEMPLATED_user_workspace_name';
const NODE_MODULES_ROOT = 'TEMPLATED_node_modules_root';
const BIN_DIR = 'TEMPLATED_bin_dir';
const GEN_DIR = 'TEMPLATED_gen_dir';
const TARGET = 'TEMPLATED_target';
log_verbose(`patching require for ${TARGET}
cwd: ${process.cwd()}
RUNFILES: ${process.env.RUNFILES}
TARGET: ${TARGET}
BIN_DIR: ${BIN_DIR}
GEN_DIR: ${GEN_DIR}
MODULE_ROOTS: ${JSON.stringify(MODULE_ROOTS, undefined, 2)}
NODE_MODULES_ROOT: ${NODE_MODULES_ROOT}
USER_WORKSPACE_NAME: ${USER_WORKSPACE_NAME}
`);
function resolveToModuleRoot(path) {
if (!path) {
throw new Error('resolveToModuleRoot missing path: ' + path);
}
// We want all possible matches.
const orderedMatches = MODULE_ROOTS.filter(m => m.module_name.test(path));
if (orderedMatches.length === 0) {
return null;
} else {
// Longest regex wins when multiple match, and the list is already ordered by length.
const m = orderedMatches[0];
return path.replace(m.module_name, m.module_root);
}
}
/**
* The runfiles manifest maps from short_path
* https://docs.bazel.build/versions/main/skylark/lib/File.html#short_path
* to the actual location on disk where the file can be read.
*
* In a sandboxed execution, it does not exist. In that case, runfiles must be
* resolved from a symlink tree under the runfiles dir.
* See https://github.com/bazelbuild/bazel/issues/3726
*/
function loadRunfilesManifest(manifestPath) {
// Normalize slashes in manifestPath so they match slashes in manifest file
manifestPath = manifestPath.replace(/\\/g, '/');
log_verbose(`using manifest ${manifestPath}`);
// Create the manifest and reverse manifest maps.
const runfilesManifest = Object.create(null);
const reverseRunfilesManifest = Object.create(null);
const input = fs.readFileSync(manifestPath, {encoding: 'utf-8'});
const outputBase = manifestPath.substring(0, manifestPath.indexOf('/execroot/'));
// Absolute path that refers to the local workspace path. We need to determine the absolute
// path to the local workspace because it allows us to support absolute path resolving
// for runfiles.
let localWorkspacePath = null;
for (const line of input.split('\n')) {
if (!line) continue;
const [runfilesPath, realPath] = line.split(' ');
runfilesManifest[runfilesPath] = realPath;
reverseRunfilesManifest[realPath] = runfilesPath;
// We don't need to try determining the local workspace path for the current runfile
// mapping in case we already determined the local workspace path, the current
// runfile refers to a different workspace, or the current runfile resolves to a file
// in the bazel-out directory (bin/genfiles directory). Also exclude the case of no
// realpath which fixes https://github.com/bazelbuild/rules_nodejs/issues/1307.
if (localWorkspacePath || !runfilesPath.startsWith(USER_WORKSPACE_NAME) || !realPath ||
realPath.startsWith(outputBase)) {
continue;
}
// Relative path for the runfile. We can compute that path by removing the leading
// workspace name. e.g. `my_workspace/src/my-runfile.js` becomes `src/my-runfile.js`.
const relativeWorkspacePath = runfilesPath.slice(USER_WORKSPACE_NAME.length + 1);
// TODO(gregmagolan): should not be needed when --nolegacy_external_runfiles is default
if (relativeWorkspacePath.startsWith('external/')) {
continue;
}
localWorkspacePath = realPath.slice(0, -relativeWorkspacePath.length);
}
// Determine bin and gen root to convert absolute paths into runfile paths.
const binRootIdx = manifestPath.indexOf(BIN_DIR);
let binRoot, genRoot;
if (binRootIdx !== -1) {
const execRoot = manifestPath.slice(0, binRootIdx);
binRoot = `${execRoot}${BIN_DIR}/`;
genRoot = `${execRoot}${GEN_DIR}/`;
}
log_verbose(`using outputBase ${outputBase}`);
log_verbose(`using binRoot ${binRoot}`);
log_verbose(`using genRoot ${genRoot}`);
log_verbose(`using localWorkspacePath ${localWorkspacePath}`);
return {runfilesManifest, reverseRunfilesManifest, binRoot, genRoot, localWorkspacePath};
}
const {runfilesManifest, reverseRunfilesManifest, binRoot, genRoot, localWorkspacePath} =
// On Windows, Bazel sets RUNFILES_MANIFEST_ONLY=1.
// On every platform, Bazel also sets RUNFILES_MANIFEST_FILE, but on Linux
// and macOS it's faster to use the symlinks in RUNFILES_DIR rather than resolve
// through the indirection of the manifest file.
// We also need to construct a reverse map to resolve relative files from existing
// manifest entries.
process.env.RUNFILES_MANIFEST_ONLY === '1' &&
loadRunfilesManifest(process.env.RUNFILES_MANIFEST_FILE);
function isFile(res) {
try {
return fs.statSync(res).isFile();
} catch (e) {
return false;
}
}
function isDirectory(res) {
try {
return fs.statSync(res).isDirectory();
} catch (e) {
return false;
}
}
function readDir(dir) {
return fs.statSync(dir).isDirectory() ?
Array.prototype.concat(...fs.readdirSync(dir).map(f => readDir(path.join(dir, f)))) :
dir.replace(/\\/g, '/');
}
function loadAsFileSync(res) {
if (isFile(res)) {
return res;
}
if (isFile(res + '.mjs')) {
return res;
}
if (isFile(res + '.js')) {
return res;
}
return null;
}
function loadAsDirectorySync(res) {
const pkgfile = path.join(res, 'package.json');
if (isFile(pkgfile)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgfile, 'UTF-8'));
const main = pkg['main'];
if (main) {
if (main === '.' || main === './') {
main = 'index';
}
let maybe = loadAsFileSync(path.resolve(res, main));
if (maybe) {
return maybe;
}
maybe = loadAsDirectorySync(path.resolve(res, main));
if (maybe) {
return maybe;
}
}
} catch (e) {
}
}
return loadAsFileSync(path.resolve(res, 'index'));
}
function resolveManifestFile(res) {
const maybe =
runfilesManifest[res] || runfilesManifest[res + '.mjs'] || runfilesManifest[res + '.js'];
if (maybe) {
return maybe;
}
// Look for tree artifacts that match and update
// the runfiles with files that are in the tree artifact.
// Attempt to resolve again with the updated runfiles
// if a tree artifact matched.
let segments = res.split('/');
segments.pop();
while (segments.length) {
const test = segments.join('/');
const tree = runfilesManifest[test];
if (tree && isDirectory(tree)) {
// We have a tree artifact that matches
const files = readDir(tree).map(f => path.relative(tree, f).replace(/\\/g, '/'));
files.forEach(f => {
runfilesManifest[path.posix.join(test, f)] = path.posix.join(tree, f);
})
return runfilesManifest[res] || runfilesManifest[res + '.mjs'] ||
runfilesManifest[res + '.js'];
}
segments.pop();
}
}
function resolveManifestDirectory(res) {
const pkgfile = runfilesManifest[path.posix.join(res, 'package.json')];
if (pkgfile) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgfile, 'UTF-8'));
const main = pkg['main'];
if (main) {
if (main === '.' || main === './') {
main = 'index';
}
let maybe = resolveManifestFile(path.posix.join(res, main));
if (maybe) {
return maybe;
}
maybe = resolveManifestDirectory(path.posix.join(res, main));
if (maybe) {
return maybe;
}
}
} catch (e) {
}
}
return resolveManifestFile(path.posix.join(res, 'index'));
}
function resolveRunfiles(parent, ...pathSegments) {
// Remove any empty strings from pathSegments
// Normalize to forward slash, because even on Windows the runfiles_manifest file
// is written with forward slash.
let runfilesEntry = pathSegments.filter(segment => segment).join('/').replace(/\\/g, '/');
// Trim `${USER_WORKSPACE_NAME}/external/` from start of runfilesEntry
const externalWorkspacePrefix = `${USER_WORKSPACE_NAME}/external/`;
if (runfilesEntry.startsWith(externalWorkspacePrefix)) {
runfilesEntry = runfilesEntry.slice(externalWorkspacePrefix.length);
}
const runfilesPath = path.join(process.env.RUNFILES, runfilesEntry);
if (runfilesManifest) {
if (parent && runfilesEntry.startsWith('.')) {
// Resolve relative paths from manifest files.
const normalizedParent = parent.replace(/\\/g, '/');
const parentRunfile = reverseRunfilesManifest[normalizedParent];
if (parentRunfile) {
runfilesEntry = path.join(path.dirname(parentRunfile), runfilesEntry);
}
} else if (
runfilesEntry.startsWith(binRoot) || runfilesEntry.startsWith(genRoot) ||
runfilesEntry.startsWith(localWorkspacePath)) {
// For absolute paths, replace binRoot, genRoot or localWorkspacePath with
// USER_WORKSPACE_NAME to enable lookups.
// It's OK to do multiple replacements because all of these are absolute paths with drive
// names (e.g. C:\), and on Windows you can't have drive names in the middle of paths.
runfilesEntry = runfilesEntry.replace(binRoot, `${USER_WORKSPACE_NAME}/`)
.replace(genRoot, `${USER_WORKSPACE_NAME}/`)
.replace(localWorkspacePath, `${USER_WORKSPACE_NAME}/`);
}
// Normalize and replace path separators to conform to the ones in the manifest.
runfilesEntry = path.normalize(runfilesEntry).replace(/\\/g, '/');
log_verbose('try to resolve in runfiles manifest', runfilesEntry);
let maybe = resolveManifestFile(runfilesEntry);
if (maybe) {
log_verbose('resolved manifest file', maybe);
return maybe;
}
maybe = resolveManifestDirectory(runfilesEntry);
if (maybe) {
log_verbose('resolved via manifest directory', maybe);
return maybe;
}
} else {
log_verbose('try to resolve in runfiles', runfilesPath);
let maybe = loadAsFileSync(runfilesPath);
if (maybe) {
log_verbose('resolved file', maybe);
return maybe;
}
maybe = loadAsDirectorySync(runfilesPath);
if (maybe) {
log_verbose('resolved via directory', maybe);
return maybe;
}
}
return runfilesPath;
}
var originalResolveFilename = module.constructor._resolveFilename;
module.constructor._resolveFilename =
function(request, parent, isMain, options) {
const parentFilename = (parent && parent.filename) ? parent.filename : undefined;
if (SILLY_VERBOSE) log_verbose(`resolve ${request} from ${parentFilename}`);
const failedResolutions = [];
// Attempt to resolve to module root.
// This should be the first attempted resolution because:
// - it's fairly cheap to check (regex over a small array);
// - it is be very common when there are a lot of packages built from source;
if (!isMain) {
// Don't resolve to module root if this is the main entry point
// as the main entry point will always be fully qualified with the
// workspace name and full path.
// See https://github.com/bazelbuild/rules_nodejs/issues/834
const moduleRoot = resolveToModuleRoot(request);
if (moduleRoot) {
const moduleRootInRunfiles = resolveRunfiles(undefined, moduleRoot);
const filename = module.constructor._findPath(moduleRootInRunfiles, []);
if (filename) {
return filename;
} else {
failedResolutions.push(
`module root ${moduleRoot} - No file ${request} found in module root ${moduleRoot}`);
}
}
}
// Built-in modules, relative, absolute imports and npm dependencies
// can be resolved using request
try {
const resolved = originalResolveFilename(request, parent, isMain, options);
if (resolved === request || request.startsWith('.') || request.startsWith('/') ||
request.match(/^[A-Z]\:[\\\/]/i)) {
if (SILLY_VERBOSE)
log_verbose(
`resolved ${request} to built-in, relative or absolute import ` +
`${resolved} from ${parentFilename}`);
return resolved;
} else {
// Resolved is not a built-in module, relative or absolute import
// but also allow imports within npm packages that are within the parent files
// node_modules, meaning it is a dependency of the npm package making the import.
const parentSegments = parentFilename ? parentFilename.replace(/\\/g, '/').split('/') : [];
const parentNodeModulesSegment = parentSegments.indexOf('node_modules');
if (parentNodeModulesSegment != -1) {
const parentRoot = parentSegments.slice(0, parentNodeModulesSegment).join('/');
const relative = path.relative(parentRoot, resolved);
if (!relative.startsWith('..')) {
// Resolved within parent node_modules
log_verbose(
`resolved ${request} within parent node_modules to ` +
`${resolved} from ${parentFilename}`);
return resolved;
} else {
throw new Error(
`Resolved to ${resolved} outside of parent node_modules ${parentFilename}`);
}
}
throw new Error('Not a built-in module, relative or absolute import');
}
} catch (e) {
failedResolutions.push(`built-in, relative, absolute, nested node_modules - ${e.toString()}`);
}
// If the import is not a built-in module, an absolute, relative import or a
// dependency of an npm package, attempt to resolve against the runfiles location
try {
const resolved =
originalResolveFilename(resolveRunfiles(parentFilename, request), parent, isMain, options);
log_verbose(`resolved ${request} within runfiles to ${resolved} from ${parentFilename}`);
return resolved;
} catch (e) {
failedResolutions.push(`runfiles - ${e.toString()}`);
}
// If the parent file is from an external repository, attempt to resolve against
// the external repositories node_modules (if they exist)
let relativeParentFilename =
parentFilename ? path.relative(process.env.RUNFILES, parent.filename) : undefined;
if (relativeParentFilename && !relativeParentFilename.startsWith('..')) {
// Remove leading USER_WORKSPACE_NAME/external so that external workspace name is
// always the first segment
// TODO(gregmagolan): should not be needed when --nolegacy_external_runfiles is default
const externalPrefix = `${USER_WORKSPACE_NAME}/external/`;
if (relativeParentFilename.startsWith(externalPrefix)) {
relativeParentFilename = relativeParentFilename.substr(externalPrefix.length);
}
const parentSegments = relativeParentFilename.split('/');
if (parentSegments[0] !== USER_WORKSPACE_NAME) {
try {
const resolved = originalResolveFilename(
resolveRunfiles(undefined, parentSegments[0], 'node_modules', request), parent, isMain,
options);
log_verbose(
`resolved ${request} within node_modules ` +
`(${parentSegments[0]}/node_modules) to ${resolved} from ${relativeParentFilename}`);
return resolved;
} catch (e) {
failedResolutions.push(`${parentSegments[0]}/node_modules - ${e.toString()}`);
}
}
}
// If import was not resolved above then attempt to resolve
// within the node_modules filegroup in use
try {
const resolved = originalResolveFilename(
resolveRunfiles(undefined, NODE_MODULES_ROOT, request), parent, isMain, options);
log_verbose(
`resolved ${request} within node_modules (${NODE_MODULES_ROOT}) to ` +
`${resolved} from ${parentFilename}`);
return resolved;
} catch (e) {
failedResolutions.push(`node_modules attribute (${NODE_MODULES_ROOT}) - ${e.toString()}`);
}
// Print the same error message that vanilla nodejs does.
// See https://github.com/bazelbuild/rules_nodejs/issues/1015
let moduleNotFoundError = `Cannot find module '${request}'. ` +
'Please verify that the package.json has a valid "main" entry';
if (VERBOSE_LOGS) {
moduleNotFoundError += `\nrequired in target ${TARGET} by '${parentFilename}'\n looked in:\n` +
failedResolutions.map(r => ` ${r}`).join('\n') + '\n';
}
const error = new Error(moduleNotFoundError);
error.code = 'MODULE_NOT_FOUND';
// todo - error.path = ?;
error.requestPath = parentFilename;
error.bazelTarget = TARGET;
error.failedResolutions = failedResolutions;
throw error;
}
// Before loading anything that might print a stack, install the
// source-map-support.
try {
const sourcemap_support_package = path.resolve(
process.cwd(), '../build_bazel_rules_nodejs/third_party/github.com/source-map-support');
require(sourcemap_support_package).install({ environment: 'node' });
} catch (_) {
log_verbose(`WARNING: source-map-support module not installed.
Stack traces from languages like TypeScript will point to generated .js files.`);
}