diff --git a/docs/generated/packages/js/generators/library.json b/docs/generated/packages/js/generators/library.json index 158fc05d2a..78de6d28ce 100644 --- a/docs/generated/packages/js/generators/library.json +++ b/docs/generated/packages/js/generators/library.json @@ -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": { diff --git a/e2e/esbuild/src/esbuild.test.ts b/e2e/esbuild/src/esbuild.test.ts index 7340d45dc2..c378b482e5 100644 --- a/e2e/esbuild/src/esbuild.test.ts +++ b/e2e/esbuild/src/esbuild.test.ts @@ -48,6 +48,7 @@ describe('EsBuild Plugin', () => { private: true, type: 'commonjs', main: './index.cjs', + typings: './index.d.ts', dependencies: {}, }); diff --git a/packages/esbuild/src/executors/esbuild/lib/normalize.ts b/packages/esbuild/src/executors/esbuild/lib/normalize.ts index 2384f10e98..6ba0776dd8 100644 --- a/packages/esbuild/src/executors/esbuild/lib/normalize.ts +++ b/packages/esbuild/src/executors/esbuild/lib/normalize.ts @@ -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; diff --git a/packages/js/src/executors/swc/swc.impl.ts b/packages/js/src/executors/swc/swc.impl.ts index 05645d52c1..7062498090 100644 --- a/packages/js/src/executors/swc/swc.impl.ts +++ b/packages/js/src/executors/swc/swc.impl.ts @@ -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 - ); }); } } diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 6089f45741..ae74a4d8fa 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -1561,6 +1561,7 @@ describe('lib', () => { "name": "@proj/my-lib", "nx": { "name": "my-lib", + "sourceRoot": "my-lib/src", }, "private": true, "version": "0.0.1", diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 5d790a88ac..7ddd9a26c0 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -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 { diff --git a/packages/js/src/generators/library/schema.json b/packages/js/src/generators/library/schema.json index 04cecb924f..f7242d1e7c 100644 --- a/packages/js/src/generators/library/schema.json +++ b/packages/js/src/generators/library/schema.json @@ -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": { diff --git a/packages/js/src/utils/schema.d.ts b/packages/js/src/utils/schema.d.ts index e4cc475ba5..a492eaca9c 100644 --- a/packages/js/src/utils/schema.d.ts +++ b/packages/js/src/utils/schema.d.ts @@ -53,6 +53,7 @@ export interface NormalizedSwcExecutorOptions skipTypeCheck: boolean; swcCliOptions: SwcCliOptions; tmpSwcrcPath: string; + isTsSolutionSetup: boolean; sourceRoot?: string; // TODO(v21): remove inline feature inline?: boolean; diff --git a/packages/js/src/utils/typescript/ts-solution-setup.ts b/packages/js/src/utils/typescript/ts-solution-setup.ts index 10b8d3a089..8b544b9788 100644 --- a/packages/js/src/utils/typescript/ts-solution-setup.ts +++ b/packages/js/src/utils/typescript/ts-solution-setup.ts @@ -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)