├── .prettierrc ├── .huskyrc ├── .gitignore ├── .lintstagedrc ├── src ├── utils │ └── ensureArray.ts ├── addExtension.ts ├── index.ts ├── extractAssignedNames.ts ├── pluginutils.d.ts ├── makeLegalIdentifier.ts ├── createFilter.ts ├── dataToEsm.ts └── attachScopes.ts ├── .travis.yml ├── .vscode └── settings.json ├── tslint.json ├── rollup.config.js ├── test ├── makeLegalIdentifier.test.ts ├── addExtension.test.ts ├── dataToEsm.test.ts ├── createFilter.test.ts ├── extractAssignedNames.test.ts └── attachScopes.test.ts ├── appveyor.yml ├── package.json ├── CHANGELOG.md ├── README.md ├── tsconfig.json └── jest.config.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "post-commit": "git reset" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | *.js 5 | *.js.map 6 | 7 | !rollup.config.js 8 | !jest.config.js 9 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "prettier --write", 4 | "tslint --project . --fix", 5 | "git add" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/ensureArray.ts: -------------------------------------------------------------------------------- 1 | export default function ensureArray(thing: Array | T | undefined | null): Array { 2 | if (Array.isArray(thing)) return thing; 3 | if (thing == undefined) return []; 4 | return [thing]; 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "8" 6 | - "10" 7 | - "12" 8 | env: 9 | global: 10 | - BUILD_TIMEOUT=10000 11 | install: npm ci --ignore-scripts 12 | before_install: 13 | - if [[ $TRAVIS_NODE_VERSION -lt 8 ]]; then npm install --global npm@5; fi 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/*.map": true, 9 | "**/*.js": { "when": "$(basename).ts" }, 10 | "**/*.d.ts": { "when": "$(basename).ts" } 11 | } 12 | } -------------------------------------------------------------------------------- /src/addExtension.ts: -------------------------------------------------------------------------------- 1 | import { extname } from 'path'; 2 | import { AddExtension } from './pluginutils'; 3 | 4 | const addExtension: AddExtension = function addExtension(filename, ext = '.js') { 5 | if (!extname(filename)) filename += ext; 6 | return filename; 7 | }; 8 | 9 | export { addExtension as default }; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as addExtension } from './addExtension'; 2 | export { default as attachScopes } from './attachScopes'; 3 | export { default as createFilter } from './createFilter'; 4 | export { default as makeLegalIdentifier } from './makeLegalIdentifier'; 5 | export { default as dataToEsm } from './dataToEsm'; 6 | export { default as extractAssignedNames } from './extractAssignedNames'; 7 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "interface-over-type-literal": true, 6 | "no-string-literal": true, 7 | "no-unnecessary-type-assertion": true, 8 | "object-literal-shorthand": true, 9 | "ordered-imports": true, 10 | "prefer-const": [true, { "destructuring": "all" }], 11 | "prefer-object-spread": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | import typescript from 'rollup-plugin-typescript'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | 6 | export default { 7 | input: 'src/index.ts', 8 | plugins: [ 9 | resolve(), 10 | commonjs({ include: 'node_modules/**' }), 11 | typescript({ include: '**/*.{ts,js}' }) 12 | ], 13 | external: ['estree-walker', 'path', 'util'], 14 | 15 | output: [ 16 | { 17 | format: 'cjs', 18 | file: pkg['main'] 19 | }, 20 | { 21 | format: 'es', 22 | file: pkg['module'] 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /test/makeLegalIdentifier.test.ts: -------------------------------------------------------------------------------- 1 | import { makeLegalIdentifier } from '..'; 2 | 3 | describe('makeLegalIdentifier', function() { 4 | it('camel-cases names', function() { 5 | expect(makeLegalIdentifier('foo-bar')).toEqual('fooBar'); 6 | }); 7 | 8 | it('replaces keywords', function() { 9 | expect(makeLegalIdentifier('typeof')).toEqual('_typeof'); 10 | }); 11 | 12 | it('blacklists arguments (https://github.com/rollup/rollup/issues/871)', function() { 13 | expect(makeLegalIdentifier('arguments')).toEqual('_arguments'); 14 | }); 15 | 16 | it('empty', function() { 17 | expect(makeLegalIdentifier('')).toEqual('_'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | version: "{build}" 4 | 5 | clone_depth: 10 6 | 7 | init: 8 | - git config --global core.autocrlf false 9 | 10 | environment: 11 | matrix: 12 | - nodejs_version: 6 13 | - nodejs_version: 8 14 | - nodejs_version: 10 15 | 16 | install: 17 | - ps: Install-Product node $env:nodejs_version 18 | - IF %nodejs_version% LSS 8 npm -g install npm@5 19 | - npm install 20 | 21 | build: off 22 | 23 | test_script: 24 | - node --version && npm --version 25 | - npm test 26 | 27 | matrix: 28 | fast_finish: false 29 | 30 | # cache: 31 | # - C:\Users\appveyor\AppData\Roaming\npm-cache -> package.json # npm cache 32 | # - node_modules -> package.json # local npm modules 33 | -------------------------------------------------------------------------------- /test/addExtension.test.ts: -------------------------------------------------------------------------------- 1 | import { addExtension } from '..'; 2 | 3 | describe('addExtension', function() { 4 | it('adds .js to an ID without an extension', function() { 5 | expect(addExtension('foo')).toEqual('foo.js'); 6 | }); 7 | 8 | it('ignores file with existing extension', function() { 9 | expect(addExtension('foo.js')).toEqual('foo.js'); 10 | expect(addExtension('foo.json')).toEqual('foo.json'); 11 | }); 12 | 13 | it('ignores file with trailing dot', function() { 14 | expect(addExtension('foo.')).toEqual('foo.'); 15 | }); 16 | 17 | it('ignores leading .', function() { 18 | expect(addExtension('./foo')).toEqual('./foo.js'); 19 | expect(addExtension('./foo.js')).toEqual('./foo.js'); 20 | }); 21 | 22 | it('adds a custom extension', function() { 23 | expect(addExtension('foo', '.wut')).toEqual('foo.wut'); 24 | expect(addExtension('foo.lol', '.wut')).toEqual('foo.lol'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/extractAssignedNames.ts: -------------------------------------------------------------------------------- 1 | import { Node } from 'estree-walker'; 2 | 3 | interface Extractors { 4 | [key: string]: (names: Array, param: Node) => void; 5 | } 6 | 7 | const extractors: Extractors = { 8 | ArrayPattern(names: Array, param: Node) { 9 | for (const element of param.elements) { 10 | if (element) extractors[element.type](names, element); 11 | } 12 | }, 13 | 14 | AssignmentPattern(names: Array, param: Node) { 15 | extractors[param.left.type](names, param.left); 16 | }, 17 | 18 | Identifier(names: Array, param: Node) { 19 | names.push(param.name); 20 | }, 21 | 22 | MemberExpression() {}, 23 | 24 | ObjectPattern(names: Array, param: Node) { 25 | for (const prop of param.properties) { 26 | if (prop.type === 'RestElement') { 27 | extractors.RestElement(names, prop); 28 | } else { 29 | extractors[prop.value.type](names, prop.value); 30 | } 31 | } 32 | }, 33 | 34 | RestElement(names: Array, param: Node) { 35 | extractors[param.argument.type](names, param.argument); 36 | } 37 | }; 38 | 39 | const extractAssignedNames = function extractAssignedNames(param: Node): Array { 40 | const names: Array = []; 41 | 42 | extractors[param.type](names, param); 43 | return names; 44 | }; 45 | 46 | export { extractAssignedNames as default }; 47 | -------------------------------------------------------------------------------- /src/pluginutils.d.ts: -------------------------------------------------------------------------------- 1 | import { Node } from 'estree-walker'; 2 | 3 | export interface AttachedScope { 4 | parent?: AttachedScope; 5 | isBlockScope: boolean; 6 | declarations: { [key: string]: boolean }; 7 | addDeclaration(node: Node, isBlockDeclaration: boolean, isVar: boolean): void; 8 | contains(name: string): boolean; 9 | } 10 | 11 | export interface DataToEsmOptions { 12 | compact?: boolean; 13 | indent?: string; 14 | namedExports?: boolean; 15 | objectShorthand?: boolean; 16 | preferConst?: boolean; 17 | } 18 | 19 | export type AddExtension = (filename: string, ext?: string) => string; 20 | export const addExtension: AddExtension; 21 | 22 | export type AttachScopes = (ast: Node, propertyName?: string) => AttachedScope; 23 | export const attachScopes: AttachScopes; 24 | 25 | export type CreateFilter = ( 26 | include?: Array | string | RegExp | null, 27 | exclude?: Array | string | RegExp | null, 28 | options?: { resolve?: string | false | null } 29 | ) => (id: string | any) => boolean; 30 | export const createFilter: CreateFilter; 31 | 32 | export type MakeLegalIdentifier = (str: string) => string; 33 | export const makeLegalIdentifier: MakeLegalIdentifier; 34 | 35 | export type DataToEsm = (data: any, options?: DataToEsmOptions) => string; 36 | export const dataToEsm: DataToEsm; 37 | 38 | export type ExtractAssignedNames = (param: Node) => Array; 39 | export const extractAssignedNames: ExtractAssignedNames; 40 | -------------------------------------------------------------------------------- /src/makeLegalIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { MakeLegalIdentifier } from './pluginutils'; 2 | 3 | const reservedWords = 4 | 'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public'; 5 | const builtins = 6 | 'arguments Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl'; 7 | 8 | const forbiddenIdentifiers = new Set(`${reservedWords} ${builtins}`.split(' ')); 9 | forbiddenIdentifiers.add(''); 10 | 11 | export const makeLegalIdentifier: MakeLegalIdentifier = function makeLegalIdentifier(str) { 12 | str = str.replace(/-(\w)/g, (_, letter) => letter.toUpperCase()).replace(/[^$_a-zA-Z0-9]/g, '_'); 13 | 14 | if (/\d/.test(str[0]) || forbiddenIdentifiers.has(str)) { 15 | str = `_${str}`; 16 | } 17 | 18 | return str || '_'; 19 | }; 20 | 21 | export { makeLegalIdentifier as default }; 22 | -------------------------------------------------------------------------------- /src/createFilter.ts: -------------------------------------------------------------------------------- 1 | import mm from 'micromatch'; 2 | import { resolve, sep } from 'path'; 3 | import { CreateFilter } from './pluginutils'; 4 | import ensureArray from './utils/ensureArray'; 5 | 6 | function getMatcherString(id: string, resolutionBase: string | false | null | undefined) { 7 | if (resolutionBase === false) { 8 | return id; 9 | } 10 | return resolve(...(typeof resolutionBase === 'string' ? [resolutionBase, id] : [id])); 11 | } 12 | 13 | const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) { 14 | const resolutionBase = options && options.resolve; 15 | 16 | const getMatcher = (id: string | RegExp) => { 17 | return id instanceof RegExp 18 | ? id 19 | : { 20 | test: mm.matcher( 21 | getMatcherString(id, resolutionBase) 22 | .split(sep) 23 | .join('/'), 24 | { dot: true } 25 | ) 26 | }; 27 | }; 28 | 29 | const includeMatchers = ensureArray(include).map(getMatcher); 30 | const excludeMatchers = ensureArray(exclude).map(getMatcher); 31 | 32 | return function(id: string | any): boolean { 33 | if (typeof id !== 'string') return false; 34 | if (/\0/.test(id)) return false; 35 | 36 | id = id.split(sep).join('/'); 37 | 38 | for (let i = 0; i < excludeMatchers.length; ++i) { 39 | const matcher = excludeMatchers[i]; 40 | if (matcher.test(id)) return false; 41 | } 42 | 43 | for (let i = 0; i < includeMatchers.length; ++i) { 44 | const matcher = includeMatchers[i]; 45 | if (matcher.test(id)) return true; 46 | } 47 | 48 | return !includeMatchers.length; 49 | }; 50 | }; 51 | 52 | export { createFilter as default }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-pluginutils", 3 | "description": "Functionality commonly needed by Rollup plugins", 4 | "version": "2.8.2", 5 | "main": "dist/pluginutils.cjs.js", 6 | "module": "dist/pluginutils.es.js", 7 | "jsnext:main": "dist/pluginutils.es.js", 8 | "typings": "dist/pluginutils.d.ts", 9 | "files": [ 10 | "src", 11 | "dist", 12 | "README.md" 13 | ], 14 | "devDependencies": { 15 | "@types/estree": "0.0.39", 16 | "@types/jest": "^24.0.13", 17 | "@types/micromatch": "^3.1.0", 18 | "@types/node": "^12.0.4", 19 | "husky": "^3.0.5", 20 | "jest": "^24.8.0", 21 | "lint-staged": "^9.2.5", 22 | "micromatch": "^4.0.2", 23 | "prettier": "^1.17.1", 24 | "rollup": "^1.13.1", 25 | "rollup-plugin-commonjs": "^10.0.0", 26 | "rollup-plugin-node-resolve": "^5.0.1", 27 | "rollup-plugin-typescript": "^1.0.1", 28 | "shx": "^0.3.2", 29 | "ts-jest": "^24.0.2", 30 | "tslint": "^5.17.0", 31 | "typescript": "^3.5.1", 32 | "typescript-eslint-parser": "^22.0.0" 33 | }, 34 | "scripts": { 35 | "test": "jest", 36 | "build": "rollup -c && shx cp src/pluginutils.d.ts dist/pluginutils.d.ts", 37 | "lint": "npm run lint:nofix -- --fix", 38 | "lint:nofix": "tslint --project .", 39 | "pretest": "npm run build", 40 | "prepublishOnly": "npm test", 41 | "prepare": "npm run build" 42 | }, 43 | "dependencies": { 44 | "estree-walker": "^0.6.1" 45 | }, 46 | "repository": "rollup/rollup-pluginutils", 47 | "keywords": [ 48 | "rollup", 49 | "utils" 50 | ], 51 | "author": "Rich Harris ", 52 | "license": "MIT", 53 | "bugs": { 54 | "url": "https://github.com/rollup/rollup-pluginutils/issues" 55 | }, 56 | "homepage": "https://github.com/rollup/rollup-pluginutils#readme" 57 | } 58 | -------------------------------------------------------------------------------- /src/dataToEsm.ts: -------------------------------------------------------------------------------- 1 | import makeLegalIdentifier from './makeLegalIdentifier'; 2 | import { DataToEsm } from './pluginutils'; 3 | 4 | export type Indent = string | null | undefined; 5 | 6 | function stringify(obj: any): string { 7 | return (JSON.stringify(obj) || 'undefined').replace(/[\u2028\u2029]/g, char => `\\u${('000' + char.charCodeAt(0).toString(16)).slice(-4)}`); 8 | } 9 | 10 | function serializeArray(arr: Array, indent: Indent, baseIndent: string): string { 11 | let output = '['; 12 | const separator = indent ? '\n' + baseIndent + indent : ''; 13 | for (let i = 0; i < arr.length; i++) { 14 | const key = arr[i]; 15 | output += `${i > 0 ? ',' : ''}${separator}${serialize(key, indent, baseIndent + indent)}`; 16 | } 17 | return output + `${indent ? '\n' + baseIndent : ''}]`; 18 | } 19 | 20 | function serializeObject(obj: { [key: string]: T }, indent: Indent, baseIndent: string): string { 21 | let output = '{'; 22 | const separator = indent ? '\n' + baseIndent + indent : ''; 23 | const keys = Object.keys(obj); 24 | for (let i = 0; i < keys.length; i++) { 25 | const key = keys[i]; 26 | const stringKey = makeLegalIdentifier(key) === key ? key : stringify(key); 27 | output += `${i > 0 ? ',' : ''}${separator}${stringKey}:${indent ? ' ' : ''}${serialize( 28 | obj[key], 29 | indent, 30 | baseIndent + indent 31 | )}`; 32 | } 33 | return output + `${indent ? '\n' + baseIndent : ''}}`; 34 | } 35 | 36 | function serialize(obj: any, indent: Indent, baseIndent: string): string { 37 | if (obj === Infinity) return 'Infinity'; 38 | if (obj === -Infinity) return '-Infinity'; 39 | if (obj === 0 && 1/obj === -Infinity) return '-0'; 40 | if (obj instanceof Date) return 'new Date(' + obj.getTime() + ')'; 41 | if (obj instanceof RegExp) return obj.toString(); 42 | if (obj !== obj) return 'NaN'; 43 | if (Array.isArray(obj)) return serializeArray(obj, indent, baseIndent); 44 | if (obj === null) return 'null'; 45 | if (typeof obj === 'object') return serializeObject(obj, indent, baseIndent); 46 | return stringify(obj); 47 | } 48 | 49 | const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) { 50 | const t = options.compact ? '' : 'indent' in options ? options.indent : '\t'; 51 | const _ = options.compact ? '' : ' '; 52 | const n = options.compact ? '' : '\n'; 53 | const declarationType = options.preferConst ? 'const' : 'var'; 54 | 55 | if ( 56 | options.namedExports === false || 57 | typeof data !== 'object' || 58 | Array.isArray(data) || 59 | data instanceof Date || 60 | data instanceof RegExp || 61 | data === null 62 | ) { 63 | const code = serialize(data, options.compact ? null : t, ''); 64 | const __ = _ || (/^[{[\-\/]/.test(code) ? '' : ' '); 65 | return `export default${__}${code};`; 66 | } 67 | 68 | let namedExportCode = ''; 69 | const defaultExportRows = []; 70 | const dataKeys = Object.keys(data); 71 | for (let i = 0; i < dataKeys.length; i++) { 72 | const key = dataKeys[i]; 73 | if (key === makeLegalIdentifier(key)) { 74 | if (options.objectShorthand) defaultExportRows.push(key); 75 | else defaultExportRows.push(`${key}:${_}${key}`); 76 | namedExportCode += `export ${declarationType} ${key}${_}=${_}${serialize( 77 | data[key], 78 | options.compact ? null : t, 79 | '' 80 | )};${n}`; 81 | } else { 82 | defaultExportRows.push( 83 | `${stringify(key)}:${_}${serialize(data[key], options.compact ? null : t, '')}` 84 | ); 85 | } 86 | } 87 | return ( 88 | namedExportCode + `export default${_}{${n}${t}${defaultExportRows.join(`,${n}${t}`)}${n}};${n}` 89 | ); 90 | }; 91 | 92 | export { dataToEsm as default }; 93 | -------------------------------------------------------------------------------- /test/dataToEsm.test.ts: -------------------------------------------------------------------------------- 1 | import { dataToEsm } from '..'; 2 | 3 | describe('dataToEsm', function() { 4 | it('outputs treeshakeable data', function() { 5 | expect(dataToEsm({ some: 'data', another: 'data' })).toEqual( 6 | 'export var some = "data";\nexport var another = "data";\nexport default {\n\tsome: some,\n\tanother: another\n};\n' 7 | ); 8 | }); 9 | 10 | it('handles illegal identifiers, object shorthand, preferConst', function() { 11 | expect( 12 | dataToEsm({ '1': 'data', default: 'data' }, { objectShorthand: true, preferConst: true }) 13 | ).toEqual('export default {\n\t"1": "data",\n\t"default": "data"\n};\n'); 14 | }); 15 | 16 | it('supports non-JSON data', function() { 17 | const date = new Date(); 18 | expect(dataToEsm({ inf: -Infinity, date, number: NaN, regexp: /.*/ })).toEqual( 19 | 'export var inf = -Infinity;\nexport var date = new Date(' + 20 | date.getTime() + 21 | ');\nexport var number = NaN;\nexport var regexp = /.*/;\nexport default {\n\tinf: inf,\n\tdate: date,\n\tnumber: number,\n\tregexp: regexp\n};\n' 22 | ); 23 | }); 24 | 25 | it('supports a compact argument', function() { 26 | expect( 27 | dataToEsm({ some: 'data', another: 'data' }, { compact: true, objectShorthand: true }) 28 | ).toEqual('export var some="data";export var another="data";export default{some,another};'); 29 | expect( 30 | dataToEsm( 31 | { some: { deep: { object: 'definition', here: 'here' } }, else: { deep: { object: 'definition', here: 'here' } } }, 32 | { compact: true, objectShorthand: false } 33 | ) 34 | ).toEqual( 35 | 'export var some={deep:{object:"definition",here:"here"}};export default{some:some,"else":{deep:{object:"definition",here:"here"}}};' 36 | ); 37 | }); 38 | 39 | it('supports nested objects', function() { 40 | const obj = { a: { b: 'c', d: ['e', 'f'] } }; 41 | expect(dataToEsm({ obj })).toEqual( 42 | 'export var obj = {\n\ta: {\n\t\tb: "c",\n\t\td: [\n\t\t\t"e",\n\t\t\t"f"\n\t\t]\n\t}\n};\nexport default {\n\tobj: obj\n};\n' 43 | ); 44 | }); 45 | 46 | it('supports nested arrays', function() { 47 | const arr = ['a', 'b']; 48 | expect(dataToEsm({ arr })).toEqual( 49 | 'export var arr = [\n\t"a",\n\t"b"\n];\nexport default {\n\tarr: arr\n};\n' 50 | ); 51 | }); 52 | 53 | it('serializes null', function() { 54 | expect(dataToEsm({ null: null })).toEqual('export default {\n\t"null": null\n};\n'); 55 | }); 56 | 57 | it('supports default only', function() { 58 | const arr = ['a', 'b']; 59 | expect(dataToEsm({ arr }, { namedExports: false })).toEqual( 60 | 'export default {\n\tarr: [\n\t\t"a",\n\t\t"b"\n\t]\n};' 61 | ); 62 | }); 63 | 64 | it('exports default only for arrays', function() { 65 | const arr = ['a', 'b']; 66 | expect(dataToEsm(arr)).toEqual('export default [\n\t"a",\n\t"b"\n];'); 67 | }); 68 | 69 | it('exports default only for null', function() { 70 | expect(dataToEsm(null, { compact: true })).toEqual('export default null;'); 71 | }); 72 | 73 | it('exports default only for primitive values', function() { 74 | expect(dataToEsm('some string')).toEqual('export default "some string";'); 75 | }); 76 | 77 | it('supports empty keys', function() { 78 | expect(dataToEsm({ a: 'x', '': 'y' })).toEqual( 79 | 'export var a = "x";\nexport default {\n\ta: a,\n' + '\t"": "y"\n};\n' 80 | ); 81 | }); 82 | 83 | it('avoid U+2029 U+2029 -0 be ignored by JSON.stringify, and avoid it return non-string (undefined) before replacing', function() { 84 | expect(dataToEsm([-0, '\u2028\u2029', undefined, function() {}], { compact: true })).toEqual( 85 | 'export default[-0,"\\u2028\\u2029",undefined,undefined];' 86 | ); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/attachScopes.ts: -------------------------------------------------------------------------------- 1 | import { Node, walk } from 'estree-walker'; 2 | import extractAssignedNames from './extractAssignedNames'; 3 | import { AttachedScope, AttachScopes } from './pluginutils'; 4 | 5 | const blockDeclarations = { 6 | const: true, 7 | let: true 8 | }; 9 | 10 | interface ScopeOptions { 11 | parent?: AttachedScope; 12 | block?: boolean; 13 | params?: Array; 14 | } 15 | 16 | class Scope implements AttachedScope { 17 | parent?: AttachedScope; 18 | isBlockScope: boolean; 19 | declarations: { [key: string]: boolean }; 20 | 21 | constructor(options: ScopeOptions = {}) { 22 | this.parent = options.parent; 23 | this.isBlockScope = !!options.block; 24 | 25 | this.declarations = Object.create(null); 26 | 27 | if (options.params) { 28 | options.params.forEach(param => { 29 | extractAssignedNames(param).forEach(name => { 30 | this.declarations[name] = true; 31 | }); 32 | }); 33 | } 34 | } 35 | 36 | addDeclaration(node: Node, isBlockDeclaration: boolean, isVar: boolean): void { 37 | if (!isBlockDeclaration && this.isBlockScope) { 38 | // it's a `var` or function node, and this 39 | // is a block scope, so we need to go up 40 | this.parent!.addDeclaration(node, isBlockDeclaration, isVar); 41 | } else if (node.id) { 42 | extractAssignedNames(node.id).forEach(name => { 43 | this.declarations[name] = true; 44 | }); 45 | } 46 | } 47 | 48 | contains(name: string): boolean { 49 | return this.declarations[name] || (this.parent ? this.parent.contains(name) : false); 50 | } 51 | } 52 | 53 | const attachScopes: AttachScopes = function attachScopes(ast, propertyName = 'scope') { 54 | let scope = new Scope(); 55 | 56 | walk(ast, { 57 | enter(node, parent) { 58 | // function foo () {...} 59 | // class Foo {...} 60 | if (/(Function|Class)Declaration/.test(node.type)) { 61 | scope.addDeclaration(node, false, false); 62 | } 63 | 64 | // var foo = 1 65 | if (node.type === 'VariableDeclaration') { 66 | const kind: keyof typeof blockDeclarations = node.kind; 67 | const isBlockDeclaration = blockDeclarations[kind]; 68 | 69 | node.declarations.forEach((declaration: Node) => { 70 | scope.addDeclaration(declaration, isBlockDeclaration, true); 71 | }); 72 | } 73 | 74 | let newScope: AttachedScope | undefined; 75 | 76 | // create new function scope 77 | if (/Function/.test(node.type)) { 78 | newScope = new Scope({ 79 | parent: scope, 80 | block: false, 81 | params: node.params 82 | }); 83 | 84 | // named function expressions - the name is considered 85 | // part of the function's scope 86 | if (node.type === 'FunctionExpression' && node.id) { 87 | newScope.addDeclaration(node, false, false); 88 | } 89 | } 90 | 91 | // create new block scope 92 | if (node.type === 'BlockStatement' && !/Function/.test(parent!.type)) { 93 | newScope = new Scope({ 94 | parent: scope, 95 | block: true 96 | }); 97 | } 98 | 99 | // catch clause has its own block scope 100 | if (node.type === 'CatchClause') { 101 | newScope = new Scope({ 102 | parent: scope, 103 | params: node.param ? [node.param] : [], 104 | block: true 105 | }); 106 | } 107 | 108 | if (newScope) { 109 | Object.defineProperty(node, propertyName, { 110 | value: newScope, 111 | configurable: true 112 | }); 113 | 114 | scope = newScope; 115 | } 116 | }, 117 | leave(node) { 118 | if (node[propertyName]) scope = scope.parent!; 119 | } 120 | }); 121 | 122 | return scope; 123 | }; 124 | 125 | export { attachScopes as default }; 126 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # rollup-pluginutils changelog 2 | 3 | ## 2.8.2 4 | *2019-09-13* 5 | * Handle optional catch parameter in attachScopes ([#70](https://github.com/rollup/rollup-pluginutils/pulls/70)) 6 | 7 | ## 2.8.1 8 | *2019-06-04* 9 | * Support serialization of many edge cases ([#64](https://github.com/rollup/rollup-pluginutils/issues/64)) 10 | 11 | ## 2.8.0 12 | *2019-05-30* 13 | * Bundle updated micromatch dependency ([#60](https://github.com/rollup/rollup-pluginutils/issues/60)) 14 | 15 | ## 2.7.1 16 | *2019-05-17* 17 | * Do not ignore files with a leading "." in createFilter ([#62](https://github.com/rollup/rollup-pluginutils/issues/62)) 18 | 19 | ## 2.7.0 20 | *2019-05-15* 21 | * Add `resolve` option to createFilter ([#59](https://github.com/rollup/rollup-pluginutils/issues/59)) 22 | 23 | ## 2.6.0 24 | *2019-04-04* 25 | * Add `extractAssignedNames` ([#59](https://github.com/rollup/rollup-pluginutils/issues/59)) 26 | * Provide dedicated TypeScript typings file ([#58](https://github.com/rollup/rollup-pluginutils/issues/58)) 27 | 28 | ## 2.5.0 29 | *2019-03-18* 30 | * Generalize dataToEsm type ([#55](https://github.com/rollup/rollup-pluginutils/issues/55)) 31 | * Handle empty keys in dataToEsm ([#56](https://github.com/rollup/rollup-pluginutils/issues/56)) 32 | 33 | ## 2.4.1 34 | *2019-02-16* 35 | * Remove unnecessary dependency 36 | 37 | ## 2.4.0 38 | *2019-02-16* 39 | Update dependencies to solve micromatch vulnerability ([#53](https://github.com/rollup/rollup-pluginutils/issues/53)) 40 | 41 | ## 2.3.3 42 | *2018-09-19* 43 | * Revert micromatch update ([#43](https://github.com/rollup/rollup-pluginutils/issues/43)) 44 | 45 | ## 2.3.2 46 | *2018-09-18* 47 | * Bumb micromatch dependency ([#36](https://github.com/rollup/rollup-pluginutils/issues/36)) 48 | * Bumb dependencies ([#41](https://github.com/rollup/rollup-pluginutils/issues/41)) 49 | * Split up tests ([#40](https://github.com/rollup/rollup-pluginutils/issues/40)) 50 | 51 | ## 2.3.1 52 | *2018-08-06* 53 | * Fixed ObjectPattern scope in attachScopes to recognise { ...rest } syntax ([#37](https://github.com/rollup/rollup-pluginutils/issues/37)) 54 | 55 | ## 2.3.0 56 | *2018-05-21* 57 | * Add option to not generate named exports ([#32](https://github.com/rollup/rollup-pluginutils/issues/32)) 58 | 59 | ## 2.2.1 60 | *2018-05-21* 61 | * Support `null` serialization ([#34](https://github.com/rollup/rollup-pluginutils/issues/34)) 62 | 63 | ## 2.2.0 64 | *2018-05-11* 65 | * Improve white-space handling in `dataToEsm` and add `prepare` script ([#31](https://github.com/rollup/rollup-pluginutils/issues/31)) 66 | 67 | ## 2.1.1 68 | *2018-05-09* 69 | * Update dependencies 70 | 71 | ## 2.1.0 72 | *2018-05-08* 73 | * Add `dataToEsm` helper to create named exports from objects ([#29](https://github.com/rollup/rollup-pluginutils/issues/29)) 74 | * Support literal keys in object patterns ([#27](https://github.com/rollup/rollup-pluginutils/issues/27)) 75 | * Support function declarations without id in `attachScopes` ([#28](https://github.com/rollup/rollup-pluginutils/issues/28)) 76 | 77 | ## 2.0.1 78 | *2017-01-03* 79 | * Don't add extension to file with trailing dot ([#14](https://github.com/rollup/rollup-pluginutils/issues/14)) 80 | 81 | ## 2.0.0 82 | *2017-01-03* 83 | * Use `micromatch` instead of `minimatch` ([#19](https://github.com/rollup/rollup-pluginutils/issues/19)) 84 | * Allow `createFilter` to take regexes ([#5](https://github.com/rollup/rollup-pluginutils/issues/5)) 85 | 86 | ## 1.5.2 87 | *2016-08-29* 88 | * Treat `arguments` as a reserved word ([#10](https://github.com/rollup/rollup-pluginutils/issues/10)) 89 | 90 | ## 1.5.1 91 | *2016-06-24* 92 | * Add all declarators in a var declaration to scope, not just the first 93 | 94 | ## 1.5.0 95 | *2016-06-07* 96 | * Exclude IDs with null character (`\0`) 97 | 98 | ## 1.4.0 99 | *2016-06-07* 100 | * Workaround minimatch issue ([#6](https://github.com/rollup/rollup-pluginutils/pull/6)) 101 | * Exclude non-string IDs in `createFilter` 102 | 103 | ## 1.3.1 104 | *2015-12-16* 105 | * Build with Rollup directly, rather than via Gobble 106 | 107 | ## 1.3.0 108 | *2015-12-16* 109 | * Use correct path separator on Windows 110 | 111 | ## 1.2.0 112 | *2015-11-02* 113 | * Add `attachScopes` and `makeLegalIdentifier` 114 | 115 | ## 1.1.0 116 | 2015-10-24* 117 | * Add `addExtension` function 118 | 119 | ## 1.0.1 120 | *2015-10-24* 121 | * Include dist files in package 122 | 123 | ## 1.0.0 124 | *2015-10-24* 125 | * First release 126 | -------------------------------------------------------------------------------- /test/createFilter.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { createFilter } from '..'; 3 | 4 | describe('createFilter', function() { 5 | it('includes by default', function() { 6 | const filter = createFilter(); 7 | expect(filter(path.resolve('x'))).toBeTruthy(); 8 | }); 9 | 10 | it('excludes IDs that are not included, if include.length > 0', function() { 11 | const filter = createFilter(['y']); 12 | expect(filter(path.resolve('x'))).toBeFalsy(); 13 | expect(filter(path.resolve('y'))).toBeTruthy(); 14 | }); 15 | 16 | it('excludes IDs explicitly', function() { 17 | const filter = createFilter(null, ['y']); 18 | expect(filter(path.resolve('x'))).toBeTruthy(); 19 | expect(filter(path.resolve('y'))).toBeFalsy(); 20 | }); 21 | 22 | it('handles non-array arguments', function() { 23 | const filter = createFilter('foo/*', 'foo/baz'); 24 | expect(filter(path.resolve('foo/bar'))).toBeTruthy(); 25 | expect(filter(path.resolve('foo/baz'))).toBeFalsy(); 26 | }); 27 | 28 | it('negation patterns', function() { 29 | const filter = createFilter(['a/!(b)/c']); 30 | expect(filter(path.resolve('a/d/c'))).toBeTruthy(); 31 | expect(filter(path.resolve('a/b/c'))).toBeFalsy(); 32 | }); 33 | 34 | it('excludes non-string IDs', function() { 35 | const filter = createFilter(null, null); 36 | expect(filter({})).toBeFalsy(); 37 | }); 38 | 39 | it('excludes strings beginning with NUL', function() { 40 | const filter = createFilter(null, null); 41 | expect(filter('\0someid')).toBeFalsy(); 42 | }); 43 | 44 | it('includes with regexp', function() { 45 | const filter = createFilter(['a/!(b)/c', /\.js$/]); 46 | expect(filter(path.resolve('a/d/c'))).toBeTruthy(); 47 | expect(filter(path.resolve('a/b/c'))).toBeFalsy(); 48 | expect(filter(path.resolve('a.js'))).toBeTruthy(); 49 | expect(filter(path.resolve('a/b.js'))).toBeTruthy(); 50 | expect(filter(path.resolve('a/b.jsx'))).toBeFalsy(); 51 | }); 52 | 53 | it('excludes with regexp', function() { 54 | const filter = createFilter(['a/!(b)/c', /\.js$/], /\.js$/); 55 | expect(filter(path.resolve('a/d/c'))).toBeTruthy(); 56 | expect(filter(path.resolve('a/b/c'))).toBeFalsy(); 57 | expect(filter(path.resolve('a.js'))).toBeFalsy(); 58 | expect(filter(path.resolve('a/b.js'))).toBeFalsy(); 59 | expect(filter(path.resolve('a/b.jsx'))).toBeFalsy(); 60 | }); 61 | 62 | it('allows setting an absolute base dir', () => { 63 | const baseDir = path.resolve('C'); 64 | const filter = createFilter(['y*'], ['yx'], { resolve: baseDir }); 65 | expect(filter(baseDir + '/x')).toBeFalsy(); 66 | expect(filter(baseDir + '/ys')).toBeTruthy(); 67 | expect(filter(baseDir + '/yx')).toBeFalsy(); 68 | expect(filter(path.resolve('C/d/ys'))).toBeFalsy(); 69 | expect(filter(path.resolve('ys'))).toBeFalsy(); 70 | expect(filter('ys')).toBeFalsy(); 71 | }); 72 | 73 | it('allows setting a relative base dir', () => { 74 | const filter = createFilter(['y*'], ['yx'], { resolve: 'C/d' }); 75 | expect(filter(path.resolve('C/d/x'))).toBeFalsy(); 76 | expect(filter(path.resolve('C/d/ys'))).toBeTruthy(); 77 | expect(filter(path.resolve('C/d/yx'))).toBeFalsy(); 78 | expect(filter(path.resolve('C') + '/ys')).toBeFalsy(); 79 | expect(filter(path.resolve('ys'))).toBeFalsy(); 80 | expect(filter('ys')).toBeFalsy(); 81 | }); 82 | 83 | it('ignores a falsy resolve value', () => { 84 | const filter = createFilter(['y*'], ['yx'], { resolve: null }); 85 | expect(filter(path.resolve('x'))).toBeFalsy(); 86 | expect(filter(path.resolve('ys'))).toBeTruthy(); 87 | expect(filter(path.resolve('yx'))).toBeFalsy(); 88 | expect(filter(path.resolve('C') + '/ys')).toBeFalsy(); 89 | expect(filter(path.resolve('C/d/ys'))).toBeFalsy(); 90 | expect(filter('ys')).toBeFalsy(); 91 | }); 92 | 93 | it('allows preventing resolution against process.cwd()', () => { 94 | const filter = createFilter(['y*'], ['yx'], { resolve: false }); 95 | expect(filter('x')).toBeFalsy(); 96 | expect(filter('ys')).toBeTruthy(); 97 | expect(filter('yx')).toBeFalsy(); 98 | expect(filter(path.resolve('C') + '/ys')).toBeFalsy(); 99 | expect(filter(path.resolve('C/d/ys'))).toBeFalsy(); 100 | expect(filter(path.resolve('ys'))).toBeFalsy(); 101 | }); 102 | 103 | it('includes names starting with a "."', () => { 104 | const filter = createFilter(['**/*a']); 105 | expect(filter(path.resolve('.a'))).toBeTruthy(); 106 | expect(filter(path.resolve('.x/a'))).toBeTruthy(); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | 3 | This package has moved and is now available at [@rollup/pluginutils](https://github.com/rollup/plugins). Please update your dependencies. This repository is no longer maintained. 4 | 5 | # rollup-pluginutils 6 | 7 | A set of functions commonly used by Rollup plugins. 8 | 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install --save rollup-pluginutils 14 | ``` 15 | 16 | 17 | ## Usage 18 | 19 | ### addExtension 20 | 21 | ```js 22 | import { addExtension } from 'rollup-pluginutils'; 23 | 24 | export default function myPlugin ( options = {} ) { 25 | return { 26 | resolveId ( code, id ) { 27 | // only adds an extension if there isn't one already 28 | id = addExtension( id ); // `foo` -> `foo.js`, `foo.js -> foo.js` 29 | id = addExtension( id, '.myext' ); // `foo` -> `foo.myext`, `foo.js -> `foo.js` 30 | } 31 | }; 32 | } 33 | ``` 34 | 35 | 36 | ### attachScopes 37 | 38 | This function attaches `Scope` objects to the relevant nodes of an AST. Each `Scope` object has a `scope.contains(name)` method that returns `true` if a given name is defined in the current scope or a parent scope. 39 | 40 | See [rollup-plugin-inject](https://github.com/rollup/rollup-plugin-inject) or [rollup-plugin-commonjs](https://github.com/rollup/rollup-plugin-commonjs) for an example of usage. 41 | 42 | ```js 43 | import { attachScopes } from 'rollup-pluginutils'; 44 | import { walk } from 'estree-walker'; 45 | 46 | export default function myPlugin ( options = {} ) { 47 | return { 48 | transform ( code ) { 49 | const ast = this.parse( code ); 50 | 51 | let scope = attachScopes( ast, 'scope' ); 52 | 53 | walk( ast, { 54 | enter ( node ) { 55 | if ( node.scope ) scope = node.scope; 56 | 57 | if ( !scope.contains( 'foo' ) ) { 58 | // `foo` is not defined, so if we encounter it, 59 | // we assume it's a global 60 | } 61 | }, 62 | leave ( node ) { 63 | if ( node.scope ) scope = scope.parent; 64 | } 65 | }); 66 | } 67 | }; 68 | } 69 | ``` 70 | 71 | 72 | ### createFilter 73 | 74 | ```js 75 | import { createFilter } from 'rollup-pluginutils'; 76 | 77 | export default function myPlugin ( options = {} ) { 78 | // `options.include` and `options.exclude` can each be a minimatch 79 | // pattern, or an array of minimatch patterns, relative to process.cwd() 80 | var filter = createFilter( options.include, options.exclude ); 81 | 82 | return { 83 | transform ( code, id ) { 84 | // if `options.include` is omitted or has zero length, filter 85 | // will return `true` by default. Otherwise, an ID must match 86 | // one or more of the minimatch patterns, and must not match 87 | // any of the `options.exclude` patterns. 88 | if ( !filter( id ) ) return; 89 | 90 | // proceed with the transformation... 91 | } 92 | }; 93 | } 94 | ``` 95 | 96 | If you want to resolve the patterns against a directory other than 97 | `process.cwd()`, you can additionally pass a `resolve` option: 98 | 99 | ```js 100 | var filter = createFilter( options.include, options.exclude, {resolve: '/my/base/dir'} ) 101 | ``` 102 | 103 | If `resolve` is a string, then this value will be used as the base directory. 104 | Relative paths will be resolved against `process.cwd()` first. If `resolve` is 105 | `false`, then the patterns will not be resolved against any directory. This can 106 | be useful if you want to create a filter for virtual module names. 107 | 108 | 109 | ### makeLegalIdentifier 110 | 111 | ```js 112 | import { makeLegalIdentifier } from 'rollup-pluginutils'; 113 | 114 | makeLegalIdentifier( 'foo-bar' ); // 'foo_bar' 115 | makeLegalIdentifier( 'typeof' ); // '_typeof' 116 | ``` 117 | 118 | ### dataToEsm 119 | 120 | Helper for treeshakable data imports 121 | 122 | ```js 123 | import { dataToEsm } from 'rollup-pluginutils'; 124 | 125 | const esModuleSource = dataToEsm({ 126 | custom: 'data', 127 | to: ['treeshake'] 128 | }, { 129 | compact: false, 130 | indent: '\t', 131 | preferConst: false, 132 | objectShorthand: false, 133 | namedExports: true 134 | }); 135 | /* 136 | Outputs the string ES module source: 137 | export const custom = 'data'; 138 | export const to = ['treeshake']; 139 | export default { custom, to }; 140 | */ 141 | ``` 142 | 143 | ### extractAssignedNames 144 | 145 | Extract the names of all assignment targets from patterns. 146 | 147 | ```js 148 | import { extractAssignedNames } from 'rollup-pluginutils'; 149 | import { walk } from 'estree-walker'; 150 | 151 | export default function myPlugin ( options = {} ) { 152 | return { 153 | transform ( code ) { 154 | const ast = this.parse( code ); 155 | 156 | walk( ast, { 157 | enter ( node ) { 158 | if ( node.type === 'VariableDeclarator' ) { 159 | const declaredNames = extractAssignedNames(node.id); 160 | // do something with the declared names 161 | // e.g. for `const {x, y: z} = ... => declaredNames = ['x', 'z'] 162 | } 163 | } 164 | }); 165 | } 166 | }; 167 | } 168 | ``` 169 | 170 | 171 | ## License 172 | 173 | MIT 174 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es6"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/extractAssignedNames.test.ts: -------------------------------------------------------------------------------- 1 | import { extractAssignedNames } from '..'; 2 | 3 | describe('extractAssignedNames', function() { 4 | it('extracts an Identifier', function() { 5 | const node = { 6 | type: 'Identifier', 7 | start: 6, 8 | end: 7, 9 | name: 'x' 10 | }; 11 | 12 | expect(extractAssignedNames(node)).toEqual(['x']); 13 | }); 14 | 15 | it('extracts from array patterns', function() { 16 | const node = { 17 | type: 'ArrayPattern', 18 | start: 6, 19 | end: 29, 20 | elements: [ 21 | null, 22 | { 23 | type: 'Identifier', 24 | start: 9, 25 | end: 10, 26 | name: 'a' 27 | }, 28 | { 29 | type: 'AssignmentPattern', 30 | start: 12, 31 | end: 17, 32 | left: { 33 | type: 'Identifier', 34 | start: 12, 35 | end: 13, 36 | name: 'b' 37 | }, 38 | right: { 39 | type: 'Identifier', 40 | start: 16, 41 | end: 17, 42 | name: 'c' 43 | } 44 | }, 45 | { 46 | type: 'ArrayPattern', 47 | start: 19, 48 | end: 22, 49 | elements: [ 50 | { 51 | type: 'Identifier', 52 | start: 20, 53 | end: 21, 54 | name: 'd' 55 | } 56 | ] 57 | }, 58 | { 59 | type: 'RestElement', 60 | start: 24, 61 | end: 28, 62 | argument: { 63 | type: 'Identifier', 64 | start: 27, 65 | end: 28, 66 | name: 'e' 67 | } 68 | } 69 | ] 70 | }; 71 | 72 | expect(extractAssignedNames(node)).toEqual(['a', 'b', 'd', 'e']); 73 | }); 74 | 75 | it('extracts from object patterns', function() { 76 | const node = { 77 | type: 'ObjectPattern', 78 | start: 6, 79 | end: 42, 80 | properties: [ 81 | { 82 | type: 'Property', 83 | start: 7, 84 | end: 8, 85 | method: false, 86 | shorthand: true, 87 | computed: false, 88 | key: { 89 | type: 'Identifier', 90 | start: 7, 91 | end: 8, 92 | name: 'a' 93 | }, 94 | kind: 'init', 95 | value: { 96 | type: 'Identifier', 97 | start: 7, 98 | end: 8, 99 | name: 'a' 100 | } 101 | }, 102 | { 103 | type: 'Property', 104 | start: 10, 105 | end: 14, 106 | method: false, 107 | shorthand: false, 108 | computed: false, 109 | key: { 110 | type: 'Identifier', 111 | start: 10, 112 | end: 11, 113 | name: 'b' 114 | }, 115 | value: { 116 | type: 'Identifier', 117 | start: 13, 118 | end: 14, 119 | name: 'c' 120 | }, 121 | kind: 'init' 122 | }, 123 | { 124 | type: 'Property', 125 | start: 16, 126 | end: 29, 127 | method: false, 128 | shorthand: false, 129 | computed: false, 130 | key: { 131 | type: 'Identifier', 132 | start: 16, 133 | end: 17, 134 | name: 'd' 135 | }, 136 | value: { 137 | type: 'ObjectPattern', 138 | start: 19, 139 | end: 29, 140 | properties: [ 141 | { 142 | type: 'Property', 143 | start: 20, 144 | end: 28, 145 | method: false, 146 | shorthand: false, 147 | computed: false, 148 | key: { 149 | type: 'Identifier', 150 | start: 20, 151 | end: 21, 152 | name: 'e' 153 | }, 154 | value: { 155 | type: 'AssignmentPattern', 156 | start: 23, 157 | end: 28, 158 | left: { 159 | type: 'Identifier', 160 | start: 23, 161 | end: 24, 162 | name: 'f' 163 | }, 164 | right: { 165 | type: 'Identifier', 166 | start: 27, 167 | end: 28, 168 | name: 'g' 169 | } 170 | }, 171 | kind: 'init' 172 | } 173 | ] 174 | }, 175 | kind: 'init' 176 | }, 177 | { 178 | type: 'Property', 179 | start: 31, 180 | end: 35, 181 | method: false, 182 | shorthand: false, 183 | computed: false, 184 | key: { 185 | type: 'Literal', 186 | start: 31, 187 | end: 32, 188 | value: 1, 189 | raw: '1' 190 | }, 191 | value: { 192 | type: 'Identifier', 193 | start: 34, 194 | end: 35, 195 | name: 'h' 196 | }, 197 | kind: 'init' 198 | }, 199 | { 200 | type: 'RestElement', 201 | start: 37, 202 | end: 41, 203 | argument: { 204 | type: 'Identifier', 205 | start: 40, 206 | end: 41, 207 | name: 'i' 208 | } 209 | } 210 | ] 211 | }; 212 | 213 | expect(extractAssignedNames(node)).toEqual(['a', 'c', 'f', 'h', 'i']); 214 | }); 215 | 216 | it('ignores updated member expressions', function() { 217 | const node = { 218 | type: 'ArrayPattern', 219 | start: 0, 220 | end: 11, 221 | elements: [ 222 | { 223 | type: 'MemberExpression', 224 | start: 1, 225 | end: 4, 226 | object: { 227 | type: 'Identifier', 228 | start: 1, 229 | end: 2, 230 | name: 'a' 231 | }, 232 | property: { 233 | type: 'Identifier', 234 | start: 3, 235 | end: 4, 236 | name: 'b' 237 | }, 238 | computed: false 239 | }, 240 | { 241 | type: 'MemberExpression', 242 | start: 6, 243 | end: 10, 244 | object: { 245 | type: 'Identifier', 246 | start: 6, 247 | end: 7, 248 | name: 'c' 249 | }, 250 | property: { 251 | type: 'Identifier', 252 | start: 8, 253 | end: 9, 254 | name: 'd' 255 | }, 256 | computed: true 257 | } 258 | ] 259 | }; 260 | 261 | expect(extractAssignedNames(node)).toEqual([]); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/var/folders/1n/hxkwxdt95zzgp53y_5ng4gy40000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | globals: { 59 | 'ts-jest': { 60 | 'tsConfig': 'tsconfig.json' 61 | } 62 | }, 63 | 64 | // An array of directory names to be searched recursively up from the requiring module's location 65 | // moduleDirectories: [ 66 | // "node_modules" 67 | // ], 68 | 69 | // An array of file extensions your modules use 70 | moduleFileExtensions: [ 71 | 'ts', 72 | 'tsx', 73 | 'js' 74 | ], 75 | 76 | // A map from regular expressions to module names that allow to stub out resources with a single module 77 | // moduleNameMapper: {}, 78 | 79 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 80 | // modulePathIgnorePatterns: [], 81 | 82 | // Activates notifications for test results 83 | // notify: false, 84 | 85 | // An enum that specifies notification mode. Requires { notify: true } 86 | // notifyMode: "always", 87 | 88 | // A preset that is used as a base for Jest's configuration 89 | // preset: null, 90 | 91 | // Run tests from one or more projects 92 | // projects: null, 93 | 94 | // Use this configuration option to add custom reporters to Jest 95 | // reporters: undefined, 96 | 97 | // Automatically reset mock state between every test 98 | // resetMocks: false, 99 | 100 | // Reset the module registry before running each individual test 101 | // resetModules: false, 102 | 103 | // A path to a custom resolver 104 | // resolver: null, 105 | 106 | // Automatically restore mock state between every test 107 | // restoreMocks: false, 108 | 109 | // The root directory that Jest should scan for tests and modules within 110 | // rootDir: null, 111 | 112 | // A list of paths to directories that Jest should use to search for files in 113 | // roots: [ 114 | // "" 115 | // ], 116 | 117 | // Allows you to use a custom runner instead of Jest's default test runner 118 | // runner: "jest-runner", 119 | 120 | // The paths to modules that run some code to configure or set up the testing environment before each test 121 | // setupFiles: [], 122 | 123 | // The path to a module that runs some code to configure or set up the testing framework before each test 124 | // setupTestFrameworkScriptFile: null, 125 | 126 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 127 | // snapshotSerializers: [], 128 | 129 | // The test environment that will be used for testing 130 | testEnvironment: 'node', 131 | 132 | // Options that will be passed to the testEnvironment 133 | // testEnvironmentOptions: {}, 134 | 135 | // Adds a location field to test results 136 | // testLocationInResults: false, 137 | 138 | // The glob patterns Jest uses to detect test files 139 | testMatch: [ 140 | '**/*.test.+(ts|tsx|js)' 141 | ], 142 | 143 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 144 | // testPathIgnorePatterns: [ 145 | // "/node_modules/" 146 | // ], 147 | 148 | // The regexp pattern Jest uses to detect test files 149 | // testRegex: "", 150 | 151 | // This option allows the use of a custom results processor 152 | // testResultsProcessor: null, 153 | 154 | // This option allows use of a custom test runner 155 | // testRunner: "jasmine2", 156 | 157 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 158 | // testURL: "http://localhost", 159 | 160 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 161 | // timers: "real", 162 | 163 | // A map from regular expressions to paths to transformers 164 | transform: { 165 | '^.+\\.(ts|tsx)$': 'ts-jest' 166 | }, 167 | 168 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 169 | // transformIgnorePatterns: [ 170 | // "/node_modules/" 171 | // ], 172 | 173 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 174 | // unmockedModulePathPatterns: undefined, 175 | 176 | // Indicates whether each individual test should be reported during the run 177 | // verbose: null, 178 | 179 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 180 | // watchPathIgnorePatterns: [], 181 | 182 | // Whether to use watchman for file crawling 183 | // watchman: true, 184 | }; 185 | -------------------------------------------------------------------------------- /test/attachScopes.test.ts: -------------------------------------------------------------------------------- 1 | import { attachScopes } from '..'; 2 | 3 | describe('attachScopes', function() { 4 | it('attaches a scope to the top level', function() { 5 | const ast = { 6 | type: 'Program', 7 | start: 0, 8 | end: 8, 9 | body: [ 10 | { 11 | type: 'VariableDeclaration', 12 | start: 0, 13 | end: 8, 14 | declarations: [ 15 | { 16 | type: 'VariableDeclarator', 17 | start: 4, 18 | end: 7, 19 | id: { 20 | type: 'Identifier', 21 | start: 4, 22 | end: 7, 23 | name: 'foo' 24 | }, 25 | init: null 26 | } 27 | ], 28 | kind: 'var' 29 | } 30 | ], 31 | sourceType: 'module' 32 | }; 33 | 34 | const scope = attachScopes(ast, 'scope'); 35 | expect(scope.contains('foo')).toBeTruthy(); 36 | expect(scope.contains('bar')).toBeFalsy(); 37 | }); 38 | 39 | it('adds multiple declarators from a single var declaration', function() { 40 | const ast = { 41 | type: 'Program', 42 | start: 0, 43 | end: 13, 44 | body: [ 45 | { 46 | type: 'VariableDeclaration', 47 | start: 0, 48 | end: 13, 49 | declarations: [ 50 | { 51 | type: 'VariableDeclarator', 52 | start: 4, 53 | end: 7, 54 | id: { 55 | type: 'Identifier', 56 | start: 4, 57 | end: 7, 58 | name: 'foo' 59 | }, 60 | init: null 61 | }, 62 | 63 | { 64 | type: 'VariableDeclarator', 65 | start: 9, 66 | end: 12, 67 | id: { 68 | type: 'Identifier', 69 | start: 9, 70 | end: 12, 71 | name: 'bar' 72 | }, 73 | init: null 74 | } 75 | ], 76 | kind: 'var' 77 | } 78 | ], 79 | sourceType: 'module' 80 | }; 81 | 82 | const scope = attachScopes(ast, 'scope'); 83 | expect(scope.contains('foo')).toBeTruthy(); 84 | expect(scope.contains('bar')).toBeTruthy(); 85 | }); 86 | 87 | it('adds named declarators from a deconstructed declaration', function() { 88 | const ast = { 89 | type: 'Program', 90 | start: 0, 91 | end: 13, 92 | body: [ 93 | { 94 | type: 'VariableDeclaration', 95 | start: 0, 96 | end: 42, 97 | declarations: [ 98 | { 99 | type: 'VariableDeclarator', 100 | start: 4, 101 | end: 41, 102 | id: { 103 | type: 'ObjectPattern', 104 | start: 4, 105 | end: 15, 106 | properties: [ 107 | { 108 | type: 'Property', 109 | start: 6, 110 | end: 10, 111 | method: false, 112 | shorthand: false, 113 | computed: false, 114 | key: { 115 | type: 'Literal', 116 | start: 6, 117 | end: 7, 118 | value: 1, 119 | raw: '1' 120 | }, 121 | value: { 122 | type: 'Identifier', 123 | start: 9, 124 | end: 10, 125 | name: 'a' 126 | }, 127 | kind: 'init' 128 | }, 129 | { 130 | type: 'Property', 131 | start: 12, 132 | end: 13, 133 | method: false, 134 | shorthand: true, 135 | computed: false, 136 | key: { 137 | type: 'Identifier', 138 | start: 12, 139 | end: 13, 140 | name: 'b' 141 | }, 142 | kind: 'init', 143 | value: { 144 | type: 'Identifier', 145 | start: 12, 146 | end: 13, 147 | name: 'b' 148 | } 149 | } 150 | ] 151 | }, 152 | init: { 153 | type: 'ObjectExpression', 154 | start: 18, 155 | end: 41, 156 | properties: [ 157 | { 158 | type: 'Property', 159 | start: 22, 160 | end: 28, 161 | method: false, 162 | shorthand: false, 163 | computed: false, 164 | key: { 165 | type: 'Literal', 166 | start: 22, 167 | end: 23, 168 | value: 1, 169 | raw: '1' 170 | }, 171 | value: { 172 | type: 'Literal', 173 | start: 25, 174 | end: 28, 175 | value: 'a', 176 | raw: "'a'" 177 | }, 178 | kind: 'init' 179 | }, 180 | { 181 | type: 'Property', 182 | start: 32, 183 | end: 38, 184 | method: false, 185 | shorthand: false, 186 | computed: false, 187 | key: { 188 | type: 'Identifier', 189 | start: 32, 190 | end: 33, 191 | name: 'b' 192 | }, 193 | value: { 194 | type: 'Literal', 195 | start: 35, 196 | end: 38, 197 | value: 'b', 198 | raw: "'b'" 199 | }, 200 | kind: 'init' 201 | } 202 | ] 203 | } 204 | } 205 | ], 206 | kind: 'var' 207 | } 208 | ], 209 | sourceType: 'module' 210 | }; 211 | 212 | const scope = attachScopes(ast, 'scope'); 213 | expect(scope.contains('a')).toBeTruthy(); 214 | expect(scope.contains('b')).toBeTruthy(); 215 | }); 216 | 217 | it('adds rest elements from a deconstructed object declaration', function() { 218 | const ast = { 219 | type: 'Program', 220 | start: 0, 221 | end: 66, 222 | body: [ 223 | { 224 | type: 'VariableDeclaration', 225 | start: 0, 226 | end: 66, 227 | declarations: [ 228 | { 229 | type: 'VariableDeclarator', 230 | start: 6, 231 | end: 66, 232 | id: { 233 | type: 'ObjectPattern', 234 | start: 6, 235 | end: 26, 236 | properties: [ 237 | { 238 | type: 'Property', 239 | start: 8, 240 | end: 9, 241 | method: false, 242 | shorthand: true, 243 | computed: false, 244 | key: { 245 | type: 'Identifier', 246 | start: 8, 247 | end: 9, 248 | name: 'x' 249 | }, 250 | kind: 'init', 251 | value: { 252 | type: 'Identifier', 253 | start: 8, 254 | end: 9, 255 | name: 'x' 256 | } 257 | }, 258 | { 259 | type: 'Property', 260 | start: 11, 261 | end: 15, 262 | method: false, 263 | shorthand: false, 264 | computed: false, 265 | key: { 266 | type: 'Identifier', 267 | start: 11, 268 | end: 12, 269 | name: 'y' 270 | }, 271 | value: { 272 | type: 'Identifier', 273 | start: 14, 274 | end: 15, 275 | name: 'z' 276 | }, 277 | kind: 'init' 278 | }, 279 | { 280 | type: 'RestElement', 281 | start: 17, 282 | end: 24, 283 | argument: { 284 | type: 'Identifier', 285 | start: 20, 286 | end: 24, 287 | name: 'rest' 288 | } 289 | } 290 | ] 291 | }, 292 | init: { 293 | type: 'ObjectExpression', 294 | start: 29, 295 | end: 66, 296 | properties: [ 297 | { 298 | type: 'Property', 299 | start: 31, 300 | end: 36, 301 | method: false, 302 | shorthand: false, 303 | computed: false, 304 | key: { 305 | type: 'Identifier', 306 | start: 31, 307 | end: 32, 308 | name: 'x' 309 | }, 310 | value: { 311 | type: 'Literal', 312 | start: 34, 313 | end: 36, 314 | value: 10, 315 | raw: '10' 316 | }, 317 | kind: 'init' 318 | }, 319 | { 320 | type: 'Property', 321 | start: 38, 322 | end: 43, 323 | method: false, 324 | shorthand: false, 325 | computed: false, 326 | key: { 327 | type: 'Identifier', 328 | start: 38, 329 | end: 39, 330 | name: 'y' 331 | }, 332 | value: { 333 | type: 'Literal', 334 | start: 41, 335 | end: 43, 336 | value: 20, 337 | raw: '20' 338 | }, 339 | kind: 'init' 340 | }, 341 | { 342 | type: 'Property', 343 | start: 45, 344 | end: 50, 345 | method: false, 346 | shorthand: false, 347 | computed: false, 348 | key: { 349 | type: 'Identifier', 350 | start: 45, 351 | end: 46, 352 | name: 'z' 353 | }, 354 | value: { 355 | type: 'Literal', 356 | start: 48, 357 | end: 50, 358 | value: 30, 359 | raw: '30' 360 | }, 361 | kind: 'init' 362 | }, 363 | { 364 | type: 'Property', 365 | start: 52, 366 | end: 57, 367 | method: false, 368 | shorthand: false, 369 | computed: false, 370 | key: { 371 | type: 'Identifier', 372 | start: 52, 373 | end: 53, 374 | name: 'w' 375 | }, 376 | value: { 377 | type: 'Literal', 378 | start: 55, 379 | end: 57, 380 | value: 40, 381 | raw: '40' 382 | }, 383 | kind: 'init' 384 | }, 385 | { 386 | type: 'Property', 387 | start: 59, 388 | end: 64, 389 | method: false, 390 | shorthand: false, 391 | computed: false, 392 | key: { 393 | type: 'Identifier', 394 | start: 59, 395 | end: 60, 396 | name: 'k' 397 | }, 398 | value: { 399 | type: 'Literal', 400 | start: 62, 401 | end: 64, 402 | value: 50, 403 | raw: '50' 404 | }, 405 | kind: 'init' 406 | } 407 | ] 408 | } 409 | } 410 | ], 411 | kind: 'const' 412 | } 413 | ], 414 | sourceType: 'module' 415 | }; 416 | 417 | const scope = attachScopes(ast, 'scope'); 418 | expect(scope.contains('x')).toBeTruthy(); 419 | expect(scope.contains('y')).toBeFalsy(); 420 | expect(scope.contains('z')).toBeTruthy(); 421 | expect(scope.contains('rest')).toBeTruthy(); 422 | }); 423 | 424 | it('adds nested declarators from a deconstructed declaration', function() { 425 | const ast = { 426 | type: 'Program', 427 | start: 0, 428 | end: 40, 429 | body: [ 430 | { 431 | type: 'VariableDeclaration', 432 | start: 0, 433 | end: 40, 434 | declarations: [ 435 | { 436 | type: 'VariableDeclarator', 437 | start: 4, 438 | end: 39, 439 | id: { 440 | type: 'ObjectPattern', 441 | start: 4, 442 | end: 19, 443 | properties: [ 444 | { 445 | type: 'Property', 446 | start: 6, 447 | end: 17, 448 | method: false, 449 | shorthand: false, 450 | computed: false, 451 | key: { 452 | type: 'Identifier', 453 | start: 6, 454 | end: 7, 455 | name: 'a' 456 | }, 457 | value: { 458 | type: 'ObjectPattern', 459 | start: 9, 460 | end: 17, 461 | properties: [ 462 | { 463 | type: 'Property', 464 | start: 11, 465 | end: 15, 466 | method: false, 467 | shorthand: false, 468 | computed: false, 469 | key: { 470 | type: 'Identifier', 471 | start: 11, 472 | end: 12, 473 | name: 'b' 474 | }, 475 | value: { 476 | type: 'Identifier', 477 | start: 14, 478 | end: 15, 479 | name: 'c' 480 | }, 481 | kind: 'init' 482 | } 483 | ] 484 | }, 485 | kind: 'init' 486 | } 487 | ] 488 | }, 489 | init: { 490 | type: 'ObjectExpression', 491 | start: 22, 492 | end: 39, 493 | properties: [ 494 | { 495 | type: 'Property', 496 | start: 24, 497 | end: 37, 498 | method: false, 499 | shorthand: false, 500 | computed: false, 501 | key: { 502 | type: 'Identifier', 503 | start: 24, 504 | end: 25, 505 | name: 'a' 506 | }, 507 | value: { 508 | type: 'ObjectExpression', 509 | start: 27, 510 | end: 37, 511 | properties: [ 512 | { 513 | type: 'Property', 514 | start: 29, 515 | end: 35, 516 | method: false, 517 | shorthand: false, 518 | computed: false, 519 | key: { 520 | type: 'Identifier', 521 | start: 29, 522 | end: 30, 523 | name: 'b' 524 | }, 525 | value: { 526 | type: 'Literal', 527 | start: 32, 528 | end: 35, 529 | value: 'b', 530 | raw: "'b'" 531 | }, 532 | kind: 'init' 533 | } 534 | ] 535 | }, 536 | kind: 'init' 537 | } 538 | ] 539 | } 540 | } 541 | ], 542 | kind: 'let' 543 | } 544 | ], 545 | sourceType: 'module' 546 | }; 547 | 548 | const scope = attachScopes(ast, 'scope'); 549 | expect(scope.contains('a')).toBeFalsy(); 550 | expect(scope.contains('b')).toBeFalsy(); 551 | expect(scope.contains('c')).toBeTruthy(); 552 | }); 553 | 554 | it('supports FunctionDeclarations without id', function() { 555 | const ast = { 556 | type: 'Program', 557 | start: 0, 558 | end: 33, 559 | body: [ 560 | { 561 | type: 'ExportDefaultDeclaration', 562 | start: 0, 563 | end: 32, 564 | declaration: { 565 | type: 'FunctionDeclaration', 566 | start: 15, 567 | end: 32, 568 | id: null, 569 | generator: false, 570 | expression: false, 571 | async: false, 572 | params: [], 573 | body: { 574 | type: 'BlockStatement', 575 | start: 26, 576 | end: 32, 577 | body: [] 578 | } 579 | } 580 | } 581 | ], 582 | sourceType: 'module' 583 | }; 584 | 585 | expect(() => { 586 | attachScopes(ast, 'scope'); 587 | }).not.toThrow(); 588 | }); 589 | 590 | it('supports catch without a parameter', function() { 591 | const ast = { 592 | "type": "Program", 593 | "start": 0, 594 | "end": 23, 595 | "body": [ 596 | { 597 | "type": "TryStatement", 598 | "start": 0, 599 | "end": 23, 600 | "block": { 601 | "type": "BlockStatement", 602 | "start": 4, 603 | "end": 10, 604 | "body": [] 605 | }, 606 | "handler": { 607 | "type": "CatchClause", 608 | "start": 11, 609 | "end": 23, 610 | "param": null, 611 | "body": { 612 | "type": "BlockStatement", 613 | "start": 17, 614 | "end": 23, 615 | "body": [] 616 | } 617 | }, 618 | "finalizer": null 619 | } 620 | ], 621 | "sourceType": "script" 622 | }; 623 | expect(() => { 624 | attachScopes(ast, 'scope'); 625 | }).not.toThrow(); 626 | }); 627 | }); 628 | --------------------------------------------------------------------------------