makeFsImporter.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { shallowIgnoreVisitors } from '../utils/traverse.js';
  2. import resolve from 'resolve';
  3. import { dirname, extname } from 'path';
  4. import fs from 'fs';
  5. import { visitors } from '@babel/traverse';
  6. import { resolveObjectPatternPropertyToValue } from '../utils/index.js';
  7. // These extensions are sorted by priority
  8. // resolve() will check for files in the order these extensions are sorted
  9. const RESOLVE_EXTENSIONS = [
  10. '.js',
  11. '.ts',
  12. '.tsx',
  13. '.mjs',
  14. '.cjs',
  15. '.mts',
  16. '.cts',
  17. '.jsx',
  18. ];
  19. function defaultLookupModule(filename, basedir) {
  20. const resolveOptions = {
  21. basedir,
  22. extensions: RESOLVE_EXTENSIONS,
  23. // we do not need to check core modules as we cannot import them anyway
  24. includeCoreModules: false,
  25. };
  26. try {
  27. return resolve.sync(filename, resolveOptions);
  28. }
  29. catch (error) {
  30. const ext = extname(filename);
  31. let newFilename;
  32. // if we try to import a JavaScript file it might be that we are actually pointing to
  33. // a TypeScript file. This can happen in ES modules as TypeScript requires to import other
  34. // TypeScript files with .js extensions
  35. // https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
  36. switch (ext) {
  37. case '.js':
  38. case '.mjs':
  39. case '.cjs':
  40. newFilename = `${filename.slice(0, -2)}ts`;
  41. break;
  42. case '.jsx':
  43. newFilename = `${filename.slice(0, -3)}tsx`;
  44. break;
  45. default:
  46. throw error;
  47. }
  48. return resolve.sync(newFilename, {
  49. ...resolveOptions,
  50. // we already know that there is an extension at this point, so no need to check other extensions
  51. extensions: [],
  52. });
  53. }
  54. }
  55. // Factory for the resolveImports importer
  56. // If this resolver is used in an environment where the source files change (e.g. watch)
  57. // then the cache needs to be cleared on file changes.
  58. export default function makeFsImporter(lookupModule = defaultLookupModule, { parseCache, resolveCache } = {
  59. parseCache: new Map(),
  60. resolveCache: new Map(),
  61. }) {
  62. function resolveImportedValue(path, name, file, seen = new Set()) {
  63. // Bail if no filename was provided for the current source file.
  64. // Also never traverse into react itself.
  65. const source = path.node.source?.value;
  66. const { filename } = file.opts;
  67. if (!source || !filename || source === 'react') {
  68. return null;
  69. }
  70. // Resolve the imported module using the Node resolver
  71. const basedir = dirname(filename);
  72. const resolveCacheKey = `${basedir}|${source}`;
  73. let resolvedSource = resolveCache.get(resolveCacheKey);
  74. // We haven't found it before, so no need to look again
  75. if (resolvedSource === null) {
  76. return null;
  77. }
  78. // First time we try to resolve this file
  79. if (resolvedSource === undefined) {
  80. try {
  81. resolvedSource = lookupModule(source, basedir);
  82. }
  83. catch (error) {
  84. const { code } = error;
  85. if (code === 'MODULE_NOT_FOUND' || code === 'INVALID_PACKAGE_MAIN') {
  86. resolveCache.set(resolveCacheKey, null);
  87. return null;
  88. }
  89. throw error;
  90. }
  91. resolveCache.set(resolveCacheKey, resolvedSource);
  92. }
  93. // Prevent recursive imports
  94. if (seen.has(resolvedSource)) {
  95. return null;
  96. }
  97. seen.add(resolvedSource);
  98. let nextFile = parseCache.get(resolvedSource);
  99. if (!nextFile) {
  100. // Read and parse the code
  101. const src = fs.readFileSync(resolvedSource, 'utf8');
  102. nextFile = file.parse(src, resolvedSource);
  103. parseCache.set(resolvedSource, nextFile);
  104. }
  105. return findExportedValue(nextFile, name, seen);
  106. }
  107. const explodedVisitors = visitors.explode({
  108. ...shallowIgnoreVisitors,
  109. ExportNamedDeclaration: {
  110. enter: function (path, state) {
  111. const { file, name, seen } = state;
  112. const declaration = path.get('declaration');
  113. // export const/var ...
  114. if (declaration.hasNode() && declaration.isVariableDeclaration()) {
  115. for (const declPath of declaration.get('declarations')) {
  116. const id = declPath.get('id');
  117. const init = declPath.get('init');
  118. if (id.isIdentifier({ name }) && init.hasNode()) {
  119. // export const/var a = <init>
  120. state.resultPath = init;
  121. break;
  122. }
  123. else if (id.isObjectPattern()) {
  124. // export const/var { a } = <init>
  125. state.resultPath = id.get('properties').find((prop) => {
  126. if (prop.isObjectProperty()) {
  127. const value = prop.get('value');
  128. return value.isIdentifier({ name });
  129. }
  130. // We don't handle RestElement here yet as complicated
  131. return false;
  132. });
  133. if (state.resultPath) {
  134. state.resultPath = resolveObjectPatternPropertyToValue(state.resultPath);
  135. break;
  136. }
  137. }
  138. // ArrayPattern not handled yet
  139. }
  140. }
  141. else if (declaration.hasNode() &&
  142. declaration.has('id') &&
  143. declaration.get('id').isIdentifier({ name })) {
  144. // export function/class/type/interface/enum ...
  145. state.resultPath = declaration;
  146. }
  147. else if (path.has('specifiers')) {
  148. // export { ... } or export x from ... or export * as x from ...
  149. for (const specifierPath of path.get('specifiers')) {
  150. if (specifierPath.isExportNamespaceSpecifier()) {
  151. continue;
  152. }
  153. const exported = specifierPath.get('exported');
  154. if (exported.isIdentifier({ name })) {
  155. // export ... from ''
  156. if (path.has('source')) {
  157. const local = specifierPath.isExportSpecifier()
  158. ? specifierPath.node.local.name
  159. : 'default';
  160. state.resultPath = resolveImportedValue(path, local, file, seen);
  161. if (state.resultPath) {
  162. break;
  163. }
  164. }
  165. else {
  166. state.resultPath = specifierPath.get('local');
  167. break;
  168. }
  169. }
  170. }
  171. }
  172. state.resultPath ? path.stop() : path.skip();
  173. },
  174. },
  175. ExportDefaultDeclaration: {
  176. enter: function (path, state) {
  177. const { name } = state;
  178. if (name === 'default') {
  179. state.resultPath = path.get('declaration');
  180. return path.stop();
  181. }
  182. path.skip();
  183. },
  184. },
  185. ExportAllDeclaration: {
  186. enter: function (path, state) {
  187. const { name, file, seen } = state;
  188. const resolvedPath = resolveImportedValue(path, name, file, seen);
  189. if (resolvedPath) {
  190. state.resultPath = resolvedPath;
  191. return path.stop();
  192. }
  193. path.skip();
  194. },
  195. },
  196. });
  197. // Traverses the program looking for an export that matches the requested name
  198. function findExportedValue(file, name, seen) {
  199. const state = {
  200. file,
  201. name,
  202. seen,
  203. };
  204. file.traverse(explodedVisitors, state);
  205. return state.resultPath || null;
  206. }
  207. return resolveImportedValue;
  208. }