Skip to content

Commit

Permalink
fix(projen): Workaround ts-node bug with Node 18.19 and newer
Browse files Browse the repository at this point in the history
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: TypeStrong/ts-node#2094
  • Loading branch information
giseburt committed Feb 23, 2024
1 parent 830ed27 commit 17ccff3
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 0 deletions.
114 changes: 114 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions src/tms-typescript-app-project.ts
Expand Up @@ -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;
}

/**
Expand All @@ -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,
Expand Down Expand Up @@ -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<TmsTypeScriptAppProjectOptions>;
const mergedOptions = deepMerge(
[
Expand Down Expand Up @@ -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)";
}
}
}

Expand Down
70 changes: 70 additions & 0 deletions test/tms-typescript-app-project.test.ts
Expand Up @@ -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",
Expand Down

0 comments on commit 17ccff3

Please sign in to comment.