From 17ccff338d45cdeb1910e4941b3a3a927a7d0f6d Mon Sep 17 00:00:00 2001 From: Rob Giseburt Date: Fri, 23 Feb 2024 16:16:33 -0600 Subject: [PATCH] fix(projen): Workaround `ts-node` bug with Node 18.19 and newer Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. Tracking: https://github.com/TypeStrong/ts-node/issues/2094 --- API.md | 114 ++++++++++++++++++++++++ src/tms-typescript-app-project.ts | 54 +++++++++++ test/tms-typescript-app-project.test.ts | 70 +++++++++++++++ 3 files changed, 238 insertions(+) diff --git a/API.md b/API.md index ec63dcf..15c6e9e 100644 --- a/API.md +++ b/API.md @@ -4698,6 +4698,7 @@ const tmsNestJSAppProjectOptions: TmsNestJSAppProjectOptions = { ... } | tsconfigBaseDev | TmsTSConfigBase | TSConfig base configuration selection for `tsconfig.dev.json`, used to run projen itslef via `ts-node`. | | tsconfigBaseNoArrayWorkaround | boolean | Workaround `ts-node` bug with "extends" in `tsconfig*.json` files. | | tsconfigBaseStrictest | boolean | Include TSConfig "strinctest" configuration to {@link tsconfigBase}. | +| tsNodeUnknownFileExtensionWorkaround | boolean | Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. | | sampleType | string | Which type of sample code to include, if `sampleCode` is true. | --- @@ -6879,6 +6880,43 @@ those. Note that only nodes18 and above are supported. --- +##### `tsNodeUnknownFileExtensionWorkaround`Optional + +```typescript +public readonly tsNodeUnknownFileExtensionWorkaround: boolean; +``` + +- *Type:* boolean +- *Default:* if (node18_19_or_newer) { true } else { false } + +Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. + +This workaround will work with node 16 and later, and has not been tested with earlier versions. + +THIS DOES NOT FIX ANY OTHER USAGE OF `ts-node` WITH `esm` SUPPORT, ONLY WHEN RUNNING `projen` ITSELF. + +If you are using `ts-node` with `esm` support in your project with Node 18.19 or newer, you will need to use the +workaround in your own: + +```bash +# instead of +ts-node --project tsconfig.special.json src/index.ts + +# use +tsc .projenrc.ts && \ + TS_NODE_PROJECT=tsconfig.special.json node --loader ts-node/esm --no-warnings=ExperimentalWarning src/index.ts +``` + +If there are any type errors, the `node --loader ts-node/esm` yields a difficult-to-read error message, so we run +`tsc` first separately to get the type errors before running the `node` command. + +The `tsc` command assumes the correct tsconfig file where the target is `include`d has `noEmit` set to `true`. If +not, add `--noemit` to the `tsc` command. + +> [https://github.com/TypeStrong/ts-node/issues/2094](https://github.com/TypeStrong/ts-node/issues/2094) + +--- + ##### `sampleType`Optional ```typescript @@ -7059,6 +7097,7 @@ const tmsTSApolloGraphQLProjectOptions: TmsTSApolloGraphQLProjectOptions = { ... | tsconfigBaseDev | TmsTSConfigBase | TSConfig base configuration selection for `tsconfig.dev.json`, used to run projen itslef via `ts-node`. | | tsconfigBaseNoArrayWorkaround | boolean | Workaround `ts-node` bug with "extends" in `tsconfig*.json` files. | | tsconfigBaseStrictest | boolean | Include TSConfig "strinctest" configuration to {@link tsconfigBase}. | +| tsNodeUnknownFileExtensionWorkaround | boolean | Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. | | prismadir | string | *No description.* | | sampleType | string | Which type of sample code to include, if `sampleCode` is true. | @@ -9241,6 +9280,43 @@ those. Note that only nodes18 and above are supported. --- +##### `tsNodeUnknownFileExtensionWorkaround`Optional + +```typescript +public readonly tsNodeUnknownFileExtensionWorkaround: boolean; +``` + +- *Type:* boolean +- *Default:* if (node18_19_or_newer) { true } else { false } + +Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. + +This workaround will work with node 16 and later, and has not been tested with earlier versions. + +THIS DOES NOT FIX ANY OTHER USAGE OF `ts-node` WITH `esm` SUPPORT, ONLY WHEN RUNNING `projen` ITSELF. + +If you are using `ts-node` with `esm` support in your project with Node 18.19 or newer, you will need to use the +workaround in your own: + +```bash +# instead of +ts-node --project tsconfig.special.json src/index.ts + +# use +tsc .projenrc.ts && \ + TS_NODE_PROJECT=tsconfig.special.json node --loader ts-node/esm --no-warnings=ExperimentalWarning src/index.ts +``` + +If there are any type errors, the `node --loader ts-node/esm` yields a difficult-to-read error message, so we run +`tsc` first separately to get the type errors before running the `node` command. + +The `tsc` command assumes the correct tsconfig file where the target is `include`d has `noEmit` set to `true`. If +not, add `--noemit` to the `tsc` command. + +> [https://github.com/TypeStrong/ts-node/issues/2094](https://github.com/TypeStrong/ts-node/issues/2094) + +--- + ##### `prismadir`Optional ```typescript @@ -9431,6 +9507,7 @@ const tmsTypeScriptAppProjectOptions: TmsTypeScriptAppProjectOptions = { ... } | tsconfigBaseDev | TmsTSConfigBase | TSConfig base configuration selection for `tsconfig.dev.json`, used to run projen itslef via `ts-node`. | | tsconfigBaseNoArrayWorkaround | boolean | Workaround `ts-node` bug with "extends" in `tsconfig*.json` files. | | tsconfigBaseStrictest | boolean | Include TSConfig "strinctest" configuration to {@link tsconfigBase}. | +| tsNodeUnknownFileExtensionWorkaround | boolean | Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. | --- @@ -11611,6 +11688,43 @@ those. Note that only nodes18 and above are supported. --- +##### `tsNodeUnknownFileExtensionWorkaround`Optional + +```typescript +public readonly tsNodeUnknownFileExtensionWorkaround: boolean; +``` + +- *Type:* boolean +- *Default:* if (node18_19_or_newer) { true } else { false } + +Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. + +This workaround will work with node 16 and later, and has not been tested with earlier versions. + +THIS DOES NOT FIX ANY OTHER USAGE OF `ts-node` WITH `esm` SUPPORT, ONLY WHEN RUNNING `projen` ITSELF. + +If you are using `ts-node` with `esm` support in your project with Node 18.19 or newer, you will need to use the +workaround in your own: + +```bash +# instead of +ts-node --project tsconfig.special.json src/index.ts + +# use +tsc .projenrc.ts && \ + TS_NODE_PROJECT=tsconfig.special.json node --loader ts-node/esm --no-warnings=ExperimentalWarning src/index.ts +``` + +If there are any type errors, the `node --loader ts-node/esm` yields a difficult-to-read error message, so we run +`tsc` first separately to get the type errors before running the `node` command. + +The `tsc` command assumes the correct tsconfig file where the target is `include`d has `noEmit` set to `true`. If +not, add `--noemit` to the `tsc` command. + +> [https://github.com/TypeStrong/ts-node/issues/2094](https://github.com/TypeStrong/ts-node/issues/2094) + +--- + ## Enums diff --git a/src/tms-typescript-app-project.ts b/src/tms-typescript-app-project.ts index 44cacb9..d9e5846 100644 --- a/src/tms-typescript-app-project.ts +++ b/src/tms-typescript-app-project.ts @@ -190,6 +190,39 @@ export interface TmsTypeScriptAppProjectOptions * */ readonly tsconfigBaseNoArrayWorkaround?: boolean; + + /** + * Workaround `ts-node` bug with Node 18.19 and newer, where running `ts-node` with `esm` support enabled will fail + * with the error `ERR_UNKNOWN_FILE_EXTENSION` when you run `projen` itself. + * + * This workaround will work with node 16 and later, and has not been tested with earlier versions. + * + * THIS DOES NOT FIX ANY OTHER USAGE OF `ts-node` WITH `esm` SUPPORT, ONLY WHEN RUNNING `projen` ITSELF. + * + * If you are using `ts-node` with `esm` support in your project with Node 18.19 or newer, you will need to use the + * workaround in your own: + * + * ```bash + * # instead of + * ts-node --project tsconfig.special.json src/index.ts + * + * # use + * tsc .projenrc.ts && \ + * TS_NODE_PROJECT=tsconfig.special.json node --loader ts-node/esm --no-warnings=ExperimentalWarning src/index.ts + * ``` + * + * If there are any type errors, the `node --loader ts-node/esm` yields a difficult-to-read error message, so we run + * `tsc` first separately to get the type errors before running the `node` command. + * + * The `tsc` command assumes the correct tsconfig file where the target is `include`d has `noEmit` set to `true`. If + * not, add `--noemit` to the `tsc` command. + * + * @see https://github.com/TypeStrong/ts-node/issues/2094 + * + * @default if (node18_19_or_newer) { true } else { false } + * + */ + readonly tsNodeUnknownFileExtensionWorkaround?: boolean; } /** @@ -208,6 +241,13 @@ export class TmsTypeScriptAppProject extends TypeScriptAppProject { (options.tsconfigBaseStrictest ?? true) && (options.tsconfigBaseNoArrayWorkaround ?? true); + const nodeVersionSplit = process.versions.node + .split(".") + .map((v) => parseInt(v, 10)); + const node18_19_or_newer = + nodeVersionSplit[0] > 18 || + (nodeVersionSplit[0] === 18 && nodeVersionSplit[1] >= 19); + const defaultOptions = { eslint: true, packageManager: NodePackageManager.NPM, @@ -270,6 +310,8 @@ export class TmsTypeScriptAppProject extends TypeScriptAppProject { tsconfigBaseDev: TmsTSConfigBase.NODE18, tsconfigBaseStrictest: true, tsconfigBaseNoArrayWorkaround: true, + tsNodeUnknownFileExtensionWorkaround: + options.tsNodeUnknownFileExtensionWorkaround ?? node18_19_or_newer, } satisfies Partial; const mergedOptions = deepMerge( [ @@ -420,6 +462,18 @@ const __dirname = (await import('node:path')).dirname(__filename); if (mergedOptions.sampleCode ?? true) { new SampleCode(this); } + + if ( + mergedOptions.tsNodeUnknownFileExtensionWorkaround && + this.defaultTask + ) { + this.defaultTask.reset( + `tsc .projenrc.ts && node --loader ts-node/esm --no-warnings=ExperimentalWarning .projenrc.ts`, + ); + this.defaultTask.env("TS_NODE_PROJECT", "tsconfig.dev.json"); + this.defaultTask.description = + "Run projen with ts-node/esm (workaround for Node 18.19+ applied)"; + } } } diff --git a/test/tms-typescript-app-project.test.ts b/test/tms-typescript-app-project.test.ts index 452cc9c..33de349 100644 --- a/test/tms-typescript-app-project.test.ts +++ b/test/tms-typescript-app-project.test.ts @@ -89,6 +89,76 @@ test("TMSTypeScriptAppProject honors esmSupportConfig=false", () => { expect(bundleCommand).toContain("--format=cjs"); }); +test.each([true, false])( + "TMSTypeScriptAppProject honors tsNodeUnknownFileExtensionWorkaround=%p", + (tsNodeUnknownFileExtensionWorkaround: boolean) => { + const project = new TmsTypeScriptAppProject({ + name: "test", + defaultReleaseBranch: "main", + eslintFixableAsWarn: false, + esmSupportConfig: false, + // default settings + tsNodeUnknownFileExtensionWorkaround, + }); + const snapshot = Testing.synth(project); + + const tasks = snapshot[".projen/tasks.json"].tasks; + const defaultTask = tasks.default; + const defaultCommand = defaultTask.steps[0].exec; + if (tsNodeUnknownFileExtensionWorkaround) { + expect(defaultCommand).toContain("--loader ts-node/esm"); + } else { + expect(defaultCommand).not.toContain("--loader ts-node/esm"); + } + }, +); +describe.each([ + { version: "16.17.1", isOver18d19: false }, + { version: "16.20.2", isOver18d19: false }, + { version: "18.15.0", isOver18d19: false }, + { version: "18.17.0", isOver18d19: false }, + { version: "18.17.1", isOver18d19: false }, + { version: "18.18.0", isOver18d19: false }, + { version: "18.18.2", isOver18d19: false }, + { version: "18.19.0", isOver18d19: true }, + { version: "20.9.0", isOver18d19: true }, + { version: "20.10.0", isOver18d19: true }, +])( + "TMSTypeScriptAppProject interprets process.version=$version as >= 18.19.x: $isOver18d19", + ({ version: nodeVersion, isOver18d19 }) => { + const originalProcess = process; + beforeEach(() => { + global.process = { + ...originalProcess, + versions: { ...originalProcess.versions, node: nodeVersion }, + }; + }); + afterEach(() => { + global.process = originalProcess; + }); + + test(`TMSTypeScriptAppProject interprets process.version=${nodeVersion} as ${isOver18d19 ? ">=" : "<"} 18.19.x`, () => { + const project = new TmsTypeScriptAppProject({ + name: "test", + defaultReleaseBranch: "main", + eslintFixableAsWarn: false, + esmSupportConfig: false, + // default settings + }); + const snapshot = Testing.synth(project); + + const tasks = snapshot[".projen/tasks.json"].tasks; + const defaultTask = tasks.default; + const defaultCommand = defaultTask.steps[0].exec; + if (isOver18d19) { + expect(defaultCommand).toContain("--loader ts-node/esm"); + } else { + expect(defaultCommand).not.toContain("--loader ts-node/esm"); + } + }); + }, +); + test("TMSTypeScriptAppProject honors esmSupportConfig=false", () => { const project = new TmsTypeScriptAppProject({ name: "test",