UNPKG

@nestjs/cli

Version:

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

280 lines (279 loc) 11.8 kB
import { cyan } from 'ansis'; import { fork, spawnSync } from 'child_process'; import { readFileSync } from 'fs'; import { stat } from 'fs/promises'; import { minimatch } from 'minimatch'; import { createRequire } from 'module'; import * as path from 'path'; import { dirname, isAbsolute, join, posix, relative } from 'path'; import { fileURLToPath } from 'url'; import { ERROR_PREFIX } from '../../ui/index.js'; import { treeKillSync } from '../../utils/tree-kill.js'; import { BaseCompiler } from '../base-compiler.js'; import { swcDefaultsFactory } from '../defaults/swc-defaults.js'; import { getValueOrDefault } from '../helpers/get-value-or-default.js'; import { PluginMetadataGenerator } from '../plugins/plugin-metadata-generator.js'; import { watchDirectoryRecursively } from '../watchers/recursive-directory-watcher.js'; import { FOUND_NO_ISSUES_GENERATING_METADATA, FOUND_NO_ISSUES_METADATA_GENERATION_SKIPPED, SWC_LOG_PREFIX, } from './constants.js'; import { TypeCheckerHost } from './type-checker-host.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const require = createRequire(import.meta.url); export class SwcCompiler extends BaseCompiler { pluginMetadataGenerator = new PluginMetadataGenerator(); typeCheckerHost = new TypeCheckerHost(); constructor(pluginsLoader) { super(pluginsLoader); } async run(configuration, tsConfigPath, appName, extras, onSuccess) { const swcOptions = swcDefaultsFactory(extras.tsOptions, configuration, extras.tsConfigExclude); const swcrcFilePath = getValueOrDefault(configuration, 'compilerOptions.builder.options.swcrcPath', appName); if (extras.watch) { if (extras.typeCheck) { this.runTypeChecker(configuration, tsConfigPath, appName, extras); } await this.runSwc(swcOptions, extras, swcrcFilePath); if (extras.emitDeclarations) { this.emitDeclarations(tsConfigPath); } if (onSuccess) { onSuccess(); const debounceTime = 150; const callback = this.debounce(onSuccess, debounceTime); void this.watchFilesInOutDir(swcOptions, callback); } } else { if (extras.typeCheck) { await this.runTypeChecker(configuration, tsConfigPath, appName, extras); } await this.runSwc(swcOptions, extras, swcrcFilePath); if (extras.emitDeclarations) { this.emitDeclarations(tsConfigPath); } if (onSuccess) { onSuccess(); } await extras.assetsManager?.closeWatchers(); } } emitDeclarations(tsConfigPath) { process.nextTick(() => console.log(SWC_LOG_PREFIX, cyan('Emitting declaration files...'))); const tscPath = join(process.cwd(), 'node_modules', '.bin', 'tsc'); const result = spawnSync(tscPath, ['--emitDeclarationOnly', '-p', tsConfigPath], { cwd: process.cwd(), stdio: 'inherit', shell: true, }); if (result.status !== 0) { console.error(ERROR_PREFIX + ' Failed to emit declaration files. Please ensure "declaration" is enabled in your tsconfig.'); } } runTypeChecker(configuration, tsConfigPath, appName, extras) { if (extras.watch) { const args = [ tsConfigPath, appName ?? 'undefined', configuration.sourceRoot ?? 'src', JSON.stringify(configuration.compilerOptions.plugins ?? []), ]; const childProcessRef = fork(join(__dirname, 'forked-type-checker.js'), args, { cwd: process.cwd(), }); process.on('exit', () => childProcessRef && treeKillSync(childProcessRef.pid)); } else { const { readonlyVisitors } = this.loadPlugins(configuration, tsConfigPath, appName); const outputDir = this.getPathToSource(configuration, tsConfigPath, appName); let fulfilled = false; return new Promise((resolve, reject) => { try { this.typeCheckerHost.run(tsConfigPath, { watch: extras.watch, onTypeCheck: (program) => { if (!fulfilled) { fulfilled = true; resolve(); } if (readonlyVisitors.length > 0) { process.nextTick(() => console.log(FOUND_NO_ISSUES_GENERATING_METADATA)); this.pluginMetadataGenerator.generate({ outputDir, visitors: readonlyVisitors, tsProgramRef: program, }); } else { process.nextTick(() => console.log(FOUND_NO_ISSUES_METADATA_GENERATION_SKIPPED)); } }, }); } catch (err) { if (!fulfilled) { fulfilled = true; reject(err); } } }); } } async runSwc(options, extras, swcrcFilePath) { if (this.shouldLogSwcStatus(extras)) { process.nextTick(() => console.log(SWC_LOG_PREFIX, cyan('Running...'))); } const swcCli = this.loadSwcCliBinary(); const swcRcFile = await this.getSwcRcFileContentIfExists(swcrcFilePath); const swcOptions = this.deepMerge(options.swcOptions, swcRcFile); if (swcOptions?.jsc?.baseUrl && !isAbsolute(swcOptions?.jsc?.baseUrl)) { // jsc.baseUrl should be resolved by the caller, if it's passed as an object. // https://github.com/swc-project/swc/pull/7827 const rootDir = process.cwd(); swcOptions.jsc.baseUrl = path.join(rootDir, swcOptions.jsc.baseUrl); } const swcCliOpts = { ...options, swcOptions, cliOptions: { ...options.cliOptions, watch: extras.watch, }, }; if (extras.watch) { // This is required since SWC no longer supports auto-compiling of newly added files in watch mode. // We need to watch the source directory and trigger SWC compilation manually. await this.watchFilesInSrcDir(options, async (file) => { // Transpile newly added file (one-shot: inheriting "watch" here would // register one more permanent @swc/cli watcher per added file, and each // async invocation constructs a Piscina pool that is never destroyed) try { await swcCli.default({ ...swcCliOpts, cliOptions: { ...swcCliOpts.cliOptions, filenames: [file], watch: false, sync: true, }, }); } catch { // Outside of watch mode "@swc/cli" rejects when a file fails to // compile, after having reported the failure itself. Swallow it so // that adding a file that does not compile yet does not tear down // the watch process. } }); } await swcCli.default(swcCliOpts); } shouldLogSwcStatus(extras) { if (extras.silent) { return false; } const npmLogLevel = process.env.npm_config_loglevel?.toLowerCase(); return npmLogLevel !== 'silent'; } loadSwcCliBinary() { try { return require('@swc/cli/lib/swc/dir'); } catch { console.error(ERROR_PREFIX + ' Failed to load "@swc/cli" and/or "@swc/core" required packages. Please, make sure to install them as development dependencies.'); process.exit(1); } } getSwcRcFileContentIfExists(swcrcFilePath) { try { return JSON.parse(readFileSync(join(process.cwd(), swcrcFilePath ?? '.swcrc'), 'utf8')); } catch { if (swcrcFilePath !== undefined) { console.error(ERROR_PREFIX + ` Failed to load "${swcrcFilePath}". Please, check if the file exists and is valid JSON.`); process.exit(1); } return {}; } } deepMerge(target, source) { if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) { return source; } if (Array.isArray(target) && Array.isArray(source)) { return source.reduce((acc, value, index) => { acc[index] = this.deepMerge(target[index], value); return acc; }, target); } const merged = { ...target }; for (const key in source) { if (Object.hasOwn(source, key)) { if (key in target) { merged[key] = this.deepMerge(target[key], source[key]); } else { merged[key] = source[key]; } } } return merged; } debounce(callback, wait) { let timeout; return () => { clearTimeout(timeout); timeout = setTimeout(callback, wait); }; } async watchFilesInSrcDir(options, onFileAdded) { const srcDir = options.cliOptions?.filenames?.[0]; const isDirectory = await stat(srcDir) .then((stats) => stats.isDirectory()) .catch(() => false); if (!srcDir || !isDirectory) { // Skip watching if source directory is not a default "src" folder // or any other specified directory return; } const extensions = options.cliOptions?.extensions ?? ['.ts']; await watchDirectoryRecursively(srcDir, { extensions, onAdd: async (file) => { if (this.isIgnoredBySwc(file, options.cliOptions?.ignore)) { return; } await onFileAdded(file); }, }); } isIgnoredBySwc(file, ignore = [], cwd = process.cwd()) { if (!ignore.length) { return false; } const relativeFile = isAbsolute(file) ? relative(cwd, file) : file; const normalizedFile = this.normalizeSwcIgnorePath(relativeFile); return ignore.some((pattern) => { const normalizedPattern = this.normalizeSwcIgnorePath(pattern); return [normalizedPattern, posix.join(normalizedPattern, '**')].some((pattern) => minimatch(normalizedFile, pattern)); }); } normalizeSwcIgnorePath(value) { return posix.normalize(value.replace(/\\/g, '/')); } async watchFilesInOutDir(options, onChange) { const dir = isAbsolute(options.cliOptions.outDir) ? options.cliOptions.outDir : join(process.cwd(), options.cliOptions.outDir); await watchDirectoryRecursively(dir, { extensions: ['.js', '.mjs'], onAdd: onChange, onChange: onChange, }); } }