mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Replaces the existing ESM loader for dealing with external module imports. This loader was introduced by Aspect for AIO `.mjs` scripts. The loader will be used as foundation for a more extensive loader that also properly handles first-party packages. Additionally another loader is added, all packed as a single loader because our current NodeJS version only supports a single loader per node invocation. So we implement chaining ourselves. The new loader will attempt rewriting `.js` extensions to `.mjs`, also it will add `.mjs` if not already done. This is necessary in the transition phase because we don't/cannot use explicit `.mts` extensions and also we don't specify extensions in imports yet. Long-term we would likely use `.mts` and explicit import extensions, but it's not yet clear how we would sync this into g3 too. PR Close #48521
125 lines
4.5 KiB
JavaScript
125 lines
4.5 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import {createRequire} from 'module';
|
|
import {pathToFileURL} from 'url';
|
|
import {resolve as resolveExports} from '../../third_party/github.com/lukeed/resolve.exports/index.mjs';
|
|
|
|
// The Bazel NodeJS rules patch `require` to support first-party
|
|
// mapped packages. We cannot replicate this logic without patching
|
|
// the Bazel rules, so instead we leverage the existing `require`
|
|
// patched function as it knows about first party mapped packages.
|
|
const requireFn = createRequire(import.meta.url);
|
|
|
|
const npmDepsWorkspace = process.env.NODE_MODULES_WORKSPACE_NAME;
|
|
const runfilesRoot = path.resolve(process.env.RUNFILES);
|
|
const nodeModulesPath = path.join(runfilesRoot, npmDepsWorkspace, 'node_modules');
|
|
|
|
/*
|
|
Custom module loader to support loading 1st-party and 3rd-party node
|
|
modules when the linker is disabled. This is required because `rules_nodejs`
|
|
only patches requires in cjs modules when the linker is disabled.
|
|
*/
|
|
export async function resolve(specifier, context, nextResolve) {
|
|
// Only activate this loader when explicitly enabled.
|
|
if (process.env.ESM_NODE_MODULE_LOADER_ENABLED !== 'true') {
|
|
return nextResolve(specifier, context);
|
|
}
|
|
|
|
if (!isNodeOrNpmPackageImport(specifier)) {
|
|
return nextResolve(specifier, context);
|
|
}
|
|
|
|
const packageImport = parsePackageImport(specifier);
|
|
const pathToNodeModule = path.join(nodeModulesPath, packageImport.packageName);
|
|
|
|
// If the module can be directly found in the `node_modules`, then we know it's
|
|
// a third-party package coming from NPM. In this case we properly respect ESM
|
|
// resolution by respecting the `exports`.
|
|
const npmModuleResult = fs.existsSync(pathToNodeModule)
|
|
? resolvePackageWithExportsSupport(pathToNodeModule, packageImport)
|
|
: null;
|
|
if (npmModuleResult !== null) {
|
|
return npmModuleResult;
|
|
}
|
|
|
|
// If the package does not exist on disk, then it may just be an invalid
|
|
// import, or the package is 1st-party one that is mapped within Bazel.
|
|
// We attempt to resolve it that way and return the path if there is a result.
|
|
const localMappingResult = tryResolveViaLocalMappings(specifier, packageImport);
|
|
if (localMappingResult !== null) {
|
|
return localMappingResult;
|
|
}
|
|
|
|
// Process built-in modules or unknown specifiers.
|
|
return nextResolve(specifier, context);
|
|
}
|
|
|
|
/** Gets whether the specifier refers to a module. */
|
|
function isNodeOrNpmPackageImport(specifier) {
|
|
return (
|
|
!specifier.startsWith('./') &&
|
|
!specifier.startsWith('../') &&
|
|
!specifier.startsWith('node:') &&
|
|
!specifier.startsWith('file:')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Attempts to resolve a specifier using the Bazel patched resolution,
|
|
* supporting first-party package mappings from `rules_nodejs`.
|
|
*/
|
|
function tryResolveViaLocalMappings(actualSpecifier) {
|
|
try {
|
|
const res = requireFn.resolve(actualSpecifier);
|
|
// Note: It may not resolve to a path if the specifier is a builtin
|
|
// module. In such cases we do not want to return it as result.
|
|
if (fs.existsSync(res)) {
|
|
return {url: pathToFileURL(res).href};
|
|
}
|
|
} catch {}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Parses the given specifier into its package and subpath. */
|
|
function parsePackageImport(specifier) {
|
|
const [, packageName, pathInPackage = ''] =
|
|
/^((?:@[^/]+\/)?[^/]+)(?:\/(.+))?$/.exec(specifier) ?? [];
|
|
if (!packageName) {
|
|
throw new Error(`Could not parse package name import statement '${specifier}'`);
|
|
}
|
|
return {packageName, pathInPackage, specifier};
|
|
}
|
|
|
|
/** Resolves an import to a module by respecting the `package.json` `exports`. */
|
|
function resolvePackageWithExportsSupport(pathToNodeModule, packageImport) {
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(path.join(pathToNodeModule, 'package.json'), 'utf8')
|
|
);
|
|
const localPackagePath = resolvePackageLocalFilepath(packageImport, packageJson);
|
|
const resolvedFilePath = path.join(pathToNodeModule, localPackagePath);
|
|
|
|
if (fs.existsSync(resolvedFilePath)) {
|
|
return {url: pathToFileURL(resolvedFilePath).href};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolves the remaining package-local portion of an import. Leverages
|
|
* the `package.json` `exports` field information for resolution.
|
|
*/
|
|
function resolvePackageLocalFilepath(packageImport, packageJson) {
|
|
if (packageJson.exports) {
|
|
return resolveExports(packageJson, packageImport.specifier);
|
|
}
|
|
return packageImport.pathInPackage || packageJson.module || packageJson.main || 'index.js';
|
|
}
|