utquidem
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
259 lines (222 loc) • 6.49 kB
text/typescript
import ScanImportsModule from 'path';
import {
isModernjsMonorepo,
getMonorepoPackages,
findMonorepoRoot,
fs,
fastGlob,
createDebugger,
isTypescript,
applyOptionsChain,
} from '@modern-js/utils';
import { parse, init } from 'es-module-lexer';
import { loadConfig } from 'tsconfig-paths';
import type { IAppContext, NormalizedConfig } from '@modern-js/core';
import { LexerParseResult } from '../plugins/import-rewrite';
import { BARE_SPECIFIER_REGEX, BABEL_MACRO_EXTENSIONS } from '../constants';
import { normalizePackageName } from '../utils';
import { fsWatcher } from '../watcher';
const debug = createDebugger('esm:scan-imports');
let monorepoRootDir: string | undefined;
let monorepoPackages: Array<{ name: string; path: string }> = [];
let modernjsMonorepo = false;
let useBabelMacros = false;
const shouldScanSpecifierPackage = (
config: NormalizedConfig,
rootDir: string,
packageDir: string,
specifier: string,
): boolean => {
const pkgName = normalizePackageName(specifier);
const {
source: { include },
} = config;
if (
modernjsMonorepo &&
packageDir.startsWith(ScanImportsModule.resolve(rootDir, './features'))
) {
return true;
}
if (
include?.some(d => (d instanceof RegExp ? d.test(pkgName) : d === pkgName))
) {
return true;
}
return false;
};
export type DepSpecifier = {
specifier: string;
importer: string;
forceCompile?: boolean;
};
// scan source code imports
export const scanImports = async (
config: NormalizedConfig,
{ appDirectory }: IAppContext,
defaultDependencies: string[],
virtualDependenciesMap: Record<string, string>,
): Promise<{ deps: DepSpecifier[]; enableBabelMacros: boolean }> => {
const {
source: { alias },
} = config;
const start = Date.now();
debug(`start scan imports start`);
const aliasNames = Object.keys(
applyOptionsChain({ '@': './src' }, alias as any),
);
if (isTypescript(appDirectory)) {
const tsConfig = loadConfig(appDirectory);
if (tsConfig.resultType === 'success' && tsConfig.paths) {
aliasNames.push(...Object.keys(tsConfig.paths));
}
}
monorepoRootDir = findMonorepoRoot(appDirectory);
if (monorepoRootDir) {
monorepoPackages = getMonorepoPackages(monorepoRootDir);
modernjsMonorepo = isModernjsMonorepo(monorepoRootDir);
}
await init;
const deps = removeDuplicate([
...(await scanFiles(
config,
appDirectory,
aliasNames,
virtualDependenciesMap,
)),
...defaultDependencies
.filter(name => {
// deps which already installed will be transformed
try {
require.resolve(name, { paths: [appDirectory] });
} catch (_err) {
// should always transform virtual deps.
return Boolean(virtualDependenciesMap[name]);
}
return true;
})
.map(name => ({
specifier: name,
importer: appDirectory,
})),
]);
debug(`scan imports result:`, deps);
debug(`scan imports end, cost time ${Date.now() - start}ms`);
return { deps, enableBabelMacros: useBabelMacros };
};
const seen: Map<string, boolean> = new Map();
const scanFiles = async (
config: NormalizedConfig,
dir: string,
aliasNames: string[],
virtualDependenciesMap: Record<string, string>,
): Promise<Array<DepSpecifier>> => {
if (seen.has(dir)) {
return [];
}
seen.set(dir, true);
const files = await fastGlob(
['.js', '.ts', '.jsx', '.tsx'].map(ext => `./src/**/*${ext}`),
{
cwd: dir,
ignore: [
'**/node_modules/**',
`**/${config.output.path!}/**`,
'**/dist/**',
'**/build/**',
'**/output/**',
'**/output_resource/**',
`**/__test__/**`,
`**/*.test.(js|ts|jsx|tsx)`,
`**/*.spec.(js|ts|jsx|tsx)`,
`**/*.stories.(js|ts|jsx|tsx)`,
`**/*.node.(js|jsx|ts|tsx)`,
'**/*.d.ts',
],
absolute: true,
},
);
debug(`scan imports src files:`, files);
const ret: Array<DepSpecifier> = [];
for (const file of files) {
const result = await require('esbuild').transform(
fs.readFileSync(file, 'utf8'),
{
// fix: can't use tsx loader for ts file
// https://esbuild.github.io/content-types/#ts-vs-tsx
loader: ScanImportsModule.extname(file).slice(1),
sourcemap: false,
sourcefile: file,
},
);
const [imports]: LexerParseResult = parse(result.code);
for (const { n: specifier } of imports) {
// check if enable babel-plugin-macro
if (specifier?.endsWith(BABEL_MACRO_EXTENSIONS)) {
useBabelMacros = true;
}
if (virtualDependenciesMap[specifier!]) {
continue;
}
if (
specifier &&
BARE_SPECIFIER_REGEX.test(specifier) &&
!aliasNames.find(
alias =>
!alias.startsWith('@modern-js/runtime') &&
alias.startsWith(specifier.split('/')[0]),
) &&
!/\.(css|less|scss)$/.test(specifier)
) {
if (monorepoRootDir) {
const found = monorepoPackages.find(({ name }) =>
specifier.startsWith(name),
);
// should compile monorepo packages
if (found?.path) {
// should scan package in monorepo features dir
// or package specified in source.transpiledDependencies
if (
shouldScanSpecifierPackage(
config,
monorepoRootDir,
found.path,
specifier,
)
) {
fsWatcher.add(found.path);
ret.push(
...(await scanFiles(
config,
found.path,
aliasNames,
virtualDependenciesMap,
)),
);
} else {
ret.push({ specifier, importer: file, forceCompile: true });
}
} else {
ret.push({ specifier, importer: file });
}
} else {
ret.push({
specifier,
importer: file,
});
}
}
}
}
return ret;
};
const removeDuplicate = (deps: DepSpecifier[]): DepSpecifier[] => {
const map = new Map<string, boolean>();
const finnal = [];
for (const dep of deps) {
if (!map.has(dep.specifier)) {
finnal.push(dep);
map.set(dep.specifier, true);
}
}
return finnal;
};