feat(js): support esbuild and swc bundlers with the new ts solution config setup (#28409)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
Leosvel Pérez Espinosa 2024-10-14 09:30:43 +02:00 committed by GitHub
parent 74bdc583b9
commit db47dc30a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 240 additions and 219 deletions

View File

@ -24,6 +24,8 @@
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"type": "string",
"enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"],
"default": "tsc",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": {

View File

@ -48,6 +48,7 @@ describe('EsBuild Plugin', () => {
private: true,
type: 'commonjs',
main: './index.cjs',
typings: './index.d.ts',
dependencies: {},
});

View File

@ -1,30 +1,39 @@
import { joinPathFragments, logger, type ExecutorContext } from '@nx/devkit';
import { readTsConfig } from '@nx/js';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as path from 'path';
import {
import * as pc from 'picocolors';
import type {
EsBuildExecutorOptions,
NormalizedEsBuildExecutorOptions,
} from '../schema';
import { ExecutorContext, joinPathFragments, logger } from '@nx/devkit';
import * as pc from 'picocolors';
import * as esbuild from 'esbuild';
import { readTsConfig } from '@nx/js';
export function normalizeOptions(
options: EsBuildExecutorOptions,
context: ExecutorContext
): NormalizedEsBuildExecutorOptions {
const isTsSolutionSetup = isUsingTsSolutionSetup();
if (isTsSolutionSetup && options.generatePackageJson) {
throw new Error(
`Setting 'generatePackageJson: true' is not allowed with the current TypeScript setup. Please update the 'package.json' file at the project root as needed and don't set the 'generatePackageJson' option.`
);
}
const tsConfig = readTsConfig(options.tsConfig);
// If we're not generating package.json file, then copy it as-is as an asset.
const assets = options.generatePackageJson
? options.assets
: [
...options.assets,
joinPathFragments(
context.projectGraph.nodes[context.projectName].data.root,
'package.json'
),
];
// If we're not generating package.json file, then copy it as-is as an asset when not using ts solution setup.
const assets =
options.generatePackageJson || isTsSolutionSetup
? options.assets
: [
...options.assets,
joinPathFragments(
context.projectGraph.nodes[context.projectName].data.root,
'package.json'
),
];
if (!options.bundle && options.thirdParty) {
logger.info(
@ -33,7 +42,7 @@ export function normalizeOptions(
'bundle:false'
)} and ${pc.bold(
'thirdParty:true'
)}. Your package.json depedencies might not be generated correctly so we added an update ${pc.bold(
)}. Your package.json dependencies might not be generated correctly so we added an update ${pc.bold(
'thirdParty:false'
)}`
)
@ -42,8 +51,6 @@ export function normalizeOptions(
const thirdParty = !options.bundle ? false : options.thirdParty;
const { root: projectRoot } =
context.projectsConfigurations.projects[context.projectName];
const declarationRootDir = options.declarationRootDir
? path.join(context.root, options.declarationRootDir)
: undefined;

View File

@ -1,9 +1,10 @@
import { ExecutorContext, readJsonFile } from '@nx/devkit';
import { assetGlobsToFiles, FileInputOutput } from '../../utils/assets/assets';
import { ExecutorContext, output, readJsonFile } from '@nx/devkit';
import { sync as globSync } from 'fast-glob';
import { rmSync } from 'node:fs';
import { dirname, join, relative, resolve, normalize } from 'path';
import { dirname, join, normalize, relative, resolve } from 'path';
import { copyAssets } from '../../utils/assets';
import { assetGlobsToFiles, FileInputOutput } from '../../utils/assets/assets';
import type { DependentBuildableProjectNode } from '../../utils/buildable-libs-utils';
import { checkDependencies } from '../../utils/check-dependencies';
import {
getHelperDependency,
@ -13,16 +14,20 @@ import {
handleInliningBuild,
isInlineGraphEmpty,
postProcessInlinedDependencies,
type InlineProjectGraph,
} from '../../utils/inline';
import { copyPackageJson } from '../../utils/package-json';
import {
copyPackageJson,
type CopyPackageJsonResult,
} from '../../utils/package-json';
import {
NormalizedSwcExecutorOptions,
SwcCliOptions,
SwcExecutorOptions,
} from '../../utils/schema';
import { compileSwc, compileSwcWatch } from '../../utils/swc/compile-swc';
import { getSwcrcPath } from '../../utils/swc/get-swcrc-path';
import { generateTmpSwcrc } from '../../utils/swc/inline';
import { isUsingTsSolutionSetup } from '../../utils/typescript/ts-solution-setup';
function normalizeOptions(
options: SwcExecutorOptions,
@ -30,6 +35,25 @@ function normalizeOptions(
sourceRoot: string,
projectRoot: string
): NormalizedSwcExecutorOptions {
const isTsSolutionSetup = isUsingTsSolutionSetup();
if (isTsSolutionSetup) {
if (options.generateLockfile) {
throw new Error(
`Setting 'generateLockfile: true' is not supported with the current TypeScript setup. Unset the 'generateLockfile' option and try again.`
);
}
if (options.generateExportsField) {
throw new Error(
`Setting 'generateExportsField: true' is not supported with the current TypeScript setup. Set 'exports' field in the 'package.json' file at the project root and unset the 'generateExportsField' option.`
);
}
if (options.additionalEntryPoints?.length) {
throw new Error(
`Setting 'additionalEntryPoints' is not supported with the current TypeScript setup. Set additional entry points in the 'package.json' file at the project root and unset the 'additionalEntryPoints' option.`
);
}
}
const outputPath = join(root, options.outputPath);
if (options.skipTypeCheck == null) {
@ -87,6 +111,7 @@ function normalizeOptions(
tsConfig: join(root, options.tsConfig),
swcCliOptions,
tmpSwcrcPath,
isTsSolutionSetup: isTsSolutionSetup,
} as NormalizedSwcExecutorOptions;
}
@ -97,56 +122,61 @@ export async function* swcExecutor(
const { sourceRoot, root } =
context.projectsConfigurations.projects[context.projectName];
const options = normalizeOptions(_options, context.root, sourceRoot, root);
const { tmpTsConfig, dependencies } = checkDependencies(
context,
options.tsConfig
);
if (tmpTsConfig) {
options.tsConfig = tmpTsConfig;
}
let swcHelperDependency: DependentBuildableProjectNode;
let inlineProjectGraph: InlineProjectGraph;
if (!options.isTsSolutionSetup) {
const { tmpTsConfig, dependencies } = checkDependencies(
context,
options.tsConfig
);
const swcHelperDependency = getHelperDependency(
HelperDependency.swc,
options.swcCliOptions.swcrcPath,
dependencies,
context.projectGraph
);
if (swcHelperDependency) {
dependencies.push(swcHelperDependency);
}
const inlineProjectGraph = handleInliningBuild(
context,
options,
options.tsConfig
);
if (!isInlineGraphEmpty(inlineProjectGraph)) {
if (options.stripLeadingPaths) {
throw new Error(`Cannot use --strip-leading-paths with inlining.`);
if (tmpTsConfig) {
options.tsConfig = tmpTsConfig;
}
options.projectRoot = '.'; // set to root of workspace to include other libs for type check
// remap paths for SWC compilation
options.inline = true;
options.swcCliOptions.swcCwd = '.';
options.swcCliOptions.srcPath = options.swcCliOptions.swcCwd;
options.swcCliOptions.destPath = join(
options.swcCliOptions.destPath.split(normalize('../')).at(-1),
options.swcCliOptions.srcPath
);
// tmp swcrc with dependencies to exclude
// - buildable libraries
// - other libraries that are not dependent on the current project
options.swcCliOptions.swcrcPath = generateTmpSwcrc(
inlineProjectGraph,
swcHelperDependency = getHelperDependency(
HelperDependency.swc,
options.swcCliOptions.swcrcPath,
options.tmpSwcrcPath
dependencies,
context.projectGraph
);
if (swcHelperDependency) {
dependencies.push(swcHelperDependency);
}
inlineProjectGraph = handleInliningBuild(
context,
options,
options.tsConfig
);
if (!isInlineGraphEmpty(inlineProjectGraph)) {
if (options.stripLeadingPaths) {
throw new Error(`Cannot use --strip-leading-paths with inlining.`);
}
options.projectRoot = '.'; // set to root of workspace to include other libs for type check
// remap paths for SWC compilation
options.inline = true;
options.swcCliOptions.swcCwd = '.';
options.swcCliOptions.srcPath = options.swcCliOptions.swcCwd;
options.swcCliOptions.destPath = join(
options.swcCliOptions.destPath.split(normalize('../')).at(-1),
options.swcCliOptions.srcPath
);
// tmp swcrc with dependencies to exclude
// - buildable libraries
// - other libraries that are not dependent on the current project
options.swcCliOptions.swcrcPath = generateTmpSwcrc(
inlineProjectGraph,
options.swcCliOptions.swcrcPath,
options.tmpSwcrcPath
);
}
}
function determineModuleFormatFromSwcrc(
@ -163,16 +193,19 @@ export async function* swcExecutor(
return yield* compileSwcWatch(context, options, async () => {
const assetResult = await copyAssets(options, context);
const packageJsonResult = await copyPackageJson(
{
...options,
additionalEntryPoints: createEntryPoints(options, context),
format: [
determineModuleFormatFromSwcrc(options.swcCliOptions.swcrcPath),
],
},
context
);
let packageJsonResult: CopyPackageJsonResult;
if (!options.isTsSolutionSetup) {
packageJsonResult = await copyPackageJson(
{
...options,
additionalEntryPoints: createEntryPoints(options, context),
format: [
determineModuleFormatFromSwcrc(options.swcCliOptions.swcrcPath),
],
},
context
);
}
removeTmpSwcrc(options.swcCliOptions.swcrcPath);
disposeFn = () => {
assetResult?.stop();
@ -182,23 +215,25 @@ export async function* swcExecutor(
} else {
return yield compileSwc(context, options, async () => {
await copyAssets(options, context);
await copyPackageJson(
{
...options,
additionalEntryPoints: createEntryPoints(options, context),
format: [
determineModuleFormatFromSwcrc(options.swcCliOptions.swcrcPath),
],
extraDependencies: swcHelperDependency ? [swcHelperDependency] : [],
},
context
);
if (!options.isTsSolutionSetup) {
await copyPackageJson(
{
...options,
additionalEntryPoints: createEntryPoints(options, context),
format: [
determineModuleFormatFromSwcrc(options.swcCliOptions.swcrcPath),
],
extraDependencies: swcHelperDependency ? [swcHelperDependency] : [],
},
context
);
postProcessInlinedDependencies(
options.outputPath,
options.originalProjectRoot,
inlineProjectGraph
);
}
removeTmpSwcrc(options.swcCliOptions.swcrcPath);
postProcessInlinedDependencies(
options.outputPath,
options.originalProjectRoot,
inlineProjectGraph
);
});
}
}

View File

@ -1561,6 +1561,7 @@ describe('lib', () => {
"name": "@proj/my-lib",
"nx": {
"name": "my-lib",
"sourceRoot": "my-lib/src",
},
"private": true,
"version": "0.0.1",

View File

@ -266,31 +266,6 @@ async function configureProject(
updateNxJson(tree, nxJson);
}
if (!options.useProjectJson) {
// we create a cleaner project configuration for the package.json file
const projectConfiguration: ProjectConfiguration = {
root: options.projectRoot,
};
if (options.name !== options.importPath) {
// if the name is different than the package.json name, we need to set
// the proper name in the configuration
projectConfiguration.name = options.name;
}
if (options.parsedTags?.length) {
projectConfiguration.tags = options.parsedTags;
}
if (options.publishable) {
await addProjectToNxReleaseConfig(tree, options, projectConfiguration);
}
updateProjectConfiguration(tree, options.name, projectConfiguration);
return;
}
const projectConfiguration: ProjectConfiguration = {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
@ -300,34 +275,42 @@ async function configureProject(
};
if (
options.bundler &&
options.bundler !== 'none' &&
options.config !== 'npm-scripts'
options.config !== 'npm-scripts' &&
(options.bundler === 'swc' ||
options.bundler === 'esbuild' ||
(!options.isUsingTsSolutionConfig && options.bundler === 'tsc'))
) {
if (options.bundler !== 'rollup') {
const outputPath = getOutputPath(options);
const executor = getBuildExecutor(options.bundler);
addBuildTargetDefaults(tree, executor);
const outputPath = getOutputPath(options);
const executor = getBuildExecutor(options.bundler);
addBuildTargetDefaults(tree, executor);
projectConfiguration.targets.build = {
executor,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main:
`${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
assets: [],
},
};
projectConfiguration.targets.build = {
executor,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
},
};
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.format = ['cjs'];
}
if (options.bundler === 'swc' && options.skipTypeCheck) {
projectConfiguration.targets.build.options.skipTypeCheck = true;
}
if (options.isUsingTsSolutionConfig) {
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.declarationRootDir = `${options.projectRoot}/src`;
}
} else {
projectConfiguration.targets.build.options.assets = [];
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.generatePackageJson = true;
projectConfiguration.targets.build.options.format = ['cjs'];
}
if (options.bundler === 'swc' && options.skipTypeCheck) {
projectConfiguration.targets.build.options.skipTypeCheck = true;
}
if (!options.minimal) {
@ -337,8 +320,10 @@ async function configureProject(
);
}
}
}
if (options.publishable) {
if (options.publishable) {
if (!options.isUsingTsSolutionConfig) {
const packageRoot = joinPathFragments(
defaultOutputDirectory,
'{projectRoot}'
@ -361,12 +346,22 @@ async function configureProject(
},
},
};
await addProjectToNxReleaseConfig(tree, options, projectConfiguration);
}
await addProjectToNxReleaseConfig(tree, options, projectConfiguration);
}
if (options.config === 'workspace' || options.config === 'project') {
if (!options.useProjectJson) {
// we want the package.json as clean as possible, with the bare minimum
if (!projectConfiguration.tags?.length) {
delete projectConfiguration.tags;
}
// automatically inferred as `library`
delete projectConfiguration.projectType;
// empty targets are cleaned up automatically by `updateProjectConfiguration`
updateProjectConfiguration(tree, options.name, projectConfiguration);
} else if (options.config === 'workspace' || options.config === 'project') {
addProjectConfiguration(tree, options.name, projectConfiguration);
} else {
addProjectConfiguration(tree, options.name, {
@ -716,30 +711,6 @@ async function normalizeOptions(
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree);
if (isUsingTsSolutionConfig) {
if (options.bundler === 'esbuild' || options.bundler === 'swc') {
throw new Error(
`Cannot use the "${options.bundler}" bundler when using the @nx/js/typescript plugin.`
);
}
if (options.bundler === undefined && options.compiler === undefined) {
options.bundler = await promptWhenInteractive<{ bundler: Bundler }>(
{
type: 'select',
name: 'bundler',
message: `Which bundler would you like to use to build the library? Choose 'none' to skip build setup.`,
choices: [
{ name: 'tsc' },
{ name: 'rollup' },
{ name: 'vite' },
{ name: 'none' },
],
initial: 0,
},
{ bundler: 'tsc' }
).then(({ bundler }) => bundler);
}
options.linter ??= await promptWhenInteractive<{
linter: 'none' | 'eslint';
}>(
@ -766,50 +737,6 @@ async function normalizeOptions(
{ unitTestRunner: 'none' }
).then(({ unitTestRunner }) => unitTestRunner);
} else {
if (options.bundler === undefined && options.compiler === undefined) {
options.bundler = await promptWhenInteractive<{ bundler: Bundler }>(
{
type: 'select',
name: 'bundler',
message: `Which bundler would you like to use to build the library? Choose 'none' to skip build setup.`,
choices: [
{ name: 'swc' },
{ name: 'tsc' },
{ name: 'rollup' },
{ name: 'vite' },
{ name: 'esbuild' },
{ name: 'none' },
],
initial: 1,
},
{ bundler: 'tsc' }
).then(({ bundler }) => bundler);
} else {
/**
* We are deprecating the compiler and the buildable options.
* However, we want to keep the existing behavior for now.
*
* So, if the user has not provided a bundler, we will use the compiler option, if any.
*
* If the user has not provided a bundler and no compiler, but has set buildable to true,
* we will use tsc, since that is the compiler the old generator used to default to, if buildable was true
* and no compiler was provided.
*
* If the user has not provided a bundler and no compiler, and has not set buildable to true, then
* set the bundler to tsc, to preserve old default behaviour (buildable: true by default).
*
* If it's publishable, we need to build the code before publishing it, so again
* we default to `tsc`. In the previous version of this, it would set `buildable` to true
* and that would default to `tsc`.
*
* In the past, the only way to get a non-buildable library was to set buildable to false.
* Now, the only way to get a non-buildble library is to set bundler to none.
* By default, with nothing provided, libraries are buildable with `@nx/js:tsc`.
*/
options.bundler ??= options.compiler;
}
options.linter ??= await promptWhenInteractive<{
linter: 'none' | 'eslint';
}>(
@ -843,6 +770,29 @@ async function normalizeOptions(
}
}
/**
* We are deprecating the compiler and the buildable options.
* However, we want to keep the existing behavior for now.
*
* So, if the user has not provided a bundler, we will use the compiler option, if any.
*
* If the user has not provided a bundler and no compiler, but has set buildable to true,
* we will use tsc, since that is the compiler the old generator used to default to, if buildable was true
* and no compiler was provided.
*
* If the user has not provided a bundler and no compiler, and has not set buildable to true, then
* set the bundler to tsc, to preserve old default behaviour (buildable: true by default).
*
* If it's publishable, we need to build the code before publishing it, so again
* we default to `tsc`. In the previous version of this, it would set `buildable` to true
* and that would default to `tsc`.
*
* In the past, the only way to get a non-buildable library was to set buildable to false.
* Now, the only way to get a non-buildble library is to set bundler to none.
* By default, with nothing provided, libraries are buildable with `@nx/js:tsc`.
*/
options.bundler ??= options.compiler ?? 'tsc';
// ensure programmatic runs have an expected default
if (!options.config) {
options.config = 'project';
@ -994,6 +944,11 @@ function getBuildExecutor(bundler: Bundler) {
}
function getOutputPath(options: NormalizedLibraryGeneratorOptions) {
if (options.isUsingTsSolutionConfig) {
// Executors expect paths relative to workspace root, so we prepend the project root
return joinPathFragments(options.projectRoot, 'dist');
}
const parts = [defaultOutputDirectory];
if (options.projectRoot === '.') {
parts.push(options.name);
@ -1170,8 +1125,12 @@ function determineEntryFields(
case 'swc':
return {
type: 'commonjs',
main: './src/index.js',
typings: './src/index.d.ts',
main: options.isUsingTsSolutionConfig
? './dist/src/index.js'
: './src/index.js',
typings: options.isUsingTsSolutionConfig
? './dist/src/index.d.ts'
: './src/index.d.ts',
};
case 'rollup':
return {
@ -1202,8 +1161,12 @@ function determineEntryFields(
// For libraries intended for Node, use CJS.
return {
type: 'commonjs',
main: './index.cjs',
// typings is missing for esbuild currently
main: options.isUsingTsSolutionConfig
? './dist/index.cjs'
: './index.cjs',
typings: options.isUsingTsSolutionConfig
? './dist/index.d.ts'
: './index.d.ts',
};
default: {
return {

View File

@ -24,6 +24,8 @@
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"type": "string",
"enum": ["swc", "tsc", "rollup", "vite", "esbuild", "none"],
"default": "tsc",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": {

View File

@ -53,6 +53,7 @@ export interface NormalizedSwcExecutorOptions
skipTypeCheck: boolean;
swcCliOptions: SwcCliOptions;
tmpSwcrcPath: string;
isTsSolutionSetup: boolean;
sourceRoot?: string;
// TODO(v21): remove inline feature
inline?: boolean;

View File

@ -1,4 +1,11 @@
import { output, readJson, readNxJson, type Tree } from '@nx/devkit';
import {
output,
readJson,
readNxJson,
workspaceRoot,
type Tree,
} from '@nx/devkit';
import { FsTree } from 'nx/src/generators/tree';
import { isUsingPackageManagerWorkspaces } from '../package-manager-workspaces';
export function isUsingTypeScriptPlugin(tree: Tree): boolean {
@ -13,7 +20,9 @@ export function isUsingTypeScriptPlugin(tree: Tree): boolean {
);
}
export function isUsingTsSolutionSetup(tree: Tree): boolean {
export function isUsingTsSolutionSetup(tree?: Tree): boolean {
tree ??= new FsTree(workspaceRoot, false);
return (
isUsingPackageManagerWorkspaces(tree) &&
isWorkspaceSetupWithTsSolution(tree)