diff --git a/package.json b/package.json index d3d8383a..0da99f24 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "acorn-private-class-elements": "^1.0.0", "acorn-static-class-features": "^1.0.0", "bindings": "^1.4.0", - "estree-walker": "^0.6.1", + "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.1.15", "micromatch": "^4.0.2", diff --git a/src/analyze.ts b/src/analyze.ts index 03e4c29a..7809dd62 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -1,6 +1,5 @@ import path from 'path'; -import { existsSync, statSync } from 'fs'; -import { walk, WalkerContext, Node } from 'estree-walker'; +import { WalkerContext, Node } from 'estree-walker'; import { attachScopes } from 'rollup-pluginutils'; import { evaluate, UNKNOWN, FUNCTION, WILDCARD, wildcardRegEx } from './utils/static-eval'; import { Parser } from 'acorn'; @@ -19,6 +18,11 @@ import mapboxPregyp from '@mapbox/node-pre-gyp'; import { Job } from './node-file-trace'; import { fileURLToPath, pathToFileURL, URL } from 'url'; + +// TypeScript fails to resolve estree-walker to the top due to the conflicting +// estree-walker version in rollup-pluginutils so we use require here instead +const asyncWalk: typeof import('../node_modules/estree-walker').asyncWalk = require('estree-walker').asyncWalk + // Note: these should be deprecated over time as they ship in Acorn core const acorn = Parser.extend( require("acorn-class-fields"), @@ -353,7 +357,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } - function computePureStaticValue (expr: Node, computeBranches = true) { + async function computePureStaticValue (expr: Node, computeBranches = true) { const vars = Object.create(null); Object.keys(globalBindings).forEach(name => { vars[name] = { value: globalBindings[name] }; @@ -363,7 +367,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi }); vars['import.meta'] = { url: importMetaUrl }; // evaluate returns undefined for non-statically-analyzable - const result = evaluate(expr, vars, computeBranches); + const result = await evaluate(expr, vars, computeBranches); return result; } @@ -410,7 +414,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi }); } - function processRequireArg (expression: Node, isImport = false) { + async function processRequireArg (expression: Node, isImport = false) { if (expression.type === 'ConditionalExpression') { processRequireArg(expression.consequent, isImport); processRequireArg(expression.alternate, isImport); @@ -422,7 +426,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi return; } - let computed = computePureStaticValue(expression, true); + let computed = await computePureStaticValue(expression, true); if (!computed) return; if ('value' in computed && typeof computed.value === 'string') { @@ -442,14 +446,14 @@ export default async function analyze(id: string, code: string, job: Job): Promi let scope = attachScopes(ast, 'scope'); if (isAst(ast)) { handleWrappers(ast); - handleSpecialCases({ id, ast, emitAsset: path => assets.add(path), emitAssetDirectory, job }); + await handleSpecialCases({ id, ast, emitAsset: path => assets.add(path), emitAssetDirectory, job }); } - function backtrack (parent: Node, context?: WalkerContext) { + async function backtrack (parent: Node, context?: WalkerContext) { // computing a static expression outward // -> compute and backtrack // Note that `context` can be undefined in `leave()` if (!staticChildNode) throw new Error('Internal error: No staticChildNode for backtrack.'); - const curStaticValue = computePureStaticValue(parent, true); + const curStaticValue = await computePureStaticValue(parent, true); if (curStaticValue) { if ('value' in curStaticValue && typeof curStaticValue.value !== 'symbol' || 'then' in curStaticValue && typeof curStaticValue.then !== 'symbol' && typeof curStaticValue.else !== 'symbol') { @@ -460,11 +464,14 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } // no static value -> see if we should emit the asset if it exists - emitStaticChildAsset(); + await emitStaticChildAsset(); } - walk(ast, { - enter (node, parent) { + await asyncWalk(ast, { + async enter (_node, _parent) { + const node: Node = _node as any + const parent: Node = _parent as any + if (node.scope) { scope = node.scope; for (const id in node.scope.declarations) { @@ -488,7 +495,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi binding && (typeof binding === 'function' || typeof binding === 'object') && binding[TRIGGER]) { staticChildValue = { value: typeof binding === 'string' ? binding : undefined }; staticChildNode = node; - backtrack(parent, this); + await backtrack(parent, this); } } } @@ -496,7 +503,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // import.meta.url leaf trigger staticChildValue = { value: importMetaUrl }; staticChildNode = node; - backtrack(parent, this); + await backtrack(parent, this); } else if (node.type === 'ImportExpression') { processRequireArg(node.source, true); @@ -528,15 +535,15 @@ export default async function analyze(id: string, code: string, job: Job): Promi return; } - const calleeValue = job.analysis.evaluatePureExpressions && computePureStaticValue(node.callee, false); + const calleeValue = job.analysis.evaluatePureExpressions && await computePureStaticValue(node.callee, false); // if we have a direct pure static function, // and that function has a [TRIGGER] symbol -> trigger asset emission from it if (calleeValue && 'value' in calleeValue && typeof calleeValue.value === 'function' && (calleeValue.value as any)[TRIGGER] && job.analysis.computeFileReferences) { - staticChildValue = computePureStaticValue(node, true); + staticChildValue = await computePureStaticValue(node, true); // if it computes, then we start backtracking if (staticChildValue && parent) { staticChildNode = node; - backtrack(parent, this); + await backtrack(parent, this); } } // handle well-known function symbol cases @@ -554,7 +561,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // require('bindings')(...) case BINDINGS: if (node.arguments.length) { - const arg = computePureStaticValue(node.arguments[0], false); + const arg = await computePureStaticValue(node.arguments[0], false); if (arg && 'value' in arg && arg.value) { let opts: any; if (typeof arg.value === 'object') @@ -573,7 +580,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; - emitStaticChildAsset(); + await emitStaticChildAsset(); } } } @@ -589,14 +596,14 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; - emitStaticChildAsset(); + await emitStaticChildAsset(); } } break; // nbind.init(...) -> require('./resolved.node') case NBIND_INIT: if (node.arguments.length) { - const arg = computePureStaticValue(node.arguments[0], false); + const arg = await computePureStaticValue(node.arguments[0], false); if (arg && 'value' in arg && (typeof arg.value === 'string' || typeof arg.value === 'undefined')) { const bindingInfo = nbind(arg.value); if (bindingInfo && bindingInfo.path) { @@ -623,11 +630,11 @@ export default async function analyze(id: string, code: string, job: Job): Promi break; case FS_FN: if (node.arguments[0] && job.analysis.computeFileReferences) { - staticChildValue = computePureStaticValue(node.arguments[0], true); + staticChildValue = await computePureStaticValue(node.arguments[0], true); // if it computes, then we start backtracking if (staticChildValue) { staticChildNode = node.arguments[0]; - backtrack(parent, this); + await backtrack(parent, this); return this.skip(); } } @@ -635,7 +642,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // strong globalize (emits intl folder) case SET_ROOT_DIR: if (node.arguments[0]) { - const rootDir = computePureStaticValue(node.arguments[0], false); + const rootDir = await computePureStaticValue(node.arguments[0], false); if (rootDir && 'value' in rootDir && rootDir.value) emitAssetDirectory(rootDir.value + '/intl'); return this.skip(); @@ -645,7 +652,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi case PKG_INFO: let pjsonPath = path.resolve(id, '../package.json'); const rootPjson = path.resolve('/package.json'); - while (pjsonPath !== rootPjson && !existsSync(pjsonPath)) + while (pjsonPath !== rootPjson && (await job.stat(pjsonPath) === null)) pjsonPath = path.resolve(pjsonPath, '../../package.json'); if (pjsonPath !== rootPjson) assets.add(pjsonPath); @@ -656,7 +663,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi else if (node.type === 'VariableDeclaration' && parent && !isVarLoop(parent) && job.analysis.evaluatePureExpressions) { for (const decl of node.declarations) { if (!decl.init) continue; - const computed = computePureStaticValue(decl.init, true); + const computed = await computePureStaticValue(decl.init, true); if (computed) { // var known = ...; if (decl.id.type === 'Identifier') { @@ -678,14 +685,14 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (!('value' in computed) && isAbsolutePathOrUrl(computed.then) && isAbsolutePathOrUrl(computed.else)) { staticChildValue = computed; staticChildNode = decl.init; - emitStaticChildAsset(); + await emitStaticChildAsset(); } } } } else if (node.type === 'AssignmentExpression' && parent && !isLoop(parent) && job.analysis.evaluatePureExpressions) { if (!hasKnownBindingValue(node.left.name)) { - const computed = computePureStaticValue(node.right, false); + const computed = await computePureStaticValue(node.right, false); if (computed && 'value' in computed) { // var known = ... if (node.left.type === 'Identifier') { @@ -707,7 +714,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (isAbsolutePathOrUrl(computed.value)) { staticChildValue = computed; staticChildNode = node.right; - emitStaticChildAsset(); + await emitStaticChildAsset(); } } } @@ -761,7 +768,10 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } }, - leave (node, parent) { + async leave (_node, _parent) { + const node: Node = _node as any + const parent: Node = _parent as any + if (node.scope) { if (scope.parent) { scope = scope.parent; @@ -776,20 +786,23 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } - if (staticChildNode && parent) backtrack(parent, this); + if (staticChildNode && parent) await backtrack(parent, this); } }); await assetEmissionPromises; return { assets, deps, imports, isESM }; - function emitAssetPath (assetPath: string) { + async function emitAssetPath (assetPath: string) { // verify the asset file / directory exists const wildcardIndex = assetPath.indexOf(WILDCARD); const dirIndex = wildcardIndex === -1 ? assetPath.length : assetPath.lastIndexOf(path.sep, wildcardIndex); const basePath = assetPath.substr(0, dirIndex); try { - var stats = statSync(basePath); + var stats = await job.stat(basePath); + if (stats === null) { + throw new Error('file not found') + } } catch (e) { return; @@ -840,7 +853,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi return value instanceof URL ? fileURLToPath(value) : value.startsWith('file:') ? fileURLToPath(new URL(value)) : path.resolve(value); } - function emitStaticChildAsset () { + async function emitStaticChildAsset () { if (!staticChildValue) { return; } @@ -848,7 +861,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if ('value' in staticChildValue && isAbsolutePathOrUrl(staticChildValue.value)) { try { const resolved = resolveAbsolutePathOrUrl(staticChildValue.value); - emitAssetPath(resolved); + await emitAssetPath(resolved); } catch (e) {} } @@ -859,14 +872,14 @@ export default async function analyze(id: string, code: string, job: Job): Promi let resolvedElse; try { resolvedElse = resolveAbsolutePathOrUrl(staticChildValue.else); } catch (e) {} - if (resolvedThen) emitAssetPath(resolvedThen); - if (resolvedElse) emitAssetPath(resolvedElse); + if (resolvedThen) await emitAssetPath(resolvedThen); + if (resolvedElse) await emitAssetPath(resolvedElse); } else if (staticChildNode && staticChildNode.type === 'ArrayExpression' && 'value' in staticChildValue && staticChildValue.value instanceof Array) { for (const value of staticChildValue.value) { try { const resolved = resolveAbsolutePathOrUrl(value); - emitAssetPath(resolved); + await emitAssetPath(resolved); } catch (e) {} } diff --git a/src/node-file-trace.ts b/src/node-file-trace.ts index 5bb4c8c0..3d6367c5 100644 --- a/src/node-file-trace.ts +++ b/src/node-file-trace.ts @@ -1,12 +1,17 @@ import { NodeFileTraceOptions, NodeFileTraceResult, NodeFileTraceReasons, Stats } from './types'; import { basename, dirname, extname, relative, resolve, sep } from 'path'; import fs from 'fs'; +import { promisify } from 'util' import analyze, { AnalyzeResult } from './analyze'; import resolveDependency from './resolve-dependency'; import { isMatch } from 'micromatch'; import { sharedLibEmit } from './utils/sharedlib-emit'; import { join } from 'path'; +const fsReadFile = promisify(fs.readFile) +const fsReadlink = promisify(fs.readlink) +const fsStat = promisify(fs.stat) + const { gracefulify } = require('graceful-fs'); gracefulify(fs); @@ -19,19 +24,19 @@ export async function nodeFileTrace(files: string[], opts: NodeFileTraceOptions const job = new Job(opts); if (opts.readFile) - job.readFile = opts.readFile; + job.readFile = opts.readFile if (opts.stat) - job.stat = opts.stat; + job.stat = opts.stat if (opts.readlink) - job.readlink = opts.readlink; + job.readlink = opts.readlink if (opts.resolve) - job.resolve = opts.resolve; + job.resolve = opts.resolve job.ts = true; - await Promise.all(files.map(file => { + await Promise.all(files.map(async file => { const path = resolve(file); - job.emitFile(path, 'initial'); + await job.emitFile(path, 'initial'); if (path.endsWith('.js') || path.endsWith('.cjs') || path.endsWith('.mjs') || path.endsWith('.node') || job.ts && (path.endsWith('.ts') || path.endsWith('.tsx'))) { return job.emitDependency(path); } @@ -151,11 +156,11 @@ export class Job { this.warnings = new Set(); } - readlink (path: string) { + async readlink (path: string) { const cached = this.symlinkCache.get(path); if (cached !== undefined) return cached; try { - const link = fs.readlinkSync(path); + const link = await fsReadlink(path); // also copy stat cache to symlink const stats = this.statCache.get(path); if (stats) @@ -171,25 +176,25 @@ export class Job { } } - isFile (path: string) { - const stats = this.stat(path); + async isFile (path: string) { + const stats = await this.stat(path); if (stats) return stats.isFile(); return false; } - isDir (path: string) { - const stats = this.stat(path); + async isDir (path: string) { + const stats = await this.stat(path); if (stats) return stats.isDirectory(); return false; } - stat (path: string) { + async stat (path: string) { const cached = this.statCache.get(path); if (cached) return cached; try { - const stats = fs.statSync(path); + const stats = await fsStat(path); this.statCache.set(path, stats); return stats; } @@ -202,15 +207,15 @@ export class Job { } } - resolve (id: string, parent: string, job: Job, cjsResolve: boolean): string | string[] { + async resolve (id: string, parent: string, job: Job, cjsResolve: boolean): Promise { return resolveDependency(id, parent, job, cjsResolve); } - readFile (path: string): string | Buffer | null { + async readFile (path: string): Promise { const cached = this.fileCache.get(path); if (cached !== undefined) return cached; try { - const source = fs.readFileSync(path).toString(); + const source = (await fsReadFile(path)).toString(); this.fileCache.set(path, source); return source; } @@ -223,28 +228,28 @@ export class Job { } } - realpath (path: string, parent?: string, seen = new Set()): string { + async realpath (path: string, parent?: string, seen = new Set()): Promise { if (seen.has(path)) throw new Error('Recursive symlink detected resolving ' + path); seen.add(path); - const symlink = this.readlink(path); + const symlink = await this.readlink(path); // emit direct symlink paths only if (symlink) { const parentPath = dirname(path); const resolved = resolve(parentPath, symlink); - const realParent = this.realpath(parentPath, parent); + const realParent = await this.realpath(parentPath, parent); if (inPath(path, realParent)) - this.emitFile(path, 'resolve', parent, true); + await this.emitFile(path, 'resolve', parent, true); return this.realpath(resolved, parent, seen); } // keep backtracking for realpath, emitting folder symlinks within base if (!inPath(path, this.base)) return path; - return join(this.realpath(dirname(path), parent, seen), basename(path)); + return join(await this.realpath(dirname(path), parent, seen), basename(path)); } - emitFile (path: string, reason: string, parent?: string, isRealpath = false) { + async emitFile (path: string, reason: string, parent?: string, isRealpath = false) { if (!isRealpath) - path = this.realpath(path, parent); + path = await this.realpath(path, parent); if (this.fileList.has(path)) return; path = relative(this.base, path); if (parent) @@ -264,12 +269,12 @@ export class Job { return true; } - getPjsonBoundary (path: string) { + async getPjsonBoundary (path: string) { const rootSeparatorIndex = path.indexOf(sep); let separatorIndex: number; while ((separatorIndex = path.lastIndexOf(sep)) > rootSeparatorIndex) { path = path.substr(0, separatorIndex); - if (this.isFile(path + sep + 'package.json')) + if (await this.isFile(path + sep + 'package.json')) return path; } return undefined; @@ -279,16 +284,16 @@ export class Job { if (this.processed.has(path)) return; this.processed.add(path); - const emitted = this.emitFile(path, 'dependency', parent); + const emitted = await this.emitFile(path, 'dependency', parent); if (!emitted) return; if (path.endsWith('.json')) return; if (path.endsWith('.node')) return await sharedLibEmit(path, this); // js files require the "type": "module" lookup, so always emit the package.json if (path.endsWith('.js')) { - const pjsonBoundary = this.getPjsonBoundary(path); + const pjsonBoundary = await this.getPjsonBoundary(path); if (pjsonBoundary) - this.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', path); + await this.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', path); } let analyzeResult: AnalyzeResult; @@ -298,7 +303,7 @@ export class Job { analyzeResult = cachedAnalysis; } else { - const source = this.readFile(path); + const source = await this.readFile(path); if (source === null) throw new Error('File ' + path + ' does not exist.'); analyzeResult = await analyze(path, source.toString(), this); this.analysisCache.set(path, analyzeResult); @@ -316,11 +321,11 @@ export class Job { this.ts && (ext === '.ts' || ext === '.tsx') && asset.startsWith(this.base) && asset.substr(this.base.length).indexOf(sep + 'node_modules' + sep) === -1) await this.emitDependency(asset, path); else - this.emitFile(asset, 'asset', path); + await this.emitFile(asset, 'asset', path); }), ...[...deps].map(async dep => { try { - var resolved = this.resolve(dep, path, this, !isESM); + var resolved = await this.resolve(dep, path, this, !isESM); } catch (e) { this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`)); @@ -341,7 +346,7 @@ export class Job { }), ...[...imports].map(async dep => { try { - var resolved = this.resolve(dep, path, this, false); + var resolved = await this.resolve(dep, path, this, false); } catch (e) { this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`)); diff --git a/src/resolve-dependency.ts b/src/resolve-dependency.ts index cbb69990..ea041177 100644 --- a/src/resolve-dependency.ts +++ b/src/resolve-dependency.ts @@ -4,21 +4,21 @@ import { Job } from './node-file-trace'; // node resolver // custom implementation to emit only needed package.json files for resolver // (package.json files are emitted as they are hit) -export default function resolveDependency (specifier: string, parent: string, job: Job, cjsResolve = true) { +export default async function resolveDependency (specifier: string, parent: string, job: Job, cjsResolve = true): Promise { let resolved: string | string[]; if (isAbsolute(specifier) || specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../')) { const trailingSlash = specifier.endsWith('/'); - resolved = resolvePath(resolve(parent, '..', specifier) + (trailingSlash ? '/' : ''), parent, job); + resolved = await resolvePath(resolve(parent, '..', specifier) + (trailingSlash ? '/' : ''), parent, job); } else if (specifier[0] === '#') { - resolved = packageImportsResolve(specifier, parent, job, cjsResolve); + resolved = await packageImportsResolve(specifier, parent, job, cjsResolve); } else { - resolved = resolvePackage(specifier, parent, job, cjsResolve); + resolved = await resolvePackage(specifier, parent, job, cjsResolve); } if (Array.isArray(resolved)) { - return resolved.map(resolved => job.realpath(resolved, parent)); + return Promise.all(resolved.map(resolved => job.realpath(resolved, parent))) } else if (resolved.startsWith('node:')) { return resolved; } else { @@ -26,34 +26,34 @@ export default function resolveDependency (specifier: string, parent: string, jo } }; -function resolvePath (path: string, parent: string, job: Job): string { - const result = resolveFile(path, parent, job) || resolveDir(path, parent, job); +async function resolvePath (path: string, parent: string, job: Job): Promise { + const result = await resolveFile(path, parent, job) || await resolveDir(path, parent, job); if (!result) { throw new NotFoundError(path, parent); } return result; } -function resolveFile (path: string, parent: string, job: Job): string | undefined { +async function resolveFile (path: string, parent: string, job: Job): Promise { if (path.endsWith('/')) return undefined; - path = job.realpath(path, parent); - if (job.isFile(path)) return path; - if (job.ts && path.startsWith(job.base) && path.substr(job.base.length).indexOf(sep + 'node_modules' + sep) === -1 && job.isFile(path + '.ts')) return path + '.ts'; - if (job.ts && path.startsWith(job.base) && path.substr(job.base.length).indexOf(sep + 'node_modules' + sep) === -1 && job.isFile(path + '.tsx')) return path + '.tsx'; - if (job.isFile(path + '.js')) return path + '.js'; - if (job.isFile(path + '.json')) return path + '.json'; - if (job.isFile(path + '.node')) return path + '.node'; + path = await job.realpath(path, parent); + if (await job.isFile(path)) return path; + if (job.ts && path.startsWith(job.base) && path.substr(job.base.length).indexOf(sep + 'node_modules' + sep) === -1 && await job.isFile(path + '.ts')) return path + '.ts'; + if (job.ts && path.startsWith(job.base) && path.substr(job.base.length).indexOf(sep + 'node_modules' + sep) === -1 && await job.isFile(path + '.tsx')) return path + '.tsx'; + if (await job.isFile(path + '.js')) return path + '.js'; + if (await job.isFile(path + '.json')) return path + '.json'; + if (await job.isFile(path + '.node')) return path + '.node'; return undefined; } -function resolveDir (path: string, parent: string, job: Job) { +async function resolveDir (path: string, parent: string, job: Job) { if (path.endsWith('/')) path = path.slice(0, -1); - if (!job.isDir(path)) return; - const pkgCfg = getPkgCfg(path, job); + if (!await job.isDir(path)) return; + const pkgCfg = await getPkgCfg(path, job); if (pkgCfg && typeof pkgCfg.main === 'string') { - const resolved = resolveFile(resolve(path, pkgCfg.main), parent, job) || resolveFile(resolve(path, pkgCfg.main, 'index'), parent, job); + const resolved = await resolveFile(resolve(path, pkgCfg.main), parent, job) || await resolveFile(resolve(path, pkgCfg.main, 'index'), parent, job); if (resolved) { - job.emitFile(path + sep + 'package.json', 'resolve', parent); + await job.emitFile(path + sep + 'package.json', 'resolve', parent); return resolved; } } @@ -86,8 +86,8 @@ interface PkgCfg { imports: { [key: string]: PackageTarget }; } -function getPkgCfg (pkgPath: string, job: Job): PkgCfg | undefined { - const pjsonSource = job.readFile(pkgPath + sep + 'package.json'); +async function getPkgCfg (pkgPath: string, job: Job): Promise { + const pjsonSource = await job.readFile(pkgPath + sep + 'package.json'); if (pjsonSource) { try { return JSON.parse(pjsonSource.toString()); @@ -162,21 +162,21 @@ function resolveExportsImports (pkgPath: string, obj: PackageTarget, subpath: st return undefined; } -function packageImportsResolve (name: string, parent: string, job: Job, cjsResolve: boolean): string { +async function packageImportsResolve (name: string, parent: string, job: Job, cjsResolve: boolean): Promise { if (name !== '#' && !name.startsWith('#/') && job.conditions) { - const pjsonBoundary = job.getPjsonBoundary(parent); + const pjsonBoundary = await job.getPjsonBoundary(parent); if (pjsonBoundary) { - const pkgCfg = getPkgCfg(pjsonBoundary, job); + const pkgCfg = await getPkgCfg(pjsonBoundary, job); const { imports: pkgImports } = pkgCfg || {}; if (pkgCfg && pkgImports !== null && pkgImports !== undefined) { let importsResolved = resolveExportsImports(pjsonBoundary, pkgImports, name, job, true, cjsResolve); if (importsResolved) { if (cjsResolve) - importsResolved = resolveFile(importsResolved, parent, job) || resolveDir(importsResolved, parent, job); - else if (!job.isFile(importsResolved)) + importsResolved = await resolveFile(importsResolved, parent, job) || await resolveDir(importsResolved, parent, job); + else if (!await job.isFile(importsResolved)) throw new NotFoundError(importsResolved, parent); if (importsResolved) { - job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); + await job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); return importsResolved; } } @@ -186,7 +186,7 @@ function packageImportsResolve (name: string, parent: string, job: Job, cjsResol throw new NotFoundError(name, parent); } -function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean): string | string [] { +async function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean): Promise { let packageParent = parent; if (nodeBuiltins.has(name)) return 'node:' + name; @@ -195,20 +195,20 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo // package own name resolution let selfResolved: string | undefined; if (job.conditions) { - const pjsonBoundary = job.getPjsonBoundary(parent); + const pjsonBoundary = await job.getPjsonBoundary(parent); if (pjsonBoundary) { - const pkgCfg = getPkgCfg(pjsonBoundary, job); + const pkgCfg = await getPkgCfg(pjsonBoundary, job); const { exports: pkgExports } = pkgCfg || {}; if (pkgCfg && pkgCfg.name && pkgCfg.name === pkgName && pkgExports !== null && pkgExports !== undefined) { selfResolved = resolveExportsImports(pjsonBoundary, pkgExports, '.' + name.slice(pkgName.length), job, false, cjsResolve); if (selfResolved) { if (cjsResolve) - selfResolved = resolveFile(selfResolved, parent, job) || resolveDir(selfResolved, parent, job); - else if (!job.isFile(selfResolved)) + selfResolved = await resolveFile(selfResolved, parent, job) || await resolveDir(selfResolved, parent, job); + else if (!await job.isFile(selfResolved)) throw new NotFoundError(selfResolved, parent); } if (selfResolved) - job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); + await job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); } } } @@ -218,23 +218,23 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo while ((separatorIndex = packageParent.lastIndexOf(sep)) > rootSeparatorIndex) { packageParent = packageParent.substr(0, separatorIndex); const nodeModulesDir = packageParent + sep + 'node_modules'; - const stat = job.stat(nodeModulesDir); + const stat = await job.stat(nodeModulesDir); if (!stat || !stat.isDirectory()) continue; - const pkgCfg = getPkgCfg(nodeModulesDir + sep + pkgName, job); + const pkgCfg = await getPkgCfg(nodeModulesDir + sep + pkgName, job); const { exports: pkgExports } = pkgCfg || {}; if (job.conditions && pkgExports !== undefined && pkgExports !== null && !selfResolved) { let legacyResolved; if (!job.exportsOnly) - legacyResolved = resolveFile(nodeModulesDir + sep + name, parent, job) || resolveDir(nodeModulesDir + sep + name, parent, job); + legacyResolved = await resolveFile(nodeModulesDir + sep + name, parent, job) || await resolveDir(nodeModulesDir + sep + name, parent, job); let resolved = resolveExportsImports(nodeModulesDir + sep + pkgName, pkgExports, '.' + name.slice(pkgName.length), job, false, cjsResolve); if (resolved) { if (cjsResolve) - resolved = resolveFile(resolved, parent, job) || resolveDir(resolved, parent, job); - else if (!job.isFile(resolved)) + resolved = await resolveFile(resolved, parent, job) || await resolveDir(resolved, parent, job); + else if (!await job.isFile(resolved)) throw new NotFoundError(resolved, parent); } if (resolved) { - job.emitFile(nodeModulesDir + sep + pkgName + sep + 'package.json', 'resolve', parent); + await job.emitFile(nodeModulesDir + sep + pkgName + sep + 'package.json', 'resolve', parent); if (legacyResolved && legacyResolved !== resolved) return [resolved, legacyResolved]; return resolved; @@ -243,7 +243,7 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo return legacyResolved; } else { - const resolved = resolveFile(nodeModulesDir + sep + name, parent, job) || resolveDir(nodeModulesDir + sep + name, parent, job); + const resolved = await resolveFile(nodeModulesDir + sep + name, parent, job) || await resolveDir(nodeModulesDir + sep + name, parent, job); if (resolved) { if (selfResolved && selfResolved !== resolved) return [resolved, selfResolved]; @@ -258,7 +258,7 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo for (const path of Object.keys(job.paths)) { if (path.endsWith('/') && name.startsWith(path)) { const pathTarget = job.paths[path] + name.slice(path.length); - const resolved = resolveFile(pathTarget, parent, job) || resolveDir(pathTarget, parent, job); + const resolved = await resolveFile(pathTarget, parent, job) || await resolveDir(pathTarget, parent, job); if (!resolved) { throw new NotFoundError(name, parent); } diff --git a/src/types.ts b/src/types.ts index 321cd4d0..6ffa4312 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,10 +45,10 @@ export interface NodeFileTraceOptions { ts?: boolean; log?: boolean; mixedModules?: boolean; - readFile?: (path: string) => Buffer | string | null; - stat?: (path: string) => Stats | null; - readlink?: (path: string) => string | null; - resolve?: (id: string, parent: string, job: Job, cjsResolve: boolean) => string | string[]; + readFile?: (path: string) => Promise; + stat?: (path: string) => Promise; + readlink?: (path: string) => Promise; + resolve?: (id: string, parent: string, job: Job, cjsResolve: boolean) => Promise; } export interface NodeFileTraceReasons { diff --git a/src/utils/sharedlib-emit.ts b/src/utils/sharedlib-emit.ts index 4b286837..b4f70a0c 100644 --- a/src/utils/sharedlib-emit.ts +++ b/src/utils/sharedlib-emit.ts @@ -25,5 +25,5 @@ export async function sharedLibEmit(path: string, job: Job) { const files = await new Promise((resolve, reject) => glob(pkgPath + sharedlibGlob, { ignore: pkgPath + '/**/node_modules/**/*' }, (err, files) => err ? reject(err) : resolve(files)) ); - files.forEach(file => job.emitFile(file, 'sharedlib', path)); + await Promise.all(files.map(file => job.emitFile(file, 'sharedlib', path))); }; diff --git a/src/utils/special-cases.ts b/src/utils/special-cases.ts index 2285ae62..32bb71d4 100644 --- a/src/utils/special-cases.ts +++ b/src/utils/special-cases.ts @@ -99,9 +99,9 @@ const specialCases: Record void> = { emitAsset(resolve(id.replace('index.js', 'preload.js'))); } }, - 'socket.io' ({ id, ast, job }) { + 'socket.io': async function ({ id, ast, job }) { if (id.endsWith('socket.io/lib/index.js')) { - function replaceResolvePathStatement (statement: Node) { + async function replaceResolvePathStatement (statement: Node) { if (statement.type === 'ExpressionStatement' && statement.expression.type === 'AssignmentExpression' && statement.expression.operator === '=' && @@ -117,7 +117,7 @@ const specialCases: Record void> = { const arg = statement.expression.right.arguments[0].arguments[0].value; let resolved: string; try { - const dep = resolveDependency(String(arg), id, job); + const dep = await resolveDependency(String(arg), id, job); if (typeof dep === 'string') { resolved = dep; } else { @@ -169,10 +169,10 @@ const specialCases: Record void> = { const ifBody = node.consequent.body; let replaced: boolean | undefined = false; if (Array.isArray(ifBody) && ifBody[0] && ifBody[0].type === 'ExpressionStatement') { - replaced = replaceResolvePathStatement(ifBody[0]); + replaced = await replaceResolvePathStatement(ifBody[0]); } if (Array.isArray(ifBody) && ifBody[1] && ifBody[1].type === 'TryStatement' && ifBody[1].block.body && ifBody[1].block.body[0]) { - replaced = replaceResolvePathStatement(ifBody[1].block.body[0]) || replaced; + replaced = await replaceResolvePathStatement(ifBody[1].block.body[0]) || replaced; } return; } @@ -224,9 +224,9 @@ interface SpecialCaseOpts { job: Job; } -export default function handleSpecialCases({ id, ast, emitAsset, emitAssetDirectory, job }: SpecialCaseOpts) { +export default async function handleSpecialCases({ id, ast, emitAsset, emitAssetDirectory, job }: SpecialCaseOpts) { const pkgName = getPackageName(id); const specialCase = specialCases[pkgName || '']; id = id.replace(/\\/g, '/'); - if (specialCase) specialCase({ id, ast, emitAsset, emitAssetDirectory, job }); + if (specialCase) await specialCase({ id, ast, emitAsset, emitAssetDirectory, job }); }; diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index 21b09d94..2c6d16db 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -4,7 +4,7 @@ import { URL } from 'url'; type Walk = (node: Node) => EvaluatedValue; type State = { computeBranches: boolean, vars: Record }; -export function evaluate(ast: Node, vars = {}, computeBranches = true): EvaluatedValue { +export async function evaluate(ast: Node, vars = {}, computeBranches = true): Promise { const state: State = { computeBranches, vars @@ -36,25 +36,25 @@ function countWildcards (str: string) { return cnt; } -const visitors: Record EvaluatedValue> = { - 'ArrayExpression': function ArrayExpression(this: State, node: Node, walk: Walk) { +const visitors: Record Promise> = { + 'ArrayExpression': async function ArrayExpression(this: State, node: Node, walk: Walk) { const arr = []; for (let i = 0, l = node.elements.length; i < l; i++) { if (node.elements[i] === null) { arr.push(null); continue; } - const x = walk(node.elements[i]); + const x = await walk(node.elements[i]); if (!x) return; if ('value' in x === false) return; arr.push((x as StaticValue).value); } return { value: arr }; }, - 'ArrowFunctionExpression': function (this: State, node: Node, walk: Walk) { + 'ArrowFunctionExpression': async function (this: State, node: Node, walk: Walk) { // () => val support only if (node.params.length === 0 && !node.generator && !node.async && node.expression) { - const innerValue = walk(node.body); + const innerValue = await walk(node.body); if (!innerValue || !('value' in innerValue)) return; return { @@ -65,14 +65,14 @@ const visitors: Record Evaluate } return undefined; }, - 'BinaryExpression': function BinaryExpression(this: State, node: Node, walk: Walk) { + 'BinaryExpression': async function BinaryExpression(this: State, node: Node, walk: Walk) { const op = node.operator; - let l = walk(node.left); + let l = await walk(node.left); if (!l && op !== '+') return; - let r = walk(node.right); + let r = await walk(node.right); if (!l && !r) return; @@ -172,8 +172,8 @@ const visitors: Record Evaluate } return; }, - 'CallExpression': function CallExpression(this: State, node: Node, walk: Walk) { - const callee = walk(node.callee); + 'CallExpression': async function CallExpression(this: State, node: Node, walk: Walk) { + const callee = await walk(node.callee); if (!callee || 'test' in callee) return; let fn: any = callee.value; @@ -182,7 +182,7 @@ const visitors: Record Evaluate let ctx = null if (node.callee.object) { - ctx = walk(node.callee.object) + ctx = await walk(node.callee.object) ctx = ctx && 'value' in ctx && ctx.value ? ctx.value : null } @@ -193,7 +193,7 @@ const visitors: Record Evaluate let allWildcards = node.arguments.length > 0 && node.callee.property?.name !== 'concat'; const wildcards: string[] = []; for (let i = 0, l = node.arguments.length; i < l; i++) { - let x = walk(node.arguments[i]); + let x = await walk(node.arguments[i]); if (x) { allWildcards = false; if ('value' in x && typeof x.value === 'string' && x.wildcards) @@ -224,7 +224,7 @@ const visitors: Record Evaluate if (allWildcards) return; try { - const result = fn.apply(ctx, args); + const result = await fn.apply(ctx, args); if (result === UNKNOWN) return; if (!predicate) { @@ -235,7 +235,7 @@ const visitors: Record Evaluate } return { value: result }; } - const resultElse = fn.apply(ctx, argsElse); + const resultElse = await fn.apply(ctx, argsElse); if (result === UNKNOWN) return; return { test: predicate, then: result, else: resultElse }; @@ -244,18 +244,18 @@ const visitors: Record Evaluate return; } }, - 'ConditionalExpression': function ConditionalExpression(this: State, node: Node, walk: Walk) { - const val = walk(node.test); + 'ConditionalExpression': async function ConditionalExpression(this: State, node: Node, walk: Walk) { + const val = await walk(node.test); if (val && 'value' in val) return val.value ? walk(node.consequent) : walk(node.alternate); if (!this.computeBranches) return; - const thenValue = walk(node.consequent); + const thenValue = await walk(node.consequent); if (!thenValue || 'wildcards' in thenValue || 'test' in thenValue) return; - const elseValue = walk(node.alternate); + const elseValue = await walk(node.alternate); if (!elseValue || 'wildcards' in elseValue || 'test' in elseValue) return; @@ -265,19 +265,19 @@ const visitors: Record Evaluate else: elseValue.value }; }, - 'ExpressionStatement': function ExpressionStatement(this: State, node: Node, walk: Walk) { + 'ExpressionStatement': async function ExpressionStatement(this: State, node: Node, walk: Walk) { return walk(node.expression); }, - 'Identifier': function Identifier(this: State, node: Node, _walk: Walk) { + 'Identifier': async function Identifier(this: State, node: Node, _walk: Walk) { if (Object.hasOwnProperty.call(this.vars, node.name)) return this.vars[node.name]; return undefined; }, - 'Literal': function Literal (this: State, node: Node, _walk: Walk) { + 'Literal': async function Literal (this: State, node: Node, _walk: Walk) { return { value: node.value }; }, - 'MemberExpression': function MemberExpression(this: State, node: Node, walk: Walk) { - const obj = walk(node.object); + 'MemberExpression': async function MemberExpression(this: State, node: Node, walk: Walk) { + const obj = await walk(node.object); if (!obj || 'test' in obj || typeof obj.value === 'function') { return undefined; } @@ -293,7 +293,7 @@ const visitors: Record Evaluate const objValue = obj.value as any; if (node.computed) { // See if we can compute the computed property - const computedProp = walk(node.property); + const computedProp = await walk(node.property); if (computedProp && 'value' in computedProp && computedProp.value) { const val = objValue[computedProp.value]; if (val === UNKNOWN) return undefined; @@ -316,7 +316,7 @@ const visitors: Record Evaluate return { value: undefined }; } } - const prop = walk(node.property); + const prop = await walk(node.property); if (!prop || 'test' in prop) return undefined; if (typeof obj.value === 'object' && obj.value !== null) { @@ -338,21 +338,21 @@ const visitors: Record Evaluate } return undefined; }, - 'MetaProperty': function MetaProperty(this: State, node: Node) { + 'MetaProperty': async function MetaProperty(this: State, node: Node) { if (node.meta.name === 'import' && node.property.name === 'meta') return { value: this.vars['import.meta'] }; return undefined; }, - 'NewExpression': function NewExpression(this: State, node: Node, walk: Walk) { + 'NewExpression': async function NewExpression(this: State, node: Node, walk: Walk) { // new URL('./local', parent) - const cls = walk(node.callee); + const cls = await walk(node.callee); if (cls && 'value' in cls && cls.value === URL && node.arguments.length) { - const arg = walk(node.arguments[0]); + const arg = await walk(node.arguments[0]); if (!arg) return undefined; let parent = null; if (node.arguments[1]) { - parent = walk(node.arguments[1]); + parent = await walk(node.arguments[1]); if (!parent || !('value' in parent)) return undefined; } @@ -400,13 +400,13 @@ const visitors: Record Evaluate } return undefined; }, - 'ObjectExpression': function ObjectExpression(this: State, node: Node, walk: Walk) { + 'ObjectExpression': async function ObjectExpression(this: State, node: Node, walk: Walk) { const obj: any = {}; for (let i = 0; i < node.properties.length; i++) { const prop = node.properties[i]; const keyValue = prop.computed ? walk(prop.key) : prop.key && { value: prop.key.name || prop.key.value }; if (!keyValue || 'test' in keyValue) return; - const value = walk(prop.value); + const value = await walk(prop.value); if (!value || 'test' in value) return; //@ts-ignore if (value.value === UNKNOWN) return; @@ -415,7 +415,7 @@ const visitors: Record Evaluate } return { value: obj }; }, - 'TemplateLiteral': function TemplateLiteral(this: State, node: Node, walk: Walk) { + 'TemplateLiteral': async function TemplateLiteral(this: State, node: Node, walk: Walk) { let val: StaticValue | ConditionalValue = { value: '' }; for (var i = 0; i < node.expressions.length; i++) { if ('value' in val) { @@ -425,7 +425,7 @@ const visitors: Record Evaluate val.then += node.quasis[i].value.cooked; val.else += node.quasis[i].value.cooked; } - let exprValue = walk(node.expressions[i]); + let exprValue = await walk(node.expressions[i]); if (!exprValue) { if (!this.computeBranches) return undefined; @@ -468,13 +468,13 @@ const visitors: Record Evaluate } return val; }, - 'ThisExpression': function ThisExpression(this: State, _node: Node, _walk: Walk) { + 'ThisExpression': async function ThisExpression(this: State, _node: Node, _walk: Walk) { if (Object.hasOwnProperty.call(this.vars, 'this')) return this.vars['this']; return undefined; }, - 'UnaryExpression': function UnaryExpression(this: State, node: Node, walk: Walk) { - const val = walk(node.argument); + 'UnaryExpression': async function UnaryExpression(this: State, node: Node, walk: Walk) { + const val = await walk(node.argument); if (!val) return undefined; if ('value' in val && 'wildcards' in val === false) { diff --git a/test/unit.test.js b/test/unit.test.js index b3cd70a0..c946320a 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -17,8 +17,9 @@ for (const { testName, isRoot } of unitTests) { console.log(`Skipping unit test on Windows: ${testSuffix}`); continue; }; + const unitPath = join(__dirname, 'unit', testName); + it(`should correctly trace ${testSuffix}`, async () => { - const unitPath = join(__dirname, 'unit', testName); // We mock readFile because when node-file-trace is integrated into @now/node // this is the hook that triggers TypeScript compilation. So if this doesn't @@ -26,9 +27,21 @@ for (const { testName, isRoot } of unitTests) { // used in the tsx-input test: const readFileMock = jest.fn(function() { const [id] = arguments; - return id.startsWith('custom-resolution-') - ? '' - : this.constructor.prototype.readFile.apply(this, arguments); + + if (id.startsWith('custom-resolution-')) { + return '' + } + + // ensure sync readFile works as expected since default is + // async now + if (testName === 'wildcard') { + try { + return fs.readFileSync(id).toString() + } catch (err) { + return null + } + } + return this.constructor.prototype.readFile.apply(this, arguments); }); let inputFileName = "input.js"; diff --git a/yarn.lock b/yarn.lock index da5eed63..fed95eca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5817,6 +5817,11 @@ estree-is-function@^1.0.0: resolved "https://registry.yarnpkg.com/estree-is-function/-/estree-is-function-1.0.0.tgz#c0adc29806d7f18a74db7df0f3b2666702e37ad2" integrity sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA== +estree-walker@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"