123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import { shallowIgnoreVisitors } from '../utils/traverse.js';
- import resolve from 'resolve';
- import { dirname, extname } from 'path';
- import fs from 'fs';
- import { visitors } from '@babel/traverse';
- import { resolveObjectPatternPropertyToValue } from '../utils/index.js';
- // These extensions are sorted by priority
- // resolve() will check for files in the order these extensions are sorted
- const RESOLVE_EXTENSIONS = [
- '.js',
- '.ts',
- '.tsx',
- '.mjs',
- '.cjs',
- '.mts',
- '.cts',
- '.jsx',
- ];
- function defaultLookupModule(filename, basedir) {
- const resolveOptions = {
- basedir,
- extensions: RESOLVE_EXTENSIONS,
- // we do not need to check core modules as we cannot import them anyway
- includeCoreModules: false,
- };
- try {
- return resolve.sync(filename, resolveOptions);
- }
- catch (error) {
- const ext = extname(filename);
- let newFilename;
- // if we try to import a JavaScript file it might be that we are actually pointing to
- // a TypeScript file. This can happen in ES modules as TypeScript requires to import other
- // TypeScript files with .js extensions
- // https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
- switch (ext) {
- case '.js':
- case '.mjs':
- case '.cjs':
- newFilename = `${filename.slice(0, -2)}ts`;
- break;
- case '.jsx':
- newFilename = `${filename.slice(0, -3)}tsx`;
- break;
- default:
- throw error;
- }
- return resolve.sync(newFilename, {
- ...resolveOptions,
- // we already know that there is an extension at this point, so no need to check other extensions
- extensions: [],
- });
- }
- }
- // Factory for the resolveImports importer
- // If this resolver is used in an environment where the source files change (e.g. watch)
- // then the cache needs to be cleared on file changes.
- export default function makeFsImporter(lookupModule = defaultLookupModule, { parseCache, resolveCache } = {
- parseCache: new Map(),
- resolveCache: new Map(),
- }) {
- function resolveImportedValue(path, name, file, seen = new Set()) {
- // Bail if no filename was provided for the current source file.
- // Also never traverse into react itself.
- const source = path.node.source?.value;
- const { filename } = file.opts;
- if (!source || !filename || source === 'react') {
- return null;
- }
- // Resolve the imported module using the Node resolver
- const basedir = dirname(filename);
- const resolveCacheKey = `${basedir}|${source}`;
- let resolvedSource = resolveCache.get(resolveCacheKey);
- // We haven't found it before, so no need to look again
- if (resolvedSource === null) {
- return null;
- }
- // First time we try to resolve this file
- if (resolvedSource === undefined) {
- try {
- resolvedSource = lookupModule(source, basedir);
- }
- catch (error) {
- const { code } = error;
- if (code === 'MODULE_NOT_FOUND' || code === 'INVALID_PACKAGE_MAIN') {
- resolveCache.set(resolveCacheKey, null);
- return null;
- }
- throw error;
- }
- resolveCache.set(resolveCacheKey, resolvedSource);
- }
- // Prevent recursive imports
- if (seen.has(resolvedSource)) {
- return null;
- }
- seen.add(resolvedSource);
- let nextFile = parseCache.get(resolvedSource);
- if (!nextFile) {
- // Read and parse the code
- const src = fs.readFileSync(resolvedSource, 'utf8');
- nextFile = file.parse(src, resolvedSource);
- parseCache.set(resolvedSource, nextFile);
- }
- return findExportedValue(nextFile, name, seen);
- }
- const explodedVisitors = visitors.explode({
- ...shallowIgnoreVisitors,
- ExportNamedDeclaration: {
- enter: function (path, state) {
- const { file, name, seen } = state;
- const declaration = path.get('declaration');
- // export const/var ...
- if (declaration.hasNode() && declaration.isVariableDeclaration()) {
- for (const declPath of declaration.get('declarations')) {
- const id = declPath.get('id');
- const init = declPath.get('init');
- if (id.isIdentifier({ name }) && init.hasNode()) {
- // export const/var a = <init>
- state.resultPath = init;
- break;
- }
- else if (id.isObjectPattern()) {
- // export const/var { a } = <init>
- state.resultPath = id.get('properties').find((prop) => {
- if (prop.isObjectProperty()) {
- const value = prop.get('value');
- return value.isIdentifier({ name });
- }
- // We don't handle RestElement here yet as complicated
- return false;
- });
- if (state.resultPath) {
- state.resultPath = resolveObjectPatternPropertyToValue(state.resultPath);
- break;
- }
- }
- // ArrayPattern not handled yet
- }
- }
- else if (declaration.hasNode() &&
- declaration.has('id') &&
- declaration.get('id').isIdentifier({ name })) {
- // export function/class/type/interface/enum ...
- state.resultPath = declaration;
- }
- else if (path.has('specifiers')) {
- // export { ... } or export x from ... or export * as x from ...
- for (const specifierPath of path.get('specifiers')) {
- if (specifierPath.isExportNamespaceSpecifier()) {
- continue;
- }
- const exported = specifierPath.get('exported');
- if (exported.isIdentifier({ name })) {
- // export ... from ''
- if (path.has('source')) {
- const local = specifierPath.isExportSpecifier()
- ? specifierPath.node.local.name
- : 'default';
- state.resultPath = resolveImportedValue(path, local, file, seen);
- if (state.resultPath) {
- break;
- }
- }
- else {
- state.resultPath = specifierPath.get('local');
- break;
- }
- }
- }
- }
- state.resultPath ? path.stop() : path.skip();
- },
- },
- ExportDefaultDeclaration: {
- enter: function (path, state) {
- const { name } = state;
- if (name === 'default') {
- state.resultPath = path.get('declaration');
- return path.stop();
- }
- path.skip();
- },
- },
- ExportAllDeclaration: {
- enter: function (path, state) {
- const { name, file, seen } = state;
- const resolvedPath = resolveImportedValue(path, name, file, seen);
- if (resolvedPath) {
- state.resultPath = resolvedPath;
- return path.stop();
- }
- path.skip();
- },
- },
- });
- // Traverses the program looking for an export that matches the requested name
- function findExportedValue(file, name, seen) {
- const state = {
- file,
- name,
- seen,
- };
- file.traverse(explodedVisitors, state);
- return state.resultPath || null;
- }
- return resolveImportedValue;
- }
|