Son CV dans un terminal web en Javascript!
https://terminal-cv.gregandev.fr
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1284 lines
34 KiB
1284 lines
34 KiB
2 years ago
|
// @flow
|
||
|
import type {
|
||
|
FilePath,
|
||
|
FileCreateInvalidation,
|
||
|
PackageJSON,
|
||
|
ResolveResult,
|
||
|
Environment,
|
||
|
SpecifierType,
|
||
|
PluginLogger,
|
||
|
SourceLocation,
|
||
|
} from '@parcel/types';
|
||
|
import type {FileSystem} from '@parcel/fs';
|
||
|
import type {PackageManager} from '@parcel/package-manager';
|
||
|
|
||
|
import invariant from 'assert';
|
||
|
import path from 'path';
|
||
|
import {
|
||
|
isGlob,
|
||
|
relativePath,
|
||
|
normalizeSeparators,
|
||
|
findAlternativeNodeModules,
|
||
|
findAlternativeFiles,
|
||
|
loadConfig,
|
||
|
globToRegex,
|
||
|
isGlobMatch,
|
||
|
} from '@parcel/utils';
|
||
|
import ThrowableDiagnostic, {
|
||
|
generateJSONCodeHighlights,
|
||
|
md,
|
||
|
} from '@parcel/diagnostic';
|
||
|
import builtins, {empty} from './builtins';
|
||
|
import nullthrows from 'nullthrows';
|
||
|
import _Module from 'module';
|
||
|
import {fileURLToPath} from 'url';
|
||
|
|
||
|
const EMPTY_SHIM = require.resolve('./_empty');
|
||
|
|
||
|
type InternalPackageJSON = PackageJSON & {pkgdir: string, pkgfile: string, ...};
|
||
|
type Options = {|
|
||
|
fs: FileSystem,
|
||
|
projectRoot: FilePath,
|
||
|
extensions: Array<string>,
|
||
|
mainFields: Array<string>,
|
||
|
packageManager?: PackageManager,
|
||
|
logger?: PluginLogger,
|
||
|
|};
|
||
|
type ResolvedFile = {|
|
||
|
path: string,
|
||
|
pkg: InternalPackageJSON | null,
|
||
|
|};
|
||
|
|
||
|
type Aliases =
|
||
|
| string
|
||
|
| {[string]: string, ...}
|
||
|
| {[string]: string | boolean, ...};
|
||
|
type ResolvedAlias = {|
|
||
|
type: 'file' | 'global',
|
||
|
sourcePath: FilePath,
|
||
|
resolved: string,
|
||
|
|};
|
||
|
type Module = {|
|
||
|
moduleName?: string,
|
||
|
subPath?: ?string,
|
||
|
moduleDir?: FilePath,
|
||
|
filePath?: FilePath,
|
||
|
code?: string,
|
||
|
query?: URLSearchParams,
|
||
|
|};
|
||
|
|
||
|
type ResolverContext = {|
|
||
|
invalidateOnFileCreate: Array<FileCreateInvalidation>,
|
||
|
invalidateOnFileChange: Set<FilePath>,
|
||
|
specifierType: SpecifierType,
|
||
|
loc: ?SourceLocation,
|
||
|
|};
|
||
|
|
||
|
/**
|
||
|
* This resolver implements a modified version of the node_modules resolution algorithm:
|
||
|
* https://nodejs.org/api/modules.html#modules_all_together
|
||
|
*
|
||
|
* In addition to the standard algorithm, Parcel supports:
|
||
|
* - All file extensions supported by Parcel.
|
||
|
* - Glob file paths
|
||
|
* - Absolute paths (e.g. /foo) resolved relative to the project root.
|
||
|
* - Tilde paths (e.g. ~/foo) resolved relative to the nearest module root in node_modules.
|
||
|
* - The package.json module, jsnext:main, and browser field as replacements for package.main.
|
||
|
* - The package.json browser and alias fields as an alias map within a local module.
|
||
|
* - The package.json alias field in the root package for global aliases across all modules.
|
||
|
*/
|
||
|
export default class NodeResolver {
|
||
|
fs: FileSystem;
|
||
|
projectRoot: FilePath;
|
||
|
extensions: Array<string>;
|
||
|
mainFields: Array<string>;
|
||
|
packageCache: Map<string, InternalPackageJSON>;
|
||
|
rootPackage: InternalPackageJSON | null;
|
||
|
packageManager: ?PackageManager;
|
||
|
logger: ?PluginLogger;
|
||
|
|
||
|
constructor(opts: Options) {
|
||
|
this.extensions = opts.extensions.map(ext =>
|
||
|
ext.startsWith('.') ? ext : '.' + ext,
|
||
|
);
|
||
|
this.mainFields = opts.mainFields;
|
||
|
this.fs = opts.fs;
|
||
|
this.projectRoot = opts.projectRoot;
|
||
|
this.packageCache = new Map();
|
||
|
this.rootPackage = null;
|
||
|
this.packageManager = opts.packageManager;
|
||
|
this.logger = opts.logger;
|
||
|
}
|
||
|
|
||
|
async resolve({
|
||
|
filename,
|
||
|
parent,
|
||
|
specifierType,
|
||
|
env,
|
||
|
sourcePath,
|
||
|
loc,
|
||
|
}: {|
|
||
|
filename: FilePath,
|
||
|
parent: ?FilePath,
|
||
|
specifierType: SpecifierType,
|
||
|
env: Environment,
|
||
|
sourcePath?: ?FilePath,
|
||
|
loc?: ?SourceLocation,
|
||
|
|}): Promise<?ResolveResult> {
|
||
|
let ctx = {
|
||
|
invalidateOnFileCreate: [],
|
||
|
invalidateOnFileChange: new Set(),
|
||
|
specifierType,
|
||
|
loc,
|
||
|
};
|
||
|
|
||
|
// Get file extensions to search
|
||
|
let extensions = this.extensions.slice();
|
||
|
|
||
|
if (parent) {
|
||
|
// parent's extension given high priority
|
||
|
let parentExt = path.extname(parent);
|
||
|
extensions = [parentExt, ...extensions.filter(ext => ext !== parentExt)];
|
||
|
}
|
||
|
|
||
|
extensions.unshift('');
|
||
|
|
||
|
try {
|
||
|
// Resolve the module directory or local file path
|
||
|
let module = await this.resolveModule({
|
||
|
filename,
|
||
|
parent,
|
||
|
env,
|
||
|
ctx,
|
||
|
sourcePath,
|
||
|
});
|
||
|
|
||
|
if (!module) {
|
||
|
return {
|
||
|
isExcluded: true,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let resolved;
|
||
|
if (module.moduleDir) {
|
||
|
resolved = await this.loadNodeModules(module, extensions, env, ctx);
|
||
|
} else if (module.filePath) {
|
||
|
if (module.code != null) {
|
||
|
return {
|
||
|
filePath: await this.fs.realpath(module.filePath),
|
||
|
code: module.code,
|
||
|
invalidateOnFileCreate: ctx.invalidateOnFileCreate,
|
||
|
invalidateOnFileChange: [...ctx.invalidateOnFileChange],
|
||
|
query: module.query,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
resolved = await this.loadRelative(
|
||
|
module.filePath,
|
||
|
extensions,
|
||
|
env,
|
||
|
parent ? path.dirname(parent) : this.projectRoot,
|
||
|
ctx,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (resolved) {
|
||
|
let _resolved = resolved; // For Flow
|
||
|
return {
|
||
|
filePath: await this.fs.realpath(_resolved.path),
|
||
|
sideEffects:
|
||
|
_resolved.pkg && !this.hasSideEffects(_resolved.path, _resolved.pkg)
|
||
|
? false
|
||
|
: undefined,
|
||
|
invalidateOnFileCreate: ctx.invalidateOnFileCreate,
|
||
|
invalidateOnFileChange: [...ctx.invalidateOnFileChange],
|
||
|
query: module.query,
|
||
|
};
|
||
|
}
|
||
|
} catch (err) {
|
||
|
if (err instanceof ThrowableDiagnostic) {
|
||
|
return {
|
||
|
diagnostics: err.diagnostics,
|
||
|
invalidateOnFileCreate: ctx.invalidateOnFileCreate,
|
||
|
invalidateOnFileChange: [...ctx.invalidateOnFileChange],
|
||
|
};
|
||
|
} else {
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
async resolveModule({
|
||
|
filename,
|
||
|
parent,
|
||
|
env,
|
||
|
ctx,
|
||
|
sourcePath,
|
||
|
}: {|
|
||
|
filename: string,
|
||
|
parent: ?FilePath,
|
||
|
env: Environment,
|
||
|
ctx: ResolverContext,
|
||
|
sourcePath: ?FilePath,
|
||
|
|}): Promise<?Module> {
|
||
|
let specifier = filename;
|
||
|
let sourceFile = parent || path.join(this.projectRoot, 'index');
|
||
|
let query;
|
||
|
|
||
|
// If this isn't the entrypoint, resolve the input file to an absolute path
|
||
|
if (parent) {
|
||
|
let res = await this.resolveFilename(
|
||
|
filename,
|
||
|
path.dirname(sourceFile),
|
||
|
ctx.specifierType,
|
||
|
);
|
||
|
|
||
|
if (!res) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
filename = res.filePath;
|
||
|
query = res.query;
|
||
|
}
|
||
|
|
||
|
// Resolve aliases in the parent module for this file.
|
||
|
let alias = await this.loadAlias(filename, sourceFile, env, ctx);
|
||
|
if (alias) {
|
||
|
if (alias.type === 'global') {
|
||
|
return {
|
||
|
filePath: path.join(this.projectRoot, `${alias.resolved}.js`),
|
||
|
code: `module.exports=${alias.resolved};`,
|
||
|
query,
|
||
|
};
|
||
|
}
|
||
|
filename = alias.resolved;
|
||
|
}
|
||
|
|
||
|
// Return just the file path if this is a file, not in node_modules
|
||
|
if (path.isAbsolute(filename)) {
|
||
|
return {
|
||
|
filePath: filename,
|
||
|
query,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let builtin = this.findBuiltin(filename, env);
|
||
|
if (builtin === null) {
|
||
|
return null;
|
||
|
} else if (builtin === empty) {
|
||
|
return {filePath: empty};
|
||
|
} else if (builtin !== undefined) {
|
||
|
filename = builtin;
|
||
|
}
|
||
|
|
||
|
if (this.shouldIncludeNodeModule(env, filename) === false) {
|
||
|
if (sourcePath && env.isLibrary && !builtin) {
|
||
|
await this.checkExcludedDependency(sourcePath, filename, ctx);
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Resolve the module in node_modules
|
||
|
let resolved: ?Module;
|
||
|
try {
|
||
|
resolved = this.findNodeModulePath(filename, sourceFile, ctx);
|
||
|
} catch (err) {
|
||
|
// ignore
|
||
|
}
|
||
|
|
||
|
// Auto install node builtin polyfills if not already available
|
||
|
if (resolved === undefined && builtin != null) {
|
||
|
let packageName = builtin.split('/')[0];
|
||
|
let packageManager = this.packageManager;
|
||
|
if (packageManager) {
|
||
|
this.logger?.warn({
|
||
|
message: md`Auto installing polyfill for Node builtin module "${specifier}"...`,
|
||
|
codeFrames: [
|
||
|
{
|
||
|
filePath: ctx.loc?.filePath ?? sourceFile,
|
||
|
codeHighlights: ctx.loc
|
||
|
? [
|
||
|
{
|
||
|
message: 'used here',
|
||
|
start: ctx.loc.start,
|
||
|
end: ctx.loc.end,
|
||
|
},
|
||
|
]
|
||
|
: [],
|
||
|
},
|
||
|
],
|
||
|
documentationURL:
|
||
|
'https://parceljs.org/features/node-emulation/#polyfilling-%26-excluding-builtin-node-modules',
|
||
|
});
|
||
|
|
||
|
await packageManager.resolve(builtin, this.projectRoot + '/index', {
|
||
|
saveDev: true,
|
||
|
shouldAutoInstall: true,
|
||
|
});
|
||
|
|
||
|
// Re-resolve
|
||
|
try {
|
||
|
resolved = this.findNodeModulePath(filename, sourceFile, ctx);
|
||
|
} catch (err) {
|
||
|
// ignore
|
||
|
}
|
||
|
} else {
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: md`Node builtin polyfill "${packageName}" is not installed, but auto install is disabled.`,
|
||
|
codeFrames: [
|
||
|
{
|
||
|
filePath: ctx.loc?.filePath ?? sourceFile,
|
||
|
codeHighlights: ctx.loc
|
||
|
? [
|
||
|
{
|
||
|
message: 'used here',
|
||
|
start: ctx.loc.start,
|
||
|
end: ctx.loc.end,
|
||
|
},
|
||
|
]
|
||
|
: [],
|
||
|
},
|
||
|
],
|
||
|
documentationURL:
|
||
|
'https://parceljs.org/features/node-emulation/#polyfilling-%26-excluding-builtin-node-modules',
|
||
|
hints: [
|
||
|
md`Install the "${packageName}" package with your package manager, and run Parcel again.`,
|
||
|
],
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (resolved === undefined && process.versions.pnp != null && parent) {
|
||
|
try {
|
||
|
let [moduleName, subPath] = this.getModuleParts(filename);
|
||
|
// $FlowFixMe[prop-missing]
|
||
|
let pnp = _Module.findPnpApi(path.dirname(parent));
|
||
|
|
||
|
let res = pnp.resolveToUnqualified(
|
||
|
moduleName +
|
||
|
// retain slash in `require('assert/')` to force loading builtin from npm
|
||
|
(filename[moduleName.length] === '/' ? '/' : ''),
|
||
|
parent,
|
||
|
);
|
||
|
|
||
|
resolved = {
|
||
|
moduleName,
|
||
|
subPath,
|
||
|
moduleDir: res,
|
||
|
filePath: path.join(res, subPath || ''),
|
||
|
};
|
||
|
|
||
|
// Invalidate whenever the .pnp.js file changes.
|
||
|
ctx.invalidateOnFileChange.add(
|
||
|
pnp.resolveToUnqualified('pnpapi', null),
|
||
|
);
|
||
|
} catch (e) {
|
||
|
if (e.code !== 'MODULE_NOT_FOUND') {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we couldn't resolve the node_modules path, just return the module name info
|
||
|
if (resolved === undefined) {
|
||
|
let [moduleName, subPath] = this.getModuleParts(filename);
|
||
|
resolved = {
|
||
|
moduleName,
|
||
|
subPath,
|
||
|
};
|
||
|
|
||
|
let alternativeModules = await findAlternativeNodeModules(
|
||
|
this.fs,
|
||
|
moduleName,
|
||
|
path.dirname(sourceFile),
|
||
|
);
|
||
|
|
||
|
if (alternativeModules.length) {
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: md`Cannot find module ${nullthrows(resolved?.moduleName)}`,
|
||
|
hints: alternativeModules.map(r => {
|
||
|
return `Did you mean '__${r}__'?`;
|
||
|
}),
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (resolved != null) {
|
||
|
resolved.query = query;
|
||
|
}
|
||
|
|
||
|
return resolved;
|
||
|
}
|
||
|
|
||
|
shouldIncludeNodeModule(
|
||
|
{includeNodeModules}: Environment,
|
||
|
name: string,
|
||
|
): ?boolean {
|
||
|
if (includeNodeModules === false) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (Array.isArray(includeNodeModules)) {
|
||
|
let [moduleName] = this.getModuleParts(name);
|
||
|
return includeNodeModules.includes(moduleName);
|
||
|
}
|
||
|
|
||
|
if (includeNodeModules && typeof includeNodeModules === 'object') {
|
||
|
let [moduleName] = this.getModuleParts(name);
|
||
|
let include = includeNodeModules[moduleName];
|
||
|
if (include != null) {
|
||
|
return !!include;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async checkExcludedDependency(
|
||
|
sourceFile: FilePath,
|
||
|
name: string,
|
||
|
ctx: ResolverContext,
|
||
|
) {
|
||
|
let [moduleName] = this.getModuleParts(name);
|
||
|
let pkg = await this.findPackage(sourceFile, ctx);
|
||
|
if (!pkg) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
!pkg.dependencies?.[moduleName] &&
|
||
|
!pkg.peerDependencies?.[moduleName] &&
|
||
|
!pkg.engines?.[moduleName]
|
||
|
) {
|
||
|
let pkgContent = await this.fs.readFile(pkg.pkgfile, 'utf8');
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: md`External dependency "${moduleName}" is not declared in package.json.`,
|
||
|
codeFrames: [
|
||
|
{
|
||
|
filePath: pkg.pkgfile,
|
||
|
language: 'json',
|
||
|
code: pkgContent,
|
||
|
codeHighlights: pkg.dependencies
|
||
|
? generateJSONCodeHighlights(pkgContent, [
|
||
|
{
|
||
|
key: `/dependencies`,
|
||
|
type: 'key',
|
||
|
},
|
||
|
])
|
||
|
: [
|
||
|
{
|
||
|
start: {
|
||
|
line: 1,
|
||
|
column: 1,
|
||
|
},
|
||
|
end: {
|
||
|
line: 1,
|
||
|
column: 1,
|
||
|
},
|
||
|
},
|
||
|
],
|
||
|
},
|
||
|
],
|
||
|
hints: [`Add "${moduleName}" as a dependency.`],
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async resolveFilename(
|
||
|
filename: string,
|
||
|
dir: string,
|
||
|
specifierType: SpecifierType,
|
||
|
): Promise<?{|filePath: string, query?: URLSearchParams|}> {
|
||
|
let url;
|
||
|
switch (filename[0]) {
|
||
|
case '/': {
|
||
|
if (specifierType === 'url' && filename[1] === '/') {
|
||
|
// A protocol-relative URL, e.g `url('//example.com/foo.png')`. Ignore.
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Absolute path. Resolve relative to project root.
|
||
|
dir = this.projectRoot;
|
||
|
filename = '.' + filename;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case '~': {
|
||
|
// Tilde path. Resolve relative to nearest node_modules directory,
|
||
|
// the nearest directory with package.json or the project root - whichever comes first.
|
||
|
const insideNodeModules = dir.includes('node_modules');
|
||
|
|
||
|
while (
|
||
|
dir !== this.projectRoot &&
|
||
|
path.basename(path.dirname(dir)) !== 'node_modules' &&
|
||
|
(insideNodeModules ||
|
||
|
!(await this.fs.exists(path.join(dir, 'package.json'))))
|
||
|
) {
|
||
|
dir = path.dirname(dir);
|
||
|
|
||
|
if (dir === path.dirname(dir)) {
|
||
|
dir = this.projectRoot;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
filename = filename.slice(1);
|
||
|
if (filename[0] === '/' || filename[0] === '\\') {
|
||
|
filename = '.' + filename;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case '.': {
|
||
|
// Relative path.
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case '#': {
|
||
|
if (specifierType === 'url') {
|
||
|
// An ID-only URL, e.g. `url(#clip-path)` for CSS rules. Ignore.
|
||
|
return null;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
default: {
|
||
|
// Bare specifier. If this is a URL, it's treated as relative,
|
||
|
// otherwise as a node_modules package.
|
||
|
if (specifierType === 'esm') {
|
||
|
// Try parsing as a URL first in case there is a scheme.
|
||
|
// Otherwise, fall back to an `npm:` specifier, parsed below.
|
||
|
try {
|
||
|
url = new URL(filename);
|
||
|
} catch (e) {
|
||
|
filename = 'npm:' + filename;
|
||
|
}
|
||
|
} else if (specifierType === 'commonjs') {
|
||
|
return {
|
||
|
filePath: filename,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If this is a URL dependency or ESM specifier, parse as a URL.
|
||
|
// Otherwise, if this is CommonJS, parse as a platform path.
|
||
|
if (specifierType === 'url' || specifierType === 'esm') {
|
||
|
url = url ?? new URL(filename, `file:${dir}/index`);
|
||
|
let filePath;
|
||
|
if (url.protocol === 'npm:') {
|
||
|
// The `npm:` scheme allows URLs to resolve to node_modules packages.
|
||
|
filePath = decodeURIComponent(url.pathname);
|
||
|
} else if (url.protocol === 'node:') {
|
||
|
// Preserve the `node:` prefix for use later.
|
||
|
// Node does not URL decode or support query params here.
|
||
|
// See https://github.com/nodejs/node/issues/39710.
|
||
|
return {
|
||
|
filePath: filename,
|
||
|
};
|
||
|
} else if (url.protocol === 'file:') {
|
||
|
// $FlowFixMe
|
||
|
filePath = fileURLToPath(url);
|
||
|
} else if (specifierType === 'url') {
|
||
|
// Don't handle other protocols like http:
|
||
|
return null;
|
||
|
} else {
|
||
|
// Throw on unsupported url schemes in ESM dependencies.
|
||
|
// We may support http: or data: urls eventually.
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: `Unknown url scheme or pipeline '${url.protocol}'`,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
filePath,
|
||
|
query: url.search ? new URLSearchParams(url.search) : undefined,
|
||
|
};
|
||
|
} else {
|
||
|
// CommonJS specifier. Query params are not supported.
|
||
|
return {
|
||
|
filePath: path.resolve(dir, filename),
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async loadRelative(
|
||
|
filename: string,
|
||
|
extensions: Array<string>,
|
||
|
env: Environment,
|
||
|
parentdir: string,
|
||
|
ctx: ResolverContext,
|
||
|
): Promise<?ResolvedFile> {
|
||
|
// Find a package.json file in the current package.
|
||
|
let pkg = await this.findPackage(filename, ctx);
|
||
|
|
||
|
// First try as a file, then as a directory.
|
||
|
let resolvedFile = await this.loadAsFile({
|
||
|
file: filename,
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg,
|
||
|
ctx,
|
||
|
});
|
||
|
|
||
|
// Don't load as a directory if this is a URL dependency.
|
||
|
if (!resolvedFile && ctx.specifierType !== 'url') {
|
||
|
resolvedFile = await this.loadDirectory({
|
||
|
dir: filename,
|
||
|
extensions,
|
||
|
env,
|
||
|
ctx,
|
||
|
pkg,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!resolvedFile) {
|
||
|
// If we can't load the file do a fuzzySearch for potential hints
|
||
|
let relativeFileSpecifier = relativePath(parentdir, filename);
|
||
|
let potentialFiles = await findAlternativeFiles(
|
||
|
this.fs,
|
||
|
relativeFileSpecifier,
|
||
|
parentdir,
|
||
|
this.projectRoot,
|
||
|
true,
|
||
|
ctx.specifierType !== 'url',
|
||
|
extensions.length === 0,
|
||
|
);
|
||
|
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: md`Cannot load file '${relativeFileSpecifier}' in '${relativePath(
|
||
|
this.projectRoot,
|
||
|
parentdir,
|
||
|
)}'.`,
|
||
|
hints: potentialFiles.map(r => {
|
||
|
return `Did you mean '__${r}__'?`;
|
||
|
}),
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return resolvedFile;
|
||
|
}
|
||
|
|
||
|
findBuiltin(filename: string, env: Environment): ?string {
|
||
|
const isExplicitNode = filename.startsWith('node:');
|
||
|
if (isExplicitNode || builtins[filename]) {
|
||
|
if (env.isNode()) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (isExplicitNode) {
|
||
|
filename = filename.substr(5);
|
||
|
}
|
||
|
|
||
|
// By default, exclude node builtins from libraries unless explicitly opted in.
|
||
|
if (
|
||
|
env.isLibrary &&
|
||
|
this.shouldIncludeNodeModule(env, filename) !== true
|
||
|
) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return builtins[filename] || empty;
|
||
|
}
|
||
|
|
||
|
if (env.isElectron() && filename === 'electron') {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
findNodeModulePath(
|
||
|
filename: string,
|
||
|
sourceFile: FilePath,
|
||
|
ctx: ResolverContext,
|
||
|
): ?Module {
|
||
|
let [moduleName, subPath] = this.getModuleParts(filename);
|
||
|
|
||
|
ctx.invalidateOnFileCreate.push({
|
||
|
fileName: `node_modules/${moduleName}`,
|
||
|
aboveFilePath: sourceFile,
|
||
|
});
|
||
|
|
||
|
let dir = path.dirname(sourceFile);
|
||
|
let moduleDir = this.fs.findNodeModule(moduleName, dir);
|
||
|
if (moduleDir) {
|
||
|
return {
|
||
|
moduleName,
|
||
|
subPath,
|
||
|
moduleDir,
|
||
|
filePath: subPath ? path.join(moduleDir, subPath) : moduleDir,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
async loadNodeModules(
|
||
|
module: Module,
|
||
|
extensions: Array<string>,
|
||
|
env: Environment,
|
||
|
ctx: ResolverContext,
|
||
|
): Promise<?ResolvedFile> {
|
||
|
// If a module was specified as a module sub-path (e.g. some-module/some/path),
|
||
|
// it is likely a file. Try loading it as a file first.
|
||
|
if (module.subPath && module.moduleDir) {
|
||
|
let pkg = await this.readPackage(module.moduleDir, ctx);
|
||
|
let res = await this.loadAsFile({
|
||
|
file: nullthrows(module.filePath),
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg,
|
||
|
ctx,
|
||
|
});
|
||
|
if (res) {
|
||
|
return res;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Otherwise, load as a directory.
|
||
|
return this.loadDirectory({
|
||
|
dir: nullthrows(module.filePath),
|
||
|
extensions,
|
||
|
env,
|
||
|
ctx,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async loadDirectory({
|
||
|
dir,
|
||
|
extensions,
|
||
|
env,
|
||
|
ctx,
|
||
|
pkg,
|
||
|
}: {|
|
||
|
dir: string,
|
||
|
extensions: Array<string>,
|
||
|
env: Environment,
|
||
|
ctx: ResolverContext,
|
||
|
pkg?: InternalPackageJSON | null,
|
||
|
|}): Promise<?ResolvedFile> {
|
||
|
let failedEntry;
|
||
|
try {
|
||
|
pkg = await this.readPackage(dir, ctx);
|
||
|
|
||
|
if (pkg) {
|
||
|
// Get a list of possible package entry points.
|
||
|
let entries = this.getPackageEntries(pkg, env);
|
||
|
|
||
|
for (let entry of entries) {
|
||
|
// First try loading package.main as a file, then try as a directory.
|
||
|
let res =
|
||
|
(await this.loadAsFile({
|
||
|
file: entry.filename,
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg,
|
||
|
ctx,
|
||
|
})) ||
|
||
|
(await this.loadDirectory({
|
||
|
dir: entry.filename,
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg,
|
||
|
ctx,
|
||
|
}));
|
||
|
|
||
|
if (res) {
|
||
|
return res;
|
||
|
} else {
|
||
|
failedEntry = entry;
|
||
|
throw new Error('');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} catch (e) {
|
||
|
if (failedEntry && pkg) {
|
||
|
// If loading the entry failed, try to load an index file, and fall back
|
||
|
// to it if it exists.
|
||
|
let indexFallback = await this.loadAsFile({
|
||
|
file: path.join(dir, 'index'),
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg,
|
||
|
ctx,
|
||
|
});
|
||
|
if (indexFallback != null) {
|
||
|
return indexFallback;
|
||
|
}
|
||
|
|
||
|
let fileSpecifier = relativePath(dir, failedEntry.filename);
|
||
|
let alternatives = await findAlternativeFiles(
|
||
|
this.fs,
|
||
|
fileSpecifier,
|
||
|
pkg.pkgdir,
|
||
|
this.projectRoot,
|
||
|
);
|
||
|
|
||
|
let alternative = alternatives[0];
|
||
|
let pkgContent = await this.fs.readFile(pkg.pkgfile, 'utf8');
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: md`Could not load '${fileSpecifier}' from module '${pkg.name}' found in package.json#${failedEntry.field}`,
|
||
|
codeFrames: [
|
||
|
{
|
||
|
filePath: pkg.pkgfile,
|
||
|
language: 'json',
|
||
|
code: pkgContent,
|
||
|
codeHighlights: generateJSONCodeHighlights(pkgContent, [
|
||
|
{
|
||
|
key: `/${failedEntry.field}`,
|
||
|
type: 'value',
|
||
|
message: md`'${fileSpecifier}' does not exist${
|
||
|
alternative ? `, did you mean '${alternative}'?` : ''
|
||
|
}'`,
|
||
|
},
|
||
|
]),
|
||
|
},
|
||
|
],
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Skip index fallback unless this is actually a directory.
|
||
|
try {
|
||
|
if (!(await this.fs.stat(dir)).isDirectory()) {
|
||
|
return;
|
||
|
}
|
||
|
} catch (err) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Fall back to an index file inside the directory.
|
||
|
return this.loadAsFile({
|
||
|
file: path.join(dir, 'index'),
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg: pkg ?? (await this.findPackage(path.join(dir, 'index'), ctx)),
|
||
|
ctx,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async readPackage(
|
||
|
dir: string,
|
||
|
ctx: ResolverContext,
|
||
|
): Promise<InternalPackageJSON> {
|
||
|
let file = path.join(dir, 'package.json');
|
||
|
let cached = this.packageCache.get(file);
|
||
|
|
||
|
if (cached) {
|
||
|
ctx.invalidateOnFileChange.add(cached.pkgfile);
|
||
|
return cached;
|
||
|
}
|
||
|
|
||
|
let json;
|
||
|
try {
|
||
|
json = await this.fs.readFile(file, 'utf8');
|
||
|
} catch (err) {
|
||
|
// If the package.json doesn't exist, watch for it to be created.
|
||
|
ctx.invalidateOnFileCreate.push({
|
||
|
filePath: file,
|
||
|
});
|
||
|
throw err;
|
||
|
}
|
||
|
|
||
|
// Add the invalidation *before* we try to parse the JSON in case of errors
|
||
|
// so that changes are picked up if the file is edited to fix the error.
|
||
|
ctx.invalidateOnFileChange.add(file);
|
||
|
let pkg = JSON.parse(json);
|
||
|
|
||
|
await this.processPackage(pkg, file, dir);
|
||
|
|
||
|
this.packageCache.set(file, pkg);
|
||
|
return pkg;
|
||
|
}
|
||
|
|
||
|
async processPackage(pkg: InternalPackageJSON, file: string, dir: string) {
|
||
|
pkg.pkgfile = file;
|
||
|
pkg.pkgdir = dir;
|
||
|
|
||
|
// If the package has a `source` field, check if it is behind a symlink.
|
||
|
// If so, we treat the module as source code rather than a pre-compiled module.
|
||
|
if (pkg.source) {
|
||
|
let realpath = await this.fs.realpath(file);
|
||
|
if (realpath === file) {
|
||
|
delete pkg.source;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getPackageEntries(
|
||
|
pkg: InternalPackageJSON,
|
||
|
env: Environment,
|
||
|
): Array<{|
|
||
|
filename: string,
|
||
|
field: string,
|
||
|
|}> {
|
||
|
return this.mainFields
|
||
|
.map(field => {
|
||
|
if (field === 'browser' && pkg.browser != null) {
|
||
|
if (!env.isBrowser()) {
|
||
|
return null;
|
||
|
} else if (typeof pkg.browser === 'string') {
|
||
|
return {field, filename: pkg.browser};
|
||
|
} else if (typeof pkg.browser === 'object' && pkg.browser[pkg.name]) {
|
||
|
return {
|
||
|
field: `browser/${pkg.name}`,
|
||
|
filename: pkg.browser[pkg.name],
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
field,
|
||
|
filename: pkg[field],
|
||
|
};
|
||
|
})
|
||
|
.filter(
|
||
|
entry => entry && entry.filename && typeof entry.filename === 'string',
|
||
|
)
|
||
|
.map(entry => {
|
||
|
invariant(entry != null && typeof entry.filename === 'string');
|
||
|
|
||
|
// Current dir refers to an index file
|
||
|
if (entry.filename === '.' || entry.filename === './') {
|
||
|
entry.filename = 'index';
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
field: entry.field,
|
||
|
filename: path.resolve(pkg.pkgdir, entry.filename),
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async loadAsFile({
|
||
|
file,
|
||
|
extensions,
|
||
|
env,
|
||
|
pkg,
|
||
|
ctx,
|
||
|
}: {|
|
||
|
file: string,
|
||
|
extensions: Array<string>,
|
||
|
env: Environment,
|
||
|
pkg: InternalPackageJSON | null,
|
||
|
ctx: ResolverContext,
|
||
|
|}): Promise<?ResolvedFile> {
|
||
|
// Try all supported extensions
|
||
|
let files = await this.expandFile(file, extensions, env, pkg);
|
||
|
let found = this.fs.findFirstFile(files);
|
||
|
|
||
|
// Add invalidations for higher priority files so we
|
||
|
// re-resolve if any of them are created.
|
||
|
for (let file of files) {
|
||
|
if (file === found) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
ctx.invalidateOnFileCreate.push({
|
||
|
filePath: file,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (found) {
|
||
|
return {path: found, pkg};
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
async expandFile(
|
||
|
file: string,
|
||
|
extensions: Array<string>,
|
||
|
env: Environment,
|
||
|
pkg: InternalPackageJSON | null,
|
||
|
expandAliases?: boolean = true,
|
||
|
): Promise<Array<string>> {
|
||
|
// Expand extensions and aliases
|
||
|
let res = [];
|
||
|
for (let ext of extensions) {
|
||
|
let f = file + ext;
|
||
|
if (expandAliases) {
|
||
|
let alias = await this.resolveAliases(f, env, pkg);
|
||
|
let aliasPath;
|
||
|
if (alias && alias.type === 'file') {
|
||
|
aliasPath = alias.resolved;
|
||
|
}
|
||
|
|
||
|
if (aliasPath && aliasPath !== f) {
|
||
|
res = res.concat(
|
||
|
await this.expandFile(aliasPath, extensions, env, pkg, false),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (path.extname(f)) {
|
||
|
res.push(f);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
async resolveAliases(
|
||
|
filename: string,
|
||
|
env: Environment,
|
||
|
pkg: InternalPackageJSON | null,
|
||
|
): Promise<?ResolvedAlias> {
|
||
|
let localAliases = await this.resolvePackageAliases(filename, env, pkg);
|
||
|
if (localAliases) {
|
||
|
return localAliases;
|
||
|
}
|
||
|
|
||
|
// First resolve local package aliases, then project global ones.
|
||
|
return this.resolvePackageAliases(filename, env, this.rootPackage);
|
||
|
}
|
||
|
|
||
|
async resolvePackageAliases(
|
||
|
filename: string,
|
||
|
env: Environment,
|
||
|
pkg: InternalPackageJSON | null,
|
||
|
): Promise<?ResolvedAlias> {
|
||
|
if (!pkg) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (pkg.source && !Array.isArray(pkg.source)) {
|
||
|
let alias = await this.getAlias(filename, pkg, pkg.source);
|
||
|
if (alias != null) {
|
||
|
return alias;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (pkg.alias) {
|
||
|
let alias = await this.getAlias(filename, pkg, pkg.alias);
|
||
|
if (alias != null) {
|
||
|
return alias;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (pkg.browser && env.isBrowser()) {
|
||
|
let alias = await this.getAlias(filename, pkg, pkg.browser);
|
||
|
if (alias != null) {
|
||
|
return alias;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
async getAlias(
|
||
|
filename: FilePath,
|
||
|
pkg: InternalPackageJSON,
|
||
|
aliases: ?Aliases,
|
||
|
): Promise<?ResolvedAlias> {
|
||
|
if (!filename || !aliases || typeof aliases !== 'object') {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
let dir = pkg.pkgdir;
|
||
|
let alias;
|
||
|
|
||
|
// If filename is an absolute path, get one relative to the package.json directory.
|
||
|
if (path.isAbsolute(filename)) {
|
||
|
filename = relativePath(dir, filename);
|
||
|
alias = this.lookupAlias(aliases, filename);
|
||
|
} else {
|
||
|
// It is a node_module. First try the entire filename as a key.
|
||
|
alias = this.lookupAlias(aliases, normalizeSeparators(filename));
|
||
|
if (alias == null) {
|
||
|
// If it didn't match, try only the module name.
|
||
|
let [moduleName, subPath] = this.getModuleParts(filename);
|
||
|
alias = this.lookupAlias(aliases, moduleName);
|
||
|
if (typeof alias === 'string' && subPath) {
|
||
|
let isRelative = alias.startsWith('./');
|
||
|
// Append the filename back onto the aliased module.
|
||
|
alias = path.posix.join(alias, subPath);
|
||
|
// because of path.join('./nested', 'sub') === 'nested/sub'
|
||
|
if (isRelative) alias = './' + alias;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the alias is set to `false`, return an empty file.
|
||
|
if (alias === false) {
|
||
|
return {
|
||
|
type: 'file',
|
||
|
sourcePath: pkg.pkgfile,
|
||
|
resolved: EMPTY_SHIM,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
if (alias instanceof Object) {
|
||
|
if (alias.global) {
|
||
|
if (typeof alias.global !== 'string' || alias.global.length === 0) {
|
||
|
throw new ThrowableDiagnostic({
|
||
|
diagnostic: {
|
||
|
message: md`The global alias for ${filename} is invalid.`,
|
||
|
hints: [`Only nonzero-length strings are valid global aliases.`],
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'global',
|
||
|
sourcePath: pkg.pkgfile,
|
||
|
resolved: alias.global,
|
||
|
};
|
||
|
} else if (alias.fileName) {
|
||
|
alias = alias.fileName;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (typeof alias === 'string') {
|
||
|
// Assume file
|
||
|
let resolved = await this.resolveFilename(alias, dir, 'commonjs');
|
||
|
if (!resolved) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'file',
|
||
|
sourcePath: pkg.pkgfile,
|
||
|
resolved: resolved.filePath,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
lookupAlias(aliases: Aliases, filename: FilePath): null | boolean | string {
|
||
|
if (typeof aliases !== 'object') {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// First, try looking up the exact filename
|
||
|
let alias = aliases[filename];
|
||
|
if (alias == null) {
|
||
|
// Otherwise, try replacing glob keys
|
||
|
for (let key in aliases) {
|
||
|
let val = aliases[key];
|
||
|
if (typeof val === 'string' && isGlob(key)) {
|
||
|
// https://github.com/micromatch/picomatch/issues/77
|
||
|
if (filename.startsWith('./')) {
|
||
|
filename = filename.slice(2);
|
||
|
}
|
||
|
let re = globToRegex(key, {capture: true});
|
||
|
if (re.test(filename)) {
|
||
|
alias = filename.replace(re, val);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return alias;
|
||
|
}
|
||
|
|
||
|
async findPackage(
|
||
|
sourceFile: string,
|
||
|
ctx: ResolverContext,
|
||
|
): Promise<InternalPackageJSON | null> {
|
||
|
ctx.invalidateOnFileCreate.push({
|
||
|
fileName: 'package.json',
|
||
|
aboveFilePath: sourceFile,
|
||
|
});
|
||
|
|
||
|
// Find the nearest package.json file within the current node_modules folder
|
||
|
let res = await loadConfig(
|
||
|
this.fs,
|
||
|
sourceFile,
|
||
|
['package.json'],
|
||
|
this.projectRoot,
|
||
|
// By default, loadConfig uses JSON5. Use normal JSON for package.json files
|
||
|
// since they don't support comments and JSON.parse is faster.
|
||
|
{parser: (...args) => JSON.parse(...args)},
|
||
|
);
|
||
|
|
||
|
if (res != null) {
|
||
|
let file = res.files[0].filePath;
|
||
|
let dir = path.dirname(file);
|
||
|
ctx.invalidateOnFileChange.add(file);
|
||
|
let pkg = res.config;
|
||
|
await this.processPackage(pkg, file, dir);
|
||
|
return pkg;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
async loadAlias(
|
||
|
filename: string,
|
||
|
sourceFile: FilePath,
|
||
|
env: Environment,
|
||
|
ctx: ResolverContext,
|
||
|
): Promise<?ResolvedAlias> {
|
||
|
// Load the root project's package.json file if we haven't already
|
||
|
if (!this.rootPackage) {
|
||
|
this.rootPackage = await this.findPackage(
|
||
|
path.join(this.projectRoot, 'index'),
|
||
|
ctx,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Load the local package, and resolve aliases
|
||
|
let pkg = await this.findPackage(sourceFile, ctx);
|
||
|
return this.resolveAliases(filename, env, pkg);
|
||
|
}
|
||
|
|
||
|
getModuleParts(name: string): [FilePath, ?string] {
|
||
|
name = path.normalize(name);
|
||
|
let splitOn = name.indexOf(path.sep);
|
||
|
if (name.charAt(0) === '@') {
|
||
|
splitOn = name.indexOf(path.sep, splitOn + 1);
|
||
|
}
|
||
|
if (splitOn < 0) {
|
||
|
return [normalizeSeparators(name), undefined];
|
||
|
} else {
|
||
|
return [
|
||
|
normalizeSeparators(name.substring(0, splitOn)),
|
||
|
name.substring(splitOn + 1) || undefined,
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
hasSideEffects(filePath: FilePath, pkg: InternalPackageJSON): boolean {
|
||
|
switch (typeof pkg.sideEffects) {
|
||
|
case 'boolean':
|
||
|
return pkg.sideEffects;
|
||
|
case 'string': {
|
||
|
let glob = pkg.sideEffects;
|
||
|
invariant(typeof glob === 'string');
|
||
|
|
||
|
let relative = path.relative(pkg.pkgdir, filePath);
|
||
|
if (!glob.includes('/')) {
|
||
|
glob = `**/${glob}`;
|
||
|
}
|
||
|
|
||
|
// Trim off "./" to make micromatch behave correctly,
|
||
|
// `path.relative` never returns a leading "./"
|
||
|
if (glob.startsWith('./')) {
|
||
|
glob = glob.substr(2);
|
||
|
}
|
||
|
|
||
|
return isGlobMatch(relative, glob, {dot: true});
|
||
|
}
|
||
|
case 'object':
|
||
|
return pkg.sideEffects.some(sideEffects =>
|
||
|
this.hasSideEffects(filePath, {...pkg, sideEffects}),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
}
|