| /** | |
| * @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.`); | |
| } |