Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,15 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Cache dependencies
uses: actions/cache@v2
with:
path: |
~/.cache/yarn
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install Packages
run: yarn install --frozen-lockfile

- name: Build
run: yarn build

- name: Clean yarn cache
run: yarn cache clean
Comment on lines +35 to +36
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yarn cache clean is run before the test step, but the test harness spins up fixture projects and runs yarn --check-cache ... installs (see test/src/project.ts). Cleaning the cache here removes the warm cache and forces network downloads during tests, increasing CI time and adding avoidable flakiness. If the goal is to free disk space, consider moving cache cleanup after tests, or restoring dependency caching (e.g. via actions/cache) instead of clearing it mid-job.

Copilot uses AI. Check for mistakes.

- name: Test
run: yarn run test && yarn run perf
env:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"ts-node": "^10.9.1",
"ts-patch": "^3.3.0",
"tsconfig-paths": "^4.2.0",
"typescript": "5.7.2",
"typescript": "~6.0.2",
"ts-next": "npm:typescript@beta",
"ts-expose-internals": "npm:ts-expose-internals@5.4.5"
},
Expand Down
11 changes: 10 additions & 1 deletion projects/core/src/actions/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LogLevel, PatchError } from '../system';
import chalk from 'chalk';
import { getTsPackage } from '../ts-package';
import { PatchDetail } from "../patch/patch-detail";
import { getTsModule } from "../module";
import { getTsModule, TsModule } from "../module";
import { getInstallerOptions, InstallerOptions } from "../options";


Expand Down Expand Up @@ -47,6 +47,15 @@ export function check(moduleNameOrNames?: string | string[], opts?: Partial<Inst
if (!tsPackage.moduleNames.includes(moduleName))
throw new PatchError(`${moduleName} is not a valid TypeScript module in ${packageDir}`);

/* Report non-patchable modules (thin wrappers in TS 6+) */
if (!TsModule.isPatchable(moduleName, tsPackage.majorVer)) {
log([ '~',
`${chalk.blueBright(moduleName)} delegates to ${chalk.blueBright('typescript.js')} (no independent patch needed)`
]);
res[moduleName] = undefined;
continue;
}

/* Report */
const tsModule = getTsModule(tsPackage, moduleName, { skipCache: options.skipCache });
const { patchDetail } = tsModule.moduleFile;
Expand Down
16 changes: 14 additions & 2 deletions projects/core/src/actions/patch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LogLevel, PatchError, TspError, } from '../system';
import { getTsPackage } from '../ts-package';
import chalk from 'chalk';
import { getModuleFile, getTsModule, ModuleFile } from '../module';
import { getModuleFile, getTsModule, TsModule, ModuleFile } from '../module';
import path from 'path';
import { getInstallerOptions, InstallerOptions } from '../options';
import { writeFileWithLock } from '../utils';
Expand All @@ -27,9 +27,21 @@ export function patch(moduleNameOrNames: string | string[], opts?: Partial<Insta
/* Load Package */
const tsPackage = getTsPackage(dir);

/* Skip non-patchable modules (e.g. thin wrappers in TS 6+) */
const patchableModuleNames = targetModuleNames.filter(m => {
if (!TsModule.isPatchable(m, tsPackage.majorVer)) {
log([ '~',
`${chalk.blueBright(m)} is not independently patchable in TS ${tsPackage.majorVer}+ ` +
`(delegates to ${chalk.blueBright('typescript.js')})`
]);
return false;
}
return true;
});

/* Get modules to patch and patch info */
const moduleFiles: [ string, ModuleFile ][] =
targetModuleNames.map(m => [ m, getModuleFile(tsPackage.getModulePath(m)) ]);
patchableModuleNames.map(m => [ m, getModuleFile(tsPackage.getModulePath(m)) ]);

/* Determine files not already patched or outdated */
const patchableFiles = moduleFiles.filter(entry => {
Expand Down
18 changes: 18 additions & 0 deletions projects/core/src/module/ts-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ export namespace TsModule {
'tsserver.js': '_tsserver.js'
} satisfies Partial<Record<typeof names[number], string>>;

/**
* Modules that are thin wrappers (re-export or import-only) in certain TS major versions.
* These delegate to typescript.js and don't need independent patching.
*
* - TS 6+: tsserverlibrary.js re-exports typescript.js; _tsserver.js is ESM-style (not IIFE)
*/
const nonPatchableModulesByMajorVer: Record<number, string[]> = {
6: [ 'tsserverlibrary.js', 'tsserver.js' ]
};

export function isPatchable(moduleName: string, majorVer: number): boolean {
for (let ver = majorVer; ver >= 6; ver--) {
const nonPatchable = nonPatchableModulesByMajorVer[ver];
if (nonPatchable?.includes(moduleName)) return false;
}
return true;
}

export function getContentFileName(moduleName: typeof names[number]): string {
return contentFileMap[moduleName] || moduleName;
}
Expand Down
4 changes: 3 additions & 1 deletion projects/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
"sourceMap" : true,
"composite" : true,
"declaration" : true,
"types" : [ "node" ],

"plugins" : [
{
"transform" : "./plugin.ts",
"transformProgram" : true
"transformProgram" : true,
"tsConfig" : "../../tsconfig.plugin.json"
}
]
}
Expand Down
18 changes: 14 additions & 4 deletions projects/patch/src/plugin/esm-intercept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,19 @@ namespace tsp {
/* Add to cache */
Module._cache[resolvedPath] = newModule;

/* Load with ESM library */
const res = getEsmLibrary()(newModule)(targetFilePath);
newModule.filename = resolvedPath;
/* Load ESM module — try native require first (Node.js 22.12.0+), fall back to esm library */
let res;
try {
res = originalRequire.call(this, targetFilePath);
newModule.exports = res;
} catch (loadErr: any) {
if (loadErr?.code === 'ERR_REQUIRE_ESM') {
res = getEsmLibrary()(newModule)(targetFilePath);
newModule.filename = resolvedPath;
} else {
throw loadErr;
}
}

return res;
}
Expand All @@ -128,4 +138,4 @@ namespace tsp {
}

// endregion
}
}
3 changes: 2 additions & 1 deletion projects/patch/src/plugin/register-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ namespace tsp {
target: isEsm ? 'ESNext' : 'ES2018',
jsx: 'react',
esModuleInterop: true,
module: isEsm ? 'ESNext' : 'commonjs',
module: isEsm ? 'ESNext' : 'node16',
moduleResolution: isEsm ? 'bundler' : 'node16',
}
});
}
Expand Down
5 changes: 4 additions & 1 deletion projects/patch/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

"compilerOptions": {
"outFile": "../../dist/resources/module-patch.js",
"rootDir": "./src",
"declaration": true,
"types": [ "@types/node" ],

Expand All @@ -17,6 +18,7 @@
"target": "ES2020",
"downlevelIteration": true,
"useUnknownInCatchVariables": false,
"ignoreDeprecations": "6.0",
"newLine": "LF",
"moduleResolution": "Node",
"esModuleInterop": true,
Expand All @@ -25,7 +27,8 @@
{
"transform": "./plugin.ts",
"transformProgram": true,
"import": "transformProgram"
"import": "transformProgram",
"tsConfig": "../../tsconfig.plugin.json"
}
]
}
Expand Down
1 change: 1 addition & 0 deletions test/assets/projects/package-config/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"include": [ "src" ],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"module": "commonjs",
"target": "esnext",
Expand Down
1 change: 1 addition & 0 deletions test/assets/projects/path-mapping/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"compilerOptions": {
"outDir" : "dist",
"moduleResolution" : "node",
"ignoreDeprecations": "6.0",
"module": "commonjs",
"target": "ES2020",
"noEmit": false,
Expand Down
2 changes: 2 additions & 0 deletions test/assets/projects/path-mapping/tsconfig.plugin.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"include": [ "src" ],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"module": "commonjs",
"target": "ES2020",
"noEmit": true,
"ignoreDeprecations": "6.0",
"baseUrl" : "src",
"paths": {
"@a/*": [ "./a/*" ],
Expand Down
1 change: 1 addition & 0 deletions test/assets/projects/webpack/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"target": "ESNext",
"declaration": false,
"moduleResolution" : "Node",
"ignoreDeprecations": "6.0",
"plugins": [
{ "transform": "./plugin.ts" }
]
Expand Down
12 changes: 10 additions & 2 deletions test/tests/webpack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,16 @@ describe('Webpack', () => {
expect(err).toContain('Error: ts-patch worked (esm)');
});

test(`Compiler with ESM transformer throws if no ESM package`, () => {
test(`Compiler with ESM transformer works without esm package on Node 22.12+ or throws if unavailable`, () => {
const [major, minor] = process.versions.node.split('.').map(Number);
const nativeEsmRequire = major > 22 || (major === 22 && minor >= 12);

const err = execAndGetErr(projectPath, './tsconfig.esm.json', 'esm');
expect(err).toContain('To enable experimental ESM support, install the \'esm\' package');
if (nativeEsmRequire) {
// Node.js 22.12.0+ can require ESM modules natively — esm package not needed
expect(err).toContain('Error: ts-patch worked (esm)');
} else {
expect(err).toContain('To enable experimental ESM support, install the \'esm\' package');
}
});
});
3 changes: 2 additions & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"noEmit": true,
"target": "ESNext",
"skipDefaultLibCheck": true,
"skipLibCheck": true
"skipLibCheck": true,
"types": [ "jest", "node" ]
}
}
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": false,
"ignoreDeprecations": "6.0",

"lib": [ "es2020", "dom" ],
"outDir": "dist",
Expand Down
9 changes: 9 additions & 0 deletions tsconfig.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"moduleResolution": "bundler",
"esModuleInterop": true,
"rootDir": "."
}
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3309,10 +3309,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==

typescript@5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
typescript@~6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.2.tgz#0b1bfb15f68c64b97032f3d78abbf98bdbba501f"
integrity sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==

uglify-js@^3.1.4:
version "3.19.3"
Expand Down
Loading