├── .gitignore ├── test ├── samples │ ├── redundant-keys │ │ └── main.js │ ├── basic │ │ └── main.js │ ├── non-js │ │ ├── foo.es6 │ │ ├── styles.css │ │ └── main.js │ ├── import-namespace │ │ └── main.js │ ├── named │ │ └── main.js │ ├── existing │ │ └── main.js │ ├── shorthand │ │ └── main.js │ ├── keypaths │ │ ├── main.js │ │ └── polyfills │ │ │ └── object-assign.js │ └── shadowing │ │ └── main.js └── test.js ├── .travis.yml ├── rollup.config.js ├── .eslintrc.yml ├── appveyor.yml ├── CHANGELOG.md ├── package.json ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .gobble* 5 | -------------------------------------------------------------------------------- /test/samples/redundant-keys/main.js: -------------------------------------------------------------------------------- 1 | Buffer.isBuffer('foo'); 2 | -------------------------------------------------------------------------------- /test/samples/basic/main.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | console.log( 'ready' ); 3 | }); 4 | -------------------------------------------------------------------------------- /test/samples/non-js/foo.es6: -------------------------------------------------------------------------------- 1 | export default relative( 'foo/bar', 'foo/baz' ); 2 | -------------------------------------------------------------------------------- /test/samples/import-namespace/main.js: -------------------------------------------------------------------------------- 1 | console.log( foo.bar ); 2 | console.log( foo.baz ); 3 | -------------------------------------------------------------------------------- /test/samples/named/main.js: -------------------------------------------------------------------------------- 1 | Promise.all([ thisThing, thatThing ]).then( () => someOtherThing ); 2 | -------------------------------------------------------------------------------- /test/samples/non-js/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: relative; 3 | font-family: 'Comic Sans MS'; 4 | } 5 | -------------------------------------------------------------------------------- /test/samples/existing/main.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | $(function () { 4 | console.log( 'ready' ); 5 | }); 6 | -------------------------------------------------------------------------------- /test/samples/shorthand/main.js: -------------------------------------------------------------------------------- 1 | const polyfills = { Promise }; 2 | polyfills.Promise.resolve().then( () => 'it works' ); 3 | -------------------------------------------------------------------------------- /test/samples/keypaths/main.js: -------------------------------------------------------------------------------- 1 | var original = { foo: 'bar' }; 2 | var clone = Object.assign( {}, original ); 3 | 4 | export default clone; 5 | -------------------------------------------------------------------------------- /test/samples/non-js/main.js: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import foo from './foo.es6'; 3 | 4 | assert.equal( foo, path.join('..','baz') ); 5 | -------------------------------------------------------------------------------- /test/samples/shadowing/main.js: -------------------------------------------------------------------------------- 1 | function launch ( $ ) { 2 | $(function () { 3 | console.log( 'ready' ); 4 | }); 5 | } 6 | 7 | launch( fn => fn() ); 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "10" 6 | env: 7 | global: 8 | - BUILD_TIMEOUT=10000 9 | install: npm ci --ignore-scripts 10 | -------------------------------------------------------------------------------- /test/samples/keypaths/polyfills/object-assign.js: -------------------------------------------------------------------------------- 1 | export default Object.assign || function ( target, ...sources ) { 2 | sources.forEach( source => { 3 | Object.keys( source ).forEach( key => target[ key ] = source[ key ] ); 4 | }); 5 | 6 | return target; 7 | }; 8 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const external = Object.keys(require("./package.json").dependencies).concat("path"); 2 | 3 | export default { 4 | input: "src/index.js", 5 | external: external, 6 | output: [ 7 | { file: "dist/rollup-plugin-inject.cjs.js", format: "cjs" }, 8 | { file: "dist/rollup-plugin-inject.es6.js", format: "esm" } 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | 2 | rules: 3 | semi: [ 2, "always" ] 4 | no-cond-assign: [ 0 ] 5 | prefer-arrow-callback: 2 6 | prefer-const: 2 7 | no-var: 2 8 | strict: 2 9 | 10 | 11 | env: 12 | es6: true 13 | browser: true 14 | mocha: true 15 | node: true 16 | 17 | extends: "eslint:recommended" 18 | 19 | parserOptions: 20 | sourceType: "module" 21 | ecmaVersion: 2018 22 | -------------------------------------------------------------------------------- /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 | # node.js 13 | - nodejs_version: "8" 14 | - nodejs_version: "10" 15 | 16 | install: 17 | - ps: Install-Product node $env:nodejs_version 18 | - npm install 19 | 20 | build: off 21 | 22 | test_script: 23 | - node --version && npm --version 24 | - npm test 25 | 26 | matrix: 27 | fast_finish: false 28 | 29 | # cache: 30 | # - C:\Users\appveyor\AppData\Roaming\npm-cache -> package.json # npm cache 31 | # - node_modules -> package.json # local npm modules 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # rollup-plugin-inject 2 | 3 | ## 3.0.1 4 | 5 | * Generate sourcemap when sourcemap enabled 6 | 7 | ## 3.0.0 8 | 9 | * Remove node v6 from support 10 | * Use modern js 11 | 12 | ## 2.1.0 13 | 14 | * Update all dependencies ([#15](https://github.com/rollup/rollup-plugin-inject/pull/15)) 15 | 16 | ## 2.0.0 17 | 18 | * Work with all file extensions, not just `.js` (unless otherwise specified via `options.include` and `options.exclude`) ([#6](https://github.com/rollup/rollup-plugin-inject/pull/6)) 19 | * Allow `*` imports ([#9](https://github.com/rollup/rollup-plugin-inject/pull/9)) 20 | * Ignore replacements that are superseded (e.g. if `Buffer.isBuffer` is replaced, ignore `Buffer` replacement) ([#10](https://github.com/rollup/rollup-plugin-inject/pull/10)) 21 | 22 | ## 1.4.1 23 | 24 | * Return a `name` 25 | 26 | ## 1.4.0 27 | 28 | * Use `string.search` instead of `regex.test` to avoid state-related mishaps ([#5](https://github.com/rollup/rollup-plugin-inject/issues/5)) 29 | * Prevent self-importing module bug 30 | 31 | ## 1.3.0 32 | 33 | * Windows support ([#2](https://github.com/rollup/rollup-plugin-inject/issues/2)) 34 | * Node 0.12 support 35 | 36 | ## 1.2.0 37 | 38 | * Generate sourcemaps by default 39 | 40 | ## 1.1.1 41 | 42 | * Use `modules` option 43 | 44 | ## 1.1.0 45 | 46 | * Handle shorthand properties 47 | 48 | ## 1.0.0 49 | 50 | * First release 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup-plugin-inject", 3 | "description": "Scan modules for global variables and inject `import` statements where necessary", 4 | "version": "3.0.2", 5 | "devDependencies": { 6 | "eslint": "^6.3.0", 7 | "mocha": "^6.2.0", 8 | "prettier": "^1.18.2", 9 | "rollup": "^1.17.0", 10 | "shx": "^0.3.2" 11 | }, 12 | "main": "dist/rollup-plugin-inject.cjs.js", 13 | "module": "dist/rollup-plugin-inject.es6.js", 14 | "jsnext:main": "dist/rollup-plugin-inject.es6.js", 15 | "scripts": { 16 | "pretest": "npm run build", 17 | "test": "mocha", 18 | "prebuild": "shx rm -rf dist", 19 | "build": "rollup -c", 20 | "prepublishOnly": "npm run lint && npm run test", 21 | "prepare": "npm run build", 22 | "lint": "eslint --fix src test/test.js" 23 | }, 24 | "files": [ 25 | "src", 26 | "dist", 27 | "README.md" 28 | ], 29 | "dependencies": { 30 | "estree-walker": "^0.6.1", 31 | "magic-string": "^0.25.3", 32 | "rollup-pluginutils": "^2.8.1" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/rollup/rollup-plugin-inject.git" 37 | }, 38 | "keywords": [ 39 | "rollup", 40 | "rollup-plugin", 41 | "es2015", 42 | "npm", 43 | "modules" 44 | ], 45 | "author": "Rich Harris ", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/rollup/rollup-plugin-inject/issues" 49 | }, 50 | "homepage": "https://github.com/rollup/rollup-plugin-inject#readme" 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | 3 | This module has moved and is now available at [@rollup/plugin-inject](https://github.com/rollup/plugins). Please update your dependencies. This repository is no longer maintained. 4 | 5 | # rollup-plugin-inject 6 | 7 | Scan modules for global variables and inject `import` statements where necessary 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm install --save-dev rollup-plugin-inject 13 | ``` 14 | 15 | 16 | ## Usage 17 | 18 | ```js 19 | import { rollup } from 'rollup'; 20 | import inject from 'rollup-plugin-inject'; 21 | 22 | rollup({ 23 | entry: 'main.js', 24 | plugins: [ 25 | inject({ 26 | // control which files this plugin applies to 27 | // with include/exclude 28 | include: '**/*.js', 29 | exclude: 'node_modules/**', 30 | 31 | /* all other options are treated as modules...*/ 32 | 33 | // use the default – i.e. insert 34 | // import $ from 'jquery' 35 | $: 'jquery', 36 | 37 | // use a named export – i.e. insert 38 | // import { Promise } from 'es6-promise' 39 | Promise: [ 'es6-promise', 'Promise' ], 40 | 41 | // use a namespace import – i.e. insert 42 | // import * as fs from 'fs' 43 | fs: [ 'fs', '*' ], 44 | 45 | // use a local module instead of a third-party one 46 | 'Object.assign': path.resolve( 'src/helpers/object-assign.js' ), 47 | 48 | /* ...but if you want to be careful about separating modules 49 | from other options, supply `options.modules` instead */ 50 | 51 | modules: { 52 | $: 'jquery', 53 | Promise: [ 'es6-promise', 'Promise' ], 54 | 'Object.assign': path.resolve( 'src/helpers/object-assign.js' ) 55 | } 56 | }) 57 | ] 58 | }).then(...) 59 | ``` 60 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const path = require("path"); 3 | const { rollup } = require("rollup"); 4 | const inject = require(".."); 5 | 6 | process.chdir(__dirname); 7 | 8 | describe("rollup-plugin-inject", () => { 9 | it("inserts a default import statement", async () => { 10 | const bundle = await rollup({ 11 | input: "samples/basic/main.js", 12 | plugins: [inject({ $: "jquery" })], 13 | external: ["jquery"] 14 | }); 15 | 16 | const { output } = await bundle.generate({ format: "es" }); 17 | 18 | const { code } = output[0]; 19 | 20 | assert.ok(code.indexOf("import $ from 'jquery'") !== -1, code); 21 | }); 22 | 23 | it("uses the modules property", async () => { 24 | const bundle = await rollup({ 25 | input: "samples/basic/main.js", 26 | plugins: [ 27 | inject({ 28 | modules: { $: "jquery" } 29 | }) 30 | ], 31 | external: ["jquery"] 32 | }); 33 | 34 | const { output } = await bundle.generate({ format: "es" }); 35 | const { code } = output[0]; 36 | 37 | assert.ok(code.indexOf("import $ from 'jquery'") !== -1, code); 38 | }); 39 | 40 | it("inserts a named import statement", async () => { 41 | const bundle = await rollup({ 42 | input: "samples/named/main.js", 43 | plugins: [inject({ Promise: ["es6-promise", "Promise"] })], 44 | external: ["es6-promise"] 45 | }); 46 | 47 | const { output } = await bundle.generate({ format: "es" }); 48 | const { code } = output[0]; 49 | 50 | assert.ok(code.indexOf("import { Promise } from 'es6-promise'") !== -1, code); 51 | }); 52 | 53 | it("overwrites keypaths", async () => { 54 | const bundle = await rollup({ 55 | input: "samples/keypaths/main.js", 56 | plugins: [ 57 | inject({ 58 | "Object.assign": path.resolve("samples/keypaths/polyfills/object-assign.js") 59 | }) 60 | ] 61 | }); 62 | const { output } = await bundle.generate({ format: "es" }); 63 | const { code } = output[0]; 64 | 65 | assert.notEqual(code.indexOf("var clone = $inject_Object_assign"), -1, code); 66 | assert.notEqual(code.indexOf("var $inject_Object_assign ="), -1, code); 67 | }); 68 | 69 | it("ignores existing imports", async () => { 70 | const bundle = await rollup({ 71 | input: "samples/existing/main.js", 72 | plugins: [inject({ $: "jquery" })], 73 | external: ["jquery"] 74 | }); 75 | const { output } = await bundle.generate({ format: "es" }); 76 | let { code } = output[0]; 77 | 78 | code = code.replace(/import \$.+/, ""); // remove first instance. there shouldn't be a second 79 | 80 | assert.ok(code.indexOf("import $ from 'jquery'") === -1, output[0].code); 81 | }); 82 | 83 | it("handles shadowed variables", async () => { 84 | const bundle = await rollup({ 85 | input: "samples/shadowing/main.js", 86 | plugins: [inject({ $: "jquery" })], 87 | external: ["jquery"] 88 | }); 89 | const { output } = await bundle.generate({ format: "es" }); 90 | 91 | const { code } = output[0]; 92 | 93 | assert.ok(code.indexOf("'jquery'") === -1, code); 94 | }); 95 | 96 | it("handles shorthand properties", async () => { 97 | const bundle = await rollup({ 98 | input: "samples/shorthand/main.js", 99 | plugins: [inject({ Promise: ["es6-promise", "Promise"] })], 100 | external: ["es6-promise"] 101 | }); 102 | const { output } = await bundle.generate({ format: "es" }); 103 | 104 | const { code } = output[0]; 105 | 106 | assert.ok(code.indexOf("import { Promise } from 'es6-promise'") !== -1, code); 107 | }); 108 | 109 | it("handles redundant keys", async () => { 110 | const bundle = await rollup({ 111 | input: "samples/redundant-keys/main.js", 112 | plugins: [ 113 | inject({ 114 | Buffer: "Buffer", 115 | "Buffer.isBuffer": "is-buffer" 116 | }) 117 | ], 118 | external: ["Buffer", "is-buffer"] 119 | }); 120 | 121 | const { output } = await bundle.generate({ format: "es" }); 122 | 123 | const { imports } = output[0]; 124 | 125 | assert.deepEqual(imports, ["is-buffer"]); 126 | }); 127 | 128 | it("generates * imports", async () => { 129 | const bundle = await rollup({ 130 | input: "samples/import-namespace/main.js", 131 | plugins: [inject({ foo: ["foo", "*"] })], 132 | external: ["foo"] 133 | }); 134 | const { output } = await bundle.generate({ format: "es" }); 135 | 136 | const { code } = output[0]; 137 | 138 | assert.ok(code.indexOf("import { bar, baz } from 'foo'") !== -1, code); 139 | }); 140 | 141 | it("transpiles non-JS files but handles failures to parse", async () => { 142 | const bundle = await rollup({ 143 | input: "samples/non-js/main.js", 144 | plugins: [ 145 | inject({ relative: ["path", "relative"] }), 146 | { 147 | transform(code, id) { 148 | if (/css/.test(id)) { 149 | return ""; 150 | } 151 | } 152 | } 153 | ], 154 | external: ["path"] 155 | }); 156 | const { output } = await bundle.generate({ format: "cjs" }); 157 | 158 | const { code } = output[0]; 159 | 160 | const fn = new Function("require", "path", "assert", code); 161 | fn(require, path, assert); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { attachScopes, createFilter, makeLegalIdentifier } from "rollup-pluginutils"; 2 | import { sep } from "path"; 3 | import { walk } from "estree-walker"; 4 | 5 | import MagicString from "magic-string"; 6 | 7 | const escape = str => { 8 | return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); 9 | }; 10 | 11 | const isReference = (node, parent) => { 12 | if (node.type === "MemberExpression") { 13 | return !node.computed && isReference(node.object, node); 14 | } 15 | 16 | if (node.type === "Identifier") { 17 | // TODO is this right? 18 | if (parent.type === "MemberExpression") return parent.computed || node === parent.object; 19 | 20 | // disregard the `bar` in { bar: foo } 21 | if (parent.type === "Property" && node !== parent.value) return false; 22 | 23 | // disregard the `bar` in `class Foo { bar () {...} }` 24 | if (parent.type === "MethodDefinition") return false; 25 | 26 | // disregard the `bar` in `export { foo as bar }` 27 | if (parent.type === "ExportSpecifier" && node !== parent.local) return; 28 | 29 | return true; 30 | } 31 | }; 32 | 33 | const flatten = node => { 34 | const parts = []; 35 | 36 | while (node.type === "MemberExpression") { 37 | parts.unshift(node.property.name); 38 | node = node.object; 39 | } 40 | 41 | const name = node.name; 42 | parts.unshift(name); 43 | 44 | return { name, keypath: parts.join(".") }; 45 | }; 46 | 47 | export default function inject(options) { 48 | if (!options) throw new Error("Missing options"); 49 | 50 | const filter = createFilter(options.include, options.exclude); 51 | 52 | let modules = options.modules; 53 | 54 | if (!modules) { 55 | modules = Object.assign({}, options); 56 | delete modules.include; 57 | delete modules.exclude; 58 | delete modules.sourceMap; 59 | delete modules.sourcemap; 60 | } 61 | 62 | const modulesMap = new Map(Object.entries(modules)); 63 | 64 | // Fix paths on Windows 65 | if (sep !== "/") { 66 | modulesMap.forEach((mod, key) => { 67 | modulesMap.set( 68 | key, 69 | Array.isArray(mod) ? [mod[0].split(sep).join("/"), mod[1]] : mod.split(sep).join("/") 70 | ); 71 | }); 72 | } 73 | 74 | const firstpass = new RegExp( 75 | `(?:${Array.from(modulesMap.keys()) 76 | .map(escape) 77 | .join("|")})`, 78 | "g" 79 | ); 80 | const sourceMap = options.sourceMap !== false && options.sourcemap !== false; 81 | 82 | return { 83 | name: "inject", 84 | 85 | transform(code, id) { 86 | if (!filter(id)) return null; 87 | if (code.search(firstpass) === -1) return null; 88 | 89 | if (sep !== "/") id = id.split(sep).join("/"); 90 | 91 | let ast = null; 92 | try { 93 | ast = this.parse(code); 94 | } catch (err) { 95 | this.warn({ 96 | code: "PARSE_ERROR", 97 | message: `rollup-plugin-inject: failed to parse ${id}. Consider restricting the plugin to particular files via options.include` 98 | }); 99 | } 100 | if (!ast) { 101 | return null; 102 | } 103 | 104 | // analyse scopes 105 | let scope = attachScopes(ast, "scope"); 106 | 107 | const imports = new Set(); 108 | ast.body.forEach(node => { 109 | if (node.type === "ImportDeclaration") { 110 | node.specifiers.forEach(specifier => { 111 | imports.add(specifier.local.name); 112 | }); 113 | } 114 | }); 115 | 116 | const magicString = new MagicString(code); 117 | 118 | const newImports = new Map(); 119 | 120 | function handleReference(node, name, keypath) { 121 | let mod = modulesMap.get(keypath); 122 | if (mod && !imports.has(name) && !scope.contains(name)) { 123 | if (typeof mod === "string") mod = [mod, "default"]; 124 | 125 | // prevent module from importing itself 126 | if (mod[0] === id) return; 127 | 128 | const hash = `${keypath}:${mod[0]}:${mod[1]}`; 129 | 130 | const importLocalName = 131 | name === keypath ? name : makeLegalIdentifier(`$inject_${keypath}`); 132 | 133 | if (!newImports.has(hash)) { 134 | if (mod[1] === "*") { 135 | newImports.set(hash, `import * as ${importLocalName} from '${mod[0]}';`); 136 | } else { 137 | newImports.set(hash, `import { ${mod[1]} as ${importLocalName} } from '${mod[0]}';`); 138 | } 139 | } 140 | 141 | if (name !== keypath) { 142 | magicString.overwrite(node.start, node.end, importLocalName, { 143 | storeName: true 144 | }); 145 | } 146 | 147 | return true; 148 | } 149 | } 150 | 151 | walk(ast, { 152 | enter(node, parent) { 153 | if (sourceMap) { 154 | magicString.addSourcemapLocation(node.start); 155 | magicString.addSourcemapLocation(node.end); 156 | } 157 | 158 | if (node.scope) scope = node.scope; 159 | 160 | // special case – shorthand properties. because node.key === node.value, 161 | // we can't differentiate once we've descended into the node 162 | if (node.type === "Property" && node.shorthand) { 163 | const name = node.key.name; 164 | handleReference(node, name, name); 165 | return this.skip(); 166 | } 167 | 168 | if (isReference(node, parent)) { 169 | const { name, keypath } = flatten(node); 170 | const handled = handleReference(node, name, keypath); 171 | if (handled) return this.skip(); 172 | } 173 | }, 174 | leave(node) { 175 | if (node.scope) scope = scope.parent; 176 | } 177 | }); 178 | 179 | if (newImports.size === 0) { 180 | return { 181 | code, 182 | ast, 183 | map: sourceMap ? magicString.generateMap({ hires: true }) : null 184 | }; 185 | } 186 | const importBlock = Array.from(newImports.values()).join("\n\n"); 187 | 188 | magicString.prepend(importBlock + "\n\n"); 189 | 190 | return { 191 | code: magicString.toString(), 192 | map: sourceMap ? magicString.generateMap({ hires: true }) : null 193 | }; 194 | } 195 | }; 196 | } 197 | --------------------------------------------------------------------------------