UNPKG

@nestjs/cli

Version:

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

173 lines (172 loc) 7.44 kB
import { requiresExplicitImportExtensions } from '../helpers/requires-explicit-import-extensions.js'; import { FOUND_NO_ISSUES_GENERATING_METADATA } from '../swc/constants.js'; import { TypeCheckerHost } from '../swc/type-checker-host.js'; import { TypeScriptBinaryLoader } from '../typescript-loader.js'; import { PluginMetadataPrinter } from './plugin-metadata-printer.js'; const RELATIVE_PATH_RE = /^\.\.?\//; // Any common JS/TS-style extension that the user could have authored or // that our rewrite would have already produced. Prevents double-appending. const HAS_KNOWN_EXTENSION_RE = /\.(m?js|c?js|m?ts|c?ts|json|node)$/i; /** * Returns the same import path with `.js` appended when (and only when) * the path is relative and does not already end in a recognized * extension. Bare specifiers (e.g. `@nestjs/common`) and absolute paths * are returned unchanged because the caller's resolver handles them. */ export function appendJsExtensionIfMissing(importPath) { if (!RELATIVE_PATH_RE.test(importPath)) { return importPath; } if (HAS_KNOWN_EXTENSION_RE.test(importPath)) { return importPath; } return `${importPath}.js`; } /** * Rewrites a single `await import("...")`-style string by appending the * `.js` extension to the inner specifier when it is a relative path * without an extension. Used to patch the visitor-supplied `typeImports` * map values. */ export function rewriteAsyncImportString(target) { return target.replace(/import\((['"])((?:\\\1|(?!\1).)*)\1\)/g, (match, quote, specifier) => `import(${quote}${appendJsExtensionIfMissing(specifier)}${quote})`); } /** * Walks the given `ts.CallExpression` tree and rewrites every dynamic * `import("...")` whose specifier is a relative path missing an * extension. Returns a new node when changes are required, or the input * node unchanged otherwise. */ export function rewriteImportExpressionForNodeNext(expression, tsBinary) { const visit = (node) => { if (tsBinary.isCallExpression(node) && node.expression.kind === tsBinary.SyntaxKind.ImportKeyword && node.arguments.length > 0 && tsBinary.isStringLiteralLike(node.arguments[0])) { const original = node.arguments[0].text; const rewritten = appendJsExtensionIfMissing(original); if (rewritten !== original) { const updatedArgs = [ tsBinary.factory.createStringLiteral(rewritten), ...node.arguments.slice(1), ]; return tsBinary.factory.updateCallExpression(node, node.expression, node.typeArguments, updatedArgs); } } return tsBinary.visitEachChild(node, visit, undefined); }; return visit(expression); } /** * Recursively walks the collected plugin metadata, rewriting every * dynamic `import("./relative")` call expression to include the `.js` * extension required by node16 / nodenext module resolution. */ export function rewriteCollectedMetadataForNodeNext(metadata, tsBinary) { for (const visitorKey of Object.keys(metadata)) { const sections = metadata[visitorKey]; for (const sectionKey of Object.keys(sections)) { const tuples = sections[sectionKey]; if (!Array.isArray(tuples)) { continue; } for (let i = 0; i < tuples.length; i++) { const [importExpr, meta] = tuples[i]; tuples[i] = [ rewriteImportExpressionForNodeNext(importExpr, tsBinary), meta, ]; } } } } /** * Generates plugins metadata by traversing the AST of the project. * @example * ```ts * const generator = new PluginMetadataGenerator(); * generator.generate({ * visitors: [ * new ReadonlyVisitor({ introspectComments: true, pathToSource: __dirname }), * ], * outputDir: __dirname, * watch: true, * tsconfigPath: 'tsconfig.build.json', * }); * ``` */ export class PluginMetadataGenerator { pluginMetadataPrinter = new PluginMetadataPrinter(); typeCheckerHost = new TypeCheckerHost(); typescriptLoader = new TypeScriptBinaryLoader(); tsBinary; constructor() { this.tsBinary = this.typescriptLoader.load(); } generate(options) { const { tsconfigPath, visitors, tsProgramRef, outputDir, watch, filename, printDiagnostics = true, } = options; if (visitors.length === 0) { return; } if (tsProgramRef) { return this.traverseAndPrintMetadata(tsProgramRef, visitors, outputDir, filename); } const onTypeCheckOrProgramInit = (program) => { this.traverseAndPrintMetadata(program, visitors, outputDir, filename); if (printDiagnostics) { const tsBinary = this.typescriptLoader.load(); const diagnostics = tsBinary.getPreEmitDiagnostics(program); if (diagnostics.length > 0) { const formatDiagnosticsHost = { getCanonicalFileName: (path) => path, getCurrentDirectory: tsBinary.sys.getCurrentDirectory, getNewLine: () => tsBinary.sys.newLine, }; console.log(); console.log(tsBinary.formatDiagnosticsWithColorAndContext(diagnostics, formatDiagnosticsHost)); } else { console.log(FOUND_NO_ISSUES_GENERATING_METADATA); } } }; this.typeCheckerHost.run(tsconfigPath, { watch, onTypeCheck: onTypeCheckOrProgramInit, onProgramInit: onTypeCheckOrProgramInit, }); } traverseAndPrintMetadata(programRef, visitors, outputDir, filename) { for (const sourceFile of programRef.getSourceFiles()) { if (!sourceFile.isDeclarationFile) { visitors.forEach((visitor) => visitor.visit(programRef, sourceFile)); } } let typeImports = {}; const collectedMetadata = {}; visitors.forEach((visitor) => { collectedMetadata[visitor.key] = visitor.collect(); typeImports = { ...typeImports, ...visitor.typeImports, }; }); // Under `node16` / `nodenext` module resolution, dynamic `import()` // specifiers must include explicit file extensions. The visitors emit // bare relative specifiers (e.g. `import("./hello.dto")`), which are // valid under classic / node10 resolution but break compilation and // runtime under nodenext. When the consuming project uses an ESM-style // resolver, rewrite both the metadata import call expressions and the // typeImports map values to include the `.js` extension. See #3364. if (requiresExplicitImportExtensions(programRef.getCompilerOptions(), this.tsBinary)) { rewriteCollectedMetadataForNodeNext(collectedMetadata, this.tsBinary); for (const key of Object.keys(typeImports)) { typeImports[key] = rewriteAsyncImportString(typeImports[key]); } } this.pluginMetadataPrinter.print(collectedMetadata, typeImports, { outputDir, filename, }, this.tsBinary); } }