diff --git a/e2e/nx-plugin/src/nx-plugin.test.ts b/e2e/nx-plugin/src/nx-plugin.test.ts index 88a6b7941b..6378d8abc5 100644 --- a/e2e/nx-plugin/src/nx-plugin.test.ts +++ b/e2e/nx-plugin/src/nx-plugin.test.ts @@ -15,7 +15,11 @@ import { readFile, removeFile, cleanupProject, + runCommand, + getPackageManagerCommand, + updateJson, } from '@nrwl/e2e/utils'; +import type { PackageJson } from 'nx/src/utils/package-json'; import { ASYNC_GENERATOR_EXECUTOR_CONTENTS } from './nx-plugin.fixtures'; @@ -360,6 +364,30 @@ describe('Nx Plugin', () => { expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow(); expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow(); }); + + it('should work with ts-node only', async () => { + const oldPackageJson: PackageJson = readJson('package.json'); + updateJson('package.json', (j) => { + delete j.dependencies['@swc-node/register']; + delete j.devDependencies['@swc-node/register']; + return j; + }); + runCommand(getPackageManagerCommand().install); + + const generator = uniq('generator'); + + expect(() => { + runCLI( + `generate @nrwl/nx-plugin:generator ${generator} --project=${plugin}` + ); + + runCLI( + `generate @${npmScope}/${plugin}:${generator} --name ${uniq('test')}` + ); + }).not.toThrow(); + updateFile('package.json', JSON.stringify(oldPackageJson, null, 2)); + runCommand(getPackageManagerCommand().install); + }); }); describe('--directory', () => { diff --git a/packages/nx/src/utils/register.spec.ts b/packages/nx/src/utils/register.spec.ts new file mode 100644 index 0000000000..7ba3b637b2 --- /dev/null +++ b/packages/nx/src/utils/register.spec.ts @@ -0,0 +1,20 @@ +import { ModuleKind, ScriptTarget } from 'typescript'; +import { getTsNodeCompilerOptions } from './register'; + +describe('getTsNodeCompilerOptions', () => { + it('should replace enum value with enum key for module', () => { + expect( + getTsNodeCompilerOptions({ + module: ModuleKind.CommonJS, + }).module + ).toEqual('CommonJS'); + }); + + it('should replace enum value with enum key for target', () => { + expect( + getTsNodeCompilerOptions({ + target: ScriptTarget.ES2020, + }).target + ).toEqual('ES2020'); + }); +}); diff --git a/packages/nx/src/utils/register.ts b/packages/nx/src/utils/register.ts index cf4fdccc4a..5739d3a9dd 100644 --- a/packages/nx/src/utils/register.ts +++ b/packages/nx/src/utils/register.ts @@ -4,6 +4,7 @@ import { logger, NX_PREFIX, stripIndent } from './logger'; const swcNodeInstalled = packageIsInstalled('@swc-node/register'); const tsNodeInstalled = packageIsInstalled('ts-node/register'); +let ts: typeof import('typescript'); /** * Optionally, if swc-node and tsconfig-paths are available in the current workspace, apply the require @@ -63,7 +64,7 @@ export function registerTranspiler( registerTranspiler = () => { const service = register({ transpileOnly: true, - compilerOptions, + compilerOptions: getTsNodeCompilerOptions(compilerOptions), }); // Don't warn if a faster transpiler is enabled if (!service.options.transpiler && !service.options.swc) { @@ -121,8 +122,10 @@ function readCompilerOptions(tsConfigPath): CompilerOptions { } function readCompilerOptionsWithTypescript(tsConfigPath) { - const { readConfigFile, parseJsonConfigFileContent, sys } = - require('typescript') as typeof import('typescript'); + if (!ts) { + ts = require('typescript'); + } + const { readConfigFile, parseJsonConfigFileContent, sys } = ts; const jsonContent = readConfigFile(tsConfigPath, sys.readFile); const { options } = parseJsonConfigFileContent( jsonContent, @@ -164,3 +167,44 @@ function packageIsInstalled(m: string) { return false; } } + +/** + * ts-node requires string values for enum based typescript options. + * `register`'s signature just types the field as `object`, so we + * unfortunately do not get any kind of type safety on this. + */ +export function getTsNodeCompilerOptions(compilerOptions: CompilerOptions) { + if (!ts) { + ts = require('typescript'); + } + + const flagMap: Partial< + Record, keyof typeof ts> + > = { + module: 'ModuleKind', + target: 'ScriptTarget', + moduleDetection: 'ModuleDetectionKind', + newLine: 'NewLineKind', + moduleResolution: 'ModuleResolutionKind', + importsNotUsedAsValues: 'ImportsNotUsedAsValues', + }; + + const result = { ...compilerOptions }; + + for (const flag in flagMap) { + if (compilerOptions[flag]) { + result[flag] = ts[flagMap[flag]][compilerOptions[flag]]; + } + } + + return result; +} + +/** + * Index keys allow empty objects, where as "real" keys + * require a value. Thus, this filters out index keys + * See: https://stackoverflow.com/a/68261113/3662471 + */ +type RemoveIndex = { + [K in keyof T as {} extends Record ? never : K]: T[K]; +};