feat(misc): set a development conditional export for buildable libraries when using the ts solution setup (#30451)

Update library generators to set a `development` conditional export for
buildable libraries' `package.json` files and set the
`customConditions` compiler options in `tsconfig.base.json`. This will
only be done for workspaces using the TS solution setup.

## Current Behavior

## Expected Behavior

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-03-21 22:00:25 +01:00 committed by GitHub
parent 533e9ffc25
commit 176c792e34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 867 additions and 39 deletions

View File

@ -167,6 +167,9 @@ function updatePackageJson(
esbuildOptions, esbuildOptions,
} = mergedTarget.options; } = mergedTarget.options;
// the file must exist in the TS solution setup
const tsconfigBase = readJson(tree, 'tsconfig.base.json');
// can't use the declarationRootDir as rootDir because it only affects the typings, // can't use the declarationRootDir as rootDir because it only affects the typings,
// not the runtime entry point // not the runtime entry point
packageJson = getUpdatedPackageJsonContent(packageJson, { packageJson = getUpdatedPackageJsonContent(packageJson, {
@ -186,6 +189,10 @@ function updatePackageJson(
outputFileExtensionForEsm: getOutExtension('esm', { outputFileExtensionForEsm: getOutExtension('esm', {
userDefinedBuildOptions: esbuildOptions, userDefinedBuildOptions: esbuildOptions,
}), }),
skipDevelopmentExports:
!tsconfigBase.compilerOptions?.customConditions?.includes(
'development'
),
}); });
if (declarationRootDir !== dirname(main)) { if (declarationRootDir !== dirname(main)) {

View File

@ -467,6 +467,7 @@ describe('lib', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(appTree, 'tsconfig.json', { writeJson(appTree, 'tsconfig.json', {
@ -630,13 +631,14 @@ describe('lib', () => {
{ {
"exports": { "exports": {
".": { ".": {
"default": "./src/index.ts", "default": "./dist/index.cjs.js",
"import": "./src/index.ts", "development": "./src/index.ts",
"import": "./dist/index.esm.js",
"types": "./dist/index.esm.d.ts", "types": "./dist/index.esm.d.ts",
}, },
"./package.json": "./package.json", "./package.json": "./package.json",
}, },
"main": "./src/index.ts", "main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js", "module": "./dist/index.esm.js",
"name": "@proj/my-lib", "name": "@proj/my-lib",
"peerDependencies": { "peerDependencies": {
@ -649,6 +651,25 @@ describe('lib', () => {
`); `);
}); });
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(appTree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await expoLibraryGenerator(appTree, {
...defaultSchema,
buildable: true,
strict: false,
useProjectJson: false,
skipFormat: true,
});
expect(
readJson(appTree, 'my-lib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => { it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await expoLibraryGenerator(appTree, { await expoLibraryGenerator(appTree, {
...defaultSchema, ...defaultSchema,

View File

@ -295,6 +295,15 @@ function createFiles(host: Tree, options: NormalizedSchema) {
function determineEntryFields( function determineEntryFields(
options: NormalizedSchema options: NormalizedSchema
): Pick<PackageJson, 'main' | 'types' | 'exports'> { ): Pick<PackageJson, 'main' | 'types' | 'exports'> {
if (
options.buildable ||
options.publishable ||
!options.isUsingTsSolutionConfig
) {
// For buildable libraries, the entries are configured by the bundler (i.e. Rollup).
return undefined;
}
return { return {
main: options.js ? './src/index.js' : './src/index.ts', main: options.js ? './src/index.js' : './src/index.ts',
types: options.js ? './src/index.js' : './src/index.ts', types: options.js ? './src/index.js' : './src/index.ts',

View File

@ -17,6 +17,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "es2022" "target": "es2022",
"customConditions": ["development"]
} }
} }

View File

@ -208,7 +208,8 @@ describe('js init generator', () => {
"noUnusedLocals": true, "noUnusedLocals": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "es2022" "target": "es2022",
"customConditions": ["development"]
} }
} }
" "

View File

@ -1817,6 +1817,7 @@ describe('lib', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -1954,6 +1955,7 @@ describe('lib', () => {
"exports": { "exports": {
".": { ".": {
"default": "./dist/index.js", "default": "./dist/index.js",
"development": "./src/index.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
}, },
@ -1987,6 +1989,7 @@ describe('lib', () => {
"exports": { "exports": {
".": { ".": {
"default": "./dist/index.js", "default": "./dist/index.js",
"development": "./src/index.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
}, },
@ -2428,5 +2431,26 @@ describe('lib', () => {
expect(readJson(tree, 'my-lib/project.json').name).toBe('my-lib'); expect(readJson(tree, 'my-lib/project.json').name).toBe('my-lib');
expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined(); expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined();
}); });
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
...defaultOptions,
directory: 'my-lib',
name: 'my-lib',
useProjectJson: true,
bundler: 'tsc',
addPlugin: true,
skipFormat: true,
});
expect(
readJson(tree, 'my-lib/package.json').exports['.']
).not.toHaveProperty('development');
});
}); });
}); });

View File

@ -10,6 +10,7 @@ import {
names, names,
offsetFromRoot, offsetFromRoot,
ProjectConfiguration, ProjectConfiguration,
readJson,
readNxJson, readNxJson,
readProjectConfiguration, readProjectConfiguration,
runTasksInSerial, runTasksInSerial,
@ -631,6 +632,9 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) {
options.isUsingTsSolutionConfig && options.isUsingTsSolutionConfig &&
!['none', 'rollup', 'vite'].includes(options.bundler) !['none', 'rollup', 'vite'].includes(options.bundler)
) { ) {
// the file must exist in the TS solution setup
const tsconfigBase = readJson(tree, 'tsconfig.base.json');
return getUpdatedPackageJsonContent(updatedPackageJson, { return getUpdatedPackageJsonContent(updatedPackageJson, {
main: join(options.projectRoot, 'src/index.ts'), main: join(options.projectRoot, 'src/index.ts'),
outputPath: joinPathFragments(options.projectRoot, 'dist'), outputPath: joinPathFragments(options.projectRoot, 'dist'),
@ -639,6 +643,10 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) {
generateExportsField: true, generateExportsField: true,
packageJsonPath, packageJsonPath,
format: ['esm'], format: ['esm'],
skipDevelopmentExports:
!tsconfigBase.compilerOptions?.customConditions?.includes(
'development'
),
}); });
} }
@ -664,6 +672,8 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) {
options.isUsingTsSolutionConfig && options.isUsingTsSolutionConfig &&
!['none', 'rollup', 'vite'].includes(options.bundler) !['none', 'rollup', 'vite'].includes(options.bundler)
) { ) {
const tsconfigBase = readJson(tree, 'tsconfig.base.json');
packageJson = getUpdatedPackageJsonContent(packageJson, { packageJson = getUpdatedPackageJsonContent(packageJson, {
main: join(options.projectRoot, 'src/index.ts'), main: join(options.projectRoot, 'src/index.ts'),
outputPath: joinPathFragments(options.projectRoot, 'dist'), outputPath: joinPathFragments(options.projectRoot, 'dist'),
@ -672,6 +682,10 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) {
generateExportsField: true, generateExportsField: true,
packageJsonPath, packageJsonPath,
format: ['esm'], format: ['esm'],
skipDevelopmentExports:
!tsconfigBase.compilerOptions?.customConditions?.includes(
'development'
),
}); });
} }

View File

@ -325,6 +325,11 @@ function updatePackageJson(
version: '0.0.1', version: '0.0.1',
}; };
} }
// the file must exist in the TS solution setup, which is the only case this
// function is called
const tsconfigBase = readJson(tree, 'tsconfig.base.json');
packageJson = getUpdatedPackageJsonContent(packageJson, { packageJson = getUpdatedPackageJsonContent(packageJson, {
main, main,
outputPath, outputPath,
@ -333,6 +338,8 @@ function updatePackageJson(
packageJsonPath, packageJsonPath,
rootDir, rootDir,
format, format,
skipDevelopmentExports:
!tsconfigBase.compilerOptions?.customConditions?.includes('development'),
}); });
writeJson(tree, packageJsonPath, packageJson); writeJson(tree, packageJsonPath, packageJson);
} }

View File

@ -2206,6 +2206,111 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`); `);
}); });
it('should not create build target when the entry points point to source files', async () => {
// Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/package.json': JSON.stringify({
exports: {
'.': {
default: './src/index.ts',
},
},
}),
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
// Reduce noise in build snapshots by disabling default typecheck target
typecheck: false,
build: true,
})
).toMatchInlineSnapshot(`
{
"projects": {
"libs/my-lib": {
"projectType": "library",
"targets": {},
},
},
}
`);
});
it('should create build target when the entry points point to dist files', async () => {
// Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/package.json': JSON.stringify({
exports: {
'.': {
// should ignore the fact that the development condition points to source
development: './src/index.ts',
types: './dist/index.d.ts',
default: './dist/index.js',
},
},
}),
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
// Reduce noise in build snapshots by disabling default typecheck target
typecheck: false,
build: true,
})
).toMatchInlineSnapshot(`
{
"projects": {
"libs/my-lib": {
"projectType": "library",
"targets": {
"build": {
"cache": true,
"command": "tsc --build tsconfig.lib.json",
"dependsOn": [
"^build",
],
"inputs": [
"production",
"^production",
{
"externalDependencies": [
"typescript",
],
},
],
"metadata": {
"description": "Builds the project with \`tsc\`.",
"help": {
"command": "npx tsc --build --help",
"example": {
"args": [
"--force",
],
},
},
"technologies": [
"typescript",
],
},
"options": {
"cwd": "libs/my-lib",
},
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});
it('should create a node with a build target when enabled, for a project level tsconfig.lib.json build file by default', async () => { it('should create a node with a build target when enabled, for a project level tsconfig.lib.json build file by default', async () => {
// Sibling package.json // Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, { await applyFilesToTempFsAndContext(tempFs, context, {

View File

@ -95,8 +95,8 @@ export function isValidPackageJsonBuildConfig(
return isPathSourceFile(value); return isPathSourceFile(value);
} else if (typeof value === 'object') { } else if (typeof value === 'object') {
return Object.entries(value).some(([currentKey, subValue]) => { return Object.entries(value).some(([currentKey, subValue]) => {
// Skip types field // Skip types and development conditions
if (currentKey === 'types') { if (currentKey === 'types' || currentKey === 'development') {
return false; return false;
} }
if (typeof subValue === 'string') { if (typeof subValue === 'string') {

View File

@ -157,6 +157,7 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1', version: '0.0.1',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
default: './src/index.js', default: './src/index.js',
import: './src/index.js', import: './src/index.js',
types: './src/index.d.ts', types: './src/index.d.ts',
@ -190,6 +191,7 @@ describe('getUpdatedPackageJsonContent', () => {
type: 'commonjs', type: 'commonjs',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
default: './src/index.cjs', default: './src/index.cjs',
types: './src/index.d.ts', types: './src/index.d.ts',
}, },
@ -228,15 +230,28 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1', version: '0.0.1',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
default: './src/index.js', default: './src/index.js',
types: './src/index.d.ts', types: './src/index.d.ts',
}, },
'./foo': './src/foo.js', './foo': {
'./bar': './src/bar.js', development: './src/foo.ts',
default: './src/foo.js',
},
'./bar': {
development: './src/bar.ts',
default: './src/bar.js',
},
'./package.json': './package.json', './package.json': './package.json',
'./migrations.json': './migrations.json', './migrations.json': './migrations.json',
'./feature': './feature/index.js', './feature': {
'./feature/index': './feature/index.js', development: './feature/index.ts',
default: './feature/index.js',
},
'./feature/index': {
development: './feature/index.ts',
default: './feature/index.js',
},
}, },
}); });
@ -269,15 +284,32 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1', version: '0.0.1',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
default: './src/index.js', default: './src/index.js',
import: './src/index.js', import: './src/index.js',
types: './src/index.d.ts', types: './src/index.d.ts',
}, },
'./foo': './src/foo.js', './foo': {
'./bar': './src/bar.js', development: './src/foo.ts',
import: './src/foo.js',
default: './src/foo.js',
},
'./bar': {
development: './src/bar.ts',
import: './src/bar.js',
default: './src/bar.js',
},
'./package.json': './package.json', './package.json': './package.json',
'./feature': './feature/index.js', './feature': {
'./feature/index': './feature/index.js', development: './feature/index.ts',
import: './feature/index.js',
default: './feature/index.js',
},
'./feature/index': {
development: './feature/index.ts',
import: './feature/index.js',
default: './feature/index.js',
},
}, },
}); });
@ -310,23 +342,28 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1', version: '0.0.1',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
import: './src/index.js', import: './src/index.js',
default: './src/index.cjs', default: './src/index.cjs',
types: './src/index.d.ts', types: './src/index.d.ts',
}, },
'./foo': { './foo': {
development: './src/foo.ts',
import: './src/foo.js', import: './src/foo.js',
default: './src/foo.cjs', default: './src/foo.cjs',
}, },
'./bar': { './bar': {
development: './src/bar.ts',
import: './src/bar.js', import: './src/bar.js',
default: './src/bar.cjs', default: './src/bar.cjs',
}, },
'./feature': { './feature': {
development: './feature/index.ts',
import: './feature/index.js', import: './feature/index.js',
default: './feature/index.cjs', default: './feature/index.cjs',
}, },
'./feature/index': { './feature/index': {
development: './feature/index.ts',
import: './feature/index.js', import: './feature/index.js',
default: './feature/index.cjs', default: './feature/index.cjs',
}, },
@ -364,6 +401,7 @@ describe('getUpdatedPackageJsonContent', () => {
version: '0.0.1', version: '0.0.1',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
import: './src/index.js', import: './src/index.js',
default: './src/index.cjs', default: './src/index.cjs',
types: './src/index.d.ts', types: './src/index.d.ts',
@ -400,6 +438,7 @@ describe('getUpdatedPackageJsonContent', () => {
type: 'module', type: 'module',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
default: './src/index.cjs', default: './src/index.cjs',
types: './src/index.d.ts', types: './src/index.d.ts',
}, },
@ -432,6 +471,7 @@ describe('getUpdatedPackageJsonContent', () => {
type: 'commonjs', type: 'commonjs',
exports: { exports: {
'.': { '.': {
development: './src/index.ts',
default: './src/index.js', default: './src/index.js',
types: './src/index.d.ts', types: './src/index.d.ts',
}, },

View File

@ -48,6 +48,7 @@ export interface UpdatePackageJsonOption {
buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies'; buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies';
generateLockfile?: boolean; generateLockfile?: boolean;
packageJsonPath?: string; packageJsonPath?: string;
skipDevelopmentExports?: boolean;
} }
export function updatePackageJson( export function updatePackageJson(
@ -113,7 +114,10 @@ export function updatePackageJson(
} }
// update package specific settings // update package specific settings
packageJson = getUpdatedPackageJsonContent(packageJson, options); packageJson = getUpdatedPackageJsonContent(packageJson, {
skipDevelopmentExports: true,
...options,
});
// save files // save files
writeJsonFile(`${options.outputPath}/package.json`, packageJson); writeJsonFile(`${options.outputPath}/package.json`, packageJson);
@ -285,6 +289,21 @@ export function getUpdatedPackageJsonContent(
packageJson.exports ??= packageJson.exports ??=
typeof packageJson.exports === 'string' ? {} : { ...packageJson.exports }; typeof packageJson.exports === 'string' ? {} : { ...packageJson.exports };
packageJson.exports['./package.json'] ??= './package.json'; packageJson.exports['./package.json'] ??= './package.json';
if (!options.skipDevelopmentExports && (hasCjsFormat || hasEsmFormat)) {
packageJson.exports['.'] ??= {};
const developmentExports = getDevelopmentExports(options);
for (const [exportEntry, filePath] of Object.entries(
developmentExports
)) {
if (!packageJson.exports[exportEntry]) {
packageJson.exports[exportEntry] ??= {};
packageJson.exports[exportEntry]['development'] ??= filePath;
} else if (typeof packageJson.exports[exportEntry] === 'object') {
packageJson.exports[exportEntry].development ??= filePath;
}
}
}
} }
if (!options.skipTypings) { if (!options.skipTypings) {
@ -364,6 +383,47 @@ export function getUpdatedPackageJsonContent(
return packageJson; return packageJson;
} }
function getDevelopmentExports(
options: Pick<
UpdatePackageJsonOption,
'additionalEntryPoints' | 'main' | 'projectRoot'
>
) {
const mainRelativeDir = getRelativeDirectoryToProjectRoot(
options.main,
options.projectRoot
);
const exports: Exports = {
'.': mainRelativeDir + basename(options.main),
};
if (options.additionalEntryPoints?.length) {
const jsRegex = /\.[jt]sx?$/;
for (const file of options.additionalEntryPoints) {
const { ext: fileExt, name: fileName, base: baseName } = parse(file);
if (!jsRegex.test(fileExt)) {
continue;
}
const relativeDir = getRelativeDirectoryToProjectRoot(
file,
options.projectRoot
);
const sourceFilePath = relativeDir + baseName;
const entryRelativeDir = relativeDir.replace(/^\.\/src\//, './');
const entryFilePath = entryRelativeDir + fileName;
if (fileName === 'index') {
const barrelEntry = entryRelativeDir.replace(/\/$/, '');
exports[barrelEntry] = sourceFilePath;
}
exports[entryFilePath] = sourceFilePath;
}
}
return exports;
}
export function getOutputDir( export function getOutputDir(
options: Pick< options: Pick<
UpdatePackageJsonOption, UpdatePackageJsonOption,

View File

@ -1,6 +1,11 @@
import type { Tree } from '@nx/devkit'; import type { Tree } from '@nx/devkit';
import * as devkit from '@nx/devkit'; import * as devkit from '@nx/devkit';
import { readJson, readProjectConfiguration, writeJson } from '@nx/devkit'; import {
readJson,
readProjectConfiguration,
updateJson,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { libraryGenerator } from './library'; import { libraryGenerator } from './library';
@ -357,6 +362,7 @@ describe('lib', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -498,6 +504,90 @@ describe('lib', () => {
`); `);
}); });
it('should create a correct package.json for buildable libraries', async () => {
await libraryGenerator(tree, {
directory: 'mylib',
unitTestRunner: 'jest',
useProjectJson: false,
buildable: true,
skipFormat: true,
});
expect(tree.read('mylib/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "@proj/mylib",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"nx": {
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": [
"{projectRoot}/test-output/jest/coverage"
],
"options": {
"jestConfig": "mylib/jest.config.ts"
}
},
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/mylib",
"tsConfig": "mylib/tsconfig.lib.json",
"packageJson": "mylib/package.json",
"main": "mylib/src/index.ts",
"assets": [
"mylib/*.md"
]
}
}
}
},
"dependencies": {
"tslib": "^2.3.0"
}
}
"
`);
});
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
directory: 'mylib',
unitTestRunner: 'jest',
useProjectJson: false,
buildable: true,
skipFormat: true,
});
expect(
readJson(tree, 'mylib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => { it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await libraryGenerator(tree, { await libraryGenerator(tree, {
directory: 'mylib', directory: 'mylib',

View File

@ -133,6 +133,7 @@ describe('next library', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -264,6 +265,109 @@ describe('next library', () => {
`); `);
}); });
it('should create a correct package.json for buildable libraries', async () => {
await libraryGenerator(tree, {
directory: 'mylib',
linter: Linter.EsLint,
skipFormat: true,
skipTsConfig: false,
unitTestRunner: 'jest',
style: 'css',
component: false,
useProjectJson: false,
buildable: true,
});
expect(tree.read('mylib/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "@proj/mylib",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.esm.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.esm.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.esm.d.ts",
"import": "./dist/index.esm.js",
"default": "./dist/index.esm.js"
}
},
"nx": {
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"build": {
"executor": "@nx/rollup:rollup",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/mylib",
"tsConfig": "mylib/tsconfig.lib.json",
"project": "mylib/package.json",
"entryFile": "mylib/src/index.ts",
"external": [
"react",
"react-dom",
"react/jsx-runtime"
],
"rollupConfig": "@nx/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": [
{
"glob": "mylib/README.md",
"input": ".",
"output": "."
}
]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": [
"{projectRoot}/test-output/jest/coverage"
],
"options": {
"jestConfig": "mylib/jest.config.ts"
}
}
},
"sourceRoot": "mylib/src",
"projectType": "library",
"tags": []
}
}
"
`);
});
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
directory: 'mylib',
linter: Linter.EsLint,
skipFormat: true,
skipTsConfig: false,
unitTestRunner: 'jest',
style: 'css',
component: false,
useProjectJson: false,
buildable: true,
});
expect(
readJson(tree, 'mylib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => { it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await libraryGenerator(tree, { await libraryGenerator(tree, {
directory: 'mylib', directory: 'mylib',

View File

@ -531,6 +531,7 @@ describe('lib', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -654,6 +655,61 @@ describe('lib', () => {
`); `);
}); });
it('should create a correct package.json for buildable libraries', async () => {
await libraryGenerator(tree, {
directory: 'mylib',
unitTestRunner: 'jest',
addPlugin: true,
useProjectJson: false,
buildable: true,
skipFormat: true,
} as Schema);
expect(tree.read('mylib/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "@proj/mylib",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {
"tslib": "^2.3.0"
}
}
"
`);
});
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
directory: 'mylib',
unitTestRunner: 'jest',
addPlugin: true,
useProjectJson: false,
buildable: true,
skipFormat: true,
} as Schema);
expect(
readJson(tree, 'mylib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set correct options for swc', async () => { it('should set correct options for swc', async () => {
await libraryGenerator(tree, { await libraryGenerator(tree, {
directory: 'mylib', directory: 'mylib',
@ -672,6 +728,7 @@ describe('lib', () => {
"exports": { "exports": {
".": { ".": {
"default": "./dist/index.js", "default": "./dist/index.js",
"development": "./src/index.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
}, },

View File

@ -339,6 +339,7 @@ describe('NxPlugin Plugin Generator', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -447,6 +448,7 @@ describe('NxPlugin Plugin Generator', () => {
"exports": { "exports": {
".": { ".": {
"default": "./dist/index.js", "default": "./dist/index.js",
"development": "./src/index.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
}, },
@ -532,6 +534,25 @@ describe('NxPlugin Plugin Generator', () => {
`); `);
}); });
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await pluginGenerator(
tree,
getSchema({
e2eTestRunner: 'jest',
skipFormat: true,
})
);
expect(
readJson(tree, 'my-plugin/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set "nx.name" in package.json when the user provides a name that is different than the package name and "useProjectJson" is "false"', async () => { it('should set "nx.name" in package.json when the user provides a name that is different than the package name and "useProjectJson" is "false"', async () => {
await pluginGenerator(tree, { await pluginGenerator(tree, {
directory: 'my-plugin', directory: 'my-plugin',

View File

@ -459,6 +459,7 @@ describe('lib', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(appTree, 'tsconfig.json', { writeJson(appTree, 'tsconfig.json', {
@ -599,6 +600,58 @@ describe('lib', () => {
`); `);
}); });
it('should create a correct package.json for buildable libraries', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
useProjectJson: false,
buildable: true,
skipFormat: true,
});
expect(appTree.read('my-lib/package.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"name": "@proj/my-lib",
"version": "0.0.1",
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.esm.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.esm.d.ts",
"import": "./dist/index.esm.js",
"default": "./dist/index.cjs.js"
}
},
"peerDependencies": {
"react": "~18.3.1",
"react-native": "~0.76.3"
}
}
"
`);
});
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(appTree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(appTree, {
...defaultSchema,
useProjectJson: false,
buildable: true,
skipFormat: true,
});
expect(
readJson(appTree, 'my-lib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => { it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await libraryGenerator(appTree, { await libraryGenerator(appTree, {
...defaultSchema, ...defaultSchema,

View File

@ -78,15 +78,15 @@ export async function reactNativeLibraryGeneratorInternal(
createFiles(host, options); createFiles(host, options);
if (options.isUsingTsSolutionConfig) {
await addProjectToTsSolutionWorkspace(host, options.projectRoot);
}
const addProjectTask = await addProject(host, options); const addProjectTask = await addProject(host, options);
if (addProjectTask) { if (addProjectTask) {
tasks.push(addProjectTask); tasks.push(addProjectTask);
} }
if (options.isUsingTsSolutionConfig) {
await addProjectToTsSolutionWorkspace(host, options.projectRoot);
}
const lintTask = await addLinting(host, { const lintTask = await addLinting(host, {
...options, ...options,
projectName: options.name, projectName: options.name,
@ -296,6 +296,15 @@ function createFiles(host: Tree, options: NormalizedSchema) {
function determineEntryFields( function determineEntryFields(
options: NormalizedSchema options: NormalizedSchema
): Pick<PackageJson, 'main' | 'types' | 'exports'> { ): Pick<PackageJson, 'main' | 'types' | 'exports'> {
if (
options.buildable ||
options.publishable ||
!options.isUsingTsSolutionConfig
) {
// For buildable libraries, the entries are configured by the bundler (i.e. Rollup).
return undefined;
}
return { return {
main: options.js ? './src/index.js' : './src/index.ts', main: options.js ? './src/index.js' : './src/index.ts',
types: options.js ? './src/index.js' : './src/index.ts', types: options.js ? './src/index.js' : './src/index.ts',

View File

@ -939,6 +939,7 @@ module.exports = withNx(
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -1246,6 +1247,7 @@ module.exports = withNx(
"exports": { "exports": {
".": { ".": {
"default": "./dist/index.esm.js", "default": "./dist/index.esm.js",
"development": "./src/index.ts",
"import": "./dist/index.esm.js", "import": "./dist/index.esm.js",
"types": "./dist/index.esm.d.ts", "types": "./dist/index.esm.d.ts",
}, },
@ -1265,6 +1267,27 @@ module.exports = withNx(
`); `);
}); });
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
...defaultSchema,
bundler: 'rollup',
directory: 'libs/mylib',
publishable: true,
importPath: '@acme/mylib',
useProjectJson: false,
skipFormat: true,
});
expect(
readJson(tree, 'libs/mylib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should add project to workspaces when using TS solution', async () => { it('should add project to workspaces when using TS solution', async () => {
tree.write('pnpm-workspace.yaml', `packages:`); tree.write('pnpm-workspace.yaml', `packages:`);

View File

@ -149,6 +149,7 @@ describe('Remix Library Generator', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -196,6 +197,59 @@ describe('Remix Library Generator', () => {
`); `);
}); });
it('should create a correct package.json for buildable libraries', async () => {
await libraryGenerator(tree, {
directory: 'packages/foo',
style: 'css',
addPlugin: true,
useProjectJson: false,
buildable: true,
skipFormat: true,
});
expect(tree.read('packages/foo/package.json', 'utf-8'))
.toMatchInlineSnapshot(`
"{
"name": "@proj/foo",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.esm.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.esm.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.esm.d.ts",
"import": "./dist/index.esm.js",
"default": "./dist/index.esm.js"
}
}
}
"
`);
});
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
directory: 'packages/foo',
style: 'css',
addPlugin: true,
useProjectJson: false,
buildable: true,
skipFormat: true,
});
expect(
readJson(tree, 'packages/foo/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should generate server entrypoint', async () => { it('should generate server entrypoint', async () => {
await libraryGenerator(tree, { await libraryGenerator(tree, {
directory: 'test', directory: 'test',

View File

@ -181,6 +181,9 @@ function updatePackageJson(
({ main, outputPath } = mergedTarget.options); ({ main, outputPath } = mergedTarget.options);
} }
// the file must exist in the TS solution setup
const tsconfigBase = readJson(tree, 'tsconfig.base.json');
packageJson = getUpdatedPackageJsonContent(packageJson, { packageJson = getUpdatedPackageJsonContent(packageJson, {
main, main,
outputPath, outputPath,
@ -191,6 +194,10 @@ function updatePackageJson(
format: options.format ?? ['esm'], format: options.format ?? ['esm'],
outputFileExtensionForCjs: '.cjs.js', outputFileExtensionForCjs: '.cjs.js',
outputFileExtensionForEsm: '.esm.js', outputFileExtensionForEsm: '.esm.js',
skipDevelopmentExports:
!tsconfigBase.compilerOptions?.customConditions?.includes(
'development'
),
}); });
// rollup has a specific declaration file generation not handled by the util above, // rollup has a specific declaration file generation not handled by the util above,

View File

@ -331,7 +331,7 @@ describe('@nx/vite:configuration', () => {
}); });
describe('TS solution setup', () => { describe('TS solution setup', () => {
beforeAll(async () => { beforeEach(async () => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
updateJson(tree, '/package.json', (json) => { updateJson(tree, '/package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*']; json.workspaces = ['packages/*', 'apps/*'];
@ -341,6 +341,7 @@ describe('@nx/vite:configuration', () => {
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -371,6 +372,7 @@ describe('@nx/vite:configuration', () => {
"exports": { "exports": {
".": { ".": {
"default": "./dist/index.js", "default": "./dist/index.js",
"development": "./src/index.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
}, },
@ -386,6 +388,31 @@ describe('@nx/vite:configuration', () => {
`); `);
}); });
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
addProjectConfiguration(tree, 'my-lib', {
root: 'packages/my-lib',
});
writeJson(tree, 'packages/my-lib/tsconfig.lib.json', {});
writeJson(tree, 'packages/my-lib/tsconfig.json', {});
await viteConfigurationGenerator(tree, {
addPlugin: true,
uiFramework: 'none',
project: 'my-lib',
projectType: 'library',
newProject: true,
skipFormat: true,
});
expect(
readJson(tree, 'packages/my-lib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should create package.json without exports field for apps', async () => { it('should create package.json without exports field for apps', async () => {
addProjectConfiguration(tree, 'my-app', { addProjectConfiguration(tree, 'my-app', {
root: 'apps/my-app', root: 'apps/my-app',

View File

@ -219,6 +219,10 @@ function updatePackageJson(
const rootDir = join(project.root, 'src'); const rootDir = join(project.root, 'src');
const outputPath = joinPathFragments(project.root, 'dist'); const outputPath = joinPathFragments(project.root, 'dist');
// the file must exist in the TS solution setup, which is the only case this
// function is called
const tsconfigBase = readJson(tree, 'tsconfig.base.json');
packageJson = getUpdatedPackageJsonContent(packageJson, { packageJson = getUpdatedPackageJsonContent(packageJson, {
main, main,
outputPath, outputPath,
@ -227,6 +231,10 @@ function updatePackageJson(
generateExportsField: true, generateExportsField: true,
packageJsonPath, packageJsonPath,
format: ['esm'], format: ['esm'],
skipDevelopmentExports:
!tsconfigBase.compilerOptions?.customConditions?.includes(
'development'
),
}); });
} }

View File

@ -4,25 +4,23 @@ import type { NormalizedSchema } from '../schema';
export function determineEntryFields( export function determineEntryFields(
options: NormalizedSchema options: NormalizedSchema
): Pick<PackageJson, 'module' | 'types' | 'exports'> { ): Pick<PackageJson, 'module' | 'types' | 'exports'> {
if (options.bundler === 'none') { if (options.bundler !== 'none' || !options.isUsingTsSolutionConfig) {
return { // for buildable libraries, the entries are configured by the bundler
module: options.js ? './src/index.js' : './src/index.ts', return undefined;
types: options.js ? './src/index.js' : './src/index.ts',
exports: {
'.': options.js
? './src/index.js'
: {
types: './src/index.ts',
import: './src/index.ts',
default: './src/index.ts',
},
'./package.json': './package.json',
},
};
} }
return { return {
module: './dist/index.mjs', module: options.js ? './src/index.js' : './src/index.ts',
types: './dist/index.d.ts', types: options.js ? './src/index.js' : './src/index.ts',
exports: {
'.': options.js
? './src/index.js'
: {
types: './src/index.ts',
import: './src/index.ts',
default: './src/index.ts',
},
'./package.json': './package.json',
},
}; };
} }

View File

@ -521,6 +521,7 @@ module.exports = [
compilerOptions: { compilerOptions: {
composite: true, composite: true,
declaration: true, declaration: true,
customConditions: ['development'],
}, },
}); });
writeJson(tree, 'tsconfig.json', { writeJson(tree, 'tsconfig.json', {
@ -585,6 +586,39 @@ module.exports = [
"nx", "nx",
] ]
`); `);
expect(tree.read('my-lib/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "@proj/my-lib",
"version": "0.0.1",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts",
"default": "./src/index.ts"
},
"./package.json": "./package.json"
},
"nx": {
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/vite:test",
"outputs": [
"{options.reportsDirectory}"
],
"options": {
"reportsDirectory": "../coverage/my-lib"
}
}
}
}
}
"
`);
expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchInlineSnapshot(`
{ {
"extends": "../tsconfig.base.json", "extends": "../tsconfig.base.json",
@ -690,6 +724,60 @@ module.exports = [
`); `);
}); });
it('should create a correct package.json for buildable libraries', async () => {
await libraryGenerator(tree, {
...defaultSchema,
setParserOptionsProject: true,
linter: 'eslint',
addPlugin: true,
useProjectJson: false,
bundler: 'vite',
skipFormat: true,
});
expect(tree.read('my-lib/package.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"name": "@proj/my-lib",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
"
`);
});
it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
delete json.compilerOptions.customConditions;
return json;
});
await libraryGenerator(tree, {
...defaultSchema,
setParserOptionsProject: true,
linter: 'eslint',
addPlugin: true,
useProjectJson: false,
bundler: 'vite',
skipFormat: true,
});
expect(
readJson(tree, 'my-lib/package.json').exports['.']
).not.toHaveProperty('development');
});
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => { it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await libraryGenerator(tree, { await libraryGenerator(tree, {
...defaultSchema, ...defaultSchema,