index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. "use strict";
  2. var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
  3. var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
  4. var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
  5. const p = require('path');
  6. const resolve = require('resolve'); // const printAST = require('ast-pretty-print')
  7. const macrosRegex = /[./]macro(\.js)?$/;
  8. const testMacrosRegex = v => macrosRegex.test(v); // https://stackoverflow.com/a/32749533/971592
  9. class MacroError extends Error {
  10. constructor(message) {
  11. super(message);
  12. this.name = 'MacroError';
  13. /* istanbul ignore else */
  14. if (typeof Error.captureStackTrace === 'function') {
  15. Error.captureStackTrace(this, this.constructor);
  16. } else if (!this.stack) {
  17. this.stack = new Error(message).stack;
  18. }
  19. }
  20. }
  21. let _configExplorer = null;
  22. function getConfigExporer() {
  23. return _configExplorer = _configExplorer || // Lazy load cosmiconfig since it is a relatively large bundle
  24. require('cosmiconfig').cosmiconfigSync('babel-plugin-macros', {
  25. searchPlaces: ['package.json', '.babel-plugin-macrosrc', '.babel-plugin-macrosrc.json', '.babel-plugin-macrosrc.yaml', '.babel-plugin-macrosrc.yml', '.babel-plugin-macrosrc.js', 'babel-plugin-macros.config.js'],
  26. packageProp: 'babelMacros'
  27. });
  28. }
  29. function createMacro(macro, options = {}) {
  30. if (options.configName === 'options') {
  31. throw new Error(`You cannot use the configName "options". It is reserved for babel-plugin-macros.`);
  32. }
  33. macroWrapper.isBabelMacro = true;
  34. macroWrapper.options = options;
  35. return macroWrapper;
  36. function macroWrapper(args) {
  37. const {
  38. source,
  39. isBabelMacrosCall
  40. } = args;
  41. if (!isBabelMacrosCall) {
  42. 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');
  43. }
  44. return macro(args);
  45. }
  46. }
  47. function nodeResolvePath(source, basedir) {
  48. return resolve.sync(source, {
  49. basedir,
  50. // This is here to support the package being globally installed
  51. // read more: https://github.com/kentcdodds/babel-plugin-macros/pull/138
  52. paths: [p.resolve(__dirname, '../../')]
  53. });
  54. }
  55. function macrosPlugin(babel, _ref = {}) {
  56. let {
  57. require: _require = require,
  58. resolvePath = nodeResolvePath,
  59. isMacrosName = testMacrosRegex
  60. } = _ref,
  61. options = (0, _objectWithoutPropertiesLoose2.default)(_ref, ["require", "resolvePath", "isMacrosName"]);
  62. function interopRequire(path) {
  63. // eslint-disable-next-line import/no-dynamic-require
  64. const o = _require(path);
  65. return o && o.__esModule && o.default ? o.default : o;
  66. }
  67. return {
  68. name: 'macros',
  69. visitor: {
  70. Program(progPath, state) {
  71. progPath.traverse({
  72. ImportDeclaration(path) {
  73. const isMacros = looksLike(path, {
  74. node: {
  75. source: {
  76. value: v => isMacrosName(v)
  77. }
  78. }
  79. });
  80. if (!isMacros) {
  81. return;
  82. }
  83. const imports = path.node.specifiers.map(s => ({
  84. localName: s.local.name,
  85. importedName: s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name
  86. }));
  87. const source = path.node.source.value;
  88. const result = applyMacros({
  89. path,
  90. imports,
  91. source,
  92. state,
  93. babel,
  94. interopRequire,
  95. resolvePath,
  96. options
  97. });
  98. if (!result || !result.keepImports) {
  99. path.remove();
  100. }
  101. },
  102. VariableDeclaration(path) {
  103. const isMacros = child => looksLike(child, {
  104. node: {
  105. init: {
  106. callee: {
  107. type: 'Identifier',
  108. name: 'require'
  109. },
  110. arguments: args => args.length === 1 && isMacrosName(args[0].value)
  111. }
  112. }
  113. });
  114. path.get('declarations').filter(isMacros).forEach(child => {
  115. const imports = child.node.id.name ? [{
  116. localName: child.node.id.name,
  117. importedName: 'default'
  118. }] : child.node.id.properties.map(property => ({
  119. localName: property.value.name,
  120. importedName: property.key.name
  121. }));
  122. const call = child.get('init');
  123. const source = call.node.arguments[0].value;
  124. const result = applyMacros({
  125. path: call,
  126. imports,
  127. source,
  128. state,
  129. babel,
  130. interopRequire,
  131. resolvePath,
  132. options
  133. });
  134. if (!result || !result.keepImports) {
  135. child.remove();
  136. }
  137. });
  138. }
  139. });
  140. }
  141. }
  142. };
  143. } // eslint-disable-next-line complexity
  144. function applyMacros({
  145. path,
  146. imports,
  147. source,
  148. state,
  149. babel,
  150. interopRequire,
  151. resolvePath,
  152. options
  153. }) {
  154. /* istanbul ignore next (pretty much only useful for astexplorer I think) */
  155. const {
  156. file: {
  157. opts: {
  158. filename = ''
  159. }
  160. }
  161. } = state;
  162. let hasReferences = false;
  163. const referencePathsByImportName = imports.reduce((byName, {
  164. importedName,
  165. localName
  166. }) => {
  167. const binding = path.scope.getBinding(localName);
  168. byName[importedName] = binding.referencePaths;
  169. hasReferences = hasReferences || Boolean(byName[importedName].length);
  170. return byName;
  171. }, {});
  172. const isRelative = source.indexOf('.') === 0;
  173. const requirePath = resolvePath(source, p.dirname(getFullFilename(filename)));
  174. const macro = interopRequire(requirePath);
  175. if (!macro.isBabelMacro) {
  176. throw new Error(`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`);
  177. }
  178. const config = getConfig(macro, filename, source, options);
  179. let result;
  180. try {
  181. /**
  182. * Other plugins that run before babel-plugin-macros might use path.replace, where a path is
  183. * put into its own replacement. Apparently babel does not update the scope after such
  184. * an operation. As a remedy, the whole scope is traversed again with an empty "Identifier"
  185. * visitor - this makes the problem go away.
  186. *
  187. * See: https://github.com/kentcdodds/import-all.macro/issues/7
  188. */
  189. state.file.scope.path.traverse({
  190. Identifier() {}
  191. });
  192. result = macro({
  193. references: referencePathsByImportName,
  194. source,
  195. state,
  196. babel,
  197. config,
  198. isBabelMacrosCall: true
  199. });
  200. } catch (error) {
  201. if (error.name === 'MacroError') {
  202. throw error;
  203. }
  204. error.message = `${source}: ${error.message}`;
  205. if (!isRelative) {
  206. error.message = `${error.message} Learn more: https://www.npmjs.com/package/${source.replace( // remove everything after package name
  207. // @org/package/macro -> @org/package
  208. // package/macro -> package
  209. /^((?:@[^/]+\/)?[^/]+).*/, '$1')}`;
  210. }
  211. throw error;
  212. }
  213. return result;
  214. }
  215. function getConfigFromFile(configName, filename) {
  216. try {
  217. const loaded = getConfigExporer().search(filename);
  218. if (loaded) {
  219. return {
  220. options: loaded.config[configName],
  221. path: loaded.filepath
  222. };
  223. }
  224. } catch (e) {
  225. return {
  226. error: e
  227. };
  228. }
  229. return {};
  230. }
  231. function getConfigFromOptions(configName, options) {
  232. if (options.hasOwnProperty(configName)) {
  233. if (options[configName] && typeof options[configName] !== 'object') {
  234. // eslint-disable-next-line no-console
  235. console.error(`The macro plugin options' ${configName} property was not an object or null.`);
  236. } else {
  237. return {
  238. options: options[configName]
  239. };
  240. }
  241. }
  242. return {};
  243. }
  244. function getConfig(macro, filename, source, options) {
  245. const {
  246. configName
  247. } = macro.options;
  248. if (configName) {
  249. const fileConfig = getConfigFromFile(configName, filename);
  250. const optionsConfig = getConfigFromOptions(configName, options);
  251. if (optionsConfig.options === undefined && fileConfig.options === undefined && fileConfig.error !== undefined) {
  252. // eslint-disable-next-line no-console
  253. console.error(`There was an error trying to load the config "${configName}" ` + `for the macro imported from "${source}. ` + `Please see the error thrown for more information.`);
  254. throw fileConfig.error;
  255. }
  256. if (fileConfig.options !== undefined && optionsConfig.options !== undefined && typeof fileConfig.options !== 'object') {
  257. throw new Error(`${fileConfig.path} specified a ${configName} config of type ` + `${typeof optionsConfig.options}, but the the macros plugin's ` + `options.${configName} did contain an object. Both configs must ` + `contain objects for their options to be mergeable.`);
  258. }
  259. return (0, _extends2.default)({}, optionsConfig.options, {}, fileConfig.options);
  260. }
  261. return undefined;
  262. }
  263. /*
  264. istanbul ignore next
  265. because this is hard to test
  266. and not worth it...
  267. */
  268. function getFullFilename(filename) {
  269. if (p.isAbsolute(filename)) {
  270. return filename;
  271. }
  272. return p.join(process.cwd(), filename);
  273. }
  274. function looksLike(a, b) {
  275. return a && b && Object.keys(b).every(bKey => {
  276. const bVal = b[bKey];
  277. const aVal = a[bKey];
  278. if (typeof bVal === 'function') {
  279. return bVal(aVal);
  280. }
  281. return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal);
  282. });
  283. }
  284. function isPrimitive(val) {
  285. // eslint-disable-next-line
  286. return val == null || /^[sbn]/.test(typeof val);
  287. }
  288. module.exports = macrosPlugin;
  289. Object.assign(module.exports, {
  290. createMacro,
  291. MacroError
  292. });