Skip to content

Commit b2df831

Browse files
authored
fix(core): tsconfig.base.json module setting should not break local plugins (#14610)
1 parent 2b5bdd5 commit b2df831

File tree

7 files changed

+104
-32
lines changed

7 files changed

+104
-32
lines changed

docs/generated/packages/nx-plugin/documents/overview.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,9 @@ To make sure that assets are copied to the dist folder, open the plugin's `proje
155155
## Using your Nx Plugin
156156

157157
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.
158+
159+
{% callout type="warning" title="string" %}
160+
161+
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
162+
163+
{% /callout %}

docs/shared/packages/nx-plugin/nx-plugin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,9 @@ To make sure that assets are copied to the dist folder, open the plugin's `proje
155155
## Using your Nx Plugin
156156

157157
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.
158+
159+
{% callout type="warning" title="string" %}
160+
161+
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
162+
163+
{% /callout %}

docs/shared/recipes/generators/local-generators.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ To run a generator, invoke the `nx generate` command with the name of the genera
9595
nx generate @myorg/my-plugin:my-generator mylib
9696
```
9797

98+
{% callout type="warning" title="string" %}
99+
100+
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
101+
102+
{% /callout %}
103+
98104
## Debugging generators
99105

100106
### With Visual Studio Code

docs/shared/recipes/plugins/local-executors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ Options: {
134134
Hello World
135135
```
136136

137+
{% callout type="warning" title="string" %}
138+
139+
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
140+
141+
{% /callout %}
142+
137143
## Using Node Child Process
138144

139145
[Node’s `childProcess`](https://nodejs.org/api/child_process.html) is often useful in executors.

e2e/nx-plugin/src/nx-plugin.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,9 @@ describe('Nx Plugin', () => {
7070
// we should change it to point to the right collection using relative path
7171
// TODO: Re-enable this to work with pnpm
7272
xit(`should run the plugin's e2e tests`, async () => {
73-
const plugin = uniq('plugin-name');
74-
runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=eslint`);
75-
7673
if (isNotWindows()) {
74+
const plugin = uniq('plugin-name');
75+
runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=eslint`);
7776
const e2eResults = runCLI(`e2e ${plugin}-e2e`);
7877
expect(e2eResults).toContain('Successfully ran target e2e');
7978
expect(await killPorts()).toBeTruthy();
@@ -281,7 +280,7 @@ describe('Nx Plugin', () => {
281280
/**
282281
* @todo(@AgentEnder): reenable after figuring out @swc-node
283282
*/
284-
xdescribe('local plugins', () => {
283+
describe('local plugins', () => {
285284
let plugin: string;
286285
beforeEach(() => {
287286
plugin = uniq('plugin');

packages/nx/src/utils/nx-plugin.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
PackageJson,
1111
readModulePackageJsonWithoutFallbacks,
1212
} from './package-json';
13-
import { registerTsProject } from './register';
13+
import { registerTranspiler, registerTsConfigPaths } from './register';
1414
import {
1515
ProjectConfiguration,
1616
ProjectsConfigurations,
@@ -22,6 +22,7 @@ import {
2222
findProjectForPath,
2323
} from '../project-graph/utils/find-project-for-path';
2424
import { normalizePath } from './path';
25+
import { join } from 'path';
2526

2627
export type ProjectTargetConfigurator = (
2728
file: string
@@ -179,9 +180,20 @@ export function resolveLocalNxPlugin(
179180
}
180181

181182
let tsNodeAndPathsRegistered = false;
183+
182184
function registerTSTranspiler() {
183185
if (!tsNodeAndPathsRegistered) {
184-
registerTsProject(workspaceRoot, 'tsconfig.base.json');
186+
// nx-ignore-next-line
187+
const ts: typeof import('typescript') = require('typescript');
188+
189+
registerTsConfigPaths(join(workspaceRoot, 'tsconfig.base.json'));
190+
registerTranspiler({
191+
lib: ['es2021'],
192+
module: ts.ModuleKind.CommonJS,
193+
target: ts.ScriptTarget.ES2021,
194+
esModuleInterop: true,
195+
skipLibCheck: true,
196+
});
185197
}
186198
tsNodeAndPathsRegistered = true;
187199
}

packages/nx/src/utils/register.ts

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { join } from 'path';
1+
import { dirname, join } from 'path';
2+
import type { CompilerOptions } from 'typescript';
23
import { logger, NX_PREFIX, stripIndent } from './logger';
34

5+
const swcNodeInstalled = packageIsInstalled('@swc-node/register');
6+
const tsNodeInstalled = packageIsInstalled('ts-node/register');
7+
48
/**
59
* Optionally, if swc-node and tsconfig-paths are available in the current workspace, apply the require
610
* register hooks so that .ts files can be used for writing custom workspace projects.
@@ -15,37 +19,51 @@ export const registerTsProject = (
1519
path: string,
1620
configFilename = 'tsconfig.json'
1721
): (() => void) => {
22+
const tsConfigPath = join(path, configFilename);
23+
24+
const compilerOptions: CompilerOptions = readCompilerOptions(tsConfigPath);
25+
const cleanupFunctions = [
26+
registerTsConfigPaths(tsConfigPath),
27+
registerTranspiler(compilerOptions),
28+
];
29+
30+
return () => {
31+
for (const fn of cleanupFunctions) {
32+
fn();
33+
}
34+
};
35+
};
36+
37+
/**
38+
* Register ts-node or swc-node given a set of compiler options.
39+
*
40+
* Note: Several options require enums from typescript. To avoid importing typescript,
41+
* use import type + raw values
42+
*
43+
* @returns cleanup method
44+
*/
45+
export function registerTranspiler(
46+
compilerOptions: CompilerOptions
47+
): () => void {
1848
// Function to register transpiler that returns cleanup function
1949
let registerTranspiler: () => () => void;
2050

21-
const tsConfigPath = join(path, configFilename);
22-
const cleanupFunctions = [registerTsConfigPaths(tsConfigPath)];
23-
24-
const swcNodeInstalled = packageIsInstalled('@swc-node/register');
2551
if (swcNodeInstalled) {
2652
// These are requires to prevent it from registering when it shouldn't
2753
const { register } =
2854
require('@swc-node/register/register') as typeof import('@swc-node/register/register');
29-
const {
30-
readDefaultTsConfig,
31-
} = require('@swc-node/register/read-default-tsconfig');
3255

33-
const tsConfig = readDefaultTsConfig(tsConfigPath);
34-
registerTranspiler = () => register(tsConfig);
56+
registerTranspiler = () => register(compilerOptions);
3557
} else {
3658
// We can fall back on ts-node if its available
37-
const tsNodeInstalled = packageIsInstalled('ts-node/register');
59+
3860
if (tsNodeInstalled) {
3961
const { register } = require('ts-node') as typeof import('ts-node');
40-
4162
// ts-node doesn't provide a cleanup method
4263
registerTranspiler = () => {
4364
const service = register({
44-
project: tsConfigPath,
4565
transpileOnly: true,
46-
compilerOptions: {
47-
module: 'commonjs',
48-
},
66+
compilerOptions,
4967
});
5068
// Don't warn if a faster transpiler is enabled
5169
if (!service.options.transpiler && !service.options.swc) {
@@ -57,19 +75,12 @@ export const registerTsProject = (
5775
}
5876

5977
if (registerTranspiler) {
60-
cleanupFunctions.push(registerTranspiler());
78+
return registerTranspiler();
6179
} else {
6280
warnNoTranspiler();
81+
return () => {};
6382
}
64-
65-
// Overall cleanup method cleans up tsconfig path resolution
66-
// as well as ts transpiler
67-
return () => {
68-
for (const f of cleanupFunctions) {
69-
f();
70-
}
71-
};
72-
};
83+
}
7384

7485
/**
7586
* @param tsConfigPath Adds the paths from a tsconfig file into node resolutions
@@ -98,6 +109,32 @@ export function registerTsConfigPaths(tsConfigPath): () => void {
98109
return () => {};
99110
}
100111

112+
function readCompilerOptions(tsConfigPath): CompilerOptions {
113+
if (swcNodeInstalled) {
114+
const {
115+
readDefaultTsConfig,
116+
}: typeof import('@swc-node/register/read-default-tsconfig') = require('@swc-node/register/read-default-tsconfig');
117+
return readDefaultTsConfig(tsConfigPath);
118+
} else {
119+
return readCompilerOptionsWithTypescript(tsConfigPath);
120+
}
121+
}
122+
123+
function readCompilerOptionsWithTypescript(tsConfigPath) {
124+
const { readConfigFile, parseJsonConfigFileContent, sys } =
125+
require('typescript') as typeof import('typescript');
126+
const jsonContent = readConfigFile(tsConfigPath, sys.readFile);
127+
const { options } = parseJsonConfigFileContent(
128+
jsonContent,
129+
sys,
130+
dirname(tsConfigPath)
131+
);
132+
// This property is returned in compiler options for some reason, but not part of the typings.
133+
// ts-node fails on unknown props, so we have to remove it.
134+
delete options.configFilePath;
135+
return options;
136+
}
137+
101138
function warnTsNodeUsage() {
102139
logger.warn(
103140
stripIndent(`${NX_PREFIX} Falling back to ts-node for local typescript execution. This may be a little slower.

0 commit comments

Comments
 (0)