Repository: indutny/common-shake Branch: master Commit: 1d9104588c62 Files: 15 Total size: 48.7 KB Directory structure: gitextract_xe61hc_u/ ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── lib/ │ ├── shake/ │ │ ├── analyzer.js │ │ ├── eval.js │ │ ├── graph.js │ │ ├── module.js │ │ └── walk.js │ └── shake.js ├── package.json └── test/ ├── analyzer-test.js ├── eval-test.js ├── fixtures.js └── graph-test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { 'env': { 'browser': false, 'commonjs': true, 'node': true, 'es6': true }, 'parserOptions': { 'ecmaVersion': 8 }, 'extends': 'eslint:recommended', 'rules': { 'indent': [ 'error', 2, { 'FunctionDeclaration': { 'parameters': 'first' }, 'FunctionExpression': { 'parameters': 'first' }, 'CallExpression': { 'arguments': 'first' } } ], 'linebreak-style': [ 'error', 'unix' ], 'quotes': [ 'error', 'single' ], 'semi': [ 'error', 'always' ], 'max-len': [ 'error', 80, 2 ] } }; ================================================ FILE: .gitignore ================================================ node_modules/ npm-debug.log .nyc_output/ coverage/ ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js node_js: - "stable" ================================================ FILE: README.md ================================================ # CommonJS Tree Shaker [![NPM version](https://badge.fury.io/js/common-shake.svg)](http://badge.fury.io/js/common-shake) [![Build Status](https://secure.travis-ci.org/indutny/common-shake.svg)](http://travis-ci.org/indutny/common-shake) See [webpack-common-shake][0] for [webpack][1] plugin. ## Usage ```js const acorn = require('acorn'); const Analyzer = require('common-shake').Analyzer; const a = new Analyzer(); a.run(acorn.parse(` 'use strict'; const lib = require('./a.js'); exports.a = lib.a; `, { locations: true }), 'index.js'); a.run(acorn.parse(` 'use strict'; exports.a = 42; `, { locations: true }), 'a.js'); a.resolve('index.js', './a.js', 'a.js'); console.log(a.isSuccess(), a.bailouts); // true false console.log(a.getModule('index.js').getInfo()); // { bailouts: false, declarations: [ 'a' ], uses: [] } console.log(a.getModule('a.js').getInfo()); // { bailouts: false, declarations: [ 'a' ], uses: [ 'a' ] } const module = a.getModule('a.js'); a.getDeclarations().forEach((decl) => { console.log(module.isUsed(decl.name) ? 'used' : 'not used'); console.log(decl.name, decl.ast); }); // If you want to mark all exported values of module as used: a.getModule('root').forceExport(); ``` ## Graphviz For debugging and inspection purposes a graph in [dot][2] format may be generated from the modules hierarchy using following API: ```js const Graph = require('common-shake').Graph; const graph = new Graph('/path/to/working/dir'); console.log(graph.generate(analyzer.getModules())); ``` ## LICENSE This software is licensed under the MIT License. Copyright Fedor Indutny, 2017. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [0]: https://github.com/indutny/webpack-common-shake [1]: https://webpack.github.io/ [2]: http://www.graphviz.org/content/dot-language ================================================ FILE: lib/shake/analyzer.js ================================================ 'use strict'; const escope = require('escope'); const debug = require('debug')('common-shake:analyzer'); const shake = require('../shake'); const walk = shake.walk; const Module = shake.Module; function Analyzer() { // All `Module` instances by resource this.modules = new Map(); // All unresolved `Module` instances by parent resource + path this.unresolved = new Map(); // Uses of required module. Map from ast node to `Module` instance this.moduleUses = null; // Uses of `exports` in module. A collection of AST nodes. this.exportsUses = null; // Uses of `require` in module. A collection of AST nodes. this.requireUses = null; // Any global bailouts this.bailouts = false; } module.exports = Analyzer; // Public API Analyzer.prototype.run = function run(ast, resource) { this.requireUses = new Set(); this.exportsUses = new Set(); this.moduleUses = new Map(); const current = this.getModule(resource); this.gather(ast, current); this.sift(ast, current); this.moduleUses = null; this.exportsUses = null; this.requireUses = null; return current; }; Analyzer.prototype.resolve = function resolve(issuer, name, to) { debug('resolve %j:%j => %j', issuer, name, to); const unresolved = this.getUnresolvedModule(issuer, name); const resolved = this.getModule(to); // Already resolved if (unresolved === resolved) return; resolved.mergeFrom(unresolved); resolved.addIssuer(this.getModule(issuer)); this.unresolved.get(issuer).set(name, to); }; Analyzer.prototype.getModule = function getModule(resource) { let module; if (this.modules.has(resource)) { module = this.modules.get(resource); } else { module = new Module(resource); this.modules.set(resource, module); } return module; }; Analyzer.prototype.getModules = function getModules() { return Array.from(this.modules.values()); }; Analyzer.prototype.isSuccess = function isSuccess() { return this.bailouts === false; }; // Private API Analyzer.prototype.gather = function gather(ast, current) { const manager = escope.analyze(ast, { ecmaVersion: 6, sourceType: 'module', optimistic: true, ignoreEval: true, impliedStrict: true }); const scope = manager.acquireAll(ast); const declarations = []; const queue = scope.slice(); while (queue.length !== 0) { const scope = queue.shift(); for (let i = 0; i < scope.childScopes.length; i++) queue.push(scope.childScopes[i]); // Skip variables declared in dynamic scopes if (scope.dynamic) continue; for (let i = 0; i < scope.variables.length; i++) declarations.push(scope.variables[i]); } // Just to avoid double-bailouts const seenDefs = new Set(); for (let i = 0; i < declarations.length; i++) { const decl = declarations[i]; const defs = decl.defs.filter(def => this.isRequireDef(def, decl)); if (defs.length === 0) continue; if (decl.defs.length !== 1) { defs.forEach((def) => { if (seenDefs.has(def.node)) return; seenDefs.add(def.node); const name = def.node.init.arguments[0].value; const module = this.getUnresolvedModule(current.resource, name); module.bailout('`require` variable override', def.node.loc, current.resource); }); continue; } const node = defs[0].node; if (seenDefs.has(node)) continue; seenDefs.add(node); const name = shake.evaluateConst(node.init.arguments[0]); const module = this.getUnresolvedModule(current.resource, name); // Destructuring if (node.id.type === 'ObjectPattern') { this.gatherDestructured(module, node.id, current); continue; } if (node.id.type !== 'Identifier') { module.bailout('`require` used in unknown way', node.loc, current.resource); continue; } for (let i = 0; i < decl.references.length; i++) { const ref = decl.references[i]; if (ref.identifier !== node.id) this.moduleUses.set(ref.identifier, module); } } }; Analyzer.prototype.gatherDestructured = function gatherDestructured(module, id, current) { for (let i = 0; i < id.properties.length; i++) { const prop = id.properties[i]; if (prop.key.type !== (prop.computed ? 'Literal' : 'Identifier')) { module.bailout('Dynamic properties in `require` destructuring', id.loc, current.resource); continue; } const key = prop.key.name || prop.key.value; module.use(key, current, false); } }; Analyzer.prototype.isRequireDef = function isRequireDef(def, decl) { if (def.type !== 'Variable') return false; const node = def.node; if (node.type !== 'VariableDeclarator') return false; if (node.id.type === 'Identifier') { if (node.id.name === 'exports') { this.markOverriddenUses(this.exportsUses, decl.references); return false; } else if (node.id.name === 'require') { this.markOverriddenUses(this.requireUses, decl.references); return false; } } const init = node.init; if (!init || init.type !== 'CallExpression') return false; if (init.callee.type !== 'Identifier' || init.callee.name !== 'require') return false; const args = init.arguments; if (args.length < 1) return false; try { shake.evaluateConst(args[0]); } catch (e) { return false; } // Overridden `require` if (this.requireUses.has(init.callee)) return false; this.requireUses.add(init.callee); return true; }; Analyzer.prototype.markOverriddenUses = function markOverriddenUses(set, refs) { for (let i = 0; i < refs.length; i++) set.add(refs[i].identifier); }; Analyzer.prototype.isExports = function isExports(node) { // `exports` if (node.type === 'Identifier' && node.name === 'exports') return true; // `module.exports` if (node.type !== 'MemberExpression') return false; const isStatic = node.property.type === (node.computed ? 'Literal' : 'Identifier'); if (!isStatic) return false; const key = node.property.name || node.property.value; return key === 'exports'; }; Analyzer.prototype.sift = function sift(ast, current) { walk(ast, { AssignmentExpression: node => this.siftAssignment(node, current), MemberExpression: node => this.siftMember(node, current, false), Identifier: node => this.siftRequireUse(node, current), CallExpression: node => this.siftCall(node, current), NewExpression: node => this.siftNew(node, current), UnaryExpression: node => this.siftUnaryExpression(node), }); this.moduleUses.forEach((module, use) => { module.bailout('Escaping value or unknown use', use.loc, current.resource); }); }; Analyzer.prototype.siftAssignment = function siftAssignment(node, current) { if (node.left.type === 'Identifier') { if (node.left.name === 'exports') { if (this.exportsUses.has(node.left)) return; this.exportsUses.add(node.left); current.bailout('`exports` assignment', node.loc); return; } if (node.left.name === 'require') { if (this.requireUses.has(node.left)) return; this.requireUses.add(node.left); current.bailout('`require` assignment', node.loc); return; } } if (node.left.type !== 'MemberExpression') return; const member = node.left; if (this.moduleUses.has(member.object)) { const module = this.moduleUses.get(member.object); this.moduleUses.delete(member.object); module.bailout('Module property assignment', node.loc, current.resource); return; } const isExports = this.isExports(member.object); if (!isExports && member.object.type !== 'Identifier') return; const object = isExports ? 'exports' : member.object.name; if (object !== 'exports' && object !== 'module') return; if (member.property.type !== (member.computed ? 'Literal' : 'Identifier')) { if (object === 'exports') { if (this.exportsUses.has(member.object)) return; this.exportsUses.add(member.object); current.bailout('Dynamic CommonJS export', member.loc); } else { current.bailout('Dynamic `module` use', member.loc); } return; } const name = member.property.name || member.property.value; if (object === 'module') { if (name !== 'exports') return; this.siftModuleExports(node, current); return; } if (this.exportsUses.has(member.object)) return; this.exportsUses.add(member.object); // `exports.a = imported.b` if (node.right.type === 'MemberExpression') this.siftMember(node.right, current, name); const decl = { type: 'exports', name, ast: node }; if (!current.declare(decl)) { current.bailout('Simultaneous assignment to both `exports` and ' + '`module.exports`', node.loc); } }; Analyzer.prototype.siftModuleExports = function siftModuleExports(node, current) { if (node.right.type !== 'ObjectExpression') { current.bailout('`module.exports` assignment', node.loc, null, 'info'); return; } // `module.exports = {}` const props = node.right.properties; const pairs = []; for (let i = 0; i < props.length; i++) { const prop = props[i]; if (prop.computed || (prop.key.type !== 'Literal' && prop.key.type !== 'Identifier')) { current.bailout('Dynamic `module.exports` property', prop.loc, null); continue; } const key = prop.key.name || prop.key.value; // `module.exports = { a: imported.b }` if (prop.kind === 'init' && prop.value.type === 'MemberExpression') this.siftMember(prop.value, current, key); pairs.push({ type: 'module.exports', name: key, ast: prop }); } if (!current.multiDeclare(pairs)) { current.bailout('Simultaneous assignment to both `exports` and ' + '`module.exports`', node.loc); } }; Analyzer.prototype.siftMember = function siftMember(node, current, recursive) { let module; if (this.isExports(node.object)) { // Do not track assignments twice if (this.exportsUses.has(node.object)) return; this.exportsUses.add(node.object); module = current; } else if (node.object.type === 'Identifier' && node.object.name === 'require') { // It is ok to use `require` properties this.requireUses.add(node.object); return; } else if (this.moduleUses.has(node.object)) { module = this.moduleUses.get(node.object); this.moduleUses.delete(node.object); } else if (node.object.type === 'CallExpression') { module = this.siftRequireCall(node.object, current); if (!module) return; } else { return; } if (node.property.type !== (node.computed ? 'Literal' : 'Identifier')) { if (module === current) { module.bailout('Dynamic CommonJS use', node.loc); } else { const reason = 'Dynamic CommonJS import'; module.bailout(reason, node.loc, current.resource); } return; } // TODO(indutny): build dependency tree for self-uses. They should not retain // themselves or others if unused. const prop = node.property.name || node.property.value; module.use(prop, current, recursive); // For recursive imports return { module, property: prop }; }; Analyzer.prototype.siftCall = function siftCall(node, current) { // `lib()` if (this.moduleUses.has(node.callee)) { const module = this.moduleUses.get(node.callee); this.moduleUses.delete(node.callee); module.bailout('Imported library call', node.loc, current.resource, 'info'); return; } const module = this.siftRequireCall(node, current); if (!module) return; // TODO(indutny): support `var lib; lib = require('...')` module.bailout('Escaping `require` call', node.loc, current.resource); }; Analyzer.prototype.siftNew = function siftNew(node, current) { // `new lib()` if (!this.moduleUses.has(node.callee)) return; const module = this.moduleUses.get(node.callee); this.moduleUses.delete(node.callee); module.bailout('Imported library new call', node.loc, current.resource, 'info'); }; Analyzer.prototype.siftUnaryExpression = function siftUnaryExpression(node) { if (node.operator !== 'typeof') return false; // Mark `typeof require` as a valid use of `require` const argument = node.argument; if (argument.type !== 'Identifier' || argument.name !== 'require') return false; if (this.requireUses.has(argument)) return false; this.requireUses.add(argument); }; Analyzer.prototype.siftRequireCall = function siftRequireCall(node, current) { const callee = node.callee; if (callee.type !== 'Identifier' || callee.name !== 'require') return false; // Valid `require` use if (this.requireUses.has(callee)) return false; this.requireUses.add(callee); const args = node.arguments; if (args.length < 1) return false; let arg; let fail = false; try { arg = shake.evaluateConst(args[0]); } catch (e) { fail = true; } if (fail || typeof arg !== 'string') { const msg = 'Dynamic argument of `require`'; current.bailout(msg, node.loc); this.bailout(msg, node.loc, current.resource); return false; } // TODO(indutny): support `require('./lib')()` return this.getUnresolvedModule(current.resource, arg); }; Analyzer.prototype.siftRequireUse = function siftRequireUse(node, current) { if (node.type !== 'Identifier' || node.name !== 'require') return; if (this.requireUses.has(node)) return; this.requireUses.add(node); current.bailout('Invalid use of `require`', node.loc); this.bailout('Invalid use of `require`', node.loc, current.resource); }; Analyzer.prototype.getUnresolvedModule = function getUnresolvedModule(issuer, name) { let issuerMap; if (this.unresolved.has(issuer)) { issuerMap = this.unresolved.get(issuer); } else { issuerMap = new Map(); this.unresolved.set(issuer, issuerMap); } let module; if (issuerMap.has(name)) { module = issuerMap.get(name); } else { module = new Module(name); issuerMap.set(name, module); } // Already resolved if (typeof module === 'string') return this.getModule(module); return module; }; Analyzer.prototype.bailout = function bailout(reason, loc, source) { if (this.bailouts) this.bailouts.push({ reason, loc, source }); else this.bailouts = [ { reason, loc, source } ]; }; ================================================ FILE: lib/shake/eval.js ================================================ 'use strict'; function evaluateBinary(node) { const op = node.operator; const left = evaluateConst(node.left); const right = evaluateConst(node.right); if (op === '+') return left + right; throw new Error(`Unsupported binary operation: "${op}"`); } function evaluateConst(node) { if (node.type === 'Literal') return node.value; if (node.type === 'BinaryExpression') return evaluateBinary(node); throw new Error(`Unsupported node type: "${node.type}"`); } module.exports = evaluateConst; ================================================ FILE: lib/shake/graph.js ================================================ 'use strict'; const path = require('path'); function Graph(dir) { this.dir = dir || '.'; this.relativeCache = new Map(); } module.exports = Graph; Graph.prototype.generate = function generate(modules) { const seen = new Set(); const queue = modules.slice(); let out = 'digraph {\n'; out += ' ranksep=1.2;\n'; while (queue.length !== 0) { const module = queue.shift(); if (seen.has(module)) continue; seen.add(module); out += this.generateModule(module); } out += '}\n'; return out; }; Graph.prototype.relative = function relative(file) { file = file || ''; if (this.relativeCache.has(file)) return this.relativeCache.get(file); const relative = path.relative(this.dir, file); this.relativeCache.set(file, relative); return relative; }; Graph.prototype.escape = function escape(str) { return `"${str.replace(/"/g, '\\"')}"`; }; Graph.prototype.declarationId = function declarationId(module, name) { return this.escape(`{${this.relative(module.resource)}}[${name}]`); }; Graph.prototype.moduleId = function moduleId(module) { return this.escape(`{${this.relative(module.resource)}}/require`); }; Graph.prototype.generateModule = function generateModule(module) { const resource = this.escape('cluster://' + this.relative(module.resource)); const label = this.escape(this.relative(module.resource)); let out = ''; const color = module.bailouts === false ? 'black' : 'red'; let cluster = ` subgraph ${resource} {\n`; cluster += ` label=${label};\n`; cluster += ` color=${color};\n`; cluster += ` ${this.moduleId(module)} [label=require shape=diamond];\n`; const issuersSeen = new Set(); const declarationsSeen = new Set(); const declare = (name) => { const id = this.declarationId(module, name); if (declarationsSeen.has(name)) return id; declarationsSeen.add(name); const color = module.bailouts === false ? module.isUsed(name) ? 'black' : 'blue' : 'red'; const shortId = this.escape(`${name}`); cluster += ` ${id} [label=${shortId} color=${color}];\n`; return id; }; // Add all declarations module.declarations.forEach((declaration) => { declare(declaration.name); }); // Add uses module.uses.forEach((issuers, name) => { issuers.forEach((issuer) => { issuersSeen.add(issuer); out += ` ${this.moduleId(issuer)} -> ${declare(name)};\n`; }); }); // Add dynamic issuer edges (without particular uses) module.issuers.forEach((issuer) => { if (issuersSeen.has(issuer)) return; issuersSeen.add(issuer); out += ` ${this.moduleId(issuer)} -> ${declare('[*]')};\n`; }); cluster += ' }\n'; return cluster + out; }; ================================================ FILE: lib/shake/module.js ================================================ 'use strict'; const debug = require('debug')('common-shake:module'); function Module(resource) { this.resource = resource; this.bailouts = false; this.issuers = new Set(); this.uses = new Map(); this.declarations = []; this.pendingUses = []; this.computing = false; this.forced = false; } module.exports = Module; // Public API Module.prototype.forceExport = function forceExport() { this.forced = true; }; Module.prototype.isUsed = function isUsed(name) { this.compute(); if (this.bailouts || this.forced) return true; // Detect loops if (this.computing) { const pending = this.pendingUses.some(use => use.property === name); if (pending) return true; } return this.uses.has(name); }; Module.prototype.getInfo = function getInfo() { this.compute(); return { bailouts: this.bailouts, declarations: this.declarations.map(decl => decl.name), uses: Array.from(this.uses.keys()) }; }; Module.prototype.getDeclarations = function getDeclarations() { return this.declarations.slice(); }; // Private API Module.prototype.bailout = function bailout(reason, loc, source, level) { const bail = { reason, loc, source: source || null, level: level || 'warning' }; if (this.bailouts) this.bailouts.push(bail); else this.bailouts = [ bail ]; this.sealed = false; }; Module.prototype.use = function use(property, from, recursive) { if (recursive !== false) { debug('pending use this=%j property=%j from=%j recursive=%j', this.resource, property, from.resource, recursive); this.pendingUses.push({ property, from, recursive }); return; } debug('use this=%j property=%j from=%j recursive=%j', this.resource, property, from.resource, recursive); if (this.uses.has(property)) this.uses.get(property).add(from); else this.uses.set(property, new Set([ from ])); }; Module.prototype.seal = function seal() { this.sealed = true; }; Module.prototype.declare = function declare(property) { this.declarations.push(property); return !this.sealed; }; Module.prototype.multiDeclare = function multiDeclare(declarations) { const success = this.declarations.length === 0 && !this.sealed; this.seal(); for (let i = 0; i < declarations.length; i++) this.declarations.push(declarations[i]); return success; }; Module.prototype.mergeFrom = function mergeFrom(unresolved) { if (unresolved.bailouts) { unresolved.bailouts.forEach((b) => { this.bailout(b.reason, b.loc, b.source, b.level); }); } unresolved.uses.forEach((from, property) => { from.forEach(resource => this.use(property, resource, false)); }); unresolved.declarations.forEach(declaration => this.declare(declaration)); this.pendingUses = this.pendingUses.concat(unresolved.pendingUses); unresolved.clear(); }; Module.prototype.addIssuer = function addIssuer(issuer) { this.issuers.add(issuer); }; Module.prototype.clear = function clear() { this.uses = null; this.declarations = null; this.pendingUses = null; }; Module.prototype.compute = function compute() { // Already computed or cleared if (this.pendingUses === null) return; if (this.computing) return; this.computing = true; debug('compute this=%j pending=%d', this.resource, this.pendingUses.length); // Do several passes until it will stabilize // TODO(indutny): what is complexity of this? Exponential? let before; do { before = this.pendingUses.length; // NOTE: it is important to overwrite this, since recursive lookups will // get to it. this.pendingUses = this.pendingUses.filter((use) => { return use.from.isUsed(use.recursive); }); debug('compute pass this=%j before=%d after=%d', this.resource, before, this.pendingUses.length); } while (this.pendingUses.length !== before); this.pendingUses.forEach(use => this.use(use.property, use.from, false)); this.pendingUses = null; this.computing = false; }; ================================================ FILE: lib/shake/walk.js ================================================ 'use strict'; const walk = require('acorn/dist/walk'); const BASE = Object.assign({ // acorn-dynamic-import support Import: () => {} }, walk.base); // Pre-order walker module.exports = (node, visitors) => { const state = null; const override = false; !function c(node, st, override) { var type = override || node.type, found = visitors[type]; if (found) found(node, st); BASE[type](node, st, c); }(node, state, override); }; ================================================ FILE: lib/shake.js ================================================ 'use strict'; exports.walk = require('./shake/walk'); exports.evaluateConst = require('./shake/eval'); exports.Module = require('./shake/module'); exports.Analyzer = require('./shake/analyzer'); exports.Graph = require('./shake/graph'); ================================================ FILE: package.json ================================================ { "name": "common-shake", "version": "2.1.0", "description": "CommonJS Tree Shake", "main": "lib/shake.js", "repository": { "type": "git", "url": "git+ssh://git@github.com/indutny/common-shake.git" }, "scripts": { "lint": "eslint lib/*.js lib/**/*.js test/*.js", "test": "nyc --reporter=html mocha --reporter=spec test/*-test.js && npm run lint" }, "files": [ "lib" ], "keywords": [ "commonjs", "tree", "shake" ], "author": "Fedor Indutny (http://darksi.de/)", "license": "MIT", "dependencies": { "acorn": "^5.1.1", "debug": "^2.6.8", "escope": "^3.6.0" }, "devDependencies": { "acorn-dynamic-import": "^2.0.2", "assert-text": "^1.1.2", "eslint": "^4.1.1", "mocha": "^3.4.2", "nyc": "^11.0.3" } } ================================================ FILE: test/analyzer-test.js ================================================ 'use strict'; /* globals describe it beforeEach afterEach */ const assert = require('assert'); const fixtures = require('./fixtures'); const parse = fixtures.parse; const shake = require('../'); const Analyzer = shake.Analyzer; const EMPTY = { bailouts: false, uses: [], declarations: [] }; function simplifyDecl(decl) { return { type: decl.type, name: decl.name, ast: decl.ast.type }; } describe('Analyzer', () => { let analyzer; beforeEach(() => { analyzer = new Analyzer(); }); afterEach(() => { analyzer = null; }); it('should find all exported values', () => { analyzer.run(parse(` exports.a = 1; exports.b = 2; !function() { module.exports.c = 3; }(); `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo(), { bailouts: false, uses: [], declarations: [ 'a', 'b', 'c' ] }); const decls = analyzer.getModule('root').getDeclarations(); assert.deepEqual(decls.map(simplifyDecl), [ { type: 'exports', name: 'a', ast: 'AssignmentExpression' }, { type: 'exports', name: 'b', ast: 'AssignmentExpression' }, { type: 'exports', name: 'c', ast: 'AssignmentExpression' } ]); }); it('should find all imported values', () => { analyzer.run(parse(` const lib = require('./a'); lib.a(); lib.b(); require('./a').c(); `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; exports.c = 3; exports.d = 4; `), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo(), { bailouts: false, uses: [ 'a', 'b', 'c' ], declarations: [ 'a', 'b', 'c', 'd' ] }); }); it('should find all self-used values', () => { analyzer.run(parse(` exports.a = 1; exports.b = () => {}; exports.c = () => { return exports.b(); }; exports.d = () => { return module.exports.c(); }; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo(), { bailouts: false, uses: [ 'b', 'c' ], declarations: [ 'a', 'b', 'c', 'd' ] }); }); it('should not count disguised `exports` use as export', () => { analyzer.run(parse(` function a() { var exports = {}; exports.a = a; } exports.b = 1; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo(), { bailouts: false, uses: [], declarations: [ 'b' ] }); }); it('should support object destructuring', () => { analyzer.run(parse(` const { a, b } = require('./a'); `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; exports.c = 3; `), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo(), { bailouts: false, uses: [ 'a', 'b' ], declarations: [ 'a', 'b', 'c' ] }); }); it('should not support dynamic object destructuring', () => { analyzer.run(parse(` const prop = 'a'; const { [prop]: name } = require('./a'); `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; exports.c = 3; `), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 12, line: 3 }, end: { column: 28, line: 3 } }, source: 'root', reason: 'Dynamic properties in `require` destructuring', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should not support array destructuring', () => { analyzer.run(parse(` const [ a, b ] = require('./a'); `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; exports.c = 3; `), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 12, line: 2 }, end: { column: 37, line: 2 } }, source: 'root', reason: '`require` used in unknown way', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should not count disguised `require` use as import', () => { analyzer.run(parse(` const lib = require('./a'); lib.a(); function a() { const require = () => {}; const lib = require('./a'); lib.b(); } `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; `), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo(), { bailouts: false, uses: [ 'a' ], declarations: [ 'a', 'b' ] }); }); it('should not count redefined variable as import', () => { analyzer.run(parse(` var lib = require('./a'); lib.a(); var lib = require('./b'); lib.b(); `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; `), 'a'); analyzer.resolve('root', './a', 'a'); analyzer.resolve('root', './b', 'b'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo(), { bailouts: [ { loc: { start: { column: 10, line: 2 }, end: { column: 30, line: 2 } }, source: 'root', reason: '`require` variable override', level: 'warning' } ], uses: [], declarations: [ 'a', 'b' ] }); assert.deepEqual(analyzer.getModule('b').getInfo(), { bailouts: [ { loc: { start: { column: 10, line: 6 }, end: { column: 30, line: 6 } }, source: 'root', reason: '`require` variable override', level: 'warning' } ], uses: [], declarations: [] }); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on assignment to `exports`', () => { analyzer.run(parse(` exports = {}; exports.a = 1; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 6, line: 2 }, end: { column: 18, line: 2 } }, source: null, reason: '`exports` assignment', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on assignment to `require`', () => { analyzer.run(parse(` require = () => {}; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 6, line: 2 }, end: { column: 24, line: 2 } }, source: null, reason: '`require` assignment', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on dynamic `require`', () => { analyzer.run(parse(` const lib = require(Math.random()); `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 18, line: 2 }, end: { column: 40, line: 2 } }, source: null, reason: 'Dynamic argument of `require`', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, [ { loc: { start: { column: 18, line: 2 }, end: { column: 40, line: 2 } }, source: 'root', reason: 'Dynamic argument of `require`' } ]); }); it('should not bailout use of `require` properties', () => { analyzer.run(parse(` require.cache[a] = 1; `), 'root'); assert(analyzer.isSuccess()); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); }); it('should bailout on invalide use of `require`', () => { analyzer.run(parse(` escape(require); `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 13, line: 2 }, end: { column: 20, line: 2 } }, source: null, reason: 'Invalid use of `require`', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, [ { loc: { start: { column: 13, line: 2 }, end: { column: 20, line: 2 } }, source: 'root', reason: 'Invalid use of `require`' } ]); }); it('should not bailout on `typeof require`', () => { analyzer.run(parse(` if (typeof require === 'function') { console.log("ok"); } `), 'root'); assert.strictEqual(analyzer.getModule('root').getInfo().bailouts, false); }); it('should bailout on assignment to `module.exports`', () => { analyzer.run(parse(` module.exports = () => {}; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 6, line: 2 }, end: { column: 31, line: 2 } }, source: null, reason: '`module.exports` assignment', level: 'info' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should not bailout on assignment to other `module` properties', () => { analyzer.run(parse(` module.lamports = () => {}; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, false); assert.deepEqual(analyzer.bailouts, false); }); it('should support object literal in `module.exports`', () => { analyzer.run(parse(` module.exports = { a: 1, "b": 2 }; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo(), { bailouts: false, uses: [], declarations: [ 'a', 'b' ] }); const decls = analyzer.getModule('root').getDeclarations(); assert.deepEqual(decls.map(simplifyDecl), [ { type: 'module.exports', name: 'a', ast: 'Property' }, { type: 'module.exports', name: 'b', ast: 'Property' } ]); }); it('should bailout on dynamic keys in `module.exports`', () => { analyzer.run(parse(` module.exports = { [a]: 1, "b": 2 }; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 8, line: 3 }, end: { column: 14, line: 3 } }, source: null, reason: 'Dynamic `module.exports` property', level: 'warning' } ]); }); it('should not support simultaneous `module.exports` and `exports`', () => { analyzer.run(parse(` exports.c = 1; module.exports = { a: 2, b: 3 }; `), 'root'); analyzer.run(parse(` module.exports = { a: 2, b: 3 }; exports.c = 1; `), 'rev-root'); assert.deepEqual(analyzer.getModule('root').getInfo(), { bailouts: [ { loc: { start: { column: 6, line: 3 }, end: { column: 7, line: 6 } }, source: null, reason: 'Simultaneous assignment to both `exports` and ' + '`module.exports`', level: 'warning' } ], uses: [], declarations: [ 'c', 'a', 'b' ] }); assert.deepEqual(analyzer.getModule('rev-root').getInfo(), { bailouts: [ { loc: { start: { column: 6, line: 6 }, end: { column: 19, line: 6 } }, source: null, reason: 'Simultaneous assignment to both `exports` and ' + '`module.exports`', level: 'warning' } ], uses: [], declarations: [ 'a', 'b', 'c' ] }); }); it('should bailout on dynamic export', () => { analyzer.run(parse(` exports[Math.random()] = 1; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 6, line: 2 }, end: { column: 28, line: 2 } }, source: null, reason: 'Dynamic CommonJS export', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on dynamic `module` use', () => { analyzer.run(parse(` module[Math.random()] = 1; `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 6, line: 2 }, end: { column: 27, line: 2 } }, source: null, reason: 'Dynamic `module` use', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on dynamic self-use', () => { analyzer.run(parse(` exports[Math.random()](); module.exports[Math.random()](); `), 'root'); assert.deepEqual(analyzer.getModule('root').getInfo().bailouts, [ { loc: { start: { column: 6, line: 2 }, end: { column: 28, line: 2 } }, source: null, reason: 'Dynamic CommonJS use', level: 'warning' }, { loc: { start: { column: 6, line: 3 }, end: { column: 35, line: 3 } }, source: null, reason: 'Dynamic CommonJS use', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on dynamic import', () => { analyzer.run(parse(` const lib = require('./a'); lib[Math.random()](); `), 'root'); analyzer.run(parse(''), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 6, line: 4 }, end: { column: 24, line: 4 } }, source: 'root', reason: 'Dynamic CommonJS import', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on assignment to imported library', () => { analyzer.run(parse(` const lib = require('./a'); lib.override = true; `), 'root'); analyzer.run(parse(''), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 6, line: 4 }, end: { column: 25, line: 4 } }, source: 'root', reason: 'Module property assignment', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on escaping imported library', () => { analyzer.run(parse(` const lib = require('./a'); send(lib); `), 'root'); analyzer.run(parse(''), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 11, line: 4 }, end: { column: 14, line: 4 } }, source: 'root', reason: 'Escaping value or unknown use', level: 'warning' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on imported library call', () => { analyzer.run(parse(` const lib = require('./a'); lib(); `), 'root'); analyzer.run(parse(''), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 6, line: 4 }, end: { column: 11, line: 4 } }, source: 'root', reason: 'Imported library call', level: 'info' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on imported library new call', () => { analyzer.run(parse(` const lib = require('./a'); new lib(); `), 'root'); analyzer.run(parse(''), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 6, line: 4 }, end: { column: 15, line: 4 } }, source: 'root', reason: 'Imported library new call', level: 'info' } ]); assert.deepEqual(analyzer.bailouts, false); }); it('should bailout on deferred require', () => { analyzer.run(parse(` var lib; lib = require('./a'); lib.a(); lib.b(); `), 'root'); analyzer.run(parse(` exports.a = 1; exports.b = 2; exports.c = 3; `), 'a'); analyzer.resolve('root', './a', 'a'); assert.deepEqual(analyzer.getModule('root').getInfo(), EMPTY); assert.deepEqual(analyzer.getModule('a').getInfo().bailouts, [ { loc: { start: { column: 12, line: 3 }, end: { column: 26, line: 3 } }, source: 'root', reason: 'Escaping `require` call', level: 'warning' } ]); }); it('should not bailout on const require argument', () => { analyzer.run(parse(` const lib = require('./a' + 'b'); lib.a(); `), 'root'); analyzer.run(parse('exports.a = 1;'), 'ab'); analyzer.resolve('root', './ab', 'ab'); assert.deepEqual(analyzer.getModule('ab').getInfo(), { bailouts: false, declarations: [ 'a' ], uses: [ 'a' ] }); assert(analyzer.isSuccess()); }); it('should not fail on dynamic import', () => { assert.doesNotThrow(() => { analyzer.run(parse('import("ohai")'), 'root'); }); }); it('should not throw on double-resolve', () => { assert.doesNotThrow(() => { analyzer.resolve('root', './a', 'a'); analyzer.resolve('root', './a', 'a'); analyzer.resolve('root', './a', 'a'); }); }); it('should find recursive dependencies', () => { analyzer.run(parse(` const lib = require('./a'); const mlib = require('./ma'); exports.a = lib.a; exports.b = mlib.a; `), 'root'); analyzer.run(parse(` exports.a = require('./b').a; exports.c = require('./b').b; exports.b = exports.c; `), 'a'); analyzer.run(parse(` module.exports = { a: require('./mb').a, b: require('./mb').b }; `), 'ma'); analyzer.getModule('root').forceExport(); analyzer.resolve('root', './a', 'a'); analyzer.resolve('root', './ma', 'ma'); analyzer.resolve('a', './b', 'b'); analyzer.resolve('ma', './mb', 'mb'); assert.deepEqual(analyzer.getModule('a').getInfo(), { bailouts: false, uses: [ 'a' ], declarations: [ 'a', 'c', 'b' ] }); assert.deepEqual(analyzer.getModule('b').getInfo(), { bailouts: false, uses: [ 'a' ], declarations: [] }); assert.deepEqual(analyzer.getModule('ma').getInfo(), { bailouts: false, uses: [ 'a' ], declarations: [ 'a', 'b' ] }); assert.deepEqual(analyzer.getModule('mb').getInfo(), { bailouts: false, uses: [ 'a' ], declarations: [] }); }); it('should not choke on async/await', () => { assert.doesNotThrow(() => { analyzer.run(parse(` 'use strict'; const fn = async function() { await other(); }; `), 'root'); }); }); }); ================================================ FILE: test/eval-test.js ================================================ 'use strict'; /* globals describe it */ const assert = require('assert'); const fixtures = require('./fixtures'); const shake = require('../'); const evaluateConst = shake.evaluateConst; const parse = (source) => { return fixtures.parse(source).body[0].expression; }; describe('Evaluator', () => { it('should evaluate number literal', () => { assert.strictEqual(evaluateConst(parse('1')), 1); }); it('should evaluate string literal', () => { assert.strictEqual(evaluateConst(parse('"1"')), '1'); }); it('should evaluate binary addition', () => { assert.strictEqual(evaluateConst(parse('"1" + "2"')), '12'); }); it('should throw on unknown binary operation', () => { assert.throws(() => { evaluateConst(parse('"1" / "2"')); }); }); it('should throw on unknown node type', () => { assert.throws(() => { evaluateConst(parse('a()')); }); }); }); ================================================ FILE: test/fixtures.js ================================================ 'use strict'; const acorn = require('acorn-dynamic-import').default; exports.parse = (source) => { return acorn.parse(source, { locations: true, sourceType: 'module', ecmaVersion: 2017, plugins: { dynamicImport: true } }); }; ================================================ FILE: test/graph-test.js ================================================ 'use strict'; /* globals describe it beforeEach afterEach */ const assertText = require('assert-text'); const fixtures = require('./fixtures'); const parse = fixtures.parse; assertText.options.trim = true; const shake = require('../'); const Analyzer = shake.Analyzer; const Graph = shake.Graph; describe('Graph', () => { let analyzer; let graph; beforeEach(() => { analyzer = new Analyzer(); graph = new Graph(__dirname); }); afterEach(() => { analyzer = null; graph = null; }); it('should find all exported values', () => { analyzer.run(parse(` // Import all require('./a')[K]; require('./b').bprop; exports.prop = 1; `), 'root'); analyzer.run(parse(` exports.aprop = 1; `), 'a'); analyzer.run(parse(` exports.bprop = 1; `), 'b'); analyzer.resolve('root', './a', 'a'); analyzer.resolve('root', './b', 'b'); const out = graph.generate(analyzer.getModules()); assertText.equal(out, `digraph { ranksep=1.2; subgraph "cluster://../root" { label="../root"; color=black; "{../root}/require" [label=require shape=diamond]; "{../root}[prop]" [label="prop" color=blue]; } subgraph "cluster://../a" { label="../a"; color=red; "{../a}/require" [label=require shape=diamond]; "{../a}[aprop]" [label="aprop" color=red]; "{../a}[[*]]" [label="[*]" color=red]; } "{../root}/require" -> "{../a}[[*]]"; subgraph "cluster://../b" { label="../b"; color=black; "{../b}/require" [label=require shape=diamond]; "{../b}[bprop]" [label="bprop" color=black]; } "{../root}/require" -> "{../b}[bprop]"; }`); }); });