You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

288 lines
9.4 KiB

  1. 'use strict';
  2. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  3. function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
  4. function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
  5. var p = require('path');
  6. var resolve = require('resolve');
  7. // const printAST = require('ast-pretty-print')
  8. var macrosRegex = /[./]macro(\.js)?$/;
  9. // https://stackoverflow.com/a/32749533/971592
  10. var MacroError = function (_Error) {
  11. _inherits(MacroError, _Error);
  12. function MacroError(message) {
  13. _classCallCheck(this, MacroError);
  14. var _this = _possibleConstructorReturn(this, (MacroError.__proto__ || Object.getPrototypeOf(MacroError)).call(this, message));
  15. _this.name = 'MacroError';
  16. /* istanbul ignore else */
  17. if (typeof Error.captureStackTrace === 'function') {
  18. Error.captureStackTrace(_this, _this.constructor);
  19. } else if (!_this.stack) {
  20. _this.stack = new Error(message).stack;
  21. }
  22. return _this;
  23. }
  24. return MacroError;
  25. }(Error);
  26. function createMacro(macro) {
  27. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  28. if (options.configName === 'options') {
  29. throw new Error(`You cannot use the configName "options". It is reserved for babel-plugin-macros.`);
  30. }
  31. macroWrapper.isBabelMacro = true;
  32. macroWrapper.options = options;
  33. return macroWrapper;
  34. function macroWrapper(args) {
  35. var source = args.source,
  36. isBabelMacrosCall = args.isBabelMacrosCall;
  37. if (!isBabelMacrosCall) {
  38. throw new MacroError(`The macro you imported from "${source}" is being executed outside the context of compilation with babel-plugin-macros. ` + `This indicates that you don't have the babel plugin "babel-plugin-macros" configured correctly. ` + `Please see the documentation for how to configure babel-plugin-macros properly: ` + 'https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md');
  39. }
  40. return macro(args);
  41. }
  42. }
  43. function macrosPlugin(babel) {
  44. var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
  45. _ref$require = _ref.require,
  46. _require = _ref$require === undefined ? require : _ref$require;
  47. function interopRequire(path) {
  48. // eslint-disable-next-line import/no-dynamic-require
  49. var o = _require(path);
  50. return o && o.__esModule && o.default ? o.default : o;
  51. }
  52. return {
  53. name: 'macros',
  54. visitor: {
  55. ImportDeclaration(path, state) {
  56. var isMacros = looksLike(path, {
  57. node: {
  58. source: {
  59. value: function value(v) {
  60. return macrosRegex.test(v);
  61. }
  62. }
  63. }
  64. });
  65. if (!isMacros) {
  66. return;
  67. }
  68. var imports = path.node.specifiers.map(function (s) {
  69. return {
  70. localName: s.local.name,
  71. importedName: s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name
  72. };
  73. });
  74. var source = path.node.source.value;
  75. var result = applyMacros({
  76. path,
  77. imports,
  78. source,
  79. state,
  80. babel,
  81. interopRequire
  82. });
  83. if (!result || !result.keepImports) {
  84. path.remove();
  85. }
  86. },
  87. VariableDeclaration(path, state) {
  88. var isMacros = function isMacros(child) {
  89. return looksLike(child, {
  90. node: {
  91. init: {
  92. callee: {
  93. type: 'Identifier',
  94. name: 'require'
  95. },
  96. arguments: function _arguments(args) {
  97. return args.length === 1 && macrosRegex.test(args[0].value);
  98. }
  99. }
  100. }
  101. });
  102. };
  103. path.get('declarations').filter(isMacros).forEach(function (child) {
  104. var imports = child.node.id.name ? [{ localName: child.node.id.name, importedName: 'default' }] : child.node.id.properties.map(function (property) {
  105. return {
  106. localName: property.value.name,
  107. importedName: property.key.name
  108. };
  109. });
  110. var call = child.get('init');
  111. var source = call.node.arguments[0].value;
  112. var result = applyMacros({
  113. path: call,
  114. imports,
  115. source,
  116. state,
  117. babel,
  118. interopRequire
  119. });
  120. if (!result || !result.keepImports) {
  121. child.remove();
  122. }
  123. });
  124. }
  125. }
  126. };
  127. }
  128. // eslint-disable-next-line complexity
  129. function applyMacros(_ref2) {
  130. var path = _ref2.path,
  131. imports = _ref2.imports,
  132. source = _ref2.source,
  133. state = _ref2.state,
  134. babel = _ref2.babel,
  135. interopRequire = _ref2.interopRequire;
  136. var filename = state.file.opts.filename;
  137. var hasReferences = false;
  138. var referencePathsByImportName = imports.reduce(function (byName, _ref3) {
  139. var importedName = _ref3.importedName,
  140. localName = _ref3.localName;
  141. var binding = path.scope.getBinding(localName);
  142. if (binding) {
  143. byName[importedName] = binding.referencePaths;
  144. hasReferences = hasReferences || Boolean(byName[importedName].length);
  145. }
  146. return byName;
  147. }, {});
  148. var isRelative = source.indexOf('.') === 0;
  149. var requirePath = resolve.sync(source, {
  150. basedir: p.dirname(getFullFilename(filename))
  151. });
  152. var macro = interopRequire(requirePath);
  153. if (!macro.isBabelMacro) {
  154. throw new Error(
  155. // eslint-disable-next-line prefer-template
  156. `The macro imported from "${source}" must be wrapped in "createMacro" ` + `which you can get from "babel-plugin-macros". ` + `Please refer to the documentation to see how to do this properly: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md#writing-a-macro`);
  157. }
  158. var config = getConfig(macro, filename, source);
  159. var result = void 0;
  160. try {
  161. /**
  162. * Other plugins that run before babel-plugin-macros might use path.replace, where a path is
  163. * put into its own replacement. Apparently babel does not update the scope after such
  164. * an operation. As a remedy, the whole scope is traversed again with an empty "Identifier"
  165. * visitor - this makes the problem go away.
  166. *
  167. * See: https://github.com/kentcdodds/import-all.macro/issues/7
  168. */
  169. state.file.scope.path.traverse({
  170. Identifier() {}
  171. });
  172. result = macro({
  173. references: referencePathsByImportName,
  174. source,
  175. state,
  176. babel,
  177. config,
  178. isBabelMacrosCall: true
  179. });
  180. } catch (error) {
  181. if (error.name === 'MacroError') {
  182. throw error;
  183. }
  184. error.message = `${source}: ${error.message}`;
  185. if (!isRelative) {
  186. error.message = `${error.message} Learn more: https://www.npmjs.com/package/${source.replace(
  187. // remove everything after package name
  188. // @org/package/macro -> @org/package
  189. // package/macro -> package
  190. /^((?:@[^/]+\/)?[^/]+).*/, '$1')}`;
  191. }
  192. throw error;
  193. }
  194. return result;
  195. }
  196. // eslint-disable-next-line consistent-return
  197. function getConfig(macro, filename, source) {
  198. if (macro.options.configName) {
  199. try {
  200. // lazy-loading it here to avoid perf issues of loading it up front.
  201. // No I did not measure. Yes I'm a bad person.
  202. // FWIW, this thing told me that cosmiconfig is 227.1 kb of minified JS
  203. // so that's probably significant... https://bundlephobia.com/result?p=cosmiconfig@3.1.0
  204. // Note that cosmiconfig will cache the babel-plugin-macros config 👍
  205. var explorer = require('cosmiconfig')('babel-plugin-macros');
  206. var loaded = explorer.searchSync(filename, {
  207. packageProp: 'babelMacros',
  208. rc: '.babel-plugin-macrosrc',
  209. js: 'babel-plugin-macros.config.js',
  210. rcExtensions: true,
  211. sync: true
  212. });
  213. if (loaded) {
  214. return loaded.config[macro.options.configName];
  215. }
  216. } catch (error) {
  217. // eslint-disable-next-line no-console
  218. console.error(`There was an error trying to load the config "${macro.options.configName}" ` + `for the macro imported from "${source}. ` + `Please see the error thrown for more information.`);
  219. throw error;
  220. }
  221. }
  222. }
  223. /*
  224. istanbul ignore next
  225. because this is hard to test
  226. and not worth it...
  227. */
  228. function getFullFilename(filename) {
  229. if (p.isAbsolute(filename)) {
  230. return filename;
  231. }
  232. return p.join(process.cwd(), filename);
  233. }
  234. function looksLike(a, b) {
  235. return a && b && Object.keys(b).every(function (bKey) {
  236. var bVal = b[bKey];
  237. var aVal = a[bKey];
  238. if (typeof bVal === 'function') {
  239. return bVal(aVal);
  240. }
  241. return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal);
  242. });
  243. }
  244. function isPrimitive(val) {
  245. // eslint-disable-next-line
  246. return val == null || /^[sbn]/.test(typeof val);
  247. }
  248. module.exports = macrosPlugin;
  249. Object.assign(module.exports, {
  250. createMacro,
  251. MacroError
  252. });