feat(js): update the setup-build generator to support the new ts setup (#28446)

Update the `@nx/js:setup-build` and the generators it depends on to
support the new TS setup with project references.

<!-- 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 #
This commit is contained in:
Leosvel Pérez Espinosa 2024-10-28 19:34:57 +01:00 committed by GitHub
parent 1fec637514
commit f357b4ed53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1037 additions and 299 deletions

View File

@ -61,6 +61,12 @@
"description": "The build target to add.",
"type": "string",
"default": "build"
},
"format": {
"description": "The format to build the library (esm or cjs).",
"type": "array",
"items": { "type": "string", "enum": ["esm", "cjs"] },
"default": ["esm"]
}
},
"required": [],

View File

@ -130,13 +130,13 @@
"description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.",
"default": false,
"x-priority": "internal"
},
"stripLeadingPaths": {
"type": "boolean",
"description": "Remove leading directory from output (e.g. src). See: https://swc.rs/docs/usage/cli#--strip-leading-paths",
"default": false
}
},
"stripLeadingPaths": {
"type": "boolean",
"description": "Remove leading directory from output (e.g. src). See: https://swc.rs/docs/usage/cli#--strip-leading-paths",
"default": false
},
"required": ["main", "outputPath", "tsConfig"],
"definitions": {
"assetPattern": {

View File

@ -26,13 +26,13 @@
},
"main": {
"type": "string",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/main.ts'.",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/index.ts'.",
"alias": "entryFile",
"x-priority": "important"
},
"tsConfig": {
"type": "string",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.app.json'.",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.lib.json'.",
"x-priority": "important"
},
"skipFormat": {

View File

@ -214,14 +214,20 @@ describe('packaging libs', () => {
expect(readJson(`dist/libs/${tscLib}/package.json`).exports).toEqual({
'./package.json': './package.json',
'.': './src/index.js',
'.': {
default: './src/index.js',
types: './src/index.d.ts',
},
'./foo/bar': './src/foo/bar.js',
'./foo/faz': './src/foo/faz.js',
});
expect(readJson(`dist/libs/${swcLib}/package.json`).exports).toEqual({
'./package.json': './package.json',
'.': './src/index.js',
'.': {
default: './src/index.js',
types: './src/index.d.ts',
},
'./foo/bar': './src/foo/bar.js',
'./foo/faz': './src/foo/faz.js',
});

View File

@ -18,7 +18,10 @@ import {
import * as esbuild from 'esbuild';
import { normalizeOptions } from './lib/normalize';
import { EsBuildExecutorOptions } from './schema';
import {
EsBuildExecutorOptions,
NormalizedEsBuildExecutorOptions,
} from './schema';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import {
buildEsbuildOptions,
@ -213,7 +216,7 @@ export async function* esbuildExecutor(
}
function getTypeCheckOptions(
options: EsBuildExecutorOptions,
options: NormalizedEsBuildExecutorOptions,
context: ExecutorContext
) {
const { watch, tsConfig, outputPath } = options;
@ -243,7 +246,7 @@ function getTypeCheckOptions(
}
async function runTypeCheck(
options: EsBuildExecutorOptions,
options: NormalizedEsBuildExecutorOptions,
context: ExecutorContext
) {
const { errors, warnings } = await _runTypeCheck(

View File

@ -125,7 +125,7 @@ export function buildEsbuildOptions(
export function getOutExtension(
format: 'cjs' | 'esm',
options: NormalizedEsBuildExecutorOptions
options: Pick<NormalizedEsBuildExecutorOptions, 'userDefinedBuildOptions'>
): '.cjs' | '.mjs' | '.js' {
const userDefinedExt = options.userDefinedBuildOptions?.outExtension?.['.js'];
// Allow users to change the output extensions from default CJS and ESM extensions.

View File

@ -17,7 +17,7 @@ export function normalizeOptions(
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.`
`Setting 'generatePackageJson: true' is not supported with the current TypeScript setup. Update the 'package.json' file at the project root as needed and unset the 'generatePackageJson' option.`
);
}
@ -26,7 +26,7 @@ export function normalizeOptions(
// 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 ?? []
: [
...options.assets,
joinPathFragments(

View File

@ -5,7 +5,7 @@ type Compiler = 'babel' | 'swc';
export interface EsBuildExecutorOptions {
additionalEntryPoints?: string[];
assets: (AssetGlob | string)[];
assets?: (AssetGlob | string)[];
bundle?: boolean;
declaration?: boolean;
declarationRootDir?: string;
@ -32,7 +32,8 @@ export interface EsBuildExecutorOptions {
export interface NormalizedEsBuildExecutorOptions
extends Omit<EsBuildExecutorOptions, 'esbuildOptions' | 'esbuildConfig'> {
assets: (AssetGlob | string)[];
singleEntry: boolean;
external: string[];
userDefinedBuildOptions: esbuild.BuildOptions;
userDefinedBuildOptions: esbuild.BuildOptions | undefined;
}

View File

@ -1,33 +1,38 @@
import {
formatFiles,
joinPathFragments,
readJson,
readNxJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { esbuildInitGenerator } from '../init/init';
import { EsBuildExecutorOptions } from '../../executors/esbuild/schema';
import { EsBuildProjectSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { getOutputDir, getUpdatedPackageJsonContent } from '@nx/js';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { basename, dirname, join } from 'node:path/posix';
import { mergeTargetConfigurations } from 'nx/src/devkit-internals';
import { PackageJson } from 'nx/src/utils/package-json';
import { getOutExtension } from '../../executors/esbuild/lib/build-esbuild-options';
import { EsBuildExecutorOptions } from '../../executors/esbuild/schema';
import { esbuildInitGenerator } from '../init/init';
import { EsBuildProjectSchema } from './schema';
export async function configurationGenerator(
tree: Tree,
options: EsBuildProjectSchema
) {
assertNotUsingTsSolutionSetup(tree, 'esbuild', 'configuration');
const task = await esbuildInitGenerator(tree, {
...options,
skipFormat: true,
});
options.buildTarget ??= 'build';
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
checkForTargetConflicts(tree, options);
addBuildTarget(tree, options);
addBuildTarget(tree, options, isTsSolutionSetup);
updatePackageJson(tree, options, isTsSolutionSetup);
await formatFiles(tree);
return task;
}
@ -42,53 +47,58 @@ function checkForTargetConflicts(tree: Tree, options: EsBuildProjectSchema) {
}
}
function addBuildTarget(tree: Tree, options: EsBuildProjectSchema) {
function addBuildTarget(
tree: Tree,
options: EsBuildProjectSchema,
isTsSolutionSetup: boolean
) {
addBuildTargetDefaults(tree, '@nx/esbuild:esbuild', options.buildTarget);
const project = readProjectConfiguration(tree, options.project);
const packageJsonPath = joinPathFragments(project.root, 'package.json');
if (!tree.exists(packageJsonPath)) {
const importPath =
options.importPath || getImportPath(tree, options.project);
writeJson(tree, packageJsonPath, {
name: importPath,
version: '0.0.1',
});
}
const prevBuildOptions = project.targets?.[options.buildTarget]?.options;
const tsConfig = prevBuildOptions?.tsConfig ?? getTsConfigFile(tree, options);
let outputPath = prevBuildOptions?.outputPath;
if (!outputPath) {
outputPath = isTsSolutionSetup
? joinPathFragments(project.root, 'dist')
: joinPathFragments(
'dist',
project.root === '.' ? options.project : project.root
);
}
const buildOptions: EsBuildExecutorOptions = {
main: prevBuildOptions?.main ?? getMainFile(tree, options),
outputPath:
prevBuildOptions?.outputPath ??
joinPathFragments(
'dist',
project.root === '.' ? options.project : project.root
),
outputPath,
outputFileName: 'main.js',
tsConfig,
assets: [],
platform: options.platform,
format: options.format,
};
if (isTsSolutionSetup) {
buildOptions.declarationRootDir =
project.sourceRoot ?? tree.exists(`${project.root}/src`)
? `${project.root}/src`
: project.root;
} else {
buildOptions.assets = [];
if (tree.exists(joinPathFragments(project.root, 'README.md'))) {
buildOptions.assets.push({
glob: `${project.root}/README.md`,
input: '.',
output: '.',
});
}
}
if (options.platform === 'browser') {
buildOptions.outputHashing = 'all';
buildOptions.minify = true;
}
if (tree.exists(joinPathFragments(project.root, 'README.md'))) {
buildOptions.assets = [
{
glob: `${project.root}/README.md`,
input: '.',
output: '.',
},
];
}
updateProjectConfiguration(tree, options.project, {
...project,
targets: {
@ -111,6 +121,89 @@ function addBuildTarget(tree: Tree, options: EsBuildProjectSchema) {
});
}
function updatePackageJson(
tree: Tree,
options: EsBuildProjectSchema,
isTsSolutionSetup: boolean
) {
const project = readProjectConfiguration(tree, options.project);
const packageJsonPath = join(project.root, 'package.json');
let packageJson: PackageJson;
if (tree.exists(packageJsonPath)) {
if (!isTsSolutionSetup) {
return;
}
packageJson = readJson(tree, packageJsonPath);
} else {
packageJson = {
name: getImportPath(tree, options.project),
version: '0.0.1',
};
}
if (isTsSolutionSetup) {
const nxJson = readNxJson(tree);
const projectTarget = project.targets[options.buildTarget];
const mergedTarget = mergeTargetConfigurations(
projectTarget,
(projectTarget.executor
? nxJson.targetDefaults?.[projectTarget.executor]
: undefined) ?? nxJson.targetDefaults?.[options.buildTarget]
);
const {
declarationRootDir = '.',
main,
outputPath,
outputFileName,
// the executor option defaults to [esm]
format = ['esm'],
esbuildOptions,
} = mergedTarget.options;
// can't use the declarationRootDir as rootDir because it only affects the typings,
// not the runtime entry point
packageJson = getUpdatedPackageJsonContent(packageJson, {
main,
outputPath,
projectRoot: project.root,
generateExportsField: true,
packageJsonPath,
format,
outputFileName,
outputFileExtensionForCjs: getOutExtension('cjs', {
// there's very little chance that the user would have defined a custom esbuild config
// since that's an Nx specific file that we're not generating here and we're setting up
// the build for esbuild now
userDefinedBuildOptions: esbuildOptions,
}),
outputFileExtensionForEsm: getOutExtension('esm', {
userDefinedBuildOptions: esbuildOptions,
}),
});
if (declarationRootDir !== dirname(main)) {
// the declaration file entry point will be output to a location
// different than the runtime entry point, adjust accodingly
const outputDir = getOutputDir({
main,
outputPath,
projectRoot: project.root,
packageJsonPath,
rootDir: declarationRootDir,
});
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const typingsFile = `${outputDir}${mainFile}.d.ts`;
packageJson.types = typingsFile;
packageJson.exports['.'].types = typingsFile;
}
}
writeJson(tree, packageJsonPath, packageJson);
}
function getMainFile(tree: Tree, options: EsBuildProjectSchema) {
const project = readProjectConfiguration(tree, options.project);
const candidates = [

View File

@ -1,3 +1,5 @@
import type { SupportedFormat } from '@nx/js';
export interface EsBuildProjectSchema {
project: string;
main?: string;
@ -10,4 +12,5 @@ export interface EsBuildProjectSchema {
esbuildConfig?: string;
platform?: 'node' | 'browser' | 'neutral';
buildTarget?: string;
format?: SupportedFormat[];
}

View File

@ -60,6 +60,15 @@
"description": "The build target to add.",
"type": "string",
"default": "build"
},
"format": {
"description": "The format to build the library (esm or cjs).",
"type": "array",
"items": {
"type": "string",
"enum": ["esm", "cjs"]
},
"default": ["esm"]
}
},
"required": [],

View File

@ -4,14 +4,11 @@ import {
GeneratorCallback,
Tree,
} from '@nx/devkit';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { esbuildVersion } from '@nx/js/src/utils/versions';
import { nxVersion } from '../../utils/versions';
import { Schema } from './schema';
export async function esbuildInitGenerator(tree: Tree, schema: Schema) {
assertNotUsingTsSolutionSetup(tree, 'esbuild', 'init');
let installTask: GeneratorCallback = () => {};
if (!schema.skipPackageJson) {
installTask = addDependenciesToPackageJson(

View File

@ -73,13 +73,13 @@ export async function expoLibraryGeneratorInternal(
}
initRootBabelConfig(host);
createFiles(host, options);
const addProjectTask = await addProject(host, options);
if (addProjectTask) {
tasks.push(addProjectTask);
}
createFiles(host, options);
const lintTask = await addLinting(host, {
...options,
projectName: options.name,

View File

@ -113,13 +113,13 @@
"description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.",
"default": false,
"x-priority": "internal"
},
"stripLeadingPaths": {
"type": "boolean",
"description": "Remove leading directory from output (e.g. src). See: https://swc.rs/docs/usage/cli#--strip-leading-paths",
"default": false
}
},
"stripLeadingPaths": {
"type": "boolean",
"description": "Remove leading directory from output (e.g. src). See: https://swc.rs/docs/usage/cli#--strip-leading-paths",
"default": false
},
"required": ["main", "outputPath", "tsConfig"],
"definitions": {
"assetPattern": {

View File

@ -1,6 +1,12 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { readProjectConfiguration, Tree } from '@nx/devkit';
import {
addProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { join } from 'path';
import { LibraryGeneratorSchema } from '../library/schema';
@ -23,10 +29,8 @@ describe('convert to swc', () => {
bundler: 'tsc',
};
beforeAll(() => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tree.write('/.gitignore', '');
tree.write('/.gitignore', '');
});
it('should convert tsc to swc', async () => {
@ -50,9 +54,33 @@ describe('convert to swc', () => {
join(readProjectConfiguration(tree, 'tsc-lib').root, '.swcrc')
)
).toEqual(true);
expect(tree.read('package.json', 'utf-8')).toContain('@swc/core');
expect(tree.read('tsc-lib/package.json', 'utf-8')).toContain(
'@swc/helpers'
);
expect(
readJson(tree, 'package.json').devDependencies['@swc/core']
).toBeDefined();
expect(
readJson(tree, 'tsc-lib/package.json').dependencies['@swc/helpers']
).toBeDefined();
});
it('should handle project configuration without targets', async () => {
addProjectConfiguration(tree, 'lib1', { root: 'lib1' });
await expect(
convertToSwcGenerator(tree, { project: 'lib1' })
).resolves.not.toThrow();
});
it('should not add swc dependencies when no target was updated', async () => {
addProjectConfiguration(tree, 'lib1', { root: 'lib1' });
writeJson(tree, 'lib1/package.json', { dependencies: {} });
await convertToSwcGenerator(tree, { project: 'lib1' });
expect(
readJson(tree, 'package.json').devDependencies['@swc/core']
).not.toBeDefined();
expect(
readJson(tree, 'lib1/package.json').dependencies['@swc/helpers']
).not.toBeDefined();
});
});

View File

@ -20,13 +20,14 @@ export async function convertToSwcGenerator(
const options = normalizeOptions(schema);
const projectConfiguration = readProjectConfiguration(tree, options.project);
updateProjectBuildTargets(
const updated = updateProjectBuildTargets(
tree,
projectConfiguration,
options.project,
options.targets
);
return checkSwcDependencies(tree, projectConfiguration);
return updated ? checkSwcDependencies(tree, projectConfiguration) : () => {};
}
function normalizeOptions(
@ -47,8 +48,9 @@ function updateProjectBuildTargets(
projectName: string,
projectTargets: string[]
) {
let updated = false;
for (const target of projectTargets) {
const targetConfiguration = projectConfiguration.targets[target];
const targetConfiguration = projectConfiguration.targets?.[target];
if (
!targetConfiguration ||
(targetConfiguration.executor !== '@nx/js:tsc' &&
@ -56,9 +58,14 @@ function updateProjectBuildTargets(
)
continue;
targetConfiguration.executor = '@nx/js:swc';
updated = true;
}
updateProjectConfiguration(tree, projectName, projectConfiguration);
if (updated) {
updateProjectConfiguration(tree, projectName, projectConfiguration);
}
return updated;
}
function checkSwcDependencies(

View File

@ -67,7 +67,7 @@ import { getProjectPackageManagerWorkspaceStateWarningTask } from './utils/packa
import {
ensureProjectIsExcludedFromPluginRegistrations,
ensureProjectIsIncludedInPluginRegistrations,
} from './utils/plugin-registrations';
} from '../../utils/typescript/plugin';
const defaultOutputDirectory = 'dist';

View File

@ -1,18 +1,37 @@
import {
ensurePackage,
formatFiles,
type GeneratorCallback,
joinPathFragments,
readJson,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
type Tree,
updateNxJson,
updateProjectConfiguration,
writeJson,
type GeneratorCallback,
type ProjectConfiguration,
type Tree,
} from '@nx/devkit';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { basename, dirname, join } from 'node:path/posix';
import { mergeTargetConfigurations } from 'nx/src/devkit-internals';
import type { PackageJson } from 'nx/src/utils/package-json';
import { ensureProjectIsIncludedInPluginRegistrations } from '../..//utils/typescript/plugin';
import { getImportPath } from '../../utils/get-import-path';
import {
getUpdatedPackageJsonContent,
type SupportedFormat,
} from '../../utils/package-json/update-package-json';
import { addSwcConfig } from '../../utils/swc/add-swc-config';
import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies';
import { ensureTypescript } from '../../utils/typescript/ensure-typescript';
import { readTsConfig } from '../../utils/typescript/ts-config';
import { isUsingTsSolutionSetup } from '../../utils/typescript/ts-solution-setup';
import { nxVersion } from '../../utils/versions';
import { SetupBuildGeneratorSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
let ts: typeof import('typescript');
export async function setupBuildGenerator(
tree: Tree,
@ -20,8 +39,8 @@ export async function setupBuildGenerator(
): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = [];
const project = readProjectConfiguration(tree, options.project);
const buildTarget = options.buildTarget ?? 'build';
const prevBuildOptions = project.targets?.[buildTarget]?.options;
options.buildTarget ??= 'build';
const prevBuildOptions = project.targets?.[options.buildTarget]?.options;
project.targets ??= {};
@ -49,6 +68,7 @@ export async function setupBuildGenerator(
`Cannot locate a main file for ${options.project}. Please specify one using --main=<file-path>.`
);
}
options.main = mainFile;
let tsConfigFile: string;
if (prevBuildOptions?.tsConfig) {
@ -73,6 +93,13 @@ export async function setupBuildGenerator(
`Cannot locate a tsConfig file for ${options.project}. Please specify one using --tsConfig=<file-path>.`
);
}
options.tsConfig = tsConfigFile;
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
const nxJson = readNxJson(tree);
const addPlugin =
process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false;
switch (options.bundler) {
case 'vite': {
@ -83,10 +110,11 @@ export async function setupBuildGenerator(
const task = await viteConfigurationGenerator(tree, {
buildTarget: options.buildTarget,
project: options.project,
newProject: true,
newProject: false,
uiFramework: 'none',
includeVitest: false,
includeLib: true,
addPlugin,
skipFormat: true,
});
tasks.push(task);
@ -103,6 +131,7 @@ export async function setupBuildGenerator(
project: options.project,
skipFormat: true,
skipValidation: true,
format: ['cjs'],
});
tasks.push(task);
break;
@ -116,6 +145,7 @@ export async function setupBuildGenerator(
project: options.project,
compiler: 'tsc',
format: ['cjs', 'esm'],
addPlugin,
skipFormat: true,
skipValidation: true,
});
@ -123,39 +153,61 @@ export async function setupBuildGenerator(
break;
}
case 'tsc': {
addBuildTargetDefaults(tree, '@nx/js:tsc');
if (isTsSolutionSetup) {
const nxJson = readNxJson(tree);
ensureProjectIsIncludedInPluginRegistrations(
nxJson,
project.root,
options.buildTarget
);
updateNxJson(tree, nxJson);
updatePackageJsonForTsc(tree, options, project);
} else {
addBuildTargetDefaults(tree, '@nx/js:tsc');
const outputPath = joinPathFragments('dist', project.root);
project.targets[buildTarget] = {
executor: `@nx/js:tsc`,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main: mainFile,
tsConfig: tsConfigFile,
assets: [],
},
};
updateProjectConfiguration(tree, options.project, project);
const outputPath = joinPathFragments('dist', project.root);
project.targets[options.buildTarget] = {
executor: `@nx/js:tsc`,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main: mainFile,
tsConfig: tsConfigFile,
assets: [],
},
};
updateProjectConfiguration(tree, options.project, project);
}
break;
}
case 'swc': {
addBuildTargetDefaults(tree, '@nx/js:swc');
const outputPath = joinPathFragments('dist', project.root);
project.targets[buildTarget] = {
const outputPath = isTsSolutionSetup
? joinPathFragments(project.root, 'dist')
: joinPathFragments('dist', project.root);
project.targets[options.buildTarget] = {
executor: `@nx/js:swc`,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main: mainFile,
tsConfig: tsConfigFile,
assets: [],
},
};
if (isTsSolutionSetup) {
project.targets[options.buildTarget].options.stripLeadingPaths = true;
} else {
project.targets[options.buildTarget].options.assets = [];
}
updateProjectConfiguration(tree, options.project, project);
addSwcDependencies(tree);
addSwcConfig(tree, project.root, 'es6');
tasks.push(addSwcDependencies(tree));
addSwcConfig(tree, project.root, 'commonjs');
if (isTsSolutionSetup) {
updatePackageJsonForSwc(tree, options, project);
}
}
}
@ -165,3 +217,138 @@ export async function setupBuildGenerator(
}
export default setupBuildGenerator;
function updatePackageJsonForTsc(
tree: Tree,
options: SetupBuildGeneratorSchema,
project: ProjectConfiguration
) {
if (!ts) {
ts = ensureTypescript();
}
const tsconfig = readTsConfig(options.tsConfig, {
...ts.sys,
readFile: (p) => tree.read(p, 'utf-8'),
fileExists: (p) => tree.exists(p),
});
let main: string;
let rootDir: string;
let outputPath: string;
if (project.targets?.[options.buildTarget]) {
const mergedTarget = mergeTargetDefaults(
tree,
project,
options.buildTarget
);
({ main, rootDir, outputPath } = mergedTarget.options);
} else {
main = options.main;
({ rootDir = project.root, outDir: outputPath } = tsconfig.options);
const tsOutFile = tsconfig.options.outFile;
if (tsOutFile) {
main = join(project.root, basename(tsOutFile));
outputPath = dirname(tsOutFile);
}
if (!outputPath) {
outputPath = project.root;
}
}
const module = Object.keys(ts.ModuleKind).find(
(m) => ts.ModuleKind[m] === tsconfig.options.module
);
const format: SupportedFormat[] = module.toLowerCase().startsWith('es')
? ['esm']
: ['cjs'];
updatePackageJson(
tree,
options.project,
project.root,
main,
outputPath,
rootDir,
format
);
}
function updatePackageJsonForSwc(
tree: Tree,
options: SetupBuildGeneratorSchema,
project: ProjectConfiguration
) {
const mergedTarget = mergeTargetDefaults(tree, project, options.buildTarget);
const {
main,
outputPath,
swcrc: swcrcPath = join(project.root, '.swcrc'),
} = mergedTarget.options;
const swcrc = readJson(tree, swcrcPath);
const format: SupportedFormat[] = swcrc.module?.type?.startsWith('es')
? ['esm']
: ['cjs'];
updatePackageJson(
tree,
options.project,
project.root,
main,
outputPath,
// we set the `stripLeadingPaths` option, so the rootDir would match the dirname of the entry point
dirname(main),
format
);
}
function updatePackageJson(
tree: Tree,
projectName: string,
projectRoot: string,
main: string,
outputPath: string,
rootDir: string,
format?: SupportedFormat[]
) {
const packageJsonPath = join(projectRoot, 'package.json');
let packageJson: PackageJson;
if (tree.exists(packageJsonPath)) {
packageJson = readJson(tree, packageJsonPath);
} else {
packageJson = {
name: getImportPath(tree, projectName),
version: '0.0.1',
};
}
packageJson = getUpdatedPackageJsonContent(packageJson, {
main,
outputPath,
projectRoot,
generateExportsField: true,
packageJsonPath,
rootDir,
format,
});
writeJson(tree, packageJsonPath, packageJson);
}
function mergeTargetDefaults(
tree: Tree,
project: ProjectConfiguration,
buildTarget: string
) {
const nxJson = readNxJson(tree);
const projectTarget = project.targets[buildTarget];
return mergeTargetConfigurations(
projectTarget,
(projectTarget.executor
? nxJson.targetDefaults?.[projectTarget.executor]
: undefined) ?? nxJson.targetDefaults?.[buildTarget]
);
}

View File

@ -156,7 +156,10 @@ describe('getUpdatedPackageJsonContent', () => {
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './src/index.js',
'.': {
import: './src/index.js',
types: './src/index.d.ts',
},
'./package.json': './package.json',
},
});
@ -185,7 +188,10 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1',
type: 'commonjs',
exports: {
'.': './src/index.cjs',
'.': {
default: './src/index.cjs',
types: './src/index.d.ts',
},
'./package.json': './package.json',
},
});
@ -220,7 +226,10 @@ describe('getUpdatedPackageJsonContent', () => {
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './src/index.js',
'.': {
default: './src/index.js',
types: './src/index.d.ts',
},
'./foo': './src/foo.js',
'./bar': './src/bar.js',
'./package.json': './package.json',
@ -258,7 +267,10 @@ describe('getUpdatedPackageJsonContent', () => {
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './src/index.js',
'.': {
import: './src/index.js',
types: './src/index.d.ts',
},
'./foo': './src/foo.js',
'./bar': './src/bar.js',
'./package.json': './package.json',
@ -298,6 +310,7 @@ describe('getUpdatedPackageJsonContent', () => {
'.': {
import: './src/index.js',
default: './src/index.cjs',
types: './src/index.d.ts',
},
'./foo': {
import: './src/foo.js',
@ -351,6 +364,7 @@ describe('getUpdatedPackageJsonContent', () => {
'.': {
import: './src/index.js',
default: './src/index.cjs',
types: './src/index.d.ts',
},
'./package.json': './package.json',
'./custom': './custom.js',
@ -383,7 +397,10 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1',
type: 'module',
exports: {
'.': './src/index.cjs',
'.': {
default: './src/index.cjs',
types: './src/index.d.ts',
},
'./package.json': './package.json',
},
});

View File

@ -22,7 +22,7 @@ import {
} from '@nx/devkit';
import { DependentBuildableProjectNode } from '../buildable-libs-utils';
import { existsSync, writeFileSync } from 'node:fs';
import { basename, join, parse } from 'path';
import { basename, dirname, join, parse, relative } from 'path';
import { fileExists } from 'nx/src/utils/fileutils';
import type { PackageJson } from 'nx/src/utils/package-json';
import { readFileMapCache } from 'nx/src/project-graph/nx-deps-cache';
@ -47,6 +47,7 @@ export interface UpdatePackageJsonOption {
updateBuildableProjectDepsInPackageJson?: boolean;
buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies';
generateLockfile?: boolean;
packageJsonPath?: string;
}
export function updatePackageJson(
@ -233,21 +234,18 @@ export function getExports(
| 'projectRoot'
| 'outputFileName'
| 'additionalEntryPoints'
| 'outputPath'
| 'packageJsonPath'
> & {
fileExt: string;
}
): Exports {
const outputDir = getOutputDir(options);
const mainFile = options.outputFileName
? options.outputFileName.replace(/\.[tj]s$/, '')
: basename(options.main).replace(/\.[tj]s$/, '');
const relativeMainFileDir = options.outputFileName
? './'
: getRelativeDirectoryToProjectRoot(
options.main,
options.rootDir ?? options.projectRoot
);
const exports: Exports = {
'.': relativeMainFileDir + mainFile + options.fileExt,
'.': outputDir + mainFile + options.fileExt,
};
if (options.additionalEntryPoints) {
@ -289,6 +287,24 @@ export function getUpdatedPackageJsonContent(
packageJson.exports['./package.json'] ??= './package.json';
}
if (!options.skipTypings) {
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const outputDir = getOutputDir(options);
const typingsFile = `${outputDir}${mainFile}.d.ts`;
packageJson.types ??= typingsFile;
if (options.generateExportsField) {
if (!packageJson.exports['.']) {
packageJson.exports['.'] = { types: typingsFile };
} else if (
typeof packageJson.exports['.'] === 'object' &&
!packageJson.exports['.'].types
) {
packageJson.exports['.'].types = typingsFile;
}
}
}
if (hasEsmFormat) {
const esmExports = getExports({
...options,
@ -304,9 +320,13 @@ export function getUpdatedPackageJsonContent(
if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(esmExports)) {
packageJson.exports[exportEntry] ??= hasCjsFormat
? { import: filePath }
: filePath;
if (!packageJson.exports[exportEntry]) {
packageJson.exports[exportEntry] ??= hasCjsFormat
? { import: filePath }
: filePath;
} else if (typeof packageJson.exports[exportEntry] === 'object') {
packageJson.exports[exportEntry].import ??= filePath;
}
}
}
}
@ -327,24 +347,39 @@ export function getUpdatedPackageJsonContent(
if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(cjsExports)) {
if (hasEsmFormat) {
packageJson.exports[exportEntry]['default'] ??= filePath;
} else {
packageJson.exports[exportEntry] ??= filePath;
if (!packageJson.exports[exportEntry]) {
packageJson.exports[exportEntry] ??= hasEsmFormat
? { default: filePath }
: filePath;
} else if (typeof packageJson.exports[exportEntry] === 'object') {
packageJson.exports[exportEntry].default ??= filePath;
}
}
}
}
if (!options.skipTypings) {
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const relativeMainFileDir = getRelativeDirectoryToProjectRoot(
options.main,
options.projectRoot
);
const typingsFile = `${relativeMainFileDir}${mainFile}.d.ts`;
packageJson.types ??= typingsFile;
}
return packageJson;
}
export function getOutputDir(
options: Pick<
UpdatePackageJsonOption,
| 'main'
| 'rootDir'
| 'projectRoot'
| 'outputFileName'
| 'outputPath'
| 'packageJsonPath'
>
): string {
const packageJsonDir = options.packageJsonPath
? dirname(options.packageJsonPath)
: options.outputPath;
const relativeOutputPath = relative(packageJsonDir, options.outputPath);
const relativeMainDir = options.outputFileName
? ''
: relative(options.rootDir ?? options.projectRoot, dirname(options.main));
const outputDir = join(relativeOutputPath, relativeMainDir);
return outputDir === '.' ? `./` : `./${outputDir}/`;
}

View File

@ -2,33 +2,9 @@ import type { NxJsonConfiguration } from '@nx/devkit';
import {
ensureProjectIsExcludedFromPluginRegistrations,
ensureProjectIsIncludedInPluginRegistrations,
} from './plugin-registrations';
} from './plugin';
describe('ensureProjectIsIncludedInPluginRegistrations', () => {
it('should do nothing when there is no `plugin` entry', () => {
const nxJson: NxJsonConfiguration = {};
ensureProjectIsIncludedInPluginRegistrations(nxJson, 'packages/pkg1');
expect(nxJson).toStrictEqual({});
});
it('should do nothing when the there are no plugins', () => {
const nxJson: NxJsonConfiguration = { plugins: [] };
ensureProjectIsIncludedInPluginRegistrations(nxJson, 'packages/pkg1');
expect(nxJson).toStrictEqual({ plugins: [] });
});
it('should do nothing when the are no registrations for the `@nx/js/typescript` plugin', () => {
const nxJson: NxJsonConfiguration = { plugins: ['@foo/bar/plugin'] };
ensureProjectIsIncludedInPluginRegistrations(nxJson, 'packages/pkg1');
expect(nxJson).toStrictEqual({ plugins: ['@foo/bar/plugin'] });
});
it('should do nothing when `include`/`exclude` are not set in a plugin registration that infers both targets', () => {
const originalNxJson: NxJsonConfiguration = {
plugins: [
@ -203,6 +179,44 @@ describe('ensureProjectIsIncludedInPluginRegistrations', () => {
});
});
it('should exclude a project from a plugin registration with a different build target nama and add a new plugin registration that includes it', () => {
const nxJson: NxJsonConfiguration = {
plugins: [
{
plugin: '@nx/js/typescript',
options: { typecheck: { targetName: 'typecheck' } },
},
],
};
ensureProjectIsIncludedInPluginRegistrations(
nxJson,
'packages/pkg1',
'build-tsc'
);
expect(nxJson).toStrictEqual({
plugins: [
{
plugin: '@nx/js/typescript',
exclude: ['packages/pkg1/*'],
options: { typecheck: { targetName: 'typecheck' } },
},
{
plugin: '@nx/js/typescript',
include: ['packages/pkg1/*'],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build-tsc',
configName: 'tsconfig.lib.json',
},
},
},
],
});
});
it('should include a project in a plugin registration that infers both targets and with `include` set but not including the project', () => {
const nxJson: NxJsonConfiguration = {
plugins: [
@ -239,6 +253,57 @@ describe('ensureProjectIsIncludedInPluginRegistrations', () => {
});
});
it('should not include a project in a plugin registration that infers both targets with a different build target name and with `include` set but not including the project', () => {
const nxJson: NxJsonConfiguration = {
plugins: [
{
plugin: '@nx/js/typescript',
include: ['packages/pkg1/*'],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build',
configName: 'tsconfig.lib.json',
},
},
},
],
};
ensureProjectIsIncludedInPluginRegistrations(
nxJson,
'packages/pkg2',
'build-tsc'
);
expect(nxJson).toStrictEqual({
plugins: [
{
plugin: '@nx/js/typescript',
include: ['packages/pkg1/*'],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build',
configName: 'tsconfig.lib.json',
},
},
},
{
plugin: '@nx/js/typescript',
include: ['packages/pkg2/*'],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build-tsc',
configName: 'tsconfig.lib.json',
},
},
},
],
});
});
it('should add a new plugin registration including the project when there is an existing plugin registration that infers both targets and with `exclude` set excluding the project', () => {
const nxJson: NxJsonConfiguration = {
plugins: [
@ -285,6 +350,42 @@ describe('ensureProjectIsIncludedInPluginRegistrations', () => {
],
});
});
it('should remove glob pattern from `exclude` when it matches exactly the project root glob pattern', () => {
const nxJson: NxJsonConfiguration = {
plugins: [
{
plugin: '@nx/js/typescript',
exclude: ['packages/pkg1/*', 'packages/pkg2/*'],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build',
configName: 'tsconfig.lib.json',
},
},
},
],
};
ensureProjectIsIncludedInPluginRegistrations(nxJson, 'packages/pkg1');
expect(nxJson).toStrictEqual({
plugins: [
{
plugin: '@nx/js/typescript',
exclude: ['packages/pkg2/*'],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build',
configName: 'tsconfig.lib.json',
},
},
},
],
});
});
});
describe('ensureProjectIsExcludedFromPluginRegistrations', () => {

View File

@ -3,19 +3,14 @@ import type {
NxJsonConfiguration,
} from '@nx/devkit';
import { findMatchingConfigFiles } from 'nx/src/devkit-internals';
import type { TscPluginOptions } from '../../../plugins/typescript/plugin';
import type { TscPluginOptions } from '../../plugins/typescript/plugin';
export function ensureProjectIsIncludedInPluginRegistrations(
nxJson: NxJsonConfiguration,
projectRoot: string
projectRoot: string,
buildTargetName: string = 'build'
): void {
if (
!nxJson.plugins?.length ||
!nxJson.plugins.some(isTypeScriptPluginRegistration)
) {
return;
}
nxJson.plugins ??= [];
let isIncluded = false;
let index = 0;
for (const registration of nxJson.plugins) {
@ -26,7 +21,7 @@ export function ensureProjectIsIncludedInPluginRegistrations(
if (typeof registration === 'string') {
// if it's a string all projects are included but the are no user-specified options
// and the `build` task is not inferred by default, so we need to exclude it
// and the build task is not inferred by default, so we need to exclude it
nxJson.plugins[index] = {
plugin: '@nx/js/typescript',
exclude: [`${projectRoot}/*`],
@ -41,43 +36,58 @@ export function ensureProjectIsIncludedInPluginRegistrations(
);
if (matchingConfigFiles.length) {
// it's included by the plugin registration, check if the user-specified options would result
// in a `build` task being inferred, if not, we need to exclude it
if (registration.options?.typecheck && registration.options?.build) {
// it has the desired options, do nothing
// in the appropriate build task being inferred, if not, we need to exclude it
if (
registration.options?.typecheck !== false &&
matchesBuildTarget(registration.options?.build, buildTargetName)
) {
// it has the desired options, do nothing, but continue processing
// other registrations to exclude as needed
isIncluded = true;
} else {
// it would not have the `build` task inferred, so we need to exclude it
// it would not have the typecheck or build task inferred, so we need to exclude it
registration.exclude ??= [];
registration.exclude.push(`${projectRoot}/*`);
}
} else if (
registration.options?.typecheck &&
registration.options?.build &&
!registration.exclude?.length
!isIncluded &&
registration.options?.typecheck !== false &&
matchesBuildTarget(registration.options?.build, buildTargetName)
) {
// negative pattern are not supported by the `exclude` option so we
// can't update it to not exclude the project, so we only update the
// plugin registration if there's no `exclude` option, in which case
// the plugin registration should have an `include` options that doesn't
// include the project
isIncluded = true;
registration.include ??= [];
registration.include.push(`${projectRoot}/*`);
if (!registration.exclude?.length) {
// negative pattern are not supported by the `exclude` option so we
// can't update it to not exclude the project, so we only update the
// plugin registration if there's no `exclude` option, in which case
// the plugin registration should have an `include` options that doesn't
// include the project
isIncluded = true;
registration.include ??= [];
registration.include.push(`${projectRoot}/*`);
} else if (registration.exclude?.includes(`${projectRoot}/*`)) {
isIncluded = true;
registration.exclude = registration.exclude.filter(
(e) => e !== `${projectRoot}/*`
);
if (!registration.exclude.length) {
// if there's no `exclude` option left, we can remove the exclude option
delete registration.exclude;
}
}
}
}
index++;
}
if (!isIncluded) {
// the project is not included by any plugin registration with an inferred `build` task,
// so we create a new plugin registration for it
// the project is not included by any plugin registration with an inferred build task
// with the given name, so we create a new plugin registration for it
nxJson.plugins.push({
plugin: '@nx/js/typescript',
include: [`${projectRoot}/*`],
options: {
typecheck: { targetName: 'typecheck' },
build: {
targetName: 'build',
targetName: buildTargetName,
configName: 'tsconfig.lib.json',
},
},
@ -139,3 +149,21 @@ function isTypeScriptPluginRegistration(
(typeof plugin !== 'string' && plugin.plugin === '@nx/js/typescript')
);
}
function matchesBuildTarget(
buildOptions: TscPluginOptions['build'],
buildTargetName: string
): boolean {
if (buildOptions === undefined || buildOptions === false) {
return false;
}
if (buildOptions === true && buildTargetName === 'build') {
return true;
}
return (
typeof buildOptions === 'object' &&
buildOptions.targetName === buildTargetName
);
}

View File

@ -1,22 +1,25 @@
import { offsetFromRoot, Tree, updateJson, workspaceRoot } from '@nx/devkit';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import * as ts from 'typescript';
import type * as ts from 'typescript';
import { ensureTypescript } from './ensure-typescript';
let tsModule: typeof import('typescript');
export function readTsConfig(tsConfigPath: string): ts.ParsedCommandLine {
export function readTsConfig(
tsConfigPath: string,
sys?: ts.System
): ts.ParsedCommandLine {
if (!tsModule) {
tsModule = require('typescript');
}
const readResult = tsModule.readConfigFile(
tsConfigPath,
tsModule.sys.readFile
);
sys ??= tsModule.sys;
const readResult = tsModule.readConfigFile(tsConfigPath, sys.readFile);
return tsModule.parseJsonConfigFileContent(
readResult.config,
tsModule.sys,
sys,
dirname(tsConfigPath)
);
}

View File

@ -71,13 +71,13 @@ export async function reactNativeLibraryGeneratorInternal(
tasks.push(ensureDependencies(host));
}
createFiles(host, options);
const addProjectTask = await addProject(host, options);
if (addProjectTask) {
tasks.push(addProjectTask);
}
createFiles(host, options);
const lintTask = await addLinting(host, {
...options,
projectName: options.name,

View File

@ -56,7 +56,7 @@ describe('configurationGenerator', () => {
it('should support --main option', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
main: './src/index.ts',
main: './libs/mypkg/src/index.ts',
});
const rollupConfig = tree.read('libs/mypkg/rollup.config.js', 'utf-8');
@ -85,7 +85,7 @@ module.exports = withNx(
it('should support --tsConfig option', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
tsConfig: './tsconfig.custom.json',
tsConfig: 'libs/mypkg/tsconfig.custom.json',
});
const rollupConfig = tree.read('libs/mypkg/rollup.config.js', 'utf-8');

View File

@ -3,24 +3,31 @@ import {
GeneratorCallback,
joinPathFragments,
offsetFromRoot,
readJson,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
stripIndents,
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { rollupInitGenerator } from '../init/init';
import { RollupExecutorOptions } from '../../executors/rollup/schema';
import { RollupProjectSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { getUpdatedPackageJsonContent, readTsConfig } from '@nx/js';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { dirname, join, relative } from 'node:path/posix';
import { mergeTargetConfigurations } from 'nx/src/devkit-internals';
import type { PackageJson } from 'nx/src/utils/package-json';
import { RollupExecutorOptions } from '../../executors/rollup/schema';
import { RollupWithNxPluginOptions } from '../../plugins/with-nx/with-nx-options';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { hasPlugin } from '../../utils/has-plugin';
import { RollupWithNxPluginOptions } from '../../plugins/with-nx/with-nx-options';
import { rollupInitGenerator } from '../init/init';
import { RollupProjectSchema } from './schema';
let ts: typeof import('typescript');
export async function configurationGenerator(
tree: Tree,
@ -39,15 +46,20 @@ export async function configurationGenerator(
tasks.push(ensureDependencies(tree, options));
}
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
let outputConfig: OutputConfig | undefined;
if (hasPlugin(tree)) {
createRollupConfig(tree, options);
outputConfig = createRollupConfig(tree, options, isTsSolutionSetup);
} else {
options.buildTarget ??= 'build';
checkForTargetConflicts(tree, options);
addBuildTarget(tree, options);
addBuildTarget(tree, options, isTsSolutionSetup);
}
addPackageJson(tree, options);
updatePackageJson(tree, options, outputConfig, isTsSolutionSetup);
if (isTsSolutionSetup) {
updateTsConfig(tree, options);
}
if (!options.skipFormat) {
await formatFiles(tree);
@ -56,20 +68,34 @@ export async function configurationGenerator(
return runTasksInSerial(...tasks);
}
function createRollupConfig(tree: Tree, options: RollupProjectSchema) {
const isUsingTsPlugin = isUsingTsSolutionSetup(tree);
type OutputConfig = {
main: string;
outputPath: string;
};
function createRollupConfig(
tree: Tree,
options: RollupProjectSchema,
isTsSolutionSetup: boolean
): OutputConfig {
const project = readProjectConfiguration(tree, options.project);
const main = options.main
? `./${relative(project.root, options.main)}`
: './src/index.ts';
const outputPath = isTsSolutionSetup
? './dist'
: joinPathFragments(
offsetFromRoot(project.root),
'dist',
project.root === '.' ? project.name : project.root
);
const buildOptions: RollupWithNxPluginOptions = {
outputPath: isUsingTsPlugin
? './dist'
: joinPathFragments(
offsetFromRoot(project.root),
'dist',
project.root === '.' ? project.name : project.root
),
outputPath,
compiler: options.compiler ?? 'babel',
main: options.main ?? './src/index.ts',
tsConfig: options.tsConfig ?? './tsconfig.lib.json',
main,
tsConfig: options.tsConfig
? `./${relative(project.root, options.tsConfig)}`
: './tsconfig.lib.json',
};
tree.write(
@ -82,8 +108,12 @@ module.exports = withNx(
outputPath: '${buildOptions.outputPath}',
tsConfig: '${buildOptions.tsConfig}',
compiler: '${buildOptions.compiler}',
format: ${JSON.stringify(options.format ?? ['esm'])},
assets: [{ input: '.', output: '.', glob:'*.md' }],
format: ${JSON.stringify(options.format ?? ['esm'])},${
!isTsSolutionSetup
? `
assets: [{ input: '.', output: '.', glob:'*.md' }],`
: ''
}
},
{
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
@ -93,6 +123,11 @@ module.exports = withNx(
);
`
);
return {
main: joinPathFragments(project.root, main),
outputPath: joinPathFragments(project.root, outputPath),
};
}
function checkForTargetConflicts(tree: Tree, options: RollupProjectSchema) {
@ -105,60 +140,129 @@ function checkForTargetConflicts(tree: Tree, options: RollupProjectSchema) {
}
}
function addPackageJson(tree: Tree, options: RollupProjectSchema) {
function updatePackageJson(
tree: Tree,
options: RollupProjectSchema,
outputConfig: OutputConfig | undefined,
isTsSolutionSetup: boolean
) {
const project = readProjectConfiguration(tree, options.project);
const packageJsonPath = joinPathFragments(project.root, 'package.json');
if (!tree.exists(packageJsonPath)) {
const importPath =
options.importPath || getImportPath(tree, options.project);
writeJson(tree, packageJsonPath, {
name: importPath,
const packageJsonPath = join(project.root, 'package.json');
let packageJson: PackageJson;
if (tree.exists(packageJsonPath)) {
if (!isTsSolutionSetup) {
return;
}
packageJson = readJson(tree, packageJsonPath);
} else {
packageJson = {
name: options.importPath || getImportPath(tree, options.project),
version: '0.0.1',
});
};
}
if (isTsSolutionSetup) {
let main: string;
let outputPath: string;
if (outputConfig) {
({ main, outputPath } = outputConfig);
} else {
// target must exist if we don't receive an outputConfig
const projectTarget = project.targets[options.buildTarget];
const nxJson = readNxJson(tree);
const mergedTarget = mergeTargetConfigurations(
projectTarget,
(projectTarget.executor
? nxJson.targetDefaults?.[projectTarget.executor]
: undefined) ?? nxJson.targetDefaults?.[options.buildTarget]
);
({ main, outputPath } = mergedTarget.options);
}
packageJson = getUpdatedPackageJsonContent(packageJson, {
main,
outputPath,
projectRoot: project.root,
rootDir: dirname(main),
generateExportsField: true,
packageJsonPath,
format: options.format ?? ['esm'],
outputFileExtensionForCjs: '.cjs.js',
outputFileExtensionForEsm: '.esm.js',
});
// rollup has a specific declaration file generation not handled by the util above,
// adjust accordingly
const typingsFile = (packageJson.module ?? packageJson.main).replace(
/\.js$/,
'.d.ts'
);
packageJson.types = typingsFile;
packageJson.exports['.'].types = typingsFile;
}
writeJson(tree, packageJsonPath, packageJson);
}
function addBuildTarget(tree: Tree, options: RollupProjectSchema) {
function addBuildTarget(
tree: Tree,
options: RollupProjectSchema,
isTsSolutionSetup: boolean
) {
addBuildTargetDefaults(tree, '@nx/rollup:rollup', options.buildTarget);
const project = readProjectConfiguration(tree, options.project);
const prevBuildOptions = project.targets?.[options.buildTarget]?.options;
options.tsConfig ??=
prevBuildOptions?.tsConfig ??
joinPathFragments(project.root, 'tsconfig.lib.json');
let outputPath = prevBuildOptions?.outputPath;
if (!outputPath) {
outputPath = isTsSolutionSetup
? joinPathFragments(project.root, 'dist')
: joinPathFragments(
'dist',
project.root === '.' ? project.name : project.root
);
}
const buildOptions: RollupExecutorOptions = {
main:
options.main ??
prevBuildOptions?.main ??
joinPathFragments(project.root, 'src/index.ts'),
outputPath:
prevBuildOptions?.outputPath ??
joinPathFragments(
'dist',
project.root === '.' ? project.name : project.root
),
tsConfig:
options.tsConfig ??
prevBuildOptions?.tsConfig ??
joinPathFragments(project.root, 'tsconfig.lib.json'),
additionalEntryPoints: prevBuildOptions?.additionalEntryPoints,
generateExportsField: prevBuildOptions?.generateExportsField,
outputPath,
tsConfig: options.tsConfig,
// TODO(leo): see if we can use this when updating the package.json for the new setup
// additionalEntryPoints: prevBuildOptions?.additionalEntryPoints,
// generateExportsField: prevBuildOptions?.generateExportsField,
compiler: options.compiler ?? 'babel',
project: `${project.root}/package.json`,
external: options.external,
format: options.format,
format: options.format ?? isTsSolutionSetup ? ['esm'] : undefined,
};
if (options.rollupConfig) {
buildOptions.rollupConfig = options.rollupConfig;
}
if (tree.exists(joinPathFragments(project.root, 'README.md'))) {
buildOptions.assets = [
{
glob: `${project.root}/README.md`,
input: '.',
output: '.',
},
];
if (!isTsSolutionSetup) {
buildOptions.additionalEntryPoints =
prevBuildOptions?.additionalEntryPoints;
buildOptions.generateExportsField = prevBuildOptions?.generateExportsField;
if (tree.exists(joinPathFragments(project.root, 'README.md'))) {
buildOptions.assets = [
{
glob: `${project.root}/README.md`,
input: '.',
output: '.',
},
];
}
}
updateProjectConfiguration(tree, options.project, {
@ -174,4 +278,35 @@ function addBuildTarget(tree: Tree, options: RollupProjectSchema) {
});
}
function updateTsConfig(tree: Tree, options: RollupProjectSchema): void {
const project = readProjectConfiguration(tree, options.project);
const tsconfigPath =
options.tsConfig ?? joinPathFragments(project.root, 'tsconfig.lib.json');
if (!tree.exists(tsconfigPath)) {
throw new Error(
`The '${tsconfigPath}' file doesn't exist. Provide the 'tsConfig' option with the correct path pointing to the tsconfig file to use for builds.`
);
}
if (!ts) {
ts = ensureTypescript();
}
const parsedTsConfig = readTsConfig(tsconfigPath, {
...ts.sys,
readFile: (p) => tree.read(p, 'utf-8'),
fileExists: (p) => tree.exists(p),
});
updateJson(tree, tsconfigPath, (json) => {
if (parsedTsConfig.options.module === ts.ModuleKind.NodeNext) {
json.compilerOptions ??= {};
json.compilerOptions.module = 'esnext';
json.compilerOptions.moduleResolution = 'bundler';
}
return json;
});
}
export default configurationGenerator;

View File

@ -25,13 +25,13 @@
},
"main": {
"type": "string",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/main.ts'.",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/index.ts'.",
"alias": "entryFile",
"x-priority": "important"
},
"tsConfig": {
"type": "string",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.app.json'.",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.lib.json'.",
"x-priority": "important"
},
"skipFormat": {

View File

@ -76,6 +76,12 @@ export interface RollupWithNxPluginOptions {
* The path to tsconfig file.
*/
tsConfig: string;
/**
* Whether to generate a package.json file in the output path. It's not supported when the workspace is
* set up with TypeScript Project References along with the package managers' Workspaces feature. Otherwise,
* it defaults to `true`.
*/
generatePackageJson?: boolean;
}
export interface AssetGlobPattern {

View File

@ -28,6 +28,7 @@ import { deleteOutput } from '../delete-output';
import { AssetGlobPattern, RollupWithNxPluginOptions } from './with-nx-options';
import { normalizeOptions } from './normalize-options';
import { PackageJson } from 'nx/src/utils/package-json';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
// These use require because the ES import isn't correct.
const commonjs = require('@rollup/plugin-commonjs');
@ -182,6 +183,22 @@ export function withNx(
}
if (!global.NX_GRAPH_CREATION) {
const isTsSolutionSetup = isUsingTsSolutionSetup();
if (isTsSolutionSetup) {
if (options.generatePackageJson) {
throw new Error(
`Setting 'generatePackageJson: true' is not supported with the current TypeScript setup. Update the 'package.json' file at the project root as needed and unset the 'generatePackageJson' option.`
);
}
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.`
);
}
} else {
options.generatePackageJson ??= true;
}
finalConfig.plugins = [
copy({
targets: convertCopyAssetsToRollupOptions(
@ -247,7 +264,7 @@ export function withNx(
}),
commonjs(),
analyze(),
generatePackageJson(options, packageJson),
options.generatePackageJson && generatePackageJson(options, packageJson),
];
if (Array.isArray(rollupConfig.plugins)) {
finalConfig.plugins.push(...rollupConfig.plugins);

View File

@ -285,6 +285,7 @@ export default defineConfig({
exports[`@nx/vite:configuration library mode should set up non buildable library which already has vite.config.ts correctly 1`] = `
"import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
@ -395,7 +396,6 @@ exports[`@nx/vite:configuration transform React app to use Vite should move inde
exports[`@nx/vite:configuration transform Web app to use Vite should create vite.config file at the root of the app 1`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';

View File

@ -2,27 +2,34 @@ import {
formatFiles,
GeneratorCallback,
joinPathFragments,
readJson,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
Tree,
updateJson,
writeJson,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import {
getUpdatedPackageJsonContent,
initGenerator as jsInitGenerator,
} from '@nx/js';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { join } from 'node:path/posix';
import type { PackageJson } from 'nx/src/utils/package-json';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import {
addBuildTarget,
addServeTarget,
addPreviewTarget,
addServeTarget,
createOrEditViteConfig,
TargetFlags,
} from '../../utils/generator-utils';
import initGenerator from '../init/init';
import vitestGenerator from '../vitest/vitest-generator';
import { ViteConfigurationGeneratorSchema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { convertNonVite } from './lib/convert-non-vite';
import { ViteConfigurationGeneratorSchema } from './schema';
export function viteConfigurationGenerator(
host: Tree,
@ -103,20 +110,10 @@ export async function viteConfigurationGeneratorInternal(
tree,
joinPathFragments(projectRoot, 'tsconfig.lib.json'),
(json) => {
if (!json.compilerOptions) {
json.compilerOptions = {};
}
if (!json.compilerOptions.types) {
json.compilerOptions.types = [];
}
json.compilerOptions ??= {};
json.compilerOptions.types ??= [];
if (!json.compilerOptions.types.includes('vite/client')) {
return {
...json,
compilerOptions: {
...json.compilerOptions,
types: [...json.compilerOptions.types, 'vite/client'],
},
};
json.compilerOptions.types.push('vite/client');
}
return json;
}
@ -168,6 +165,10 @@ export async function viteConfigurationGeneratorInternal(
tasks.push(vitestTask);
}
if (isUsingTsSolutionSetup(tree)) {
updatePackageJson(tree, schema);
}
if (!schema.skipFormat) {
await formatFiles(tree);
}
@ -176,3 +177,44 @@ export async function viteConfigurationGeneratorInternal(
}
export default viteConfigurationGenerator;
function updatePackageJson(
tree: Tree,
options: ViteConfigurationGeneratorSchema
) {
const project = readProjectConfiguration(tree, options.project);
const packageJsonPath = join(project.root, 'package.json');
let packageJson: PackageJson;
if (tree.exists(packageJsonPath)) {
packageJson = readJson(tree, packageJsonPath);
} else {
packageJson = {
name: getImportPath(tree, options.project),
version: '0.0.1',
};
}
// we always write/override the vite and project config with some set values,
// so we can rely on them
const main = join(project.root, 'src/index.ts');
// we configure the dts plugin with the entryRoot set to `src`
const rootDir = join(project.root, 'src');
const outputPath = joinPathFragments(project.root, 'dist');
packageJson = getUpdatedPackageJsonContent(packageJson, {
main,
outputPath,
projectRoot: project.root,
rootDir,
generateExportsField: true,
packageJsonPath,
format: ['esm', 'cjs'],
// when building both formats, we don't set the package.json "type" field, so
// we need to set the esm extension to ".mjs" to match vite output
// see the "File Extensions" callout in https://vite.dev/guide/build.html#library-mode
outputFileExtensionForEsm: '.mjs',
});
writeJson(tree, packageJsonPath, packageJson);
}

View File

@ -215,7 +215,6 @@ describe('generator utils', () => {
.toMatchInlineSnapshot(`
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';

View File

@ -118,11 +118,15 @@ export function addBuildTarget(
) {
addBuildTargetDefaults(tree, '@nx/vite:build');
const project = readProjectConfiguration(tree, options.project);
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
const buildOptions: ViteBuildExecutorOptions = {
outputPath: joinPathFragments(
'dist',
project.root != '.' ? project.root : options.project
),
outputPath: isTsSolutionSetup
? joinPathFragments(project.root, 'dist')
: joinPathFragments(
'dist',
project.root != '.' ? project.root : options.project
),
};
project.targets ??= {};
project.targets[target] = {
@ -228,7 +232,15 @@ export function editTsConfig(
) {
const projectConfig = readProjectConfiguration(tree, options.project);
const config = readJson(tree, `${projectConfig.root}/tsconfig.json`);
let tsconfigPath = joinPathFragments(projectConfig.root, 'tsconfig.json');
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
if (isTsSolutionSetup) {
tsconfigPath = [
joinPathFragments(projectConfig.root, 'tsconfig.app.json'),
joinPathFragments(projectConfig.root, 'tsconfig.lib.json'),
].find((p) => tree.exists(p));
}
const config = readJson(tree, tsconfigPath);
switch (options.uiFramework) {
case 'react':
@ -241,21 +253,23 @@ export function editTsConfig(
};
break;
case 'none':
config.compilerOptions = {
module: 'commonjs',
forceConsistentCasingInFileNames: true,
strict: true,
noImplicitOverride: true,
noPropertyAccessFromIndexSignature: true,
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
};
if (!isTsSolutionSetup) {
config.compilerOptions = {
module: 'commonjs',
forceConsistentCasingInFileNames: true,
strict: true,
noImplicitOverride: true,
noPropertyAccessFromIndexSignature: true,
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
};
}
break;
default:
break;
}
writeJson(tree, `${projectConfig.root}/tsconfig.json`, config);
writeJson(tree, tsconfigPath, config);
}
export function deleteWebpackConfig(
@ -405,7 +419,8 @@ export function createOrEditViteConfig(
},
},`;
const imports: string[] = options.imports ? options.imports : [];
const imports: string[] = options.imports ? [...options.imports] : [];
const plugins: string[] = options.plugins ? [...options.plugins] : [];
if (!onlyVitest && options.includeLib) {
imports.push(
@ -414,11 +429,13 @@ export function createOrEditViteConfig(
);
}
let viteConfigContent = '';
const plugins = options.plugins
? [...options.plugins, `nxViteTsPaths()`, `nxCopyAssetsPlugin(['*.md'])`]
: [`nxViteTsPaths()`, `nxCopyAssetsPlugin(['*.md'])`];
if (!isUsingTsPlugin) {
imports.push(
`import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'`,
`import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'`
);
plugins.push(`nxViteTsPaths()`, `nxCopyAssetsPlugin(['*.md'])`);
}
if (!onlyVitest && options.includeLib) {
plugins.push(
@ -507,11 +524,9 @@ export function createOrEditViteConfig(
return;
}
viteConfigContent = `/// <reference types='vitest' />
const viteConfigContent = `/// <reference types='vitest' />
import { defineConfig } from 'vite';
${imports.join(';\n')}${imports.length ? ';' : ''}
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig({
root: __dirname,