From b2df83107639d78aa1c1524f887ae45abda7c3a8 Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Mon, 13 Feb 2023 16:28:32 -0500 Subject: [PATCH] fix(core): tsconfig.base.json module setting should not break local plugins (#14610) --- .../packages/nx-plugin/documents/overview.md | 6 ++ docs/shared/packages/nx-plugin/nx-plugin.md | 6 ++ .../recipes/generators/local-generators.md | 6 ++ .../shared/recipes/plugins/local-executors.md | 6 ++ e2e/nx-plugin/src/nx-plugin.test.ts | 7 +- packages/nx/src/utils/nx-plugin.ts | 16 +++- packages/nx/src/utils/register.ts | 89 +++++++++++++------ 7 files changed, 104 insertions(+), 32 deletions(-) diff --git a/docs/generated/packages/nx-plugin/documents/overview.md b/docs/generated/packages/nx-plugin/documents/overview.md index 0d8632e0964aa..c0606a5df9667 100644 --- a/docs/generated/packages/nx-plugin/documents/overview.md +++ b/docs/generated/packages/nx-plugin/documents/overview.md @@ -155,3 +155,9 @@ To make sure that assets are copied to the dist folder, open the plugin's `proje ## Using your Nx Plugin To use your plugin, simply list it in `nx.json` or use its generators and executors as you would for any other plugin. This could look like `nx g @my-org/my-plugin:lib` for generators or `"executor": "@my-org/my-plugin:build"` for executors. It should be usable in all of the same ways as published plugins in your local workspace immediately after generating it. This includes setting it up as the default collection in `nx.json`, which would allow you to run `nx g lib` and hit your plugin's generator. + +{% callout type="warning" title="string" %} + +Nx uses the paths from tsconfig.base.json when running plugins locally, but uses the recommended tsconfig for node 16 for other compiler options. See https://github.com/tsconfig/bases/blob/main/bases/node16.json + +{% /callout %} diff --git a/docs/shared/packages/nx-plugin/nx-plugin.md b/docs/shared/packages/nx-plugin/nx-plugin.md index 0d8632e0964aa..c0606a5df9667 100644 --- a/docs/shared/packages/nx-plugin/nx-plugin.md +++ b/docs/shared/packages/nx-plugin/nx-plugin.md @@ -155,3 +155,9 @@ To make sure that assets are copied to the dist folder, open the plugin's `proje ## Using your Nx Plugin To use your plugin, simply list it in `nx.json` or use its generators and executors as you would for any other plugin. This could look like `nx g @my-org/my-plugin:lib` for generators or `"executor": "@my-org/my-plugin:build"` for executors. It should be usable in all of the same ways as published plugins in your local workspace immediately after generating it. This includes setting it up as the default collection in `nx.json`, which would allow you to run `nx g lib` and hit your plugin's generator. + +{% callout type="warning" title="string" %} + +Nx uses the paths from tsconfig.base.json when running plugins locally, but uses the recommended tsconfig for node 16 for other compiler options. See https://github.com/tsconfig/bases/blob/main/bases/node16.json + +{% /callout %} diff --git a/docs/shared/recipes/generators/local-generators.md b/docs/shared/recipes/generators/local-generators.md index f02a1a9b30cd0..27bcb360f923a 100644 --- a/docs/shared/recipes/generators/local-generators.md +++ b/docs/shared/recipes/generators/local-generators.md @@ -95,6 +95,12 @@ To run a generator, invoke the `nx generate` command with the name of the genera nx generate @myorg/my-plugin:my-generator mylib ``` +{% callout type="warning" title="string" %} + +Nx uses the paths from `tsconfig.base.json` when running plugins locally, but uses the recommended tsconfig for node 16 for other compiler options. See https://github.com/tsconfig/bases/blob/main/bases/node16.json + +{% /callout %} + ## Debugging generators ### With Visual Studio Code diff --git a/docs/shared/recipes/plugins/local-executors.md b/docs/shared/recipes/plugins/local-executors.md index e46292b39fd07..b43d6aea4fa44 100644 --- a/docs/shared/recipes/plugins/local-executors.md +++ b/docs/shared/recipes/plugins/local-executors.md @@ -134,6 +134,12 @@ Options: { Hello World ``` +{% callout type="warning" title="string" %} + +Nx uses the paths from `tsconfig.base.json` when running plugins locally, but uses the recommended tsconfig for node 16 for other compiler options. See https://github.com/tsconfig/bases/blob/main/bases/node16.json + +{% /callout %} + ## Using Node Child Process [Node’s `childProcess`](https://nodejs.org/api/child_process.html) is often useful in executors. diff --git a/e2e/nx-plugin/src/nx-plugin.test.ts b/e2e/nx-plugin/src/nx-plugin.test.ts index d90636376a1ef..88a6b7941b626 100644 --- a/e2e/nx-plugin/src/nx-plugin.test.ts +++ b/e2e/nx-plugin/src/nx-plugin.test.ts @@ -70,10 +70,9 @@ describe('Nx Plugin', () => { // we should change it to point to the right collection using relative path // TODO: Re-enable this to work with pnpm xit(`should run the plugin's e2e tests`, async () => { - const plugin = uniq('plugin-name'); - runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=eslint`); - if (isNotWindows()) { + const plugin = uniq('plugin-name'); + runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=eslint`); const e2eResults = runCLI(`e2e ${plugin}-e2e`); expect(e2eResults).toContain('Successfully ran target e2e'); expect(await killPorts()).toBeTruthy(); @@ -281,7 +280,7 @@ describe('Nx Plugin', () => { /** * @todo(@AgentEnder): reenable after figuring out @swc-node */ - xdescribe('local plugins', () => { + describe('local plugins', () => { let plugin: string; beforeEach(() => { plugin = uniq('plugin'); diff --git a/packages/nx/src/utils/nx-plugin.ts b/packages/nx/src/utils/nx-plugin.ts index 523c66da70e0d..36d20b6c8c5d8 100644 --- a/packages/nx/src/utils/nx-plugin.ts +++ b/packages/nx/src/utils/nx-plugin.ts @@ -10,7 +10,7 @@ import { PackageJson, readModulePackageJsonWithoutFallbacks, } from './package-json'; -import { registerTsProject } from './register'; +import { registerTranspiler, registerTsConfigPaths } from './register'; import { ProjectConfiguration, ProjectsConfigurations, @@ -22,6 +22,7 @@ import { findProjectForPath, } from '../project-graph/utils/find-project-for-path'; import { normalizePath } from './path'; +import { join } from 'path'; export type ProjectTargetConfigurator = ( file: string @@ -179,9 +180,20 @@ export function resolveLocalNxPlugin( } let tsNodeAndPathsRegistered = false; + function registerTSTranspiler() { if (!tsNodeAndPathsRegistered) { - registerTsProject(workspaceRoot, 'tsconfig.base.json'); + // nx-ignore-next-line + const ts: typeof import('typescript') = require('typescript'); + + registerTsConfigPaths(join(workspaceRoot, 'tsconfig.base.json')); + registerTranspiler({ + lib: ['es2021'], + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2021, + esModuleInterop: true, + skipLibCheck: true, + }); } tsNodeAndPathsRegistered = true; } diff --git a/packages/nx/src/utils/register.ts b/packages/nx/src/utils/register.ts index 8a1cfb225997d..cf4fdccc4ab98 100644 --- a/packages/nx/src/utils/register.ts +++ b/packages/nx/src/utils/register.ts @@ -1,6 +1,10 @@ -import { join } from 'path'; +import { dirname, join } from 'path'; +import type { CompilerOptions } from 'typescript'; import { logger, NX_PREFIX, stripIndent } from './logger'; +const swcNodeInstalled = packageIsInstalled('@swc-node/register'); +const tsNodeInstalled = packageIsInstalled('ts-node/register'); + /** * Optionally, if swc-node and tsconfig-paths are available in the current workspace, apply the require * register hooks so that .ts files can be used for writing custom workspace projects. @@ -15,37 +19,51 @@ export const registerTsProject = ( path: string, configFilename = 'tsconfig.json' ): (() => void) => { + const tsConfigPath = join(path, configFilename); + + const compilerOptions: CompilerOptions = readCompilerOptions(tsConfigPath); + const cleanupFunctions = [ + registerTsConfigPaths(tsConfigPath), + registerTranspiler(compilerOptions), + ]; + + return () => { + for (const fn of cleanupFunctions) { + fn(); + } + }; +}; + +/** + * Register ts-node or swc-node given a set of compiler options. + * + * Note: Several options require enums from typescript. To avoid importing typescript, + * use import type + raw values + * + * @returns cleanup method + */ +export function registerTranspiler( + compilerOptions: CompilerOptions +): () => void { // Function to register transpiler that returns cleanup function let registerTranspiler: () => () => void; - const tsConfigPath = join(path, configFilename); - const cleanupFunctions = [registerTsConfigPaths(tsConfigPath)]; - - const swcNodeInstalled = packageIsInstalled('@swc-node/register'); if (swcNodeInstalled) { // These are requires to prevent it from registering when it shouldn't const { register } = require('@swc-node/register/register') as typeof import('@swc-node/register/register'); - const { - readDefaultTsConfig, - } = require('@swc-node/register/read-default-tsconfig'); - const tsConfig = readDefaultTsConfig(tsConfigPath); - registerTranspiler = () => register(tsConfig); + registerTranspiler = () => register(compilerOptions); } else { // We can fall back on ts-node if its available - const tsNodeInstalled = packageIsInstalled('ts-node/register'); + if (tsNodeInstalled) { const { register } = require('ts-node') as typeof import('ts-node'); - // ts-node doesn't provide a cleanup method registerTranspiler = () => { const service = register({ - project: tsConfigPath, transpileOnly: true, - compilerOptions: { - module: 'commonjs', - }, + compilerOptions, }); // Don't warn if a faster transpiler is enabled if (!service.options.transpiler && !service.options.swc) { @@ -57,19 +75,12 @@ export const registerTsProject = ( } if (registerTranspiler) { - cleanupFunctions.push(registerTranspiler()); + return registerTranspiler(); } else { warnNoTranspiler(); + return () => {}; } - - // Overall cleanup method cleans up tsconfig path resolution - // as well as ts transpiler - return () => { - for (const f of cleanupFunctions) { - f(); - } - }; -}; +} /** * @param tsConfigPath Adds the paths from a tsconfig file into node resolutions @@ -98,6 +109,32 @@ export function registerTsConfigPaths(tsConfigPath): () => void { return () => {}; } +function readCompilerOptions(tsConfigPath): CompilerOptions { + if (swcNodeInstalled) { + const { + readDefaultTsConfig, + }: typeof import('@swc-node/register/read-default-tsconfig') = require('@swc-node/register/read-default-tsconfig'); + return readDefaultTsConfig(tsConfigPath); + } else { + return readCompilerOptionsWithTypescript(tsConfigPath); + } +} + +function readCompilerOptionsWithTypescript(tsConfigPath) { + const { readConfigFile, parseJsonConfigFileContent, sys } = + require('typescript') as typeof import('typescript'); + const jsonContent = readConfigFile(tsConfigPath, sys.readFile); + const { options } = parseJsonConfigFileContent( + jsonContent, + sys, + dirname(tsConfigPath) + ); + // This property is returned in compiler options for some reason, but not part of the typings. + // ts-node fails on unknown props, so we have to remove it. + delete options.configFilePath; + return options; +} + function warnTsNodeUsage() { logger.warn( stripIndent(`${NX_PREFIX} Falling back to ts-node for local typescript execution. This may be a little slower.