rollup-plugin-inject.es6.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { createFilter, attachScopes, makeLegalIdentifier } from 'rollup-pluginutils';
  2. import { sep } from 'path';
  3. import { walk } from 'estree-walker';
  4. import MagicString from 'magic-string';
  5. const escape = str => {
  6. return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
  7. };
  8. const isReference = (node, parent) => {
  9. if (node.type === "MemberExpression") {
  10. return !node.computed && isReference(node.object, node);
  11. }
  12. if (node.type === "Identifier") {
  13. // TODO is this right?
  14. if (parent.type === "MemberExpression") return parent.computed || node === parent.object;
  15. // disregard the `bar` in { bar: foo }
  16. if (parent.type === "Property" && node !== parent.value) return false;
  17. // disregard the `bar` in `class Foo { bar () {...} }`
  18. if (parent.type === "MethodDefinition") return false;
  19. // disregard the `bar` in `export { foo as bar }`
  20. if (parent.type === "ExportSpecifier" && node !== parent.local) return;
  21. return true;
  22. }
  23. };
  24. const flatten = node => {
  25. const parts = [];
  26. while (node.type === "MemberExpression") {
  27. parts.unshift(node.property.name);
  28. node = node.object;
  29. }
  30. const name = node.name;
  31. parts.unshift(name);
  32. return { name, keypath: parts.join(".") };
  33. };
  34. function inject(options) {
  35. if (!options) throw new Error("Missing options");
  36. const filter = createFilter(options.include, options.exclude);
  37. let modules = options.modules;
  38. if (!modules) {
  39. modules = Object.assign({}, options);
  40. delete modules.include;
  41. delete modules.exclude;
  42. delete modules.sourceMap;
  43. delete modules.sourcemap;
  44. }
  45. const modulesMap = new Map(Object.entries(modules));
  46. // Fix paths on Windows
  47. if (sep !== "/") {
  48. modulesMap.forEach((mod, key) => {
  49. modulesMap.set(
  50. key,
  51. Array.isArray(mod) ? [mod[0].split(sep).join("/"), mod[1]] : mod.split(sep).join("/")
  52. );
  53. });
  54. }
  55. const firstpass = new RegExp(
  56. `(?:${Array.from(modulesMap.keys())
  57. .map(escape)
  58. .join("|")})`,
  59. "g"
  60. );
  61. const sourceMap = options.sourceMap !== false && options.sourcemap !== false;
  62. return {
  63. name: "inject",
  64. transform(code, id) {
  65. if (!filter(id)) return null;
  66. if (code.search(firstpass) === -1) return null;
  67. if (sep !== "/") id = id.split(sep).join("/");
  68. let ast = null;
  69. try {
  70. ast = this.parse(code);
  71. } catch (err) {
  72. this.warn({
  73. code: "PARSE_ERROR",
  74. message: `rollup-plugin-inject: failed to parse ${id}. Consider restricting the plugin to particular files via options.include`
  75. });
  76. }
  77. if (!ast) {
  78. return null;
  79. }
  80. // analyse scopes
  81. let scope = attachScopes(ast, "scope");
  82. const imports = new Set();
  83. ast.body.forEach(node => {
  84. if (node.type === "ImportDeclaration") {
  85. node.specifiers.forEach(specifier => {
  86. imports.add(specifier.local.name);
  87. });
  88. }
  89. });
  90. const magicString = new MagicString(code);
  91. const newImports = new Map();
  92. function handleReference(node, name, keypath) {
  93. let mod = modulesMap.get(keypath);
  94. if (mod && !imports.has(name) && !scope.contains(name)) {
  95. if (typeof mod === "string") mod = [mod, "default"];
  96. // prevent module from importing itself
  97. if (mod[0] === id) return;
  98. const hash = `${keypath}:${mod[0]}:${mod[1]}`;
  99. const importLocalName =
  100. name === keypath ? name : makeLegalIdentifier(`$inject_${keypath}`);
  101. if (!newImports.has(hash)) {
  102. if (mod[1] === "*") {
  103. newImports.set(hash, `import * as ${importLocalName} from '${mod[0]}';`);
  104. } else {
  105. newImports.set(hash, `import { ${mod[1]} as ${importLocalName} } from '${mod[0]}';`);
  106. }
  107. }
  108. if (name !== keypath) {
  109. magicString.overwrite(node.start, node.end, importLocalName, {
  110. storeName: true
  111. });
  112. }
  113. return true;
  114. }
  115. }
  116. walk(ast, {
  117. enter(node, parent) {
  118. if (sourceMap) {
  119. magicString.addSourcemapLocation(node.start);
  120. magicString.addSourcemapLocation(node.end);
  121. }
  122. if (node.scope) scope = node.scope;
  123. // special case – shorthand properties. because node.key === node.value,
  124. // we can't differentiate once we've descended into the node
  125. if (node.type === "Property" && node.shorthand) {
  126. const name = node.key.name;
  127. handleReference(node, name, name);
  128. return this.skip();
  129. }
  130. if (isReference(node, parent)) {
  131. const { name, keypath } = flatten(node);
  132. const handled = handleReference(node, name, keypath);
  133. if (handled) return this.skip();
  134. }
  135. },
  136. leave(node) {
  137. if (node.scope) scope = scope.parent;
  138. }
  139. });
  140. if (newImports.size === 0) {
  141. return {
  142. code,
  143. ast,
  144. map: sourceMap ? magicString.generateMap({ hires: true }) : null
  145. };
  146. }
  147. const importBlock = Array.from(newImports.values()).join("\n\n");
  148. magicString.prepend(importBlock + "\n\n");
  149. return {
  150. code: magicString.toString(),
  151. map: sourceMap ? magicString.generateMap({ hires: true }) : null
  152. };
  153. }
  154. };
  155. }
  156. export default inject;