From c5c404107a59bf95b43dc693a5fd46673796b639 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 7 Jul 2020 21:45:24 -0700 Subject: [PATCH 01/19] Add typescript support --- package.json | 16 +- src/{analyze.js => analyze.ts} | 144 ++++++++++-------- src/{cli.js => cli.ts} | 12 +- ...{node-file-trace.js => node-file-trace.ts} | 97 +++++++----- ...ve-dependency.js => resolve-dependency.ts} | 44 +++--- node-file-trace.d.ts => src/types.ts | 20 ++- src/utils/{ast-helpers.js => ast-helpers.ts} | 8 +- ...{binary-locators.js => binary-locators.ts} | 15 +- ...et-package-base.js => get-package-base.ts} | 11 +- ...{interop-require.js => interop-require.ts} | 6 +- .../{sharedlib-emit.js => sharedlib-emit.ts} | 13 +- .../{special-cases.js => special-cases.ts} | 30 ++-- src/utils/{static-eval.js => static-eval.ts} | 46 +++--- src/utils/{wrappers.js => wrappers.ts} | 5 +- test/cli.test.js | 12 +- test/ecmascript.test.js | 10 +- test/integration.test.js | 13 +- test/integration/dogfood.js | 2 +- test/unit.test.js | 2 +- tsconfig.json | 21 +++ yarn.lock | 43 +++++- 21 files changed, 343 insertions(+), 227 deletions(-) rename src/{analyze.js => analyze.ts} (86%) rename src/{cli.js => cli.ts} (78%) rename src/{node-file-trace.js => node-file-trace.ts} (77%) rename src/{resolve-dependency.js => resolve-dependency.ts} (83%) rename node-file-trace.d.ts => src/types.ts (79%) rename src/utils/{ast-helpers.js => ast-helpers.ts} (87%) rename src/utils/{binary-locators.js => binary-locators.ts} (86%) rename src/utils/{get-package-base.js => get-package-base.ts} (84%) rename src/utils/{interop-require.js => interop-require.ts} (70%) rename src/utils/{sharedlib-emit.js => sharedlib-emit.ts} (68%) rename src/utils/{special-cases.js => special-cases.ts} (92%) rename src/utils/{static-eval.js => static-eval.ts} (92%) rename src/utils/{wrappers.js => wrappers.ts} (99%) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 5fe57495..a28e8b0b 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,20 @@ "version": "0.6.5", "repository": "vercel/node-file-trace", "license": "MIT", - "main": "./src/node-file-trace.js", - "types": "./node-file-trace.d.ts", + "main": "./dist/node-file-trace.js", + "types": "./dist/node-file-trace.d.ts", "bin": { - "nft": "./src/cli.js" + "nft": "./dist/cli.js" }, "scripts": { + "tsc": "tsc", "test": "jest --verbose", "test-verbose": "jest --verbose --coverage --globals \"{\\\"coverage\\\":true}\"", "codecov": "codecov", "test-coverage": "jest --verbose --coverage --globals \"{\\\"coverage\\\":true}\" && codecov" }, "files": [ - "node-file-trace.d.ts", - "src/*" + "dist" ], "dependencies": { "acorn": "^7.1.1", @@ -44,6 +44,10 @@ "@google-cloud/firestore": "^2.2.3", "@sentry/node": "^5.5.0", "@tensorflow/tfjs-node": "^1.2.3", + "@types/bindings": "^1.3.0", + "@types/glob": "^7.1.2", + "@types/micromatch": "^4.0.1", + "@types/node": "^14.0.14", "@zeit/ncc": "^0.17.0", "analytics-node": "^3.4.0-beta.1", "apollo-server-express": "^2.14.2", @@ -112,7 +116,7 @@ "swig": "^1.4.2", "tiny-json-http": "^7.1.2", "twilio": "^3.33.0", - "typescript": "^3.5.2", + "typescript": "^3.9.6", "uglify-js": "^3.6.0", "vm2": "^3.8.2", "vue": "^2.6.10", diff --git a/src/analyze.js b/src/analyze.ts similarity index 86% rename from src/analyze.js rename to src/analyze.ts index 04f58504..7efc59c2 100644 --- a/src/analyze.js +++ b/src/analyze.ts @@ -1,32 +1,33 @@ -const path = require('path'); -const { existsSync, statSync } = require('fs'); -const { walk } = require('estree-walker'); -const { attachScopes } = require('rollup-pluginutils'); -const evaluate = require('./utils/static-eval'); -let acorn = require('acorn'); -const bindings = require('bindings'); -const { isIdentifierRead, isLoop, isVarLoop } = require('./utils/ast-helpers'); -const glob = require('glob'); -const getPackageBase = require('./utils/get-package-base'); -const { pregyp, nbind } = require('./utils/binary-locators'); -const interopRequire = require('./utils/interop-require'); -const handleSpecialCases = require('./utils/special-cases'); -const resolve = require('./resolve-dependency.js'); -const nodeGypBuild = require('node-gyp-build'); +import path from 'path'; +import { existsSync, statSync } from 'fs'; +import { walk, WalkerContext, Node } from 'estree-walker'; +import { attachScopes } from 'rollup-pluginutils'; +import { evaluate, UNKNOWN, FUNCTION, WILDCARD, wildcardRegEx } from './utils/static-eval'; +import { Parser } from 'acorn'; +import bindings from 'bindings'; +import { isIdentifierRead, isLoop, isVarLoop } from './utils/ast-helpers'; +import glob from 'glob'; +import { getPackageBase } from './utils/get-package-base'; +import { pregyp, nbind } from './utils/binary-locators'; +import { normalizeDefaultRequire, normalizeWildcardRequire } from './utils/interop-require'; +import handleSpecialCases from './utils/special-cases'; +import resolve from './resolve-dependency.js'; +//@ts-ignore +import nodeGypBuild from 'node-gyp-build'; +import { Job } from './node-file-trace'; // Note: these should be deprecated over time as they ship in Acorn core -acorn = acorn.Parser.extend( +const acorn = Parser.extend( require("acorn-class-fields"), require("acorn-export-ns-from"), require("acorn-import-meta"), require("acorn-numeric-separator"), require("acorn-static-class-features"), ); -const os = require('os'); -const handleWrappers = require('./utils/wrappers.js'); -const resolveFrom = require('resolve-from'); - -const { UNKNOWN, FUNCTION, WILDCARD, wildcardRegEx } = evaluate; +import os from 'os'; +import { handleWrappers } from './utils/wrappers'; +import resolveFrom from 'resolve-from'; +import { SingleValue, ConditionalValue } from './types'; const staticProcess = { cwd: () => { @@ -118,13 +119,13 @@ const staticModules = Object.assign(Object.create(null), { default: PKG_INFO } }); -const globalBindings = { +const globalBindings: any = { // Support for require calls generated from `import` statements by babel - _interopRequireDefault: interopRequire.normalizeDefaultRequire, - _interopRequireWildcard: interopRequire.normalizeWildcardRequire, + _interopRequireDefault: normalizeDefaultRequire, + _interopRequireWildcard: normalizeWildcardRequire, // Support for require calls generated from `import` statements by tsc - __importDefault: interopRequire.normalizeDefaultRequire, - __importStar: interopRequire.normalizeWildcardRequire, + __importDefault: normalizeDefaultRequire, + __importStar: normalizeWildcardRequire, MONGOOSE_DRIVER_PATH: undefined }; globalBindings.global = globalBindings.GLOBAL = globalBindings.globalThis = globalBindings; @@ -148,28 +149,34 @@ Object.keys(path).forEach(name => { }); // overload path.resolve to support custom cwd -staticPath.resolve = staticPath.default.resolve = function (...args) { +staticPath.resolve = staticPath.default.resolve = function (...args: string[]) { return path.resolve.apply(this, [cwd, ...args]); }; staticPath.resolve[TRIGGER] = true; const excludeAssetExtensions = new Set(['.h', '.cmake', '.c', '.cpp']); const excludeAssetFiles = new Set(['CHANGELOG.md', 'README.md', 'readme.md', 'changelog.md']); -let cwd; +let cwd: string; const absoluteRegEx = /^\/[^\/]+|^[a-z]:[\\/][^\\/]+/i; -function isAbsolutePathStr (str) { - return typeof str === 'string' && str.match(absoluteRegEx); +function isAbsolutePathStr(str: any): str is string { + return typeof str === 'string' && absoluteRegEx.test(str); } const BOUND_REQUIRE = Symbol(); +const repeatGlobRegEx = /([\/\\]\*\*[\/\\]\*)+/g; -const repeatGlobRegEx = /([\/\\]\*\*[\/\\]\*)+/g +export interface AnalyzeResult { + assets: Set; + deps: Set; + imports: Set; + isESM: boolean; +}; -module.exports = async function (id, code, job) { - const assets = new Set(); - const deps = new Set(); - const imports = new Set(); +export default async function analyze(id: string, code: string, job: Job): Promise { + const assets = new Set(); + const deps = new Set(); + const imports = new Set(); const dir = path.dirname(id); // if (typeof options.production === 'boolean' && staticProcess.env.NODE_ENV === UNKNOWN) @@ -177,7 +184,7 @@ module.exports = async function (id, code, job) { cwd = job.cwd; const pkgBase = getPackageBase(id); - const emitAssetDirectory = (wildcardPath) => { + const emitAssetDirectory = (wildcardPath: string) => { if (!job.analysis.emitGlobs) return; const wildcardIndex = wildcardPath.indexOf(WILDCARD); const dirIndex = wildcardIndex === -1 ? wildcardPath.length : wildcardPath.lastIndexOf(path.sep, wildcardIndex); @@ -193,7 +200,7 @@ module.exports = async function (id, code, job) { assetEmissionPromises = assetEmissionPromises.then(async () => { if (job.log) console.log('Globbing ' + assetDirPath + wildcardPattern); - const files = (await new Promise((resolve, reject) => + const files = (await new Promise((resolve, reject) => glob(assetDirPath + wildcardPattern, { mark: true, ignore: assetDirPath + '/**/node_modules/**/*' }, (err, files) => err ? reject(err) : resolve(files)) )); files @@ -211,7 +218,9 @@ module.exports = async function (id, code, job) { // remove shebang code = code.replace(/^#![^\n\r]*[\r\n]/, ''); - let ast, isESM; + let ast: Node; + let isESM = false; + try { ast = acorn.parse(code, { ecmaVersion: 2020, allowReturnOutsideFunction: true }); isESM = false; @@ -253,12 +262,12 @@ module.exports = async function (id, code, job) { knownBindings.require = { shadowDepth: 0, value: { - [FUNCTION] (specifier) { + [FUNCTION] (specifier: string) { deps.add(specifier); const m = staticModules[specifier]; return m.default; }, - resolve (specifier) { + resolve (specifier: string) { return resolve(specifier, id, job); } } @@ -266,7 +275,7 @@ module.exports = async function (id, code, job) { knownBindings.require.value.resolve[TRIGGER] = true; } - function setKnownBinding (name, value) { + function setKnownBinding (name: string, value: any) { // require is somewhat special in that we shadow it but don't // statically analyze it ("known unknown" of sorts) if (name === 'require') return; @@ -275,7 +284,7 @@ module.exports = async function (id, code, job) { value: value }; } - function getKnownBinding (name) { + function getKnownBinding (name: string) { const binding = knownBindings[name]; if (binding) { if (binding.shadowDepth === 0) { @@ -283,7 +292,7 @@ module.exports = async function (id, code, job) { } } } - function hasKnownBindingValue (name) { + function hasKnownBindingValue (name: string) { const binding = knownBindings[name]; return binding && binding.shadowDepth === 0; } @@ -311,7 +320,7 @@ module.exports = async function (id, code, job) { } } - function computePureStaticValue (expr, computeBranches = true) { + function computePureStaticValue (expr: Node, computeBranches = true) { const vars = Object.create(null); Object.keys(knownBindings).forEach(name => { vars[name] = getKnownBinding(name); @@ -326,12 +335,13 @@ module.exports = async function (id, code, job) { // statically determinable leaves are tracked, and inlined when the // greatest parent statically known leaf computation corresponds to an asset path - let staticChildNode, staticChildValue; + let staticChildNode: Node | undefined; + let staticChildValue: SingleValue | ConditionalValue; // Express engine opt-out let definedExpressEngines = false; - function emitWildcardRequire (wildcardRequire) { + function emitWildcardRequire (wildcardRequire: string) { if (!job.analysis.emitGlobs || !wildcardRequire.startsWith('./') && !wildcardRequire.startsWith('../')) return; wildcardRequire = path.resolve(dir, wildcardRequire); @@ -353,7 +363,7 @@ module.exports = async function (id, code, job) { assetEmissionPromises = assetEmissionPromises.then(async () => { if (job.log) console.log('Globbing ' + wildcardDirPath + wildcardPattern); - const files = (await new Promise((resolve, reject) => + const files = (await new Promise((resolve, reject) => glob(wildcardDirPath + wildcardPattern, { mark: true, ignore: wildcardDirPath + '/**/node_modules/**/*' }, (err, files) => err ? reject(err) : resolve(files)) )); files @@ -366,7 +376,7 @@ module.exports = async function (id, code, job) { }); } - function processRequireArg (expression, isImport) { + function processRequireArg (expression: Node, isImport = false) { if (expression.type === 'ConditionalExpression') { processRequireArg(expression.consequent, isImport); processRequireArg(expression.alternate, isImport); @@ -381,16 +391,16 @@ module.exports = async function (id, code, job) { let computed = computePureStaticValue(expression, true); if (!computed) return; - if (typeof computed.value === 'string') { + if ('value' in computed && typeof computed.value === 'string') { if (!computed.wildcards) (isImport ? imports : deps).add(computed.value); else if (computed.wildcards.length >= 1) emitWildcardRequire(computed.value); } else { - if (typeof computed.then === 'string') + if ('then' in computed && typeof computed.then === 'string') (isImport ? imports : deps).add(computed.then); - if (typeof computed.else === 'string') + if ('else' in computed && typeof computed.else === 'string') (isImport ? imports : deps).add(computed.else); } } @@ -399,17 +409,17 @@ module.exports = async function (id, code, job) { handleWrappers(ast); ({ ast = ast, scope = scope } = handleSpecialCases({ id, ast, scope, emitAsset: path => assets.add(path), emitAssetDirectory, job }) || {}); - function backtrack (self, parent) { + function backtrack (self: WalkerContext, parent: Node) { // computing a static expression outward // -> compute and backtrack if (!staticChildNode) throw new Error('Internal error: No staticChildNode for backtrack.'); const curStaticValue = computePureStaticValue(parent, true); if (curStaticValue) { if ('value' in curStaticValue && typeof curStaticValue.value !== 'symbol' || - typeof curStaticValue.then !== 'symbol' && typeof curStaticValue.else !== 'symbol') { + 'then' in curStaticValue && typeof curStaticValue.then !== 'symbol' && typeof curStaticValue.else !== 'symbol') { staticChildValue = curStaticValue; staticChildNode = parent; - if (self.skip) self.skip(); + if (self && self.skip) self.skip(); return; } } @@ -477,7 +487,7 @@ module.exports = async function (id, code, job) { const calleeValue = job.analysis.evaluatePureExpressions && 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 && typeof calleeValue.value === 'function' && calleeValue.value[TRIGGER] && job.analysis.computeFileReferences) { + if (calleeValue && 'value' in calleeValue && typeof calleeValue.value === 'function' && calleeValue.value[TRIGGER] && job.analysis.computeFileReferences) { staticChildValue = computePureStaticValue(node, true); // if it computes, then we start backtracking if (staticChildValue) { @@ -486,7 +496,7 @@ module.exports = async function (id, code, job) { } } // handle well-known function symbol cases - else if (calleeValue && typeof calleeValue.value === 'symbol') { + else if (calleeValue && 'value' in calleeValue && typeof calleeValue.value === 'symbol') { switch (calleeValue.value) { // customRequireWrapper('...') case BOUND_REQUIRE: @@ -501,7 +511,7 @@ module.exports = async function (id, code, job) { case BINDINGS: if (node.arguments.length) { const arg = computePureStaticValue(node.arguments[0], false); - if (arg && arg.value) { + if (arg && 'value' in arg && arg.value) { let staticBindingsInstance = false; let opts; if (typeof arg.value === 'object') @@ -529,8 +539,7 @@ module.exports = async function (id, code, job) { case NODE_GYP_BUILD: if (node.arguments.length === 1 && node.arguments[0].type === 'Identifier' && node.arguments[0].name === '__dirname' && knownBindings.__dirname.shadowDepth === 0) { - transformed = true; - let resolved; + let resolved: string | undefined; try { resolved = nodeGypBuild.path(dir); } @@ -546,7 +555,7 @@ module.exports = async function (id, code, job) { case NBIND_INIT: if (node.arguments.length) { const arg = computePureStaticValue(node.arguments[0], false); - if (arg && arg.value) { + if (arg && 'value' in arg && !Array.isArray(arg.value)) { const bindingInfo = nbind(arg.value); if (bindingInfo) { deps.add(path.relative(dir, bindingInfo.path).replace(/\\/g, '/')); @@ -726,7 +735,7 @@ module.exports = async function (id, code, job) { await assetEmissionPromises; return { assets, deps, imports, isESM }; - function emitAssetPath (assetPath) { + 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); @@ -748,7 +757,7 @@ module.exports = async function (id, code, job) { } } - function validWildcard (assetPath) { + function validWildcard (assetPath: string) { let wildcardSuffix = ''; if (assetPath.endsWith(path.sep)) wildcardSuffix = path.sep; @@ -780,13 +789,14 @@ module.exports = async function (id, code, job) { } function emitStaticChildAsset () { - if (isAbsolutePathStr(staticChildValue.value)) { - let resolved; - try { resolved = path.resolve(staticChildValue.value); } + if ('value' in staticChildValue && isAbsolutePathStr(staticChildValue.value)) { + try { + const resolved = path.resolve(staticChildValue.value); + emitAssetPath(resolved); + } catch (e) {} - emitAssetPath(resolved); } - else if (isAbsolutePathStr(staticChildValue.then) && isAbsolutePathStr(staticChildValue.else)) { + else if ('then' in staticChildValue && 'else' in staticChildValue && isAbsolutePathStr(staticChildValue.then) && isAbsolutePathStr(staticChildValue.else)) { let resolvedThen; try { resolvedThen = path.resolve(staticChildValue.then); } catch (e) {} diff --git a/src/cli.js b/src/cli.ts similarity index 78% rename from src/cli.js rename to src/cli.ts index c3187625..cf89dc65 100755 --- a/src/cli.js +++ b/src/cli.ts @@ -1,12 +1,10 @@ #!/usr/bin/env node -const { join, dirname } = require('path'); -const fs = require('fs'); -const { promisify } = require('util'); -const copyFile = promisify(fs.copyFile); -const mkdir = promisify(fs.mkdir); +import { join, dirname } from 'path'; +import { promises } from 'fs'; +const { copyFile, mkdir } = promises; const rimraf = require('rimraf'); -const trace = require('./node-file-trace'); +import { nodeFileTrace } from './node-file-trace'; async function cli( action = process.argv[2], @@ -20,7 +18,7 @@ async function cli( log: true }; - const { fileList, esmFileList, warnings } = await trace(files, opts); + const { fileList, esmFileList, warnings } = await nodeFileTrace(files, opts); const allFiles = fileList.concat(esmFileList); const stdout = []; diff --git a/src/node-file-trace.js b/src/node-file-trace.ts similarity index 77% rename from src/node-file-trace.js rename to src/node-file-trace.ts index e13d6031..2c3bdd64 100644 --- a/src/node-file-trace.js +++ b/src/node-file-trace.ts @@ -1,18 +1,19 @@ -const { basename, dirname, extname, relative, resolve, sep } = require('path'); -const fs = require('fs'); -const analyze = require('./analyze'); -const resolveDependency = require('./resolve-dependency'); -const { isMatch } = require('micromatch'); -const sharedlibEmit = require('./utils/sharedlib-emit'); +import { NodeFileTraceOptions, Stats } from './types'; +import { basename, dirname, extname, relative, resolve, sep } from 'path'; +import fs from 'fs'; +import analyze, { AnalyzeResult } from './analyze'; +import resolveDependency from './resolve-dependency'; +import { isMatch } from 'micromatch'; +import { sharedLibEmit } from './utils/sharedlib-emit'; const { gracefulify } = require('graceful-fs'); gracefulify(fs); -function inPath (path, parent) { +function inPath (path: string, parent: string) { return path.startsWith(parent) && path[parent.length] === sep; } -module.exports = async function (files, opts = {}) { +export async function nodeFileTrace(files: string[], opts: NodeFileTraceOptions = {}) { const job = new Job(opts); if (opts.readFile) @@ -39,7 +40,27 @@ module.exports = async function (files, opts = {}) { }; }; -class Job { +export class Job { + public ts: boolean; + public base: string; + public cwd: string; + public exports: string[]; + public exportsOnly: boolean; + public paths: Record; + public ignoreFn: (path: string, parent?: string) => boolean; + public log: boolean; + public mixedModules: boolean; + public analysis: { emitGlobs?: boolean, computeFileReferences?: boolean, evaluatePureExpressions?: boolean }; + private fileCache: Map; + private statCache: Map; + private symlinkCache: Map; + private analysisCache: Map; + public fileList: Set; + public esmFileList: Set; + public processed: Set; + public warnings: Set; + public reasons = Object.create(null); + constructor ({ base = process.cwd(), processCwd, @@ -49,25 +70,28 @@ class Job { ignore, log = false, mixedModules = false, + ts = true, analysis = {}, cache, - }) { + }: NodeFileTraceOptions) { + this.ts = ts; base = resolve(base); - this.ignoreFn = path => { + this.ignoreFn = (path: string) => { if (path.startsWith('..' + sep)) return true; return false; }; if (typeof ignore === 'string') ignore = [ignore]; if (typeof ignore === 'function') { - this.ignoreFn = path => { + const ig = ignore; + this.ignoreFn = (path: string) => { if (path.startsWith('..' + sep)) return true; - if (ignore(path)) return true; + if (ig(path)) return true; return false; }; } else if (Array.isArray(ignore)) { const resolvedIgnores = ignore.map(ignore => relative(base, resolve(base || process.cwd(), ignore))); - this.ignoreFn = path => { + this.ignoreFn = (path: string) => { if (path.startsWith('..' + sep)) return true; if (isMatch(path, resolvedIgnores)) return true; return false; @@ -77,7 +101,7 @@ class Job { this.cwd = resolve(processCwd || base); this.exports = exports; this.exportsOnly = exportsOnly; - const resolvedPaths = {}; + const resolvedPaths: Record = {}; for (const path of Object.keys(paths)) { const trailer = paths[path].endsWith('/'); const resolvedPath = resolve(base, paths[path]); @@ -86,7 +110,6 @@ class Job { this.paths = resolvedPaths; this.log = log; this.mixedModules = mixedModules; - this.reasons = Object.create(null); this.analysis = {}; if (analysis !== false) { @@ -117,11 +140,10 @@ class Job { this.fileList = new Set(); this.esmFileList = new Set(); this.processed = new Set(); - this.warnings = new Set(); } - readlink (path) { + readlink (path: string) { const cached = this.symlinkCache.get(path); if (cached !== undefined) return cached; try { @@ -141,21 +163,21 @@ class Job { } } - isFile (path) { + isFile (path: string) { const stats = this.stat(path); if (stats) return stats.isFile(); return false; } - isDir (path) { + isDir (path: string) { const stats = this.stat(path); if (stats) return stats.isDirectory(); return false; } - stat (path) { + stat (path: string) { const cached = this.statCache.get(path); if (cached) return cached; try { @@ -172,7 +194,7 @@ class Job { } } - readFile (path) { + readFile (path: string): string | Buffer | null { const cached = this.fileCache.get(path); if (cached !== undefined) return cached; try { @@ -189,7 +211,7 @@ class Job { } } - realpath (path, parent, seen = new Set()) { + realpath (path: string, parent?: string, seen = new Set()): string { if (seen.has(path)) throw new Error('Recursive symlink detected resolving ' + path); seen.add(path); const symlink = this.readlink(path); @@ -208,7 +230,7 @@ class Job { return this.realpath(dirname(path), parent, seen) + sep + basename(path); } - emitFile (path, reason, parent) { + emitFile (path: string, reason: string, parent?: string) { if (this.fileList.has(path)) return; path = relative(this.base, path); if (parent) @@ -228,24 +250,25 @@ class Job { return true; } - getPjsonBoundary (path) { + getPjsonBoundary (path: string) { const rootSeparatorIndex = path.indexOf(sep); - let separatorIndex; + let separatorIndex: number; while ((separatorIndex = path.lastIndexOf(sep)) > rootSeparatorIndex) { path = path.substr(0, separatorIndex); if (this.isFile(path + sep + 'package.json')) return path; } + return undefined; } - async emitDependency (path, parent) { + async emitDependency (path: string, parent: string) { if (this.processed.has(path)) return; this.processed.add(path); const emitted = this.emitFile(path, 'dependency', parent); if (!emitted) return; if (path.endsWith('.json')) return; - if (path.endsWith('.node')) return await sharedlibEmit(path, this); + 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')) { @@ -254,24 +277,24 @@ class Job { this.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', path); } - let deps, imports, assets, isESM; + let analyzeResult: AnalyzeResult; const cachedAnalysis = this.analysisCache.get(path); if (cachedAnalysis) { - ({ deps, imports, assets, isESM } = cachedAnalysis); + analyzeResult = cachedAnalysis; } else { const source = this.readFile(path); if (source === null) throw new Error('File ' + path + ' does not exist.'); - ({ deps, imports, assets, isESM } = await analyze(path, source, this)); - this.analysisCache.set(path, { deps, imports, assets, isESM }); + analyzeResult = await analyze(path, source.toString(), this); + this.analysisCache.set(path, analyzeResult); } - if (isESM) + if (analyzeResult.isESM) this.esmFileList.add(relative(this.base, path)); await Promise.all([ - ...[...assets].map(async asset => { + ...[...analyzeResult.assets].map(async asset => { const ext = extname(asset); if (ext === '.js' || ext === '.mjs' || ext === '.node' || ext === '' || this.ts && (ext === '.ts' || ext === '.tsx') && asset.startsWith(this.base) && asset.substr(this.base.length).indexOf(sep + 'node_modules' + sep) === -1) @@ -279,9 +302,9 @@ class Job { else this.emitFile(this.realpath(asset, path), 'asset', path); }), - ...[...deps].map(async dep => { + ...[...analyzeResult.deps].map(async dep => { try { - var resolved = await resolveDependency(dep, path, this, !isESM); + var resolved = await resolveDependency(dep, path, this, !analyzeResult.isESM); } catch (e) { this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`)); @@ -300,7 +323,7 @@ class Job { await this.emitDependency(resolved, path); } }), - ...[...imports].map(async dep => { + ...[...analyzeResult.imports].map(async dep => { try { var resolved = await resolveDependency(dep, path, this, false); } diff --git a/src/resolve-dependency.js b/src/resolve-dependency.ts similarity index 83% rename from src/resolve-dependency.js rename to src/resolve-dependency.ts index f55aa3d3..556661e9 100644 --- a/src/resolve-dependency.js +++ b/src/resolve-dependency.ts @@ -1,28 +1,34 @@ -const { isAbsolute, resolve, sep } = require('path'); +import { isAbsolute, resolve, sep } from 'path'; +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) -module.exports = function resolveDependency (specifier, parent, job, cjsResolve = true) { - let resolved; +export default function resolveDependency (specifier: string, parent: string, job: Job, cjsResolve = true) { + let resolved: string | string[] | undefined; if (isAbsolute(specifier) || specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../')) { const trailingSlash = specifier.endsWith('/'); resolved = resolvePath(resolve(parent, '..', specifier) + (trailingSlash ? '/' : ''), parent, job); - } - else { + } else { resolved = resolvePackage(specifier, parent, job, cjsResolve); } - if (typeof resolved === 'string' && resolved.startsWith('node:')) return resolved; - if (Array.isArray(resolved)) + if (typeof resolved === 'string' && !resolved.startsWith('node:')) { + return job.realpath(resolved, parent); + } else if (Array.isArray(resolved)) { return resolved.map(resolved => job.realpath(resolved, parent)); - return job.realpath(resolved, parent); + } + return resolved; }; -function resolvePath (path, parent, job) { - return resolveFile(path, parent, job) || resolveDir(path, parent, job) || notFound(path, parent); +function resolvePath (path: string, parent: string, job: Job): string | undefined { + const result = resolveFile(path, parent, job) || resolveDir(path, parent, job); + if (!result) { + notFound(path, parent); + } + return result; } -function resolveFile (path, parent, job) { +function resolveFile (path: string, parent: string, job: Job) { if (path.endsWith('/')) return; path = job.realpath(path, parent); if (job.isFile(path)) return path; @@ -33,7 +39,7 @@ function resolveFile (path, parent, job) { if (job.isFile(path + '.node')) return path + '.node'; } -function resolveDir (path, parent, job) { +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); @@ -47,22 +53,22 @@ function resolveDir (path, parent, job) { return resolveFile(resolve(path, 'index'), parent, job); } -function notFound (specifier, parent) { +function notFound (specifier: string, parent: string) { const e = new Error("Cannot find module '" + specifier + "' loaded from " + parent); - e.code = 'MODULE_NOT_FOUND'; + (e as any).code = 'MODULE_NOT_FOUND'; throw e; } const nodeBuiltins = new Set([...require("repl")._builtinLibs, "constants", "module", "timers", "console", "_stream_writable", "_stream_readable", "_stream_duplex", "process", "sys"]); -function getPkgName (name) { +function getPkgName (name: string) { const segments = name.split('/'); if (name[0] === '@' && segments.length > 1) return segments.length > 1 ? segments.slice(0, 2).join('/') : null; return segments.length ? segments[0] : null; } -function getPkgCfg (pkgPath, job) { +function getPkgCfg (pkgPath: string, job: Job) { const pjsonSource = job.readFile(pkgPath + sep + 'package.json'); if (pjsonSource) { try { @@ -72,7 +78,7 @@ function getPkgCfg (pkgPath, job) { } } -function getExportsTarget (exports, conditions, cjsResolve) { +function getExportsTarget(exports: string | string[] | Record | null, conditions: string[], cjsResolve: boolean): string | null { if (typeof exports === 'string') { return exports; } @@ -100,7 +106,7 @@ function getExportsTarget (exports, conditions, cjsResolve) { } } -function resolveExportsTarget (pkgPath, exports, subpath, job, cjsResolve) { +function resolveExportsTarget (pkgPath, exports, subpath: string, job: Job, cjsResolve: boolean) { if (typeof exports === 'string' || typeof exports === 'object' && !Array.isArray(exports) && Object.keys(exports).length && Object.keys(exports)[0][0] !== '.') exports = { '.' : exports }; @@ -120,7 +126,7 @@ function resolveExportsTarget (pkgPath, exports, subpath, job, cjsResolve) { } } -function resolvePackage (name, parent, job, cjsResolve) { +function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean) { let packageParent = parent; if (nodeBuiltins.has(name)) return 'node:' + name; diff --git a/node-file-trace.d.ts b/src/types.ts similarity index 79% rename from node-file-trace.d.ts rename to src/types.ts index 36096d3f..b264fbf3 100644 --- a/node-file-trace.d.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -interface Stats { +export interface Stats { isFile(): boolean; isDirectory(): boolean; isBlockDevice(): boolean; @@ -29,6 +29,8 @@ interface Stats { export interface NodeFileTraceOptions { base?: string; processCwd?: string; + exports?: string[]; + exportsOnly?: boolean; ignore?: string | string[] | ((path: string) => boolean); analysis?: boolean | { emitGlobs?: boolean; @@ -60,7 +62,15 @@ export interface NodeFileTraceResult { warnings: Error[]; } -export default function NodeFileTrace( - files: string[], - opts: NodeFileTraceOptions -): Promise; +export interface SingleValue { + value: string | string[] | undefined; + wildcards?: string[]; +} + +export interface ConditionalValue { + test: string; + then: string; + else: string; +} + +export type StaticResult = SingleValue | ConditionalValue | undefined; diff --git a/src/utils/ast-helpers.js b/src/utils/ast-helpers.ts similarity index 87% rename from src/utils/ast-helpers.js rename to src/utils/ast-helpers.ts index c4ca428c..074929c6 100644 --- a/src/utils/ast-helpers.js +++ b/src/utils/ast-helpers.ts @@ -1,4 +1,6 @@ -exports.isIdentifierRead = function (node, parent) { +import { Node } from 'estree-walker'; + +export function isIdentifierRead(node: Node, parent: Node) { switch (parent.type) { case 'ObjectPattern': case 'ArrayPattern': @@ -31,10 +33,10 @@ exports.isIdentifierRead = function (node, parent) { } } -exports.isVarLoop = function (node) { +export function isVarLoop(node: Node) { return node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement'; } -exports.isLoop = function (node) { +export function isLoop(node: Node) { return node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement' || node.type === 'WhileStatement' || node.type === 'DoWhileStatement'; } \ No newline at end of file diff --git a/src/utils/binary-locators.js b/src/utils/binary-locators.ts similarity index 86% rename from src/utils/binary-locators.js rename to src/utils/binary-locators.ts index b4ca979b..dac6f9c6 100644 --- a/src/utils/binary-locators.js +++ b/src/utils/binary-locators.ts @@ -1,10 +1,10 @@ -const path = require("path"); -const fs = require("fs"); +import path from 'path'; +import fs from 'fs'; // pregyp const versioning = require("node-pre-gyp/lib/util/versioning.js"); const napi = require("node-pre-gyp/lib/util/napi.js"); -const pregypFind = (package_json_path, opts) => { +const pregypFind = (package_json_path: string, opts: any) => { const package_json = JSON.parse(fs.readFileSync(package_json_path).toString()); versioning.validate_config(package_json, opts); var napi_build_version; @@ -16,11 +16,11 @@ const pregypFind = (package_json_path, opts) => { var meta = versioning.evaluate(package_json,opts,napi_build_version); return meta.module; }; -exports.pregyp = { default: { find: pregypFind }, find: pregypFind }; +export const pregyp = { default: { find: pregypFind }, find: pregypFind }; // nbind // Adapted from nbind.js -function makeModulePathList(root, name) { +function makeModulePathList(root: string, name: string) { return ([ [root, name], [root, "build", name], @@ -41,7 +41,7 @@ function makeModulePathList(root, name) { ] ]); } -function findCompiledModule(basePath, specList) { +function findCompiledModule(basePath: string, specList) { var resolvedList = []; var ext = path.extname(basePath); for (var _i = 0, specList_1 = specList; _i < specList_1.length; _i++) { @@ -73,12 +73,11 @@ function findCompiledModule(basePath, specList) { } return null; } -function find(basePath = process.cwd()) { +export function nbind(basePath = process.cwd()) { const found = findCompiledModule(basePath, [ { ext: ".node", name: "nbind.node", type: "node" }, { ext: ".js", name: "nbind.js", type: "emcc" } ]); return found; } -exports.nbind = find; diff --git a/src/utils/get-package-base.js b/src/utils/get-package-base.ts similarity index 84% rename from src/utils/get-package-base.js rename to src/utils/get-package-base.ts index 6b4325fd..26e4a6c2 100644 --- a/src/utils/get-package-base.js +++ b/src/utils/get-package-base.ts @@ -1,7 +1,8 @@ // returns the base-level package folder based on detecting "node_modules" // package name boundaries const pkgNameRegEx = /^(@[^\\\/]+[\\\/])?[^\\\/]+/; -module.exports = function (id) { + +export function getPackageBase(id: string): string | undefined { const pkgIndex = id.lastIndexOf('node_modules'); if (pkgIndex !== -1 && (id[pkgIndex - 1] === '/' || id[pkgIndex - 1] === '\\') && @@ -10,9 +11,10 @@ module.exports = function (id) { if (pkgNameMatch) return id.substr(0, pkgIndex + 13 + pkgNameMatch[0].length); } -}; + return undefined; +} -module.exports.getPackageName = function (id) { +export function getPackageName(id: string): string | undefined { const pkgIndex = id.lastIndexOf('node_modules'); if (pkgIndex !== -1 && (id[pkgIndex - 1] === '/' || id[pkgIndex - 1] === '\\') && @@ -22,6 +24,5 @@ module.exports.getPackageName = function (id) { return pkgNameMatch[0].replace(/\\/g, '/'); } } + return undefined; }; - -module.exports.pkgNameRegEx = pkgNameRegEx; \ No newline at end of file diff --git a/src/utils/interop-require.js b/src/utils/interop-require.ts similarity index 70% rename from src/utils/interop-require.js rename to src/utils/interop-require.ts index 19794b83..9d5d65b4 100644 --- a/src/utils/interop-require.js +++ b/src/utils/interop-require.ts @@ -1,13 +1,12 @@ 'use strict'; -function normalizeDefaultRequire(obj) { +export function normalizeDefaultRequire(obj: any) { if (obj && obj.__esModule) return obj; return { default: obj }; } -exports.normalizeDefaultRequire = normalizeDefaultRequire; const hasOwnProperty = Object.prototype.hasOwnProperty; -function normalizeWildcardRequire(obj) { +export function normalizeWildcardRequire(obj: any) { if (obj && obj.__esModule) return obj; // Note: This implements only value properties and doesn't preserve getters. // This follows the simpler helpers generated by TypeScript. @@ -19,4 +18,3 @@ function normalizeWildcardRequire(obj) { out['default'] = obj; return out; } -exports.normalizeWildcardRequire = normalizeWildcardRequire; diff --git a/src/utils/sharedlib-emit.js b/src/utils/sharedlib-emit.ts similarity index 68% rename from src/utils/sharedlib-emit.js rename to src/utils/sharedlib-emit.ts index 137a9f8a..bab810d6 100644 --- a/src/utils/sharedlib-emit.js +++ b/src/utils/sharedlib-emit.ts @@ -1,8 +1,9 @@ -const os = require('os'); -const glob = require('glob'); -const getPackageBase = require('./get-package-base'); +import os from 'os'; +import glob from 'glob'; +import { getPackageBase } from './get-package-base'; +import { Job } from '../node-file-trace'; -let sharedlibGlob; +let sharedlibGlob = ''; switch (os.platform()) { case 'darwin': sharedlibGlob = '/**/*.@(dylib|so?(.*))'; @@ -15,13 +16,13 @@ switch (os.platform()) { } // helper for emitting the associated shared libraries when a binary is emitted -module.exports = async function (path, job) { +export async function sharedLibEmit(path: string, job: Job) { // console.log('Emitting shared libs for ' + path); const pkgPath = getPackageBase(path); if (!pkgPath) return; - const files = await new Promise((resolve, reject) => + 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(job.realpath(file, path), 'sharedlib', path)); diff --git a/src/utils/special-cases.js b/src/utils/special-cases.ts similarity index 92% rename from src/utils/special-cases.js rename to src/utils/special-cases.ts index ab1885cd..4292b993 100644 --- a/src/utils/special-cases.js +++ b/src/utils/special-cases.ts @@ -1,9 +1,11 @@ -const path = require('path'); -const resolve = require('../resolve-dependency'); -const { getPackageName } = require('./get-package-base'); -const fs = require('fs'); +import path from 'path'; +import resolve from '../resolve-dependency'; +import { getPackageName } from './get-package-base'; +import fs from 'fs'; +import { Job } from '../node-file-trace'; +import { Node } from 'estree-walker'; -const specialCases = { +const specialCases: Record void> = { '@generated/photon' ({ id, emitAssetDirectory }) { if (id.endsWith('@generated/photon/index.js')) { emitAssetDirectory(path.resolve(path.dirname(id), 'runtime/')); @@ -77,9 +79,9 @@ const specialCases = { emitAsset(path.resolve(id.replace('index.js', 'preload.js'))); } }, - 'socket.io' ({ id, ast }) { + 'socket.io' ({ id, ast, job }) { if (id.endsWith('socket.io/lib/index.js')) { - function replaceResolvePathStatement (statement) { + function replaceResolvePathStatement (statement: Node) { if (statement.type === 'ExpressionStatement' && statement.expression.type === 'AssignmentExpression' && statement.expression.operator === '=' && @@ -177,9 +179,17 @@ const specialCases = { } }; -module.exports = function ({ id, ast, emitAsset, emitAssetDirectory, job }) { - const pkgName = getPackageName(id); - const specialCase = specialCases[pkgName]; +interface SpecialCaseOpts { + id: string; + ast: Node; + emitAsset: (filename: string) => void; + emitAssetDirectory: (dirname: string) => void; + job: Job; +} + +export default function specialCase({ 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 }); }; diff --git a/src/utils/static-eval.js b/src/utils/static-eval.ts similarity index 92% rename from src/utils/static-eval.js rename to src/utils/static-eval.ts index 9dd3661a..79d7213e 100644 --- a/src/utils/static-eval.js +++ b/src/utils/static-eval.ts @@ -1,4 +1,8 @@ -module.exports = function (ast, vars = {}, computeBranches = true) { +import { Node } from 'estree-walker'; +import { StaticResult, SingleValue } from '../types'; +type Walk = (node: Node) => StaticResult; + +export function evaluate(ast: Node, vars = {}, computeBranches = true): StaticResult { const state = { computeBranches, vars @@ -9,19 +13,19 @@ module.exports = function (ast, vars = {}, computeBranches = true) { // 1. Single known value: { value: value } // 2. Conditional value: { test, then, else } // 3. Unknown value: undefined - function walk (node) { + function walk (node: Node) { const visitor = visitors[node.type]; if (visitor) return visitor.call(state, node, walk); } }; -const UNKNOWN = module.exports.UNKNOWN = Symbol(); -const FUNCTION = module.exports.FUNCTION = Symbol(); -const WILDCARD = module.exports.WILDCARD = '\x1a'; -const wildcardRegEx = module.exports.wildcardRegEx = /\x1a/g; +export const UNKNOWN = Symbol(); +export const FUNCTION = Symbol(); +export const WILDCARD = '\x1a'; +export const wildcardRegEx = /\x1a/g; -function countWildcards (str) { +function countWildcards (str: string) { wildcardRegEx.lastIndex = 0; let cnt = 0; while (wildcardRegEx.exec(str)) cnt++; @@ -29,7 +33,7 @@ function countWildcards (str) { } const visitors = { - ArrayExpression (node, walk) { + ArrayExpression (node: Node, walk: Walk) { const arr = []; for (let i = 0, l = node.elements.length; i < l; i++) { if (node.elements[i] === null) { @@ -39,11 +43,11 @@ const visitors = { const x = walk(node.elements[i]); if (!x) return; if ('value' in x === false) return; - arr.push(x.value); + arr.push((x as SingleValue).value); } return { value: arr }; }, - BinaryExpression (node, walk) { + BinaryExpression (node: Node, walk: Walk) { const op = node.operator; let l = walk(node.left); @@ -124,7 +128,7 @@ const visitors = { if (op === '!=') return { value: l.value != r.value }; if (op === '!==') return { value: l.value !== r.value }; if (op === '+') { - const val = { value: l.value + r.value }; + const val: SingleValue = { value: l.value + r.value }; if (l.wildcards || r.wildcards) val.wildcards = [...l.wildcards || [], ...r.wildcards || []]; return val; @@ -145,7 +149,7 @@ const visitors = { } return; }, - CallExpression (node, walk) { + CallExpression (node: Node, walk: Walk) { const callee = walk(node.callee); if (!callee || 'test' in callee) return; let fn = callee.value; @@ -168,7 +172,7 @@ const visitors = { let x = walk(node.arguments[i]); if (x) { allWildcards = false; - if (typeof x.value === 'string' && x.wildcards) + if ('value' in x && typeof x.value === 'string' && x.wildcards) x.wildcards.forEach(w => wildcards.push(w)); } else { @@ -216,7 +220,7 @@ const visitors = { return; } }, - ConditionalExpression (node, walk) { + ConditionalExpression (node: Node, walk: Walk) { const val = walk(node.test); if (val && 'value' in val) return val.value ? walk(node.consequent) : walk(node.alternate); @@ -237,10 +241,10 @@ const visitors = { else: elseValue.value }; }, - ExpressionStatement (node, walk) { + ExpressionStatement (node: Node, walk: Walk) { return walk(node.expression); }, - Identifier (node) { + Identifier (node: Node, _walk: Walk) { if (Object.hasOwnProperty.call(this.vars, node.name)) { const val = this.vars[node.name]; if (val === UNKNOWN) @@ -249,10 +253,10 @@ const visitors = { } return; }, - Literal (node) { + Literal (node: Node, _walk: Walk) { return { value: node.value }; }, - MemberExpression (node, walk) { + MemberExpression (node: Node, walk: Walk) { const obj = walk(node.object); // do not allow access to methods on Function if (!obj || 'test' in obj || typeof obj.value === 'function') @@ -303,7 +307,7 @@ const visitors = { return { value: undefined }; } }, - ObjectExpression (node, walk) { + ObjectExpression (node: Node, walk: Walk) { const obj = {}; for (let i = 0; i < node.properties.length; i++) { const prop = node.properties[i]; @@ -316,7 +320,7 @@ const visitors = { } return { value: obj }; }, - TemplateLiteral (node, walk) { + TemplateLiteral (node: Node, walk: Walk) { let val = { value: '' }; for (var i = 0; i < node.expressions.length; i++) { if ('value' in val) { @@ -369,7 +373,7 @@ const visitors = { if (Object.hasOwnProperty.call(this.vars, 'this')) return { value: this.vars['this'] }; }, - UnaryExpression (node, walk) { + UnaryExpression (node: Node, walk: Walk) { const val = walk(node.argument); if (!val) return; diff --git a/src/utils/wrappers.js b/src/utils/wrappers.ts similarity index 99% rename from src/utils/wrappers.js rename to src/utils/wrappers.ts index 3f1bcd6b..8eba7084 100644 --- a/src/utils/wrappers.js +++ b/src/utils/wrappers.ts @@ -1,7 +1,7 @@ -const { walk } = require('estree-walker'); +import { walk, Node } from 'estree-walker'; // Wrapper detection pretransforms to enable static analysis -function handleWrappers (ast) { +export function handleWrappers(ast: Node) { // UglifyJS will convert function wrappers into !function(){} let wrapper; if (ast.body.length === 1 && @@ -448,4 +448,3 @@ function handleWrappers (ast) { } } -module.exports = handleWrappers; diff --git a/test/cli.test.js b/test/cli.test.js index 301876c3..36279249 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -16,7 +16,7 @@ function normalizeOutput(output) { } it('should correctly print trace from cli', async () => { - const { stderr, stdout } = await exec(`node ../src/cli.js print ${inputjs}`, { cwd: __dirname }); + const { stderr, stdout } = await exec(`node ../dist/cli.js print ${inputjs}`, { cwd: __dirname }); if (stderr) { throw new Error(stderr); } @@ -24,7 +24,7 @@ it('should correctly print trace from cli', async () => { }); it('should correctly build dist from cli', async () => { - const { stderr } = await exec(`node ../src/cli.js build ${inputjs}`, { cwd: __dirname }); + const { stderr } = await exec(`node ../dist/cli.js build ${inputjs}`, { cwd: __dirname }); if (stderr) { throw new Error(stderr); } @@ -33,7 +33,7 @@ it('should correctly build dist from cli', async () => { }); it('should correctly print help when unknown action is used', async () => { - const { stderr, stdout } = await exec(`node ../src/cli.js unknown ${inputjs}`, { cwd: __dirname }); + const { stderr, stdout } = await exec(`node ../dist/cli.js unknown ${inputjs}`, { cwd: __dirname }); if (stderr) { throw new Error(stderr); } @@ -42,7 +42,7 @@ it('should correctly print help when unknown action is used', async () => { it('[codecov] should correctly print trace from required cli', async () => { // This test is only here to satisfy code coverage - const cli = require('../src/cli.js') + const cli = require('../dist/cli.js') const files = [join(__dirname, inputjs)]; const stdout = await cli('print', files); expect(stdout).toMatch(normalizeOutput(outputjs)); @@ -50,7 +50,7 @@ it('[codecov] should correctly print trace from required cli', async () => { it('[codecov] should correctly build dist from required cli', async () => { // This test is only here to satisfy code coverage - const cli = require('../src/cli.js') + const cli = require('../dist/cli.js') const files = [join(__dirname, inputjs)]; await cli('build', files); const found = existsSync(join(__dirname, outputjs)); @@ -59,7 +59,7 @@ it('[codecov] should correctly build dist from required cli', async () => { it('[codecov] should correctly print help when unknown action is used', async () => { // This test is only here to satisfy code coverage - const cli = require('../src/cli.js') + const cli = require('../dist/cli.js') const files = [join(__dirname, inputjs)]; const stdout = await cli('unknown', files); expect(stdout).toMatch('provide an action'); diff --git a/test/ecmascript.test.js b/test/ecmascript.test.js index 95387b05..30b0f61a 100644 --- a/test/ecmascript.test.js +++ b/test/ecmascript.test.js @@ -1,16 +1,14 @@ -const fs = require('fs'); +const { promises, mkdirSync } = require('fs'); const path = require('path'); -const nodeFileTrace = require('../src/node-file-trace'); +const { nodeFileTrace } = require('../dist/node-file-trace'); const os = require('os'); -const { promisify } = require('util'); const rimraf = require('rimraf'); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); +const { writeFile } = promises; const randomTmpId = Math.random().toString().slice(2); const tmpdir = path.resolve(os.tmpdir(), `node-file-trace-ecmascript${randomTmpId}`); rimraf.sync(tmpdir); -fs.mkdirSync(tmpdir); +mkdirSync(tmpdir); console.log('created directory ' + tmpdir); // These are tests known to fail so we skip them diff --git a/test/integration.test.js b/test/integration.test.js index a1b2c1af..c88bbe25 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,21 +1,18 @@ -const fs = require('fs'); +const { promises, readdirSync, mkdirSync } = require('fs'); const path = require('path'); -const nodeFileTrace = require('../src/node-file-trace'); +const { nodeFileTrace } = require('../dist/node-file-trace'); const os = require('os'); const { promisify } = require('util'); const rimraf = require('rimraf'); const mkdirp = promisify(require('mkdirp')); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); -const readlink = promisify(fs.readlink); -const symlink = promisify(fs.symlink); +const { readFile, writeFile, readlink, symlink } = promises; const { fork } = require('child_process'); jest.setTimeout(200000); const integrationDir = `${__dirname}${path.sep}integration`; -for (const integrationTest of fs.readdirSync(integrationDir)) { +for (const integrationTest of readdirSync(integrationDir)) { it(`should correctly trace and correctly execute ${integrationTest}`, async () => { console.log('Tracing and executing ' + integrationTest); const fails = integrationTest.endsWith('failure.js'); @@ -30,7 +27,7 @@ for (const integrationTest of fs.readdirSync(integrationDir)) { const randomTmpId = Math.random().toString().slice(2) const tmpdir = path.resolve(os.tmpdir(), `node-file-trace-${randomTmpId}`); rimraf.sync(tmpdir); - fs.mkdirSync(tmpdir); + mkdirSync(tmpdir); await Promise.all(fileList.map(async file => { const inPath = path.resolve(__dirname, '..', file); const outPath = path.resolve(tmpdir, file); diff --git a/test/integration/dogfood.js b/test/integration/dogfood.js index 814e7daf..2c6dd2f7 100644 --- a/test/integration/dogfood.js +++ b/test/integration/dogfood.js @@ -1 +1 @@ -require('../../src/node-file-trace'); +require('../../dist/node-file-trace'); diff --git a/test/unit.test.js b/test/unit.test.js index 15fc9bc8..962830b0 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -1,6 +1,6 @@ const fs = require('fs'); const { join } = require('path'); -const nodeFileTrace = require('../src/node-file-trace'); +const { nodeFileTrace } = require('../dist/node-file-trace'); global._unit = true; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..60f82541 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "lib": ["esnext"], + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitThis": true, + "outDir": "dist", + "target": "esnext", + "types": ["node"], + "strict": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "test/**/*"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 48e80c4a..0135d0bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1255,6 +1255,11 @@ dependencies: "@types/babel-types" "*" +"@types/bindings@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/bindings/-/bindings-1.3.0.tgz#e9cd75a96d7abc1ecba0dc7eecb09a9f96cd417c" + integrity sha512-mTWOE6wC64MoEpv33otJNpQob81l5Pi+NsUkdiiP8EkESraQM94zuus/2s/Vz2Idy1qQkctNINYDZ61nfG1ngQ== + "@types/body-parser@*", "@types/body-parser@1.19.0": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -1263,6 +1268,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/braces@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb" + integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw== + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -1329,6 +1339,14 @@ dependencies: "@types/node" "*" +"@types/glob@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" + integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/graphql-upload@^8.0.0": version "8.0.3" resolved "https://registry.yarnpkg.com/@types/graphql-upload/-/graphql-upload-8.0.3.tgz#b371edb5f305a2a1f7b7843a890a2a7adc55c3ec" @@ -1379,11 +1397,23 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== +"@types/micromatch@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7" + integrity sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw== + dependencies: + "@types/braces" "*" + "@types/mime@*": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + "@types/node-fetch@2.5.7": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" @@ -1409,6 +1439,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.12.tgz#0eec3155a46e6c4db1f27c3e588a205f767d622f" integrity sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg== +"@types/node@^14.0.14": + version "14.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" + integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== + "@types/node@^8.0.53", "@types/node@^8.0.7": version "8.10.50" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.50.tgz#f3d68482b1f54b5f4fba8daaac385db12bb6a706" @@ -13555,10 +13590,10 @@ typeface-oswald@0.0.54: resolved "https://registry.yarnpkg.com/typeface-oswald/-/typeface-oswald-0.0.54.tgz#1e253011622cdd50f580c04e7d625e7f449763d7" integrity sha512-U1WMNp4qfy4/3khIfHMVAIKnNu941MXUfs3+H9R8PFgnoz42Hh9pboSFztWr86zut0eXC8byalmVhfkiKON/8Q== -typescript@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.2.tgz#a09e1dc69bc9551cadf17dba10ee42cf55e5d56c" - integrity sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA== +typescript@^3.9.6: + version "3.9.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" + integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== uglify-es@^3.3.9: version "3.3.9" From 076a20f4ef1fa201d7c78e2044f16cf11137498f Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 7 Jul 2020 22:59:16 -0700 Subject: [PATCH 02/19] Fix special cases and static eval state --- src/analyze.ts | 20 +++-- src/utils/special-cases.ts | 78 +++++++++++--------- src/utils/static-eval.ts | 145 +++++++++++++++++++++---------------- src/utils/wrappers.ts | 2 +- 4 files changed, 136 insertions(+), 109 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index 7efc59c2..3377a172 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -27,7 +27,7 @@ const acorn = Parser.extend( import os from 'os'; import { handleWrappers } from './utils/wrappers'; import resolveFrom from 'resolve-from'; -import { SingleValue, ConditionalValue } from './types'; +import { StaticResult } from './types'; const staticProcess = { cwd: () => { @@ -132,13 +132,13 @@ globalBindings.global = globalBindings.GLOBAL = globalBindings.globalThis = glob // call expression triggers const TRIGGER = Symbol(); -pregyp.find[TRIGGER] = true; +(pregyp.find as any)[TRIGGER] = true; const staticPath = staticModules.path; Object.keys(path).forEach(name => { - const pathFn = path[name]; + const pathFn = (path as any)[name]; if (typeof pathFn === 'function') { - const fn = function () { - return pathFn.apply(this, arguments); + const fn: any = function mockPath() { + return pathFn.apply(mockPath, arguments); }; fn[TRIGGER] = true; staticPath[name] = staticPath.default[name] = fn; @@ -231,6 +231,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi job.warnings.add(new Error(`Failed to parse ${id} as script:\n${e && e.message}`)); } } + //@ts-ignore if (!ast) { try { ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' }); @@ -336,7 +337,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // statically determinable leaves are tracked, and inlined when the // greatest parent statically known leaf computation corresponds to an asset path let staticChildNode: Node | undefined; - let staticChildValue: SingleValue | ConditionalValue; + let staticChildValue: StaticResult; // Express engine opt-out let definedExpressEngines = false; @@ -407,8 +408,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi let scope = attachScopes(ast, 'scope'); handleWrappers(ast); - ({ ast = ast, scope = scope } = handleSpecialCases({ id, ast, scope, emitAsset: path => assets.add(path), emitAssetDirectory, job }) || {}); - + handleSpecialCases({ id, ast, emitAsset: path => assets.add(path), emitAssetDirectory, job }); function backtrack (self: WalkerContext, parent: Node) { // computing a static expression outward // -> compute and backtrack @@ -789,6 +789,10 @@ export default async function analyze(id: string, code: string, job: Job): Promi } function emitStaticChildAsset () { + if (!staticChildValue) { + return; + } + if ('value' in staticChildValue && isAbsolutePathStr(staticChildValue.value)) { try { const resolved = path.resolve(staticChildValue.value); diff --git a/src/utils/special-cases.ts b/src/utils/special-cases.ts index 4292b993..eb06b78c 100644 --- a/src/utils/special-cases.ts +++ b/src/utils/special-cases.ts @@ -1,37 +1,37 @@ -import path from 'path'; -import resolve from '../resolve-dependency'; +import { resolve, dirname, relative } from 'path'; +import resolveDependency from '../resolve-dependency'; import { getPackageName } from './get-package-base'; -import fs from 'fs'; +import { readFileSync } from 'fs'; import { Job } from '../node-file-trace'; import { Node } from 'estree-walker'; const specialCases: Record void> = { '@generated/photon' ({ id, emitAssetDirectory }) { if (id.endsWith('@generated/photon/index.js')) { - emitAssetDirectory(path.resolve(path.dirname(id), 'runtime/')); + emitAssetDirectory(resolve(dirname(id), 'runtime/')); } }, 'argon2' ({ id, emitAssetDirectory }) { if (id.endsWith('argon2/argon2.js')) { - emitAssetDirectory(path.resolve(path.dirname(id), 'build', 'Release')); - emitAssetDirectory(path.resolve(path.dirname(id), 'prebuilds')); + emitAssetDirectory(resolve(dirname(id), 'build', 'Release')); + emitAssetDirectory(resolve(dirname(id), 'prebuilds')); } }, 'bull' ({ id, emitAssetDirectory }) { if (id.endsWith('bull/lib/commands/index.js')) { - emitAssetDirectory(path.resolve(path.dirname(id))); + emitAssetDirectory(resolve(dirname(id))); } }, 'google-gax' ({ id, ast, emitAssetDirectory }) { if (id.endsWith('google-gax/build/src/grpc.js')) { // const googleProtoFilesDir = path.normalize(google_proto_files_1.getProtoPath('..')); // -> - // const googleProtoFilesDir = path.resolve(__dirname, '../../../google-proto-files'); + // const googleProtoFilesDir = resolve(__dirname, '../../../google-proto-files'); for (const statement of ast.body) { if (statement.type === 'VariableDeclaration' && statement.declarations[0].id.type === 'Identifier' && statement.declarations[0].id.name === 'googleProtoFilesDir') { - emitAssetDirectory(path.resolve(path.dirname(id), '../../../google-proto-files')); + emitAssetDirectory(resolve(dirname(id), '../../../google-proto-files')); } } } @@ -60,23 +60,23 @@ const specialCases: Record void> = { statement.body.body[0].block.body[0].expression.right.arguments[0].property.type === 'Identifier' && statement.body.body[0].block.body[0].expression.right.arguments[0].property.name === 'i') { statement.body.body[0].block.body[0].expression.right.arguments = [{ type: 'Literal', value: '_' }]; - const version = global._unit ? '3.0.0' : JSON.parse(fs.readFileSync(id.slice(0, -15) + 'package.json')).version; + const version = (global as any)._unit ? '3.0.0' : JSON.parse(readFileSync(id.slice(0, -15) + 'package.json', 'utf8')).version; const useVersion = Number(version.slice(0, version.indexOf('.'))) >= 4; const binaryName = 'oracledb-' + (useVersion ? version : 'abi' + process.versions.modules) + '-' + process.platform + '-' + process.arch + '.node'; - emitAsset(path.resolve(id, '../../build/Release/' + binaryName)); + emitAsset(resolve(id, '../../build/Release/' + binaryName)); } } } }, 'phantomjs-prebuilt' ({ id, emitAssetDirectory }) { if (id.endsWith('phantomjs-prebuilt/lib/phantomjs.js')) { - emitAssetDirectory(path.resolve(path.dirname(id), '..', 'bin')); + emitAssetDirectory(resolve(dirname(id), '..', 'bin')); } }, 'semver' ({ id, emitAsset }) { if (id.endsWith('semver/index.js')) { // See https://github.com/npm/node-semver/blob/master/CHANGELOG.md#710 - emitAsset(path.resolve(id.replace('index.js', 'preload.js'))); + emitAsset(resolve(id.replace('index.js', 'preload.js'))); } }, 'socket.io' ({ id, ast, job }) { @@ -95,14 +95,20 @@ const specialCases: Record void> = { statement.expression.right.arguments[0].arguments.length === 1 && statement.expression.right.arguments[0].arguments[0].type === 'Literal') { const arg = statement.expression.right.arguments[0].arguments[0].value; + let resolved: string; try { - var resolved = resolve(arg, id, job); + const dep = resolveDependency(arg, id, job); + if (typeof dep === 'string') { + resolved = dep; + } else { + return undefined; + } } catch (e) { - return; + return undefined; } // The asset relocator will then pick up the AST rewriting from here - const relResolved = '/' + path.relative(path.dirname(id), resolved); + const relResolved = '/' + relative(dirname(id), resolved); statement.expression.right.arguments[0] = { type: 'BinaryExpression', start: statement.expression.right.arguments[0].start, @@ -119,7 +125,7 @@ const specialCases: Record void> = { } }; } - return; + return undefined; } for (const statement of ast.body) { @@ -139,10 +145,10 @@ const specialCases: Record void> = { for (const node of statement.expression.right.body.body) if (node.type === 'IfStatement') ifStatement = node; const ifBody = ifStatement && ifStatement.consequent.body; - let replaced = false; + let replaced: boolean | undefined = false; if (ifBody && ifBody[0] && ifBody[0].type === 'ExpressionStatement') replaced = replaceResolvePathStatement(ifBody[0]); - const tryBody = ifBody && ifBody[1] && ifBody[1].type === 'TryStatement' && ifBody[1].block.body; + const tryBody: Node[] = ifBody && ifBody[1] && ifBody[1].type === 'TryStatement' && ifBody[1].block.body; if (tryBody && tryBody[0]) replaced = replaceResolvePathStatement(tryBody[0]) || replaced; return; @@ -152,29 +158,29 @@ const specialCases: Record void> = { }, 'typescript' ({ id, emitAssetDirectory }) { if (id.endsWith('typescript/lib/tsc.js')) { - emitAssetDirectory(path.resolve(id, '../')); + emitAssetDirectory(resolve(id, '../')); } }, 'uglify-es' ({ id, emitAsset }) { if (id.endsWith('uglify-es/tools/node.js')) { - emitAsset(path.resolve(id, '../../lib/utils.js')); - emitAsset(path.resolve(id, '../../lib/ast.js')); - emitAsset(path.resolve(id, '../../lib/parse.js')); - emitAsset(path.resolve(id, '../../lib/transform.js')); - emitAsset(path.resolve(id, '../../lib/scope.js')); - emitAsset(path.resolve(id, '../../lib/output.js')); - emitAsset(path.resolve(id, '../../lib/compress.js')); - emitAsset(path.resolve(id, '../../lib/sourcemap.js')); - emitAsset(path.resolve(id, '../../lib/mozilla-ast.js')); - emitAsset(path.resolve(id, '../../lib/propmangle.js')); - emitAsset(path.resolve(id, '../../lib/minify.js')); - emitAsset(path.resolve(id, '../exports.js')); + emitAsset(resolve(id, '../../lib/utils.js')); + emitAsset(resolve(id, '../../lib/ast.js')); + emitAsset(resolve(id, '../../lib/parse.js')); + emitAsset(resolve(id, '../../lib/transform.js')); + emitAsset(resolve(id, '../../lib/scope.js')); + emitAsset(resolve(id, '../../lib/output.js')); + emitAsset(resolve(id, '../../lib/compress.js')); + emitAsset(resolve(id, '../../lib/sourcemap.js')); + emitAsset(resolve(id, '../../lib/mozilla-ast.js')); + emitAsset(resolve(id, '../../lib/propmangle.js')); + emitAsset(resolve(id, '../../lib/minify.js')); + emitAsset(resolve(id, '../exports.js')); } }, 'uglify-js' ({ id, emitAsset, emitAssetDirectory }) { if (id.endsWith('uglify-js/tools/node.js')) { - emitAssetDirectory(path.resolve(id, '../../lib')); - emitAsset(path.resolve(id, '../exports.js')); + emitAssetDirectory(resolve(id, '../../lib')); + emitAsset(resolve(id, '../exports.js')); } } }; @@ -187,8 +193,8 @@ interface SpecialCaseOpts { job: Job; } -export default function specialCase({ id, ast, emitAsset, emitAssetDirectory, job }: SpecialCaseOpts) { - const pkgName = getPackageName(id) +export default 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 }); diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index 79d7213e..ca2241ad 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -1,22 +1,25 @@ import { Node } from 'estree-walker'; import { StaticResult, SingleValue } from '../types'; -type Walk = (node: Node) => StaticResult; +type Walk = (node: Node, state: State) => StaticResult; +type State = {computeBranches: boolean, vars: Record }; export function evaluate(ast: Node, vars = {}, computeBranches = true): StaticResult { - const state = { + const state: State = { computeBranches, vars }; - return walk(ast); + return walk(ast, state); // walk returns: // 1. Single known value: { value: value } // 2. Conditional value: { test, then, else } // 3. Unknown value: undefined - function walk (node: Node) { + function walk (node: Node, state: State) { const visitor = visitors[node.type]; - if (visitor) - return visitor.call(state, node, walk); + if (visitor) { + return visitor(node, walk, state); + } + return undefined; } }; @@ -32,42 +35,42 @@ function countWildcards (str: string) { return cnt; } -const visitors = { - ArrayExpression (node: Node, walk: Walk) { +const visitors: Record StaticResult> = { + 'ArrayExpression': (node: Node, walk: Walk, state: State) => { 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 = walk(node.elements[i], state); if (!x) return; if ('value' in x === false) return; arr.push((x as SingleValue).value); } return { value: arr }; }, - BinaryExpression (node: Node, walk: Walk) { + 'BinaryExpression': (node: Node, walk: Walk, state: State) => { const op = node.operator; - let l = walk(node.left); + let l = walk(node.left, state); if (!l && op !== '+') return; - let r = walk(node.right); + let r = walk(node.right, state); if (!l && !r) return; if (!l) { // UNKNOWN + 'str' -> wildcard string value - if (this.computeBranches && typeof r.value === 'string') + if (state.computeBranches && typeof r.value === 'string') return { value: WILDCARD + r.value, wildcards: [node.left, ...r.wildcards || []] }; return; } if (!r) { // 'str' + UKNOWN -> wildcard string value - if (this.computeBranches && op === '+') { + if (state.computeBranches && op === '+') { if (typeof l.value === 'string') return { value: l.value + WILDCARD, wildcards: [...l.wildcards || [], node.right] }; } @@ -80,7 +83,7 @@ const visitors = { if ('test' in l && 'test' in r) return; - if ('test' in l) { + if ('test' in l && 'value' in r) { r = r.value; if (op === '==') return { test: l.test, then: l.then == r, else: l.else == r }; if (op === '===') return { test: l.test, then: l.then === r, else: l.else === r }; @@ -101,7 +104,7 @@ const visitors = { if (op === '&&') return { test: l.test, then: l.then && r, else: l.else && r }; if (op === '||') return { test: l.test, then: l.then || r, else: l.else || r }; } - else if ('test' in r) { + else if ('test' in r && 'value' in l) { l = l.value; if (op === '==') return { test: r.test, then: l == r.then, else: l == r.else }; if (op === '===') return { test: r.test, then: l === r.then, else: l === r.else }; @@ -129,8 +132,16 @@ const visitors = { if (op === '!==') return { value: l.value !== r.value }; if (op === '+') { const val: SingleValue = { value: l.value + r.value }; - if (l.wildcards || r.wildcards) - val.wildcards = [...l.wildcards || [], ...r.wildcards || []]; + let wildcards: string[] = []; + if ('wildcards' in l && l.wildcards) { + wildcards = wildcards.concat(l.wildcards); + } + if ('wildcards' in r && r.wildcards) { + wildcards = wildcards.concat(r.wildcards); + } + if (wildcards.length > 0) { + val.wildcards = wildcards; + } return val; } if (op === '-') return { value: l.value - r.value }; @@ -149,17 +160,17 @@ const visitors = { } return; }, - CallExpression (node: Node, walk: Walk) { - const callee = walk(node.callee); + 'CallExpression': (node: Node, walk: Walk, state: State) => { + const callee = walk(node.callee, state); if (!callee || 'test' in callee) return; - let fn = callee.value; + let fn: any = callee.value; if (typeof fn === 'object' && fn !== null) fn = fn[FUNCTION]; if (typeof fn !== 'function') return; let ctx = null if (node.callee.object) { - ctx = walk(node.callee.object) - ctx = ctx && ctx.value ? ctx.value : null + ctx = walk(node.callee.object, state) + ctx = ctx && 'value' in ctx && ctx.value ? ctx.value : null } // we allow one conditional argument to create a conditional expression @@ -169,14 +180,14 @@ const visitors = { let allWildcards = node.arguments.length > 0; const wildcards = []; for (let i = 0, l = node.arguments.length; i < l; i++) { - let x = walk(node.arguments[i]); + let x = walk(node.arguments[i], state); if (x) { allWildcards = false; if ('value' in x && typeof x.value === 'string' && x.wildcards) x.wildcards.forEach(w => wildcards.push(w)); } else { - if (!this.computeBranches) + if (!state.computeBranches) return; // this works because provided static functions // operate on known string inputs @@ -220,18 +231,18 @@ const visitors = { return; } }, - ConditionalExpression (node: Node, walk: Walk) { - const val = walk(node.test); + 'ConditionalExpression': (node: Node, walk: Walk, state: State) => { + const val = walk(node.test, state); if (val && 'value' in val) - return val.value ? walk(node.consequent) : walk(node.alternate); + return val.value ? walk(node.consequent, state) : walk(node.alternate, state); - if (!this.computeBranches) + if (!state.computeBranches) return; - const thenValue = walk(node.consequent); + const thenValue = walk(node.consequent, state); if (!thenValue || 'wildcards' in thenValue || 'test' in thenValue) return; - const elseValue = walk(node.alternate); + const elseValue = walk(node.alternate, state); if (!elseValue || 'wildcards' in elseValue || 'test' in elseValue) return; @@ -241,34 +252,34 @@ const visitors = { else: elseValue.value }; }, - ExpressionStatement (node: Node, walk: Walk) { - return walk(node.expression); + 'ExpressionStatement': (node: Node, walk: Walk, state: State) => { + return walk(node.expression, state); }, - Identifier (node: Node, _walk: Walk) { - if (Object.hasOwnProperty.call(this.vars, node.name)) { - const val = this.vars[node.name]; + 'Identifier': (node: Node, _walk: Walk, state: State) => { + if (Object.hasOwnProperty.call(state.vars, node.name)) { + const val = state.vars[node.name]; if (val === UNKNOWN) - return; + return undefined; return { value: val }; } - return; + return undefined; }, - Literal (node: Node, _walk: Walk) { + 'Literal': (node: Node, _walk: Walk) => { return { value: node.value }; }, - MemberExpression (node: Node, walk: Walk) { - const obj = walk(node.object); + 'MemberExpression': (node: Node, walk: Walk, state: State) => { + const obj = walk(node.object, state); // do not allow access to methods on Function if (!obj || 'test' in obj || typeof obj.value === 'function') - return; + return undefined; if (node.property.type === 'Identifier') { if (typeof obj.value === 'object' && obj.value !== null) { if (node.computed) { // See if we can compute the computed property - const computedProp = walk(node.property); + const computedProp = walk(node.property, state); if (computedProp && computedProp.value) { const val = obj.value[computedProp.value]; - if (val === UNKNOWN) return; + if (val === UNKNOWN) return undefined; return { value: val }; } // Special case for empty object @@ -279,48 +290,53 @@ const visitors = { else if (node.property.name in obj.value) { const val = obj.value[node.property.name]; if (val === UNKNOWN) - return; + return undefined; return { value: val }; } else if (obj.value[UNKNOWN]) - return; + return undefined; } else { return { value: undefined }; } } - const prop = walk(node.property); + const prop = walk(node.property, state); if (!prop || 'test' in prop) - return; + return undefined; if (typeof obj.value === 'object' && obj.value !== null) { + //@ts-ignore if (prop.value in obj.value) { + //@ts-ignore const val = obj.value[prop.value]; if (val === UNKNOWN) - return; + return undefined; return { value: val }; } + //@ts-ignore else if (obj.value[UNKNOWN]) { - return; + return undefined; } } else { return { value: undefined }; } }, - ObjectExpression (node: Node, walk: Walk) { - const obj = {}; + 'ObjectExpression': (node: Node, walk: Walk, state: State) => { + 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 }; + const keyValue = prop.computed ? walk(prop.key, state) : prop.key && { value: prop.key.name || prop.key.value }; if (!keyValue || 'test' in keyValue) return; - const value = walk(prop.value); + const value = walk(prop.value, state); if (!value || 'test' in value) return; + //@ts-ignore if (value.value === UNKNOWN) return; + //@ts-ignore obj[keyValue.value] = value.value; } return { value: obj }; }, - TemplateLiteral (node: Node, walk: Walk) { + 'TemplateLiteral': (node: Node, walk: Walk, state: State) => { let val = { value: '' }; for (var i = 0; i < node.expressions.length; i++) { if ('value' in val) { @@ -332,8 +348,8 @@ const visitors = { } let exprValue = walk(node.expressions[i]); if (!exprValue) { - if (!this.computeBranches) - return; + if (!state.computeBranches) + return undefined; exprValue = { value: WILDCARD, wildcards: [node.expressions[i]] }; } if ('value' in exprValue) { @@ -369,14 +385,15 @@ const visitors = { } return val; }, - ThisExpression () { - if (Object.hasOwnProperty.call(this.vars, 'this')) - return { value: this.vars['this'] }; + 'ThisExpression': (_node: Node, _walk: Walk, state: State) => { + if (Object.hasOwnProperty.call(state.vars, 'this')) + return { value: state.vars['this'] }; + return undefined; }, - UnaryExpression (node: Node, walk: Walk) { - const val = walk(node.argument); + 'UnaryExpression': (node: Node, walk: Walk, state: State) => { + const val = walk(node.argument, state); if (!val) - return; + return undefined; if ('value' in val && 'wildcards' in val === false) { if (node.operator === '+') return { value: +val.value }; if (node.operator === '-') return { value: -val.value }; @@ -389,7 +406,7 @@ const visitors = { if (node.operator === '~') return { test: val.test, then: ~val.then, else: ~val.else }; if (node.operator === '!') return { test: val.test, then: !val.then, else: !val.else }; } - return; + return undefined; } }; visitors.LogicalExpression = visitors.BinaryExpression; diff --git a/src/utils/wrappers.ts b/src/utils/wrappers.ts index 8eba7084..650fa463 100644 --- a/src/utils/wrappers.ts +++ b/src/utils/wrappers.ts @@ -133,7 +133,7 @@ export function handleWrappers(ast: Node) { // verify modules is the expected data structure // in the process, extract external requires - const externals = {}; + const externals: Record = {}; if (modules.every(m => { if (m.type !== 'Property' || m.computed !== false || From 66a43c8d31f151350369baada633c430135be133 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 7 Jul 2020 23:12:39 -0700 Subject: [PATCH 03/19] Add build step to CI --- .github/workflows/ci.yml | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9208eeb..a88577eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: run: node prepare-install.js - name: Install Dependencies run: yarn install + - name: Build + run: yarn build - name: Run Tests env: BULL_REDIS_CONNECTION: ${{ secrets.BULL_REDIS_CONNECTION }} diff --git a/package.json b/package.json index a28e8b0b..e5b2cb06 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "nft": "./dist/cli.js" }, "scripts": { - "tsc": "tsc", + "build": "tsc", "test": "jest --verbose", "test-verbose": "jest --verbose --coverage --globals \"{\\\"coverage\\\":true}\"", "codecov": "codecov", From e27f2b673d4b6102754f8a0626ca1fa333dda2bf Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 12:39:16 -0700 Subject: [PATCH 04/19] Several updates --- src/analyze.ts | 28 ++++++++++++++++------------ src/resolve-dependency.ts | 7 ++++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index 3377a172..e47ada3a 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -441,7 +441,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (staticChildNode) return; if (node.type === 'Identifier') { - if (isIdentifierRead(node, parent) && job.analysis.computeFileReferences) { + if (isIdentifierRead(node, parent!) && job.analysis.computeFileReferences) { let binding; // detect asset leaf expression triggers (if not already) // __dirname, __filename @@ -450,7 +450,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(this, parent); + backtrack(this, parent!); } } } @@ -490,7 +490,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (calleeValue && 'value' in calleeValue && typeof calleeValue.value === 'function' && calleeValue.value[TRIGGER] && job.analysis.computeFileReferences) { staticChildValue = computePureStaticValue(node, true); // if it computes, then we start backtracking - if (staticChildValue) { + if (staticChildValue && parent) { staticChildNode = node; backtrack(this, parent); } @@ -531,7 +531,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; - emitStaticChildAsset(staticBindingsInstance); + emitStaticChildAsset(); } } } @@ -547,7 +547,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; - emitStaticChildAsset(path); + emitStaticChildAsset(); } } break; @@ -585,7 +585,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // if it computes, then we start backtracking if (staticChildValue) { staticChildNode = node.arguments[0]; - backtrack(this, parent); + backtrack(this, parent!); return this.skip(); } } @@ -594,7 +594,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi case SET_ROOT_DIR: if (node.arguments[0]) { const rootDir = computePureStaticValue(node.arguments[0], false); - if (rootDir && rootDir.value) + if (rootDir && 'value' in rootDir && rootDir.value) emitAssetDirectory(rootDir.value + '/intl'); return this.skip(); } @@ -611,7 +611,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } } - else if (node.type === 'VariableDeclaration' && !isVarLoop(parent) && job.analysis.evaluatePureExpressions) { + 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, false); @@ -641,7 +641,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } } - else if (node.type === 'AssignmentExpression' && !isLoop(parent) && job.analysis.evaluatePureExpressions) { + else if (node.type === 'AssignmentExpression' && parent && !isLoop(parent) && job.analysis.evaluatePureExpressions) { if (!hasKnownBindingValue(node.left.name)) { const computed = computePureStaticValue(node.right, false); if (computed && 'value' in computed) { @@ -674,8 +674,10 @@ export default async function analyze(id: string, code: string, job: Job): Promi else if ((!isESM || job.mixedModules) && (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && (node.arguments || node.params)[0] && (node.arguments || node.params)[0].type === 'Identifier') { - let fnName, args; + let fnName: any; + let args: any[]; if ((node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') && + parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') { fnName = parent.id; @@ -717,7 +719,9 @@ export default async function analyze(id: string, code: string, job: Job): Promi }, leave (node, parent) { if (node.scope) { - scope = scope.parent; + if (scope.parent) { + scope = scope.parent; + } for (const id in node.scope.declarations) { if (id in knownBindings) { if (knownBindings[id].shadowDepth > 0) @@ -728,7 +732,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } - if (staticChildNode) backtrack(this, parent); + if (staticChildNode && parent) backtrack(this, parent); } }); diff --git a/src/resolve-dependency.ts b/src/resolve-dependency.ts index 556661e9..18a374eb 100644 --- a/src/resolve-dependency.ts +++ b/src/resolve-dependency.ts @@ -28,8 +28,8 @@ function resolvePath (path: string, parent: string, job: Job): string | undefine return result; } -function resolveFile (path: string, parent: string, job: Job) { - if (path.endsWith('/')) return; +function resolveFile (path: string, parent: string, job: Job): string | undefined { + 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'; @@ -37,6 +37,7 @@ function resolveFile (path: string, parent: string, job: Job) { if (job.isFile(path + '.js')) return path + '.js'; if (job.isFile(path + '.json')) return path + '.json'; if (job.isFile(path + '.node')) return path + '.node'; + return undefined; } function resolveDir (path: string, parent: string, job: Job) { @@ -72,7 +73,7 @@ function getPkgCfg (pkgPath: string, job: Job) { const pjsonSource = job.readFile(pkgPath + sep + 'package.json'); if (pjsonSource) { try { - return JSON.parse(pjsonSource); + return JSON.parse(pjsonSource.toString()); } catch (e) {} } From 1940e97250aeb83bf77cdd71dfe2c676bbc6180f Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 13:34:46 -0700 Subject: [PATCH 05/19] Fix a bunch of type errors --- src/analyze.ts | 12 +++++----- src/node-file-trace.ts | 10 ++++---- src/resolve-dependency.ts | 44 ++++++++++++++++++++++-------------- src/types.ts | 2 +- src/utils/binary-locators.ts | 4 +++- src/utils/interop-require.ts | 2 +- 6 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index e47ada3a..bbb9381c 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -487,7 +487,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi const calleeValue = job.analysis.evaluatePureExpressions && 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[TRIGGER] && job.analysis.computeFileReferences) { + if (calleeValue && 'value' in calleeValue && typeof calleeValue.value === 'function' && (calleeValue.value as any)[TRIGGER] && job.analysis.computeFileReferences) { staticChildValue = computePureStaticValue(node, true); // if it computes, then we start backtracking if (staticChildValue && parent) { @@ -512,14 +512,12 @@ export default async function analyze(id: string, code: string, job: Job): Promi if (node.arguments.length) { const arg = computePureStaticValue(node.arguments[0], false); if (arg && 'value' in arg && arg.value) { - let staticBindingsInstance = false; - let opts; + let opts: any; if (typeof arg.value === 'object') opts = arg.value; else if (typeof arg.value === 'string') opts = { bindings: arg.value }; if (!opts.path) { - staticBindingsInstance = true; opts.path = true; } opts.module_root = pkgBase; @@ -555,7 +553,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi case NBIND_INIT: if (node.arguments.length) { const arg = computePureStaticValue(node.arguments[0], false); - if (arg && 'value' in arg && !Array.isArray(arg.value)) { + if (arg && 'value' in arg && (typeof arg.value === 'string' || typeof arg.value === 'undefined')) { const bindingInfo = nbind(arg.value); if (bindingInfo) { deps.add(path.relative(dir, bindingInfo.path).replace(/\\/g, '/')); @@ -691,7 +689,9 @@ export default async function analyze(id: string, code: string, job: Job): Promi let requireDecl, returned = false; for (let i = 0; i < node.body.body.length; i++) { if (node.body.body[i].type === 'VariableDeclaration' && !requireDecl) { - requireDecl = node.body.body[i].declarations.find(decl => + requireDecl = node.body.body[i].declarations.find((decl: any) => + decl && + decl.id && decl.id.type === 'Identifier' && decl.init && decl.init.type === 'CallExpression' && diff --git a/src/node-file-trace.ts b/src/node-file-trace.ts index 2c3bdd64..adc6b611 100644 --- a/src/node-file-trace.ts +++ b/src/node-file-trace.ts @@ -28,8 +28,10 @@ export async function nodeFileTrace(files: string[], opts: NodeFileTraceOptions await Promise.all(files.map(file => { const path = resolve(file); job.emitFile(job.realpath(path), 'initial'); - if (path.endsWith('.js') || path.endsWith('.cjs') || path.endsWith('.mjs') || path.endsWith('.node') || job.ts && (path.endsWith('.ts') || path.endsWith('.tsx'))) + 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); + } + return undefined; })); return { @@ -261,7 +263,7 @@ export class Job { return undefined; } - async emitDependency (path: string, parent: string) { + async emitDependency (path: string, parent?: string) { if (this.processed.has(path)) return; this.processed.add(path); @@ -317,7 +319,7 @@ export class Job { await this.emitDependency(item, path); } } - else { + else if (resolved) { // ignore builtins if (resolved.startsWith('node:')) return; await this.emitDependency(resolved, path); @@ -338,7 +340,7 @@ export class Job { await this.emitDependency(item, path); } } - else { + else if (resolved) { // ignore builtins if (resolved.startsWith('node:')) return; await this.emitDependency(resolved, path); diff --git a/src/resolve-dependency.ts b/src/resolve-dependency.ts index 18a374eb..aa439d4a 100644 --- a/src/resolve-dependency.ts +++ b/src/resolve-dependency.ts @@ -23,7 +23,7 @@ export default function resolveDependency (specifier: string, parent: string, jo function resolvePath (path: string, parent: string, job: Job): string | undefined { const result = resolveFile(path, parent, job) || resolveDir(path, parent, job); if (!result) { - notFound(path, parent); + throw new NotFoundError(path, parent); } return result; } @@ -54,13 +54,15 @@ function resolveDir (path: string, parent: string, job: Job) { return resolveFile(resolve(path, 'index'), parent, job); } -function notFound (specifier: string, parent: string) { - const e = new Error("Cannot find module '" + specifier + "' loaded from " + parent); - (e as any).code = 'MODULE_NOT_FOUND'; - throw e; +class NotFoundError extends Error { + public code: string; + constructor(specifier: string, parent: string) { + super("Cannot find module '" + specifier + "' loaded from " + parent); + this.code = 'MODULE_NOT_FOUND'; + } } -const nodeBuiltins = new Set([...require("repl")._builtinLibs, "constants", "module", "timers", "console", "_stream_writable", "_stream_readable", "_stream_duplex", "process", "sys"]); +const nodeBuiltins = new Set([...require("repl")._builtinLibs, "constants", "module", "timers", "console", "_stream_writable", "_stream_readable", "_stream_duplex", "process", "sys"]); function getPkgName (name: string) { const segments = name.split('/'); @@ -79,10 +81,13 @@ function getPkgCfg (pkgPath: string, job: Job) { } } -function getExportsTarget(exports: string | string[] | Record | null, conditions: string[], cjsResolve: boolean): string | null { +function getExportsTarget(exports: string | string[] | { [key: string]: string } | null, conditions: string[], cjsResolve: boolean): string | null { if (typeof exports === 'string') { return exports; } + else if (exports === null) { + return exports; + } else if (Array.isArray(exports)) { for (const item of exports) { const target = getExportsTarget(item, conditions, cjsResolve); @@ -102,15 +107,19 @@ function getExportsTarget(exports: string | string[] | Record | } } } - else if (exports === null) { - return exports; - } + + return null; } -function resolveExportsTarget (pkgPath, exports, subpath: string, job: Job, cjsResolve: boolean) { - if (typeof exports === 'string' || - typeof exports === 'object' && !Array.isArray(exports) && Object.keys(exports).length && Object.keys(exports)[0][0] !== '.') - exports = { '.' : exports }; +function resolveExportsTarget (pkgPath: string, exp: string | { [key: string]: string }, subpath: string, job: Job, cjsResolve: boolean): string | undefined { + let exports: { [key: string]: string }; + if (typeof exp === 'string' || + typeof exp === 'object' && !Array.isArray(exp) && Object.keys(exp).length && Object.keys(exp)[0][0] !== '.') { + exports = { '.' : exp }; + } else { + exports = exp; + } + if (subpath in exports) { const target = getExportsTarget(exports[subpath], job.exports, cjsResolve); if (typeof target === 'string' && target.startsWith('./')) @@ -125,16 +134,17 @@ function resolveExportsTarget (pkgPath, exports, subpath: string, job: Job, cjsR return pkgPath + match.slice(2) + subpath.slice(match.length); } } + return undefined; } function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean) { let packageParent = parent; if (nodeBuiltins.has(name)) return 'node:' + name; - const pkgName = getPkgName(name); + const pkgName = getPkgName(name) || ''; // package own name resolution - let selfResolved; + let selfResolved: string | undefined; if (job.exports) { const pjsonBoundary = job.getPjsonBoundary(parent); if (pjsonBoundary) { @@ -190,5 +200,5 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo return resolveFile(pathTarget, parent, job) || resolveDir(pathTarget, parent, job); } } - notFound(name, parent); + throw new NotFoundError(name, parent); } diff --git a/src/types.ts b/src/types.ts index b264fbf3..7b1df689 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,7 +63,7 @@ export interface NodeFileTraceResult { } export interface SingleValue { - value: string | string[] | undefined; + value: unknown; wildcards?: string[]; } diff --git a/src/utils/binary-locators.ts b/src/utils/binary-locators.ts index dac6f9c6..829ad44d 100644 --- a/src/utils/binary-locators.ts +++ b/src/utils/binary-locators.ts @@ -41,7 +41,9 @@ function makeModulePathList(root: string, name: string) { ] ]); } -function findCompiledModule(basePath: string, specList) { +type Spec = { ext: string, name: string, type: string, path?: string }; + +function findCompiledModule(basePath: string, specList: Spec[]): Spec | null { var resolvedList = []; var ext = path.extname(basePath); for (var _i = 0, specList_1 = specList; _i < specList_1.length; _i++) { diff --git a/src/utils/interop-require.ts b/src/utils/interop-require.ts index 9d5d65b4..56a9f0c3 100644 --- a/src/utils/interop-require.ts +++ b/src/utils/interop-require.ts @@ -10,7 +10,7 @@ export function normalizeWildcardRequire(obj: any) { if (obj && obj.__esModule) return obj; // Note: This implements only value properties and doesn't preserve getters. // This follows the simpler helpers generated by TypeScript. - const out = {}; + const out: { [key: string]: string } = {}; for (const key in obj) { if (!hasOwnProperty.call(obj, key)) continue; out[key] = obj[key]; From 22a656fcce4adc8f8785c1b9db51c45f41929f0c Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 14:51:06 -0700 Subject: [PATCH 06/19] Fix more types --- src/analyze.ts | 2 +- src/types.ts | 6 +- src/utils/static-eval.ts | 118 ++++++++++++++++++++------------------- src/utils/wrappers.ts | 31 +++++----- 4 files changed, 82 insertions(+), 75 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index bbb9381c..5aa53854 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -555,7 +555,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi const arg = 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) { + if (bindingInfo && bindingInfo.path) { deps.add(path.relative(dir, bindingInfo.path).replace(/\\/g, '/')); return this.skip(); } diff --git a/src/types.ts b/src/types.ts index 7b1df689..fd95aab1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,14 +63,14 @@ export interface NodeFileTraceResult { } export interface SingleValue { - value: unknown; + value: any; wildcards?: string[]; } export interface ConditionalValue { test: string; - then: string; - else: string; + then: any; + else: any; } export type StaticResult = SingleValue | ConditionalValue | undefined; diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index ca2241ad..53875658 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -1,5 +1,5 @@ import { Node } from 'estree-walker'; -import { StaticResult, SingleValue } from '../types'; +import { StaticResult, SingleValue, ConditionalValue } from '../types'; type Walk = (node: Node, state: State) => StaticResult; type State = {computeBranches: boolean, vars: Record }; @@ -63,7 +63,7 @@ const visitors: Record StaticR if (!l) { // UNKNOWN + 'str' -> wildcard string value - if (state.computeBranches && typeof r.value === 'string') + if (state.computeBranches && r && 'value' in r && typeof r.value === 'string') return { value: WILDCARD + r.value, wildcards: [node.left, ...r.wildcards || []] }; return; } @@ -71,7 +71,7 @@ const visitors: Record StaticR if (!r) { // 'str' + UKNOWN -> wildcard string value if (state.computeBranches && op === '+') { - if (typeof l.value === 'string') + if (l && 'value' in l && typeof l.value === 'string') return { value: l.value + WILDCARD, wildcards: [...l.wildcards || [], node.right] }; } // A || UNKNOWN -> A if A is truthy @@ -84,46 +84,46 @@ const visitors: Record StaticR return; if ('test' in l && 'value' in r) { - r = r.value; - if (op === '==') return { test: l.test, then: l.then == r, else: l.else == r }; - if (op === '===') return { test: l.test, then: l.then === r, else: l.else === r }; - if (op === '!=') return { test: l.test, then: l.then != r, else: l.else != r }; - if (op === '!==') return { test: l.test, then: l.then !== r, else: l.else !== r }; - if (op === '+') return { test: l.test, then: l.then + r, else: l.else + r }; - if (op === '-') return { test: l.test, then: l.then - r, else: l.else - r }; - if (op === '*') return { test: l.test, then: l.then * r, else: l.else * r }; - if (op === '/') return { test: l.test, then: l.then / r, else: l.else / r }; - if (op === '%') return { test: l.test, then: l.then % r, else: l.else % r }; - if (op === '<') return { test: l.test, then: l.then < r, else: l.else < r }; - if (op === '<=') return { test: l.test, then: l.then <= r, else: l.else <= r }; - if (op === '>') return { test: l.test, then: l.then > r, else: l.else > r }; - if (op === '>=') return { test: l.test, then: l.then >= r, else: l.else >= r }; - if (op === '|') return { test: l.test, then: l.then | r, else: l.else | r }; - if (op === '&') return { test: l.test, then: l.then & r, else: l.else & r }; - if (op === '^') return { test: l.test, then: l.then ^ r, else: l.else ^ r }; - if (op === '&&') return { test: l.test, then: l.then && r, else: l.else && r }; - if (op === '||') return { test: l.test, then: l.then || r, else: l.else || r }; + const v: any = r.value; + if (op === '==') return { test: l.test, then: l.then == v, else: l.else == v }; + if (op === '===') return { test: l.test, then: l.then === v, else: l.else === v }; + if (op === '!=') return { test: l.test, then: l.then != v, else: l.else != v }; + if (op === '!==') return { test: l.test, then: l.then !== v, else: l.else !== v }; + if (op === '+') return { test: l.test, then: l.then + v, else: l.else + v }; + if (op === '-') return { test: l.test, then: l.then - v, else: l.else - v }; + if (op === '*') return { test: l.test, then: l.then * v, else: l.else * v }; + if (op === '/') return { test: l.test, then: l.then / v, else: l.else / v }; + if (op === '%') return { test: l.test, then: l.then % v, else: l.else % v }; + if (op === '<') return { test: l.test, then: l.then < v, else: l.else < v }; + if (op === '<=') return { test: l.test, then: l.then <= v, else: l.else <= v }; + if (op === '>') return { test: l.test, then: l.then > v, else: l.else > v }; + if (op === '>=') return { test: l.test, then: l.then >= v, else: l.else >= v }; + if (op === '|') return { test: l.test, then: l.then | v, else: l.else | v }; + if (op === '&') return { test: l.test, then: l.then & v, else: l.else & v }; + if (op === '^') return { test: l.test, then: l.then ^ v, else: l.else ^ v }; + if (op === '&&') return { test: l.test, then: l.then && v, else: l.else && v }; + if (op === '||') return { test: l.test, then: l.then || v, else: l.else || v }; } else if ('test' in r && 'value' in l) { - l = l.value; - if (op === '==') return { test: r.test, then: l == r.then, else: l == r.else }; - if (op === '===') return { test: r.test, then: l === r.then, else: l === r.else }; - if (op === '!=') return { test: r.test, then: l != r.then, else: l != r.else }; - if (op === '!==') return { test: r.test, then: l !== r.then, else: l !== r.else }; - if (op === '+') return { test: r.test, then: l + r.then, else: l + r.else }; - if (op === '-') return { test: r.test, then: l - r.then, else: l - r.else }; - if (op === '*') return { test: r.test, then: l * r.then, else: l * r.else }; - if (op === '/') return { test: r.test, then: l / r.then, else: l / r.else }; - if (op === '%') return { test: r.test, then: l % r.then, else: l % r.else }; - if (op === '<') return { test: r.test, then: l < r.then, else: l < r.else }; - if (op === '<=') return { test: r.test, then: l <= r.then, else: l <= r.else }; - if (op === '>') return { test: r.test, then: l > r.then, else: l > r.else }; - if (op === '>=') return { test: r.test, then: l >= r.then, else: l >= r.else }; - if (op === '|') return { test: r.test, then: l | r.then, else: l | r.else }; - if (op === '&') return { test: r.test, then: l & r.then, else: l & r.else }; - if (op === '^') return { test: r.test, then: l ^ r.then, else: l ^ r.else }; - if (op === '&&') return { test: r.test, then: l && r.then, else: l && r.else }; - if (op === '||') return { test: r.test, then: l || r.then, else: l || r.else }; + const v: any = l.value; + if (op === '==') return { test: r.test, then: v == r.then, else: v == r.else }; + if (op === '===') return { test: r.test, then: v === r.then, else: v === r.else }; + if (op === '!=') return { test: r.test, then: v != r.then, else: v != r.else }; + if (op === '!==') return { test: r.test, then: v !== r.then, else: v !== r.else }; + if (op === '+') return { test: r.test, then: v + r.then, else: v + r.else }; + if (op === '-') return { test: r.test, then: v - r.then, else: v - r.else }; + if (op === '*') return { test: r.test, then: v * r.then, else: v * r.else }; + if (op === '/') return { test: r.test, then: v / r.then, else: v / r.else }; + if (op === '%') return { test: r.test, then: v % r.then, else: v % r.else }; + if (op === '<') return { test: r.test, then: v < r.then, else: v < r.else }; + if (op === '<=') return { test: r.test, then: v <= r.then, else: v <= r.else }; + if (op === '>') return { test: r.test, then: v > r.then, else: v > r.else }; + if (op === '>=') return { test: r.test, then: v >= r.then, else: v >= r.else }; + if (op === '|') return { test: r.test, then: v | r.then, else: v | r.else }; + if (op === '&') return { test: r.test, then: v & r.then, else: v & r.else }; + if (op === '^') return { test: r.test, then: v ^ r.then, else: v ^ r.else }; + if (op === '&&') return { test: r.test, then: v && r.then, else: l && r.else }; + if (op === '||') return { test: r.test, then: v || r.then, else: l || r.else }; } else { if (op === '==') return { value: l.value == r.value }; @@ -178,7 +178,7 @@ const visitors: Record StaticR let args = []; let argsElse; let allWildcards = node.arguments.length > 0; - const wildcards = []; + const wildcards: string[] = []; for (let i = 0, l = node.arguments.length; i < l; i++) { let x = walk(node.arguments[i], state); if (x) { @@ -270,30 +270,31 @@ const visitors: Record StaticR 'MemberExpression': (node: Node, walk: Walk, state: State) => { const obj = walk(node.object, state); // do not allow access to methods on Function - if (!obj || 'test' in obj || typeof obj.value === 'function') + if (!obj || 'test' in obj || typeof obj.value === 'function') { return undefined; + } if (node.property.type === 'Identifier') { if (typeof obj.value === 'object' && obj.value !== null) { + const objValue = obj.value as any; if (node.computed) { // See if we can compute the computed property const computedProp = walk(node.property, state); - if (computedProp && computedProp.value) { - const val = obj.value[computedProp.value]; + if (computedProp && 'value' in computedProp && computedProp.value) { + const val = objValue[computedProp.value]; if (val === UNKNOWN) return undefined; return { value: val }; } // Special case for empty object - if (!obj.value[UNKNOWN] && Object.keys(obj).length === 0) { + if (!objValue[UNKNOWN] && Object.keys(obj).length === 0) { return { value: undefined }; } } - else if (node.property.name in obj.value) { - const val = obj.value[node.property.name]; - if (val === UNKNOWN) - return undefined; + else if (node.property.name in objValue) { + const val = objValue[node.property.name]; + if (val === UNKNOWN) return undefined; return { value: val }; } - else if (obj.value[UNKNOWN]) + else if (objValue[UNKNOWN]) return undefined; } else { @@ -320,6 +321,7 @@ const visitors: Record StaticR else { return { value: undefined }; } + return undefined; }, 'ObjectExpression': (node: Node, walk: Walk, state: State) => { const obj: any = {}; @@ -337,7 +339,7 @@ const visitors: Record StaticR return { value: obj }; }, 'TemplateLiteral': (node: Node, walk: Walk, state: State) => { - let val = { value: '' }; + let val: SingleValue | ConditionalValue = { value: '' }; for (var i = 0; i < node.expressions.length; i++) { if ('value' in val) { val.value += node.quasis[i].value.cooked; @@ -346,7 +348,7 @@ const visitors: Record StaticR val.then += node.quasis[i].value.cooked; val.else += node.quasis[i].value.cooked; } - let exprValue = walk(node.expressions[i]); + let exprValue = walk(node.expressions[i], state); if (!exprValue) { if (!state.computeBranches) return undefined; @@ -365,15 +367,19 @@ const visitors: Record StaticR val.else += exprValue.value; } } - else { - // only support a single branch in a template - if ('value' in val === false || val.wildcards) + else if ('value' in val) { + if ('wildcards' in val) { + // only support a single branch in a template return; + } val = { test: exprValue.test, then: val.value + exprValue.then, else: val.value + exprValue.else }; + } else { + // only support a single branch in a template + return; } } if ('value' in val) { diff --git a/src/utils/wrappers.ts b/src/utils/wrappers.ts index 650fa463..974e7906 100644 --- a/src/utils/wrappers.ts +++ b/src/utils/wrappers.ts @@ -114,13 +114,13 @@ export function handleWrappers(ast: Node) { wrapper.arguments[0].body.body.length === 2 && wrapper.arguments[0].body.body[0].type === 'VariableDeclaration' && wrapper.arguments[0].body.body[0].declarations.length === 3 && - wrapper.arguments[0].body.body[0].declarations.every(decl => decl.init === null && decl.id.type === 'Identifier') + wrapper.arguments[0].body.body[0].declarations.every((decl: any) => decl.init === null && decl.id.type === 'Identifier') ) && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].type === 'ReturnStatement' && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.type === 'CallExpression' && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.callee.type === 'CallExpression' && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.arguments.length && - wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.arguments.every(arg => arg.type === 'Literal' && typeof arg.value === 'number') && + wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.arguments.every((arg: any) => arg && arg.type === 'Literal' && typeof arg.value === 'number') && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.callee.callee.type === 'CallExpression' && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.callee.callee.callee.type === 'FunctionExpression' && wrapper.arguments[0].body.body[wrapper.arguments[0].body.body.length - 1].argument.callee.callee.arguments.length === 0 && @@ -134,7 +134,7 @@ export function handleWrappers(ast: Node) { // verify modules is the expected data structure // in the process, extract external requires const externals: Record = {}; - if (modules.every(m => { + if (modules.every((m: any) => { if (m.type !== 'Property' || m.computed !== false || m.key.type !== 'Literal' || @@ -321,18 +321,18 @@ export function handleWrappers(ast: Node) { wrapper.arguments[0] && wrapper.arguments[0].type === 'ArrayExpression' && wrapper.arguments[0].elements.length > 0 && - wrapper.arguments[0].elements.every(el => el && el.type === 'FunctionExpression') || + wrapper.arguments[0].elements.every((el: any) => el && el.type === 'FunctionExpression') || wrapper.arguments[0].type === 'ObjectExpression' && wrapper.arguments[0].properties && wrapper.arguments[0].properties.length > 0 && - wrapper.arguments[0].properties.every(prop => prop && prop.key && prop.key.type === 'Literal' && prop.value && prop.value.type === 'FunctionExpression') + wrapper.arguments[0].properties.every((prop: any) => prop && prop.key && prop.key.type === 'Literal' && prop.value && prop.value.type === 'FunctionExpression') )) { - const externalMap = new Map(); - let modules; + const externalMap = new Map(); + let modules: [number, any][]; if (wrapper.arguments[0].type === 'ArrayExpression') - modules = wrapper.arguments[0].elements.map((el, i) => [i, el]); + modules = wrapper.arguments[0].elements.map((el: any, i: number) => [i, el]); else - modules = wrapper.arguments[0].properties.map(prop => [prop.key.value, prop.value]); + modules = wrapper.arguments[0].properties.map((prop: any) => [prop.key.value, prop.value]); for (const [k, m] of modules) { if (m.body.body.length === 1 && m.body.body[0].type === 'ExpressionStatement' && @@ -355,13 +355,13 @@ export function handleWrappers(ast: Node) { if (m.params.length === 3 && m.params[2].type === 'Identifier') { const assignedVars = new Map(); walk(m.body, { - enter (node, parent) { + enter (node, maybeParent) { if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression' || node.type === 'BlockStatement' || node.type === 'TryStatement') { - if (parent) + if (maybeParent) return this.skip(); } if (node.type === 'CallExpression' && @@ -382,6 +382,7 @@ export function handleWrappers(ast: Node) { value: externalId }] }; + const parent = maybeParent!; if (parent.right === node) { parent.right = replacement; } @@ -394,8 +395,8 @@ export function handleWrappers(ast: Node) { else if (parent.callee === node) { parent.callee = replacement; } - else if (parent.arguments && parent.arguments.some(arg => arg === node)) { - parent.arguments = parent.arguments.map(arg => arg === node ? replacement : arg); + else if (parent.arguments && parent.arguments.some((arg: any) => arg === node)) { + parent.arguments = parent.arguments.map((arg: any) => arg === node ? replacement : arg); } else if (parent.init === node) { if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') @@ -413,8 +414,8 @@ export function handleWrappers(ast: Node) { node.arguments.length === 1 && node.arguments[0].type === 'Identifier' && assignedVars.get(node.arguments[0].name)) { - if (parent.init === node) { - parent.init = { + if (maybeParent && maybeParent.init === node) { + maybeParent.init = { type: 'ObjectExpression', properties: [{ type: 'ObjectProperty', From 4b85568c68470892bd46f5cd64bf6249a4704ff2 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 14:54:52 -0700 Subject: [PATCH 07/19] Rename SingleValue to StaticValue --- src/types.ts | 4 ++-- src/utils/static-eval.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types.ts b/src/types.ts index fd95aab1..582e6865 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,7 +62,7 @@ export interface NodeFileTraceResult { warnings: Error[]; } -export interface SingleValue { +export interface StaticValue { value: any; wildcards?: string[]; } @@ -73,4 +73,4 @@ export interface ConditionalValue { else: any; } -export type StaticResult = SingleValue | ConditionalValue | undefined; +export type StaticResult = StaticValue | ConditionalValue | undefined; diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index 53875658..7e4a31df 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -1,5 +1,5 @@ import { Node } from 'estree-walker'; -import { StaticResult, SingleValue, ConditionalValue } from '../types'; +import { StaticResult, StaticValue, ConditionalValue } from '../types'; type Walk = (node: Node, state: State) => StaticResult; type State = {computeBranches: boolean, vars: Record }; @@ -46,7 +46,7 @@ const visitors: Record StaticR const x = walk(node.elements[i], state); if (!x) return; if ('value' in x === false) return; - arr.push((x as SingleValue).value); + arr.push((x as StaticValue).value); } return { value: arr }; }, @@ -131,7 +131,7 @@ const visitors: Record StaticR if (op === '!=') return { value: l.value != r.value }; if (op === '!==') return { value: l.value !== r.value }; if (op === '+') { - const val: SingleValue = { value: l.value + r.value }; + const val: StaticValue = { value: l.value + r.value }; let wildcards: string[] = []; if ('wildcards' in l && l.wildcards) { wildcards = wildcards.concat(l.wildcards); @@ -339,7 +339,7 @@ const visitors: Record StaticR return { value: obj }; }, 'TemplateLiteral': (node: Node, walk: Walk, state: State) => { - let val: SingleValue | ConditionalValue = { value: '' }; + let val: StaticValue | ConditionalValue = { value: '' }; for (var i = 0; i < node.expressions.length; i++) { if ('value' in val) { val.value += node.quasis[i].value.cooked; From 2a4a5a7355657fc9ce141c02f7e13f33f7044d54 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 14:57:15 -0700 Subject: [PATCH 08/19] Rename StaticResult to EvaluatedValue --- src/analyze.ts | 4 ++-- src/types.ts | 2 +- src/utils/static-eval.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index 5aa53854..67e90e38 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -27,7 +27,7 @@ const acorn = Parser.extend( import os from 'os'; import { handleWrappers } from './utils/wrappers'; import resolveFrom from 'resolve-from'; -import { StaticResult } from './types'; +import { EvaluatedValue } from './types'; const staticProcess = { cwd: () => { @@ -337,7 +337,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // statically determinable leaves are tracked, and inlined when the // greatest parent statically known leaf computation corresponds to an asset path let staticChildNode: Node | undefined; - let staticChildValue: StaticResult; + let staticChildValue: EvaluatedValue; // Express engine opt-out let definedExpressEngines = false; diff --git a/src/types.ts b/src/types.ts index 582e6865..ae602ba2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,4 +73,4 @@ export interface ConditionalValue { else: any; } -export type StaticResult = StaticValue | ConditionalValue | undefined; +export type EvaluatedValue = StaticValue | ConditionalValue | undefined; diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index 7e4a31df..4b0d6768 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -1,9 +1,9 @@ import { Node } from 'estree-walker'; -import { StaticResult, StaticValue, ConditionalValue } from '../types'; -type Walk = (node: Node, state: State) => StaticResult; +import { EvaluatedValue, StaticValue, ConditionalValue } from '../types'; +type Walk = (node: Node, state: State) => EvaluatedValue; type State = {computeBranches: boolean, vars: Record }; -export function evaluate(ast: Node, vars = {}, computeBranches = true): StaticResult { +export function evaluate(ast: Node, vars = {}, computeBranches = true): EvaluatedValue { const state: State = { computeBranches, vars @@ -35,7 +35,7 @@ function countWildcards (str: string) { return cnt; } -const visitors: Record StaticResult> = { +const visitors: Record EvaluatedValue> = { 'ArrayExpression': (node: Node, walk: Walk, state: State) => { const arr = []; for (let i = 0, l = node.elements.length; i < l; i++) { From f1f41af9d9f093a837e762c86d84be823f9426b8 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 15:12:49 -0700 Subject: [PATCH 09/19] Fix static eval types --- src/utils/static-eval.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index 4b0d6768..5ed65f13 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -80,9 +80,6 @@ const visitors: Record Evaluat return; } - if ('test' in l && 'test' in r) - return; - if ('test' in l && 'value' in r) { const v: any = r.value; if (op === '==') return { test: l.test, then: l.then == v, else: l.else == v }; @@ -125,7 +122,7 @@ const visitors: Record Evaluat if (op === '&&') return { test: r.test, then: v && r.then, else: l && r.else }; if (op === '||') return { test: r.test, then: v || r.then, else: l || r.else }; } - else { + else if ('value' in l && 'value' in r) { if (op === '==') return { value: l.value == r.value }; if (op === '===') return { value: l.value === r.value }; if (op === '!=') return { value: l.value != r.value }; From ce618b071b351fbc4d521026e150186089dcc355 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 15:20:23 -0700 Subject: [PATCH 10/19] Update lockfile --- package.json | 2 +- yarn.lock | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e5b2cb06..0e480c8b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "acorn-numeric-separator": "^0.3.0", "acorn-static-class-features": "^0.2.1", "bindings": "^1.4.0", - "estree-walker": "^0.6.0", + "estree-walker": "0.6.1", "glob": "^7.1.3", "graceful-fs": "^4.1.15", "micromatch": "^4.0.2", diff --git a/yarn.lock b/yarn.lock index 0135d0bb..e264bdc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4754,12 +4754,7 @@ estraverse@~1.5.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.5.1.tgz#867a3e8e58a9f84618afb6c2ddbcd916b7cbaf71" integrity sha1-hno+jlip+EYYr7bC3bzZFrfLr3E= -estree-walker@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.0.tgz#5d865327c44a618dde5699f763891ae31f257dae" - integrity sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw== - -estree-walker@^0.6.1: +estree-walker@0.6.1, estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== From 18ae62af39d2122da71dbc6e1a34574e034a10e9 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 16:08:44 -0700 Subject: [PATCH 11/19] Change outDir to avoid conflicts --- .gitignore | 1 + package.json | 8 ++++---- test/cli.test.js | 12 ++++++------ test/ecmascript.test.js | 2 +- test/integration.test.js | 2 +- test/integration/dogfood.js | 2 +- test/unit.test.js | 2 +- tsconfig.json | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index b762ab27..ea00b8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules dist +out coverage test/**/dist test/**/actual.js diff --git a/package.json b/package.json index 0e480c8b..8f832706 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "0.6.5", "repository": "vercel/node-file-trace", "license": "MIT", - "main": "./dist/node-file-trace.js", - "types": "./dist/node-file-trace.d.ts", + "main": "./out/node-file-trace.js", + "types": "./out/node-file-trace.d.ts", "bin": { - "nft": "./dist/cli.js" + "nft": "./out/cli.js" }, "scripts": { "build": "tsc", @@ -16,7 +16,7 @@ "test-coverage": "jest --verbose --coverage --globals \"{\\\"coverage\\\":true}\" && codecov" }, "files": [ - "dist" + "out" ], "dependencies": { "acorn": "^7.1.1", diff --git a/test/cli.test.js b/test/cli.test.js index 36279249..ffabbaf2 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -16,7 +16,7 @@ function normalizeOutput(output) { } it('should correctly print trace from cli', async () => { - const { stderr, stdout } = await exec(`node ../dist/cli.js print ${inputjs}`, { cwd: __dirname }); + const { stderr, stdout } = await exec(`node ../out/cli.js print ${inputjs}`, { cwd: __dirname }); if (stderr) { throw new Error(stderr); } @@ -24,7 +24,7 @@ it('should correctly print trace from cli', async () => { }); it('should correctly build dist from cli', async () => { - const { stderr } = await exec(`node ../dist/cli.js build ${inputjs}`, { cwd: __dirname }); + const { stderr } = await exec(`node ../out/cli.js build ${inputjs}`, { cwd: __dirname }); if (stderr) { throw new Error(stderr); } @@ -33,7 +33,7 @@ it('should correctly build dist from cli', async () => { }); it('should correctly print help when unknown action is used', async () => { - const { stderr, stdout } = await exec(`node ../dist/cli.js unknown ${inputjs}`, { cwd: __dirname }); + const { stderr, stdout } = await exec(`node ../out/cli.js unknown ${inputjs}`, { cwd: __dirname }); if (stderr) { throw new Error(stderr); } @@ -42,7 +42,7 @@ it('should correctly print help when unknown action is used', async () => { it('[codecov] should correctly print trace from required cli', async () => { // This test is only here to satisfy code coverage - const cli = require('../dist/cli.js') + const cli = require('../out/cli.js') const files = [join(__dirname, inputjs)]; const stdout = await cli('print', files); expect(stdout).toMatch(normalizeOutput(outputjs)); @@ -50,7 +50,7 @@ it('[codecov] should correctly print trace from required cli', async () => { it('[codecov] should correctly build dist from required cli', async () => { // This test is only here to satisfy code coverage - const cli = require('../dist/cli.js') + const cli = require('../out/cli.js') const files = [join(__dirname, inputjs)]; await cli('build', files); const found = existsSync(join(__dirname, outputjs)); @@ -59,7 +59,7 @@ it('[codecov] should correctly build dist from required cli', async () => { it('[codecov] should correctly print help when unknown action is used', async () => { // This test is only here to satisfy code coverage - const cli = require('../dist/cli.js') + const cli = require('../out/cli.js') const files = [join(__dirname, inputjs)]; const stdout = await cli('unknown', files); expect(stdout).toMatch('provide an action'); diff --git a/test/ecmascript.test.js b/test/ecmascript.test.js index 30b0f61a..e7fa5fe3 100644 --- a/test/ecmascript.test.js +++ b/test/ecmascript.test.js @@ -1,6 +1,6 @@ const { promises, mkdirSync } = require('fs'); const path = require('path'); -const { nodeFileTrace } = require('../dist/node-file-trace'); +const { nodeFileTrace } = require('../out/node-file-trace'); const os = require('os'); const rimraf = require('rimraf'); const { writeFile } = promises; diff --git a/test/integration.test.js b/test/integration.test.js index c88bbe25..919e0763 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,6 +1,6 @@ const { promises, readdirSync, mkdirSync } = require('fs'); const path = require('path'); -const { nodeFileTrace } = require('../dist/node-file-trace'); +const { nodeFileTrace } = require('../out/node-file-trace'); const os = require('os'); const { promisify } = require('util'); const rimraf = require('rimraf'); diff --git a/test/integration/dogfood.js b/test/integration/dogfood.js index 2c6dd2f7..39faa9f3 100644 --- a/test/integration/dogfood.js +++ b/test/integration/dogfood.js @@ -1 +1 @@ -require('../../dist/node-file-trace'); +require('../../out/node-file-trace'); diff --git a/test/unit.test.js b/test/unit.test.js index 962830b0..5e8a5ae3 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -1,6 +1,6 @@ const fs = require('fs'); const { join } = require('path'); -const { nodeFileTrace } = require('../dist/node-file-trace'); +const { nodeFileTrace } = require('../out/node-file-trace'); global._unit = true; diff --git a/tsconfig.json b/tsconfig.json index 60f82541..dce74615 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitThis": true, - "outDir": "dist", + "outDir": "out", "target": "esnext", "types": ["node"], "strict": true From 9a35ce071f99279b2686d8cf0a874ee4b888747b Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Jul 2020 23:21:51 -0400 Subject: [PATCH 12/19] Revert vistor.call() to use this=state --- src/analyze.ts | 2 +- src/utils/static-eval.ts | 90 ++++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index 67e90e38..93132138 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -419,7 +419,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi 'then' in curStaticValue && typeof curStaticValue.then !== 'symbol' && typeof curStaticValue.else !== 'symbol') { staticChildValue = curStaticValue; staticChildNode = parent; - if (self && self.skip) self.skip(); + if (self.skip) self.skip(); return; } } diff --git a/src/utils/static-eval.ts b/src/utils/static-eval.ts index 5ed65f13..b310b714 100644 --- a/src/utils/static-eval.ts +++ b/src/utils/static-eval.ts @@ -1,23 +1,23 @@ import { Node } from 'estree-walker'; import { EvaluatedValue, StaticValue, ConditionalValue } from '../types'; -type Walk = (node: Node, state: State) => EvaluatedValue; -type State = {computeBranches: boolean, vars: Record }; +type Walk = (node: Node) => EvaluatedValue; +type State = { computeBranches: boolean, vars: Record }; export function evaluate(ast: Node, vars = {}, computeBranches = true): EvaluatedValue { const state: State = { computeBranches, vars }; - return walk(ast, state); + return walk(ast); // walk returns: // 1. Single known value: { value: value } // 2. Conditional value: { test, then, else } // 3. Unknown value: undefined - function walk (node: Node, state: State) { + function walk(node: Node) { const visitor = visitors[node.type]; if (visitor) { - return visitor(node, walk, state); + return visitor.call(state, node, walk); } return undefined; } @@ -35,42 +35,42 @@ function countWildcards (str: string) { return cnt; } -const visitors: Record EvaluatedValue> = { - 'ArrayExpression': (node: Node, walk: Walk, state: State) => { +const visitors: Record EvaluatedValue> = { + 'ArrayExpression': 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], state); + const x = walk(node.elements[i]); if (!x) return; if ('value' in x === false) return; arr.push((x as StaticValue).value); } return { value: arr }; }, - 'BinaryExpression': (node: Node, walk: Walk, state: State) => { + 'BinaryExpression': function BinaryExpression(this: State, node: Node, walk: Walk) { const op = node.operator; - let l = walk(node.left, state); + let l = walk(node.left); if (!l && op !== '+') return; - let r = walk(node.right, state); + let r = walk(node.right); if (!l && !r) return; if (!l) { // UNKNOWN + 'str' -> wildcard string value - if (state.computeBranches && r && 'value' in r && typeof r.value === 'string') + if (this.computeBranches && r && 'value' in r && typeof r.value === 'string') return { value: WILDCARD + r.value, wildcards: [node.left, ...r.wildcards || []] }; return; } if (!r) { // 'str' + UKNOWN -> wildcard string value - if (state.computeBranches && op === '+') { + if (this.computeBranches && op === '+') { if (l && 'value' in l && typeof l.value === 'string') return { value: l.value + WILDCARD, wildcards: [...l.wildcards || [], node.right] }; } @@ -157,8 +157,8 @@ const visitors: Record Evaluat } return; }, - 'CallExpression': (node: Node, walk: Walk, state: State) => { - const callee = walk(node.callee, state); + 'CallExpression': function CallExpression(this: State, node: Node, walk: Walk) { + const callee = walk(node.callee); if (!callee || 'test' in callee) return; let fn: any = callee.value; if (typeof fn === 'object' && fn !== null) fn = fn[FUNCTION]; @@ -166,7 +166,7 @@ const visitors: Record Evaluat let ctx = null if (node.callee.object) { - ctx = walk(node.callee.object, state) + ctx = walk(node.callee.object) ctx = ctx && 'value' in ctx && ctx.value ? ctx.value : null } @@ -177,14 +177,14 @@ const visitors: Record Evaluat let allWildcards = node.arguments.length > 0; const wildcards: string[] = []; for (let i = 0, l = node.arguments.length; i < l; i++) { - let x = walk(node.arguments[i], state); + let x = walk(node.arguments[i]); if (x) { allWildcards = false; if ('value' in x && typeof x.value === 'string' && x.wildcards) x.wildcards.forEach(w => wildcards.push(w)); } else { - if (!state.computeBranches) + if (!this.computeBranches) return; // this works because provided static functions // operate on known string inputs @@ -228,18 +228,18 @@ const visitors: Record Evaluat return; } }, - 'ConditionalExpression': (node: Node, walk: Walk, state: State) => { - const val = walk(node.test, state); + 'ConditionalExpression': function ConditionalExpression(this: State, node: Node, walk: Walk) { + const val = walk(node.test); if (val && 'value' in val) - return val.value ? walk(node.consequent, state) : walk(node.alternate, state); + return val.value ? walk(node.consequent) : walk(node.alternate); - if (!state.computeBranches) + if (!this.computeBranches) return; - const thenValue = walk(node.consequent, state); + const thenValue = walk(node.consequent); if (!thenValue || 'wildcards' in thenValue || 'test' in thenValue) return; - const elseValue = walk(node.alternate, state); + const elseValue = walk(node.alternate); if (!elseValue || 'wildcards' in elseValue || 'test' in elseValue) return; @@ -249,23 +249,23 @@ const visitors: Record Evaluat else: elseValue.value }; }, - 'ExpressionStatement': (node: Node, walk: Walk, state: State) => { - return walk(node.expression, state); + 'ExpressionStatement': function ExpressionStatement(this: State, node: Node, walk: Walk) { + return walk(node.expression); }, - 'Identifier': (node: Node, _walk: Walk, state: State) => { - if (Object.hasOwnProperty.call(state.vars, node.name)) { - const val = state.vars[node.name]; + 'Identifier': function Identifier(this: State, node: Node, _walk: Walk) { + if (Object.hasOwnProperty.call(this.vars, node.name)) { + const val = this.vars[node.name]; if (val === UNKNOWN) return undefined; return { value: val }; } return undefined; }, - 'Literal': (node: Node, _walk: Walk) => { + 'Literal': function Literal (this: State, node: Node, _walk: Walk) { return { value: node.value }; }, - 'MemberExpression': (node: Node, walk: Walk, state: State) => { - const obj = walk(node.object, state); + 'MemberExpression': function MemberExpression(this: State, node: Node, walk: Walk) { + const obj = walk(node.object); // do not allow access to methods on Function if (!obj || 'test' in obj || typeof obj.value === 'function') { return undefined; @@ -275,7 +275,7 @@ const visitors: Record Evaluat const objValue = obj.value as any; if (node.computed) { // See if we can compute the computed property - const computedProp = walk(node.property, state); + const computedProp = walk(node.property); if (computedProp && 'value' in computedProp && computedProp.value) { const val = objValue[computedProp.value]; if (val === UNKNOWN) return undefined; @@ -298,7 +298,7 @@ const visitors: Record Evaluat return { value: undefined }; } } - const prop = walk(node.property, state); + const prop = walk(node.property); if (!prop || 'test' in prop) return undefined; if (typeof obj.value === 'object' && obj.value !== null) { @@ -320,13 +320,13 @@ const visitors: Record Evaluat } return undefined; }, - 'ObjectExpression': (node: Node, walk: Walk, state: State) => { + 'ObjectExpression': 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, state) : prop.key && { value: prop.key.name || prop.key.value }; + 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, state); + const value = walk(prop.value); if (!value || 'test' in value) return; //@ts-ignore if (value.value === UNKNOWN) return; @@ -335,7 +335,7 @@ const visitors: Record Evaluat } return { value: obj }; }, - 'TemplateLiteral': (node: Node, walk: Walk, state: State) => { + 'TemplateLiteral': 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) { @@ -345,9 +345,9 @@ const visitors: Record Evaluat val.then += node.quasis[i].value.cooked; val.else += node.quasis[i].value.cooked; } - let exprValue = walk(node.expressions[i], state); + let exprValue = walk(node.expressions[i]); if (!exprValue) { - if (!state.computeBranches) + if (!this.computeBranches) return undefined; exprValue = { value: WILDCARD, wildcards: [node.expressions[i]] }; } @@ -388,13 +388,13 @@ const visitors: Record Evaluat } return val; }, - 'ThisExpression': (_node: Node, _walk: Walk, state: State) => { - if (Object.hasOwnProperty.call(state.vars, 'this')) - return { value: state.vars['this'] }; + 'ThisExpression': function ThisExpression(this: State, _node: Node, _walk: Walk) { + if (Object.hasOwnProperty.call(this.vars, 'this')) + return { value: this.vars['this'] }; return undefined; }, - 'UnaryExpression': (node: Node, walk: Walk, state: State) => { - const val = walk(node.argument, state); + 'UnaryExpression': function UnaryExpression(this: State, node: Node, walk: Walk) { + const val = walk(node.argument); if (!val) return undefined; if ('value' in val && 'wildcards' in val === false) { From 6c96af1b9399aa2367cde54f6b2a7c540452555e Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Jul 2020 12:42:43 -0400 Subject: [PATCH 13/19] Fix undefined backtrack --- src/analyze.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index 93132138..ddd458f2 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -409,9 +409,10 @@ export default async function analyze(id: string, code: string, job: Job): Promi let scope = attachScopes(ast, 'scope'); handleWrappers(ast); handleSpecialCases({ id, ast, emitAsset: path => assets.add(path), emitAssetDirectory, job }); - function backtrack (self: WalkerContext, parent: Node) { + 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); if (curStaticValue) { @@ -419,7 +420,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi 'then' in curStaticValue && typeof curStaticValue.then !== 'symbol' && typeof curStaticValue.else !== 'symbol') { staticChildValue = curStaticValue; staticChildNode = parent; - if (self.skip) self.skip(); + if (context) context.skip(); return; } } @@ -450,7 +451,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(this, parent!); + backtrack(parent!, this); } } } @@ -492,7 +493,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // if it computes, then we start backtracking if (staticChildValue && parent) { staticChildNode = node; - backtrack(this, parent); + backtrack(parent, this); } } // handle well-known function symbol cases @@ -583,7 +584,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi // if it computes, then we start backtracking if (staticChildValue) { staticChildNode = node.arguments[0]; - backtrack(this, parent!); + backtrack(parent!, this); return this.skip(); } } @@ -732,7 +733,7 @@ export default async function analyze(id: string, code: string, job: Job): Promi } } - if (staticChildNode && parent) backtrack(this, parent); + if (staticChildNode && parent) backtrack(parent, this); } }); From c05486e0a07979f78e85409503bb8aee4178f7f4 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Jul 2020 12:55:45 -0400 Subject: [PATCH 14/19] Return undefined per Guy --- src/resolve-dependency.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolve-dependency.ts b/src/resolve-dependency.ts index aa439d4a..73bc7387 100644 --- a/src/resolve-dependency.ts +++ b/src/resolve-dependency.ts @@ -81,7 +81,7 @@ function getPkgCfg (pkgPath: string, job: Job) { } } -function getExportsTarget(exports: string | string[] | { [key: string]: string } | null, conditions: string[], cjsResolve: boolean): string | null { +function getExportsTarget(exports: string | string[] | { [key: string]: string } | null, conditions: string[], cjsResolve: boolean): string | null | undefined { if (typeof exports === 'string') { return exports; } @@ -108,7 +108,7 @@ function getExportsTarget(exports: string | string[] | { [key: string]: string } } } - return null; + return undefined; } function resolveExportsTarget (pkgPath: string, exp: string | { [key: string]: string }, subpath: string, job: Job, cjsResolve: boolean): string | undefined { From be95ec02aecac4d0ab039b06195967fa573c368b Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Jul 2020 10:10:29 -0400 Subject: [PATCH 15/19] Update types per Guy --- src/node-file-trace.ts | 4 ++-- src/resolve-dependency.ts | 49 ++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/node-file-trace.ts b/src/node-file-trace.ts index adc6b611..1b5ea401 100644 --- a/src/node-file-trace.ts +++ b/src/node-file-trace.ts @@ -306,7 +306,7 @@ export class Job { }), ...[...analyzeResult.deps].map(async dep => { try { - var resolved = await resolveDependency(dep, path, this, !analyzeResult.isESM); + var resolved = resolveDependency(dep, path, this, !analyzeResult.isESM); } catch (e) { this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`)); @@ -327,7 +327,7 @@ export class Job { }), ...[...analyzeResult.imports].map(async dep => { try { - var resolved = await resolveDependency(dep, path, this, false); + var resolved = resolveDependency(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 73bc7387..9cb8efc4 100644 --- a/src/resolve-dependency.ts +++ b/src/resolve-dependency.ts @@ -5,22 +5,24 @@ import { Job } from './node-file-trace'; // 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) { - let resolved: string | string[] | undefined; + 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); } else { resolved = resolvePackage(specifier, parent, job, cjsResolve); } - if (typeof resolved === 'string' && !resolved.startsWith('node:')) { - return job.realpath(resolved, parent); - } else if (Array.isArray(resolved)) { + + if (Array.isArray(resolved)) { return resolved.map(resolved => job.realpath(resolved, parent)); + } else if (resolved.startsWith('node:')) { + return resolved; + } else { + return job.realpath(resolved, parent); } - return resolved; }; -function resolvePath (path: string, parent: string, job: Job): string | undefined { +function resolvePath (path: string, parent: string, job: Job): string { const result = resolveFile(path, parent, job) || resolveDir(path, parent, job); if (!result) { throw new NotFoundError(path, parent); @@ -71,7 +73,15 @@ function getPkgName (name: string) { return segments.length ? segments[0] : null; } -function getPkgCfg (pkgPath: string, job: Job) { +type Exports = string | string[] | { [key: string]: string } | null | undefined; + +interface PkgCfg { + name: string | undefined; + main: string | undefined; + exports: Exports; +} + +function getPkgCfg (pkgPath: string, job: Job): PkgCfg | undefined { const pjsonSource = job.readFile(pkgPath + sep + 'package.json'); if (pjsonSource) { try { @@ -79,6 +89,7 @@ function getPkgCfg (pkgPath: string, job: Job) { } catch (e) {} } + return undefined; } function getExportsTarget(exports: string | string[] | { [key: string]: string } | null, conditions: string[], cjsResolve: boolean): string | null | undefined { @@ -111,8 +122,8 @@ function getExportsTarget(exports: string | string[] | { [key: string]: string } return undefined; } -function resolveExportsTarget (pkgPath: string, exp: string | { [key: string]: string }, subpath: string, job: Job, cjsResolve: boolean): string | undefined { - let exports: { [key: string]: string }; +function resolveExportsTarget (pkgPath: string, exp: string | string[] | { [key: string]: string }, subpath: string, job: Job, cjsResolve: boolean): string | undefined { + let exports: { [key: string]: string | string[] | { [key: string]: string } }; if (typeof exp === 'string' || typeof exp === 'object' && !Array.isArray(exp) && Object.keys(exp).length && Object.keys(exp)[0][0] !== '.') { exports = { '.' : exp }; @@ -137,7 +148,7 @@ function resolveExportsTarget (pkgPath: string, exp: string | { [key: string]: s return undefined; } -function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean) { +function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean): string | string [] { let packageParent = parent; if (nodeBuiltins.has(name)) return 'node:' + name; @@ -149,15 +160,16 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo const pjsonBoundary = job.getPjsonBoundary(parent); if (pjsonBoundary) { const pkgCfg = getPkgCfg(pjsonBoundary, job); - if (pkgCfg && pkgCfg.name && pkgCfg.exports !== null && pkgCfg.exports !== undefined) { - selfResolved = resolveExportsTarget(pjsonBoundary, pkgCfg.exports, '.' + name.slice(pkgName.length), job, cjsResolve); + const { exports: pkgExports } = pkgCfg || {}; + if (pkgCfg && pkgCfg.name && pkgExports !== null && pkgExports !== undefined) { + selfResolved = resolveExportsTarget(pjsonBoundary, pkgExports, '.' + name.slice(pkgName.length), job, cjsResolve); if (selfResolved) job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); } } } - let separatorIndex; + let separatorIndex: number; const rootSeparatorIndex = packageParent.indexOf(sep); while ((separatorIndex = packageParent.lastIndexOf(sep)) > rootSeparatorIndex) { packageParent = packageParent.substr(0, separatorIndex); @@ -165,11 +177,12 @@ function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boo const stat = job.stat(nodeModulesDir); if (!stat || !stat.isDirectory()) continue; const pkgCfg = getPkgCfg(nodeModulesDir + sep + pkgName, job); - if (pkgCfg && job.exports && pkgCfg.exports !== undefined && pkgCfg.exports !== null && !selfResolved) { + const { exports: pkgExports } = pkgCfg || {}; + if (job.exports && pkgExports !== undefined && pkgExports !== null && !selfResolved) { let legacyResolved; if (!job.exportsOnly) legacyResolved = resolveFile(nodeModulesDir + sep + name, parent, job) || resolveDir(nodeModulesDir + sep + name, parent, job); - let resolved = resolveExportsTarget(nodeModulesDir + sep + pkgName, pkgCfg.exports, '.' + name.slice(pkgName.length), job, cjsResolve); + let resolved = resolveExportsTarget(nodeModulesDir + sep + pkgName, pkgExports, '.' + name.slice(pkgName.length), job, cjsResolve); if (resolved && cjsResolve) resolved = resolveFile(resolved, parent, job) || resolveDir(resolved, parent, job); if (resolved) { @@ -197,7 +210,11 @@ 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); - return resolveFile(pathTarget, parent, job) || resolveDir(pathTarget, parent, job); + const resolved = resolveFile(pathTarget, parent, job) || resolveDir(pathTarget, parent, job); + if (!resolved) { + throw new NotFoundError(name, parent); + } + return resolved; } } throw new NotFoundError(name, parent); From 5ea94c54f753334a6300b762a61dc3d4a761fd56 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Jul 2020 10:58:10 -0400 Subject: [PATCH 16/19] Use upcaret for estree-walker@^0.6.1 --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5d3eb0b0..c69661b4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "acorn-numeric-separator": "^0.3.0", "acorn-static-class-features": "^0.2.1", "bindings": "^1.4.0", - "estree-walker": "0.6.1", + "estree-walker": "^0.6.1", "glob": "^7.1.3", "graceful-fs": "^4.1.15", "micromatch": "^4.0.2", diff --git a/yarn.lock b/yarn.lock index e264bdc3..a64ac2eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4754,7 +4754,7 @@ estraverse@~1.5.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.5.1.tgz#867a3e8e58a9f84618afb6c2ddbcd916b7cbaf71" integrity sha1-hno+jlip+EYYr7bC3bzZFrfLr3E= -estree-walker@0.6.1, estree-walker@^0.6.1: +estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== From 03c1b82cccda14d9c53a741a651c5fe149107adf Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Jul 2020 11:02:43 -0400 Subject: [PATCH 17/19] Revert a few lines since types are updated --- src/node-file-trace.ts | 14 ++++++++------ src/resolve-dependency.ts | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/node-file-trace.ts b/src/node-file-trace.ts index 1b5ea401..b1da45a4 100644 --- a/src/node-file-trace.ts +++ b/src/node-file-trace.ts @@ -292,11 +292,13 @@ export class Job { this.analysisCache.set(path, analyzeResult); } - if (analyzeResult.isESM) + const { deps, imports, assets, isESM } = analyzeResult; + + if (isESM) this.esmFileList.add(relative(this.base, path)); await Promise.all([ - ...[...analyzeResult.assets].map(async asset => { + ...[...assets].map(async asset => { const ext = extname(asset); if (ext === '.js' || ext === '.mjs' || ext === '.node' || ext === '' || this.ts && (ext === '.ts' || ext === '.tsx') && asset.startsWith(this.base) && asset.substr(this.base.length).indexOf(sep + 'node_modules' + sep) === -1) @@ -304,9 +306,9 @@ export class Job { else this.emitFile(this.realpath(asset, path), 'asset', path); }), - ...[...analyzeResult.deps].map(async dep => { + ...[...deps].map(async dep => { try { - var resolved = resolveDependency(dep, path, this, !analyzeResult.isESM); + var resolved = resolveDependency(dep, path, this, !isESM); } catch (e) { this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`)); @@ -319,7 +321,7 @@ export class Job { await this.emitDependency(item, path); } } - else if (resolved) { + else { // ignore builtins if (resolved.startsWith('node:')) return; await this.emitDependency(resolved, path); @@ -340,7 +342,7 @@ export class Job { await this.emitDependency(item, path); } } - else if (resolved) { + else { // ignore builtins if (resolved.startsWith('node:')) return; await this.emitDependency(resolved, path); diff --git a/src/resolve-dependency.ts b/src/resolve-dependency.ts index 9cb8efc4..b41d4067 100644 --- a/src/resolve-dependency.ts +++ b/src/resolve-dependency.ts @@ -9,7 +9,8 @@ export default function resolveDependency (specifier: string, parent: string, jo if (isAbsolute(specifier) || specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../')) { const trailingSlash = specifier.endsWith('/'); resolved = resolvePath(resolve(parent, '..', specifier) + (trailingSlash ? '/' : ''), parent, job); - } else { + } + else { resolved = resolvePackage(specifier, parent, job, cjsResolve); } From a8423db48beafef7d9249ff54714334732ed62cb Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Jul 2020 11:04:26 -0400 Subject: [PATCH 18/19] Add cancel step --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a88577eb..d40cdbad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,10 @@ jobs: node: [10, 12] runs-on: ${{ matrix.os }} steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.4.0 + with: + access_token: ${{ github.token }} - uses: actions/checkout@v1 - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@v1 From f8ea78412281e005f7ee5350833f04a39725a17a Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 Jul 2020 11:05:42 -0400 Subject: [PATCH 19/19] Oops - missed one --- src/node-file-trace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node-file-trace.ts b/src/node-file-trace.ts index b1da45a4..59741201 100644 --- a/src/node-file-trace.ts +++ b/src/node-file-trace.ts @@ -327,7 +327,7 @@ export class Job { await this.emitDependency(resolved, path); } }), - ...[...analyzeResult.imports].map(async dep => { + ...[...imports].map(async dep => { try { var resolved = resolveDependency(dep, path, this, false); }