From 7ad5b238327c5e365873ae2d8f02c850ad6e394b Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 25 Jul 2025 23:40:49 +0000 Subject: [PATCH] [heft-swc] Support watch mode --- build-tests/heft-swc-test/package.json | 1 + .../swc-incremental_2025-07-25-23-40.json | 10 ++ .../swc-incremental_2025-07-25-23-40.json | 10 ++ .../src/SwcIsolatedTranspilePlugin.ts | 116 +++++++++++++++++- .../internalTypings/TypeScriptInternals.ts | 2 + 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/swc-incremental_2025-07-25-23-40.json create mode 100644 common/changes/@rushstack/heft-typescript-plugin/swc-incremental_2025-07-25-23-40.json diff --git a/build-tests/heft-swc-test/package.json b/build-tests/heft-swc-test/package.json index 2bc2d1d338a..e78b9a27ba1 100644 --- a/build-tests/heft-swc-test/package.json +++ b/build-tests/heft-swc-test/package.json @@ -7,6 +7,7 @@ "license": "MIT", "scripts": { "build": "heft build --clean", + "build-watch": "heft build-watch --clean", "_phase:build": "heft run --only build -- --clean", "_phase:test": "heft run --only test -- --clean" }, diff --git a/common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/swc-incremental_2025-07-25-23-40.json b/common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/swc-incremental_2025-07-25-23-40.json new file mode 100644 index 00000000000..0a5757c83bf --- /dev/null +++ b/common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/swc-incremental_2025-07-25-23-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-isolated-typescript-transpile-plugin", + "comment": "Add support for watch mode.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-isolated-typescript-transpile-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-typescript-plugin/swc-incremental_2025-07-25-23-40.json b/common/changes/@rushstack/heft-typescript-plugin/swc-incremental_2025-07-25-23-40.json new file mode 100644 index 00000000000..4ab829a905f --- /dev/null +++ b/common/changes/@rushstack/heft-typescript-plugin/swc-incremental_2025-07-25-23-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-typescript-plugin", + "comment": "Update internal typings.", + "type": "patch" + } + ], + "packageName": "@rushstack/heft-typescript-plugin" +} \ No newline at end of file diff --git a/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts b/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts index 00133063bac..c1921dd1526 100644 --- a/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts +++ b/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts @@ -1,11 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import type { Dirent, Stats } from 'node:fs'; import path from 'node:path'; import { type ChildProcess, fork } from 'node:child_process'; -import { Path } from '@rushstack/node-core-library'; -import type { HeftConfiguration, IHeftTaskPlugin, IHeftTaskSession, IScopedLogger } from '@rushstack/heft'; +import { Async, Path } from '@rushstack/node-core-library'; +import type { + HeftConfiguration, + IHeftTaskPlugin, + IHeftTaskSession, + IScopedLogger, + IWatchFileSystem, + IWatchedFileState +} from '@rushstack/heft'; import { LookupByPath } from '@rushstack/lookup-by-path'; import { _loadTypeScriptToolAsync as loadTypeScriptToolAsync, @@ -120,6 +128,18 @@ export default class SwcIsolatedTranspilePlugin implements IHeftTaskPlugin { + const { logger } = heftSession; + + await transpileProjectAsync( + heftConfiguration, + pluginOptions, + logger, + this.accessor, + () => incrementalOptions.watchFs + ); + }); } } @@ -127,7 +147,8 @@ async function transpileProjectAsync( heftConfiguration: HeftConfiguration, pluginOptions: ISwcIsolatedTranspileOptions, logger: IScopedLogger, - { hooks: { getSwcOptions: getSwcOptionsHook } }: ISwcIsolatedTranspilePluginAccessor + { hooks: { getSwcOptions: getSwcOptionsHook } }: ISwcIsolatedTranspilePluginAccessor, + getWatchFs?: (() => IWatchFileSystem) | undefined ): Promise { const { buildFolderPath } = heftConfiguration; const { emitKinds = [] } = pluginOptions; @@ -138,6 +159,68 @@ async function transpileProjectAsync( }); const { ts } = tool; + if (getWatchFs) { + const watchFs: IWatchFileSystem = getWatchFs(); + const { system } = tool; + const emptyFileSystemEntries: { files: string[]; directories: string[] } = { files: [], directories: [] }; + // Copied from TypeScript, but using the watch file system + function getAccessibleFileSystemEntries(directory: string): { files: string[]; directories: string[] } { + try { + const entries: Dirent[] = watchFs.readdirSync(directory || '.', { withFileTypes: true }); + const files: string[] = []; + const directories: string[] = []; + for (const dirent of entries) { + const entry: string = dirent.name; + if (entry === '.' || entry === '..') { + continue; + } + let stat: Stats | Dirent | undefined; + if (dirent.isSymbolicLink()) { + const name: string = ts.combinePaths(directory, entry); + stat = watchFs.statSync(name); + if (!stat) { + continue; + } + } else { + stat = dirent; + } + + if (stat.isFile()) { + files.push(entry); + } else if (stat.isDirectory()) { + directories.push(entry); + } + } + files.sort(); + directories.sort(); + return { files, directories }; + } catch { + return emptyFileSystemEntries; + } + } + + system.readDirectory = ( + dirPath: string, + extensions?: string[], + excludes?: string[], + includes?: string[], + depth?: number + ): string[] => { + return ts.matchFiles( + dirPath, + extensions, + excludes, + includes, + system.useCaseSensitiveFileNames, + buildFolderPath, + depth, + getAccessibleFileSystemEntries, + system.realpath!, + system.directoryExists + ); + }; + } + const tsconfigPath: string = getTsconfigFilePath(heftConfiguration, pluginOptions.tsConfigPath); const parsedTsConfig: TTypeScript.ParsedCommandLine | undefined = loadTsconfig({ tool, tsconfigPath }); @@ -170,6 +253,29 @@ async function transpileProjectAsync( } const sourceFilePaths: string[] = filesFromTsConfig.filter((filePath) => !filePath.endsWith('.d.ts')); + const changedFilePaths: string[] = getWatchFs ? [] : sourceFilePaths; + if (getWatchFs) { + const watchFs: IWatchFileSystem = getWatchFs(); + await Async.forEachAsync( + sourceFilePaths, + async (file: string) => { + const fileState: IWatchedFileState = await watchFs.getStateAndTrackAsync(path.normalize(file)); + if (fileState.changed) { + changedFilePaths.push(file); + } + }, + { + concurrency: 4 + } + ); + } + + if (changedFilePaths.length < 1) { + logger.terminal.writeLine('No changed files found. Skipping transpile.'); + return; + } + + changedFilePaths.sort(); logger.terminal.writeVerboseLine('Reading Config'); @@ -289,7 +395,7 @@ async function transpileProjectAsync( }; const indexForOptions: Map = new Map(); - for (const srcFilePath of sourceFilePaths) { + for (const srcFilePath of changedFilePaths) { const rootPrefixLength: number | undefined = rootDirsPaths.findChildPath(srcFilePath); if (rootPrefixLength === undefined) { @@ -327,7 +433,7 @@ async function transpileProjectAsync( } } - logger.terminal.writeLine(`Transpiling ${tasks.length} files...`); + logger.terminal.writeLine(`Transpiling ${changedFilePaths.length} changed source files...`); const result: IWorkerResult = await new Promise((resolve, reject) => { const workerPath: string = require.resolve('./TranspileWorker.js'); diff --git a/heft-plugins/heft-typescript-plugin/src/internalTypings/TypeScriptInternals.ts b/heft-plugins/heft-typescript-plugin/src/internalTypings/TypeScriptInternals.ts index fb46d226614..40be5463261 100644 --- a/heft-plugins/heft-typescript-plugin/src/internalTypings/TypeScriptInternals.ts +++ b/heft-plugins/heft-typescript-plugin/src/internalTypings/TypeScriptInternals.ts @@ -81,6 +81,8 @@ export interface IExtendedTypeScript { system?: TTypescript.System ): TTypescript.CompilerHost; + combinePaths(path1: string, path2: string): string; + /** * https://github.com/microsoft/TypeScript/blob/782c09d783e006a697b4ba6d1e7ec2f718ce8393/src/compiler/utilities.ts#L6540 */