diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9208eeb..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 @@ -29,6 +33,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/.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 fa3619dc..c69661b4 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,20 @@ "version": "0.7.0", "repository": "vercel/node-file-trace", "license": "MIT", - "main": "./src/node-file-trace.js", - "types": "./node-file-trace.d.ts", + "main": "./out/node-file-trace.js", + "types": "./out/node-file-trace.d.ts", "bin": { - "nft": "./src/cli.js" + "nft": "./out/cli.js" }, "scripts": { + "build": "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/*" + "out" ], "dependencies": { "acorn": "^7.1.1", @@ -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", @@ -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 82% rename from src/analyze.js rename to src/analyze.ts index 04f58504..ddd458f2 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 { EvaluatedValue } from './types'; const staticProcess = { cwd: () => { @@ -118,26 +119,26 @@ 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; // 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; @@ -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; @@ -222,6 +231,7 @@ module.exports = async function (id, code, job) { 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' }); @@ -253,12 +263,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 +276,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 +285,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 +293,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 +321,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 +336,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: EvaluatedValue; // 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 +364,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 +377,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,35 +392,35 @@ 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); } } let scope = attachScopes(ast, 'scope'); handleWrappers(ast); - ({ ast = ast, scope = scope } = handleSpecialCases({ id, ast, scope, emitAsset: path => assets.add(path), emitAssetDirectory, job }) || {}); - - function backtrack (self, parent) { + handleSpecialCases({ id, ast, emitAsset: path => assets.add(path), emitAssetDirectory, job }); + 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) { 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 (context) context.skip(); return; } } @@ -431,7 +442,7 @@ module.exports = async function (id, code, job) { 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 @@ -440,7 +451,7 @@ module.exports = async function (id, code, job) { binding && (typeof binding === 'function' || typeof binding === 'object') && binding[TRIGGER]) { staticChildValue = { value: typeof binding === 'string' ? binding : undefined }; staticChildNode = node; - backtrack(this, parent); + backtrack(parent!, this); } } } @@ -477,16 +488,16 @@ 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 as any)[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); + backtrack(parent, this); } } // 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,15 +512,13 @@ module.exports = async function (id, code, job) { case BINDINGS: if (node.arguments.length) { const arg = computePureStaticValue(node.arguments[0], false); - if (arg && arg.value) { - let staticBindingsInstance = false; - let opts; + if (arg && 'value' in arg && arg.value) { + 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; @@ -521,7 +530,7 @@ module.exports = async function (id, code, job) { if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; - emitStaticChildAsset(staticBindingsInstance); + emitStaticChildAsset(); } } } @@ -529,8 +538,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); } @@ -538,7 +546,7 @@ module.exports = async function (id, code, job) { if (resolved) { staticChildValue = { value: resolved }; staticChildNode = node; - emitStaticChildAsset(path); + emitStaticChildAsset(); } } break; @@ -546,9 +554,9 @@ 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 && (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(); } @@ -576,7 +584,7 @@ module.exports = async function (id, code, job) { // if it computes, then we start backtracking if (staticChildValue) { staticChildNode = node.arguments[0]; - backtrack(this, parent); + backtrack(parent!, this); return this.skip(); } } @@ -585,7 +593,7 @@ module.exports = async function (id, code, job) { 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(); } @@ -602,7 +610,7 @@ module.exports = async function (id, code, job) { } } } - 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); @@ -632,7 +640,7 @@ module.exports = async function (id, code, job) { } } } - 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) { @@ -665,8 +673,10 @@ module.exports = async function (id, code, job) { 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; @@ -680,7 +690,9 @@ module.exports = async function (id, code, job) { 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' && @@ -708,7 +720,9 @@ module.exports = async function (id, code, job) { }, 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) @@ -719,14 +733,14 @@ module.exports = async function (id, code, job) { } } - if (staticChildNode) backtrack(this, parent); + if (staticChildNode && parent) backtrack(parent, this); } }); 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 +762,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 +794,18 @@ module.exports = async function (id, code, job) { } function emitStaticChildAsset () { - if (isAbsolutePathStr(staticChildValue.value)) { - let resolved; - try { resolved = path.resolve(staticChildValue.value); } + if (!staticChildValue) { + return; + } + + 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..59741201 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) @@ -27,8 +28,10 @@ module.exports = async function (files, opts = {}) { 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 { @@ -39,7 +42,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 +72,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 +103,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 +112,6 @@ class Job { this.paths = resolvedPaths; this.log = log; this.mixedModules = mixedModules; - this.reasons = Object.create(null); this.analysis = {}; if (analysis !== false) { @@ -117,11 +142,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 +165,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 +196,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 +213,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 +232,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 +252,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,19 +279,21 @@ 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); } + const { deps, imports, assets, isESM } = analyzeResult; + if (isESM) this.esmFileList.add(relative(this.base, path)); @@ -281,7 +308,7 @@ class Job { }), ...[...deps].map(async dep => { try { - var resolved = await resolveDependency(dep, path, this, !isESM); + var resolved = resolveDependency(dep, path, this, !isESM); } catch (e) { this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`)); @@ -302,7 +329,7 @@ class Job { }), ...[...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.js b/src/resolve-dependency.ts similarity index 63% rename from src/resolve-dependency.js rename to src/resolve-dependency.ts index f55aa3d3..b41d4067 100644 --- a/src/resolve-dependency.js +++ b/src/resolve-dependency.ts @@ -1,10 +1,11 @@ -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[]; if (isAbsolute(specifier) || specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../')) { const trailingSlash = specifier.endsWith('/'); resolved = resolvePath(resolve(parent, '..', specifier) + (trailingSlash ? '/' : ''), parent, job); @@ -12,18 +13,26 @@ module.exports = function resolveDependency (specifier, parent, job, cjsResolve else { resolved = resolvePackage(specifier, parent, job, cjsResolve); } - if (typeof resolved === 'string' && resolved.startsWith('node:')) return resolved; - if (Array.isArray(resolved)) + + if (Array.isArray(resolved)) { return resolved.map(resolved => job.realpath(resolved, parent)); - return job.realpath(resolved, parent); + } else if (resolved.startsWith('node:')) { + return resolved; + } else { + return job.realpath(resolved, parent); + } }; -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 { + const result = resolveFile(path, parent, job) || resolveDir(path, parent, job); + if (!result) { + throw new NotFoundError(path, parent); + } + return result; } -function resolveFile (path, parent, 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'; @@ -31,9 +40,10 @@ function resolveFile (path, parent, 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, 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,35 +57,49 @@ function resolveDir (path, parent, job) { return resolveFile(resolve(path, 'index'), parent, job); } -function notFound (specifier, parent) { - const e = new Error("Cannot find module '" + specifier + "' loaded from " + parent); - e.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) { +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) { +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 { - return JSON.parse(pjsonSource); + return JSON.parse(pjsonSource.toString()); } catch (e) {} } + return undefined; } -function getExportsTarget (exports, conditions, cjsResolve) { +function getExportsTarget(exports: string | string[] | { [key: string]: string } | null, conditions: string[], cjsResolve: boolean): string | null | undefined { 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); @@ -95,15 +119,19 @@ function getExportsTarget (exports, conditions, cjsResolve) { } } } - else if (exports === null) { - return exports; - } + + return undefined; } -function resolveExportsTarget (pkgPath, exports, subpath, job, cjsResolve) { - 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 | 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 }; + } else { + exports = exp; + } + if (subpath in exports) { const target = getExportsTarget(exports[subpath], job.exports, cjsResolve); if (typeof target === 'string' && target.startsWith('./')) @@ -118,29 +146,31 @@ function resolveExportsTarget (pkgPath, exports, subpath, job, cjsResolve) { return pkgPath + match.slice(2) + subpath.slice(match.length); } } + return undefined; } -function resolvePackage (name, parent, job, cjsResolve) { +function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean): string | string [] { 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) { 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); @@ -148,11 +178,12 @@ function resolvePackage (name, parent, job, cjsResolve) { 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) { @@ -180,8 +211,12 @@ function resolvePackage (name, parent, job, cjsResolve) { 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; } } - notFound(name, parent); + throw new NotFoundError(name, parent); } diff --git a/node-file-trace.d.ts b/src/types.ts similarity index 81% rename from node-file-trace.d.ts rename to src/types.ts index 36096d3f..ae602ba2 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 StaticValue { + value: any; + wildcards?: string[]; +} + +export interface ConditionalValue { + test: string; + then: any; + else: any; +} + +export type EvaluatedValue = StaticValue | 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 83% rename from src/utils/binary-locators.js rename to src/utils/binary-locators.ts index b4ca979b..829ad44d 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,9 @@ function makeModulePathList(root, name) { ] ]); } -function findCompiledModule(basePath, 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++) { @@ -73,12 +75,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 68% rename from src/utils/interop-require.js rename to src/utils/interop-require.ts index 19794b83..56a9f0c3 100644 --- a/src/utils/interop-require.js +++ b/src/utils/interop-require.ts @@ -1,17 +1,16 @@ '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. - const out = {}; + const out: { [key: string]: string } = {}; for (const key in obj) { if (!hasOwnProperty.call(obj, key)) continue; out[key] = obj[key]; @@ -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 70% rename from src/utils/special-cases.js rename to src/utils/special-cases.ts index ab1885cd..eb06b78c 100644 --- a/src/utils/special-cases.js +++ b/src/utils/special-cases.ts @@ -1,35 +1,37 @@ -const path = require('path'); -const resolve = require('../resolve-dependency'); -const { getPackageName } = require('./get-package-base'); -const fs = require('fs'); +import { resolve, dirname, relative } from 'path'; +import resolveDependency from '../resolve-dependency'; +import { getPackageName } from './get-package-base'; +import { readFileSync } 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/')); + 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')); } } } @@ -58,28 +60,28 @@ const specialCases = { 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 }) { + '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 === '=' && @@ -93,14 +95,20 @@ const specialCases = { 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, @@ -117,7 +125,7 @@ const specialCases = { } }; } - return; + return undefined; } for (const statement of ast.body) { @@ -137,10 +145,10 @@ const specialCases = { 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; @@ -150,36 +158,44 @@ const specialCases = { }, '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')); } } }; -module.exports = function ({ id, ast, emitAsset, emitAssetDirectory, job }) { +interface SpecialCaseOpts { + id: string; + ast: Node; + emitAsset: (filename: string) => void; + emitAssetDirectory: (dirname: string) => void; + job: Job; +} + +export default function handleSpecialCases({ id, ast, emitAsset, emitAssetDirectory, job }: SpecialCaseOpts) { const pkgName = getPackageName(id); - const specialCase = specialCases[pkgName]; + 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 57% rename from src/utils/static-eval.js rename to src/utils/static-eval.ts index 9dd3661a..b310b714 100644 --- a/src/utils/static-eval.js +++ b/src/utils/static-eval.ts @@ -1,5 +1,10 @@ -module.exports = function (ast, vars = {}, computeBranches = true) { - const state = { +import { Node } from 'estree-walker'; +import { EvaluatedValue, StaticValue, ConditionalValue } from '../types'; +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 }; @@ -9,27 +14,29 @@ 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) + if (visitor) { return visitor.call(state, node, walk); + } + return undefined; } }; -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++; return cnt; } -const visitors = { - ArrayExpression (node, walk) { +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) { @@ -39,11 +46,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 StaticValue).value); } return { value: arr }; }, - BinaryExpression (node, walk) { + 'BinaryExpression': function BinaryExpression(this: State, node: Node, walk: Walk) { const op = node.operator; let l = walk(node.left); @@ -56,7 +63,7 @@ const visitors = { if (!l) { // UNKNOWN + 'str' -> wildcard string value - if (this.computeBranches && 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; } @@ -64,7 +71,7 @@ const visitors = { if (!r) { // 'str' + UKNOWN -> wildcard string value if (this.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 @@ -73,60 +80,65 @@ const visitors = { return; } - if ('test' in l && 'test' in r) - return; - - if ('test' in l) { - 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 }; + 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 }; + 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) { - 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 }; + else if ('test' in r && 'value' in l) { + 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 { + 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 }; if (op === '!==') return { value: l.value !== r.value }; if (op === '+') { - const val = { value: l.value + r.value }; - if (l.wildcards || r.wildcards) - val.wildcards = [...l.wildcards || [], ...r.wildcards || []]; + const val: StaticValue = { value: l.value + r.value }; + 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 }; @@ -145,17 +157,17 @@ const visitors = { } return; }, - CallExpression (node, walk) { + 'CallExpression': function CallExpression(this: State, node: Node, walk: Walk) { const callee = walk(node.callee); 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 = ctx && 'value' in ctx && ctx.value ? ctx.value : null } // we allow one conditional argument to create a conditional expression @@ -163,12 +175,12 @@ const visitors = { 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]); 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 +228,7 @@ const visitors = { return; } }, - ConditionalExpression (node, walk) { + '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) : walk(node.alternate); @@ -237,49 +249,50 @@ const visitors = { else: elseValue.value }; }, - ExpressionStatement (node, walk) { + 'ExpressionStatement': function ExpressionStatement(this: State, node: Node, walk: Walk) { return walk(node.expression); }, - Identifier (node) { + '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; + return undefined; return { value: val }; } - return; + return undefined; }, - Literal (node) { + 'Literal': function Literal (this: State, node: Node, _walk: Walk) { return { value: node.value }; }, - MemberExpression (node, walk) { + '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; + 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); - if (computedProp && computedProp.value) { - const val = obj.value[computedProp.value]; - if (val === UNKNOWN) return; + 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; + 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]) - return; + else if (objValue[UNKNOWN]) + return undefined; } else { return { value: undefined }; @@ -287,37 +300,43 @@ const visitors = { } const prop = walk(node.property); 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 }; } + return undefined; }, - ObjectExpression (node, walk) { - const obj = {}; + '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) : prop.key && { value: prop.key.name || prop.key.value }; if (!keyValue || 'test' in keyValue) return; const value = walk(prop.value); 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, walk) { - let val = { value: '' }; + '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) { val.value += node.quasis[i].value.cooked; @@ -329,7 +348,7 @@ const visitors = { let exprValue = walk(node.expressions[i]); if (!exprValue) { if (!this.computeBranches) - return; + return undefined; exprValue = { value: WILDCARD, wildcards: [node.expressions[i]] }; } if ('value' in exprValue) { @@ -345,15 +364,19 @@ const visitors = { 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) { @@ -365,14 +388,15 @@ const visitors = { } return val; }, - ThisExpression () { + '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, walk) { + 'UnaryExpression': function UnaryExpression(this: State, node: Node, walk: Walk) { const val = walk(node.argument); 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 }; @@ -385,7 +409,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.js b/src/utils/wrappers.ts similarity index 94% rename from src/utils/wrappers.js rename to src/utils/wrappers.ts index 3f1bcd6b..974e7906 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 && @@ -114,13 +114,13 @@ function handleWrappers (ast) { 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 && @@ -133,8 +133,8 @@ function handleWrappers (ast) { // verify modules is the expected data structure // in the process, extract external requires - const externals = {}; - if (modules.every(m => { + const externals: Record = {}; + if (modules.every((m: any) => { if (m.type !== 'Property' || m.computed !== false || m.key.type !== 'Literal' || @@ -321,18 +321,18 @@ function handleWrappers (ast) { 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 @@ function handleWrappers (ast) { 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 @@ function handleWrappers (ast) { value: externalId }] }; + const parent = maybeParent!; if (parent.right === node) { parent.right = replacement; } @@ -394,8 +395,8 @@ function handleWrappers (ast) { 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 @@ function handleWrappers (ast) { 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', @@ -448,4 +449,3 @@ function handleWrappers (ast) { } } -module.exports = handleWrappers; diff --git a/test/cli.test.js b/test/cli.test.js index 301876c3..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 ../src/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 ../src/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 ../src/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('../src/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('../src/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('../src/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 95387b05..e7fa5fe3 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('../out/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..919e0763 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('../out/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..39faa9f3 100644 --- a/test/integration/dogfood.js +++ b/test/integration/dogfood.js @@ -1 +1 @@ -require('../../src/node-file-trace'); +require('../../out/node-file-trace'); diff --git a/test/unit.test.js b/test/unit.test.js index 15fc9bc8..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('../src/node-file-trace'); +const { nodeFileTrace } = require('../out/node-file-trace'); global._unit = true; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..dce74615 --- /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": "out", + "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..a64ac2eb 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" @@ -4719,11 +4754,6 @@ 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: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" @@ -13555,10 +13585,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"