UNPKG

@nestjs/cli

Version:

Nest - modern, fast, powerful node.js web framework (@cli)

156 lines (155 loc) 6.72 kB
import { existsSync } from 'fs'; import { createRequire } from 'module'; import { dirname, posix } from 'path'; import * as tsPaths from 'tsconfig-paths'; import { requiresExplicitImportExtensions } from '../helpers/requires-explicit-import-extensions.js'; import { TypeScriptBinaryLoader } from '../typescript-loader.js'; const require = createRequire(import.meta.url); export function tsconfigPathsBeforeHookFactory(compilerOptions) { const tsBinary = new TypeScriptBinaryLoader().load(); const { paths = {}, baseUrl = './' } = compilerOptions; const matcher = tsPaths.createMatchPath(baseUrl, paths, ['main']); const esm = requiresExplicitImportExtensions(compilerOptions, tsBinary); const extensions = sourceToOutputExtension(compilerOptions, tsBinary); // Resolution is immutable within a build, and the hook runs per import. const emittedPaths = new Map(); return (ctx) => { return (sf) => { const visitNode = (node) => { if (tsBinary.isImportDeclaration(node) || (tsBinary.isExportDeclaration(node) && node.moduleSpecifier)) { try { const text = getModuleSpecifierText(node.moduleSpecifier, sf); if (!text) { return node; } const result = getNotAliasedPath(sf, matcher, text, esm, extensions, emittedPaths); if (!result) { return node; } const moduleSpecifier = tsBinary.factory.createStringLiteral(result); moduleSpecifier.parent = node.moduleSpecifier.parent; if (tsBinary.isImportDeclaration(node)) { const updatedNode = tsBinary.factory.updateImportDeclaration(node, node.modifiers, node.importClause, moduleSpecifier, node.assertClause); updatedNode.flags = node.flags; return updatedNode; } else { const updatedNode = tsBinary.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, moduleSpecifier, node.assertClause); updatedNode.flags = node.flags; return updatedNode; } } catch { return node; } } return tsBinary.visitEachChild(node, visitNode, ctx); }; return tsBinary.visitNode(sf, visitNode); }; }; } function getModuleSpecifierText(moduleSpecifier, sourceFile) { if (!moduleSpecifier) { return; } if (typeof moduleSpecifier.text === 'string') { return moduleSpecifier.text; } const importPathWithQuotes = moduleSpecifier.getText(sourceFile); if (!importPathWithQuotes) { return; } return importPathWithQuotes.substring(1, importPathWithQuotes.length - 1); } // The matcher strips the extension off the file it found, so the path it // returns cannot be used to tell which source file that was. function sourceToOutputExtension(compilerOptions, tsBinary) { // `jsx: preserve` leaves the syntax in place and emits `.jsx`; every other // mode compiles it away. const jsxOutput = compilerOptions.jsx === tsBinary.JsxEmit.Preserve ? '.jsx' : '.js'; return { '.mts': '.mjs', '.cts': '.cjs', '.ts': '.js', '.tsx': jsxOutput, '.js': '.js', '.jsx': jsxOutput, }; } const MATCHER_EXTENSIONS = ['.mts', '.cts', '.ts', '.tsx', '.js', '.jsx']; /** * Resolves an alias that ESM writes with the extension the file will have * *after* emit. The matcher looks for `./foo.js` on disk, finds only * `./foo.ts`, and gives up, so the alias is emitted untouched and fails at * runtime. Retrying without that extension resolves it to the source file. */ function matchPath(matcher, text, esm) { const result = matcher(text, undefined, undefined, MATCHER_EXTENSIONS); if (result || !esm) { return result; } const withoutOutputExtension = text.replace(/\.(m|c)?js$/, ''); return withoutOutputExtension === text ? undefined : matcher(withoutOutputExtension, undefined, undefined, MATCHER_EXTENSIONS); } /** * Probes the disk to learn which source file the matcher resolved, and returns * it with the extension it will carry after emit. ESM has no directory or * extensionless resolution, so the specifier must name the emitted file, and * the extension cannot be read off a path the matcher already stripped. */ function toEmittedPath(resolvedPath, extensions, cache) { const cached = cache.get(resolvedPath); if (cached !== undefined) { return cached; } const entries = Object.entries(extensions); // An alias that names the file keeps its extension through the matcher. for (const [source, output] of entries) { if (resolvedPath.endsWith(source)) { return remember(cache, resolvedPath, resolvedPath.slice(0, -source.length) + output); } } for (const candidate of [resolvedPath, posix.join(resolvedPath, 'index')]) { for (const [source, output] of entries) { if (existsSync(`${candidate}${source}`)) { return remember(cache, resolvedPath, `${candidate}${output}`); } } } return remember(cache, resolvedPath, resolvedPath); } function remember(cache, key, value) { cache.set(key, value); return value; } function getNotAliasedPath(sf, matcher, text, esm = false, extensions = {}, emittedPaths = new Map()) { let result = matchPath(matcher, text, esm); if (!result) { return; } if (process.platform === 'win32') { result = result.replace(/\\/g, '/'); } try { // Installed packages (node modules) should take precedence over root files with the same name. // Ref: https://github.com/nestjs/nest-cli/issues/838 const packagePath = require.resolve(text, { paths: [process.cwd(), ...(require.resolve.paths(text) ?? [])], }); if (packagePath) { return text; } } catch { // package resolution failed, fall through to relative path } if (esm) { result = toEmittedPath(result, extensions, emittedPaths); } const resolvedPath = posix.relative(dirname(sf.fileName), result) || './'; return resolvedPath[0] === '.' ? resolvedPath : './' + resolvedPath; }