feat(js): add generateExportsField and additionalEntryPoints options to update exports field when building with tsc/swc (#18319)

This commit is contained in:
Jack Hsu 2023-08-01 11:23:48 -04:00 committed by GitHub
parent 90e4e7e7de
commit d63d3573c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 557 additions and 174 deletions

View File

@ -14,18 +14,34 @@
"type": "string",
"description": "The name of the main entry-point file.",
"x-completion-type": "file",
"x-completion-glob": "main@(.js|.ts|.tsx)"
"x-completion-glob": "main@(.js|.ts|.tsx)",
"x-priority": "important"
},
"generateExportsField": {
"type": "boolean",
"alias": "exports",
"description": "Update the output package.json file's 'exports' field. This field is used by Node and bundles.",
"x-priority": "important",
"default": false
},
"additionalEntryPoints": {
"type": "array",
"description": "Additional entry-points to add to exports field in the package.json file.",
"items": { "type": "string" },
"x-priority": "important"
},
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
"x-completion-type": "directory",
"x-priority": "important"
},
"tsConfig": {
"type": "string",
"description": "The path to the Typescript configuration file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
"x-completion-glob": "tsconfig.*.json",
"x-priority": "important"
},
"swcrc": {
"type": "string",

View File

@ -14,7 +14,21 @@
"type": "string",
"description": "The name of the main entry-point file.",
"x-completion-type": "file",
"x-completion-glob": "main@(.js|.ts|.jsx|.tsx)"
"x-completion-glob": "main@(.js|.ts|.jsx|.tsx)",
"x-priority": "important"
},
"generateExportsField": {
"type": "boolean",
"alias": "exports",
"description": "Update the output package.json file's 'exports' field. This field is used by Node and bundles.",
"default": false,
"x-priority": "important"
},
"additionalEntryPoints": {
"type": "array",
"description": "Additional entry-points to add to exports field in the package.json file.",
"items": { "type": "string" },
"x-priority": "important"
},
"rootDir": {
"type": "string",
@ -23,13 +37,15 @@
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
"x-completion-type": "directory",
"x-priority": "important"
},
"tsConfig": {
"type": "string",
"description": "The path to the Typescript configuration file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
"x-completion-glob": "tsconfig.*.json",
"x-priority": "important"
},
"assets": {
"type": "array",

View File

@ -1,5 +1,6 @@
import {
checkFilesExist,
updateJson,
updateProjectConfig,
cleanupProject,
newProject,
runCLI,
@ -8,6 +9,8 @@ import {
createFile,
uniq,
getPackageManagerCommand,
readJson,
updateFile,
} from '@nx/e2e/utils';
import { join } from 'path';
import { ensureDirSync } from 'fs-extra';
@ -123,17 +126,74 @@ describe('bundling libs', () => {
it('should support tsc and swc for building libs', () => {
const tscLib = uniq('tsclib');
const swcLib = uniq('swclib');
const tscEsmLib = uniq('tscesmlib');
const swcEsmLib = uniq('swcesmlib');
runCLI(`generate @nx/js:lib ${tscLib} --bundler=tsc --no-interactive`);
runCLI(`generate @nx/js:lib ${swcLib} --bundler=swc --no-interactive`);
runCLI(`generate @nx/js:lib ${tscEsmLib} --bundler=tsc --no-interactive`);
runCLI(`generate @nx/js:lib ${swcEsmLib} --bundler=swc --no-interactive`);
runCLI(`build ${tscLib}`);
runCLI(`build ${swcLib}`);
// Change module format to ESM
updateJson(`libs/${tscEsmLib}/tsconfig.json`, (json) => {
json.compilerOptions.module = 'esnext';
return json;
});
updateJson(`libs/${swcEsmLib}/.swcrc`, (json) => {
json.module.type = 'es6';
return json;
});
// Node ESM requires file extensions in imports so must add them before building
updateFile(
`libs/${tscEsmLib}/src/index.ts`,
`export * from './lib/${tscEsmLib}.js';`
);
updateFile(
`libs/${swcEsmLib}/src/index.ts`,
`export * from './lib/${swcEsmLib}.js';`
);
// Add additional entry points for `exports` field
updateProjectConfig(tscLib, (json) => {
json.targets.build.options.additionalEntryPoints = [
`libs/${tscLib}/src/foo/*.ts`,
];
return json;
});
updateFile(`libs/${tscLib}/src/foo/bar.ts`, `export const bar = 'bar';`);
updateFile(`libs/${tscLib}/src/foo/faz.ts`, `export const faz = 'faz';`);
updateProjectConfig(swcLib, (json) => {
json.targets.build.options.additionalEntryPoints = [
`libs/${swcLib}/src/foo/*.ts`,
];
return json;
});
updateFile(`libs/${swcLib}/src/foo/bar.ts`, `export const bar = 'bar';`);
updateFile(`libs/${swcLib}/src/foo/faz.ts`, `export const faz = 'faz';`);
runCLI(`build ${tscLib} --generateExportsField`);
runCLI(`build ${swcLib} --generateExportsField`);
runCLI(`build ${tscEsmLib} --generateExportsField`);
runCLI(`build ${swcEsmLib} --generateExportsField`);
expect(readJson(`dist/libs/${tscLib}/package.json`).exports).toEqual({
'./package.json': './package.json',
'.': './src/index.js',
'./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',
'./foo/bar': './src/foo/bar.js',
'./foo/faz': './src/foo/faz.js',
});
const pmc = getPackageManagerCommand();
let output: string;
// Make sure outputs in commonjs project
// Make sure CJS output is correct
createFile(
'test-cjs/package.json',
JSON.stringify(
@ -155,8 +215,13 @@ describe('bundling libs', () => {
`
const { ${tscLib} } = require('@proj/${tscLib}');
const { ${swcLib} } = require('@proj/${swcLib}');
// additional entry-points
const { bar } = require('@proj/${tscLib}/foo/bar');
const { faz } = require('@proj/${swcLib}/foo/faz');
console.log(${tscLib}());
console.log(${swcLib}());
console.log(bar);
console.log(faz);
`
);
runCommand(pmc.install, {
@ -167,5 +232,42 @@ describe('bundling libs', () => {
});
expect(output).toContain(tscLib);
expect(output).toContain(swcLib);
expect(output).toContain('bar');
expect(output).toContain('faz');
// Make sure ESM output is correct
createFile(
'test-esm/package.json',
JSON.stringify(
{
name: 'test-esm',
private: true,
type: 'module',
dependencies: {
[`@proj/${tscEsmLib}`]: `file:../dist/libs/${tscEsmLib}`,
[`@proj/${swcEsmLib}`]: `file:../dist/libs/${swcEsmLib}`,
},
},
null,
2
)
);
createFile(
'test-esm/index.js',
`
import { ${tscEsmLib} } from '@proj/${tscEsmLib}';
import { ${swcEsmLib} } from '@proj/${swcEsmLib}';
console.log(${tscEsmLib}());
console.log(${swcEsmLib}());
`
);
runCommand(pmc.install, {
cwd: join(tmpProjPath(), 'test-esm'),
});
output = runCommand('node index.js', {
cwd: join(tmpProjPath(), 'test-esm'),
});
expect(output).toContain(tscEsmLib);
expect(output).toContain(swcEsmLib);
}, 500_000);
});

View File

@ -20,7 +20,7 @@ import { InspectType, NodeExecutorOptions } from './schema';
import { calculateProjectBuildableDependencies } from '../../utils/buildable-libs-utils';
import { killTree } from './lib/kill-tree';
import { fileExists } from 'nx/src/utils/fileutils';
import { getMainFileDirRelativeToProjectRoot } from '../../utils/get-main-file-dir';
import { getRelativeDirectoryToProjectRoot } from '../../utils/get-main-file-dir';
interface ActiveTask {
id: string;
@ -379,10 +379,7 @@ function getFileToRun(
buildTargetExecutor === '@nx/js:swc'
) {
outputFileName = path.join(
getMainFileDirRelativeToProjectRoot(
buildOptions.main,
project.data.root
),
getRelativeDirectoryToProjectRoot(buildOptions.main, project.data.root),
fileName
);
} else {

View File

@ -11,18 +11,36 @@
"type": "string",
"description": "The name of the main entry-point file.",
"x-completion-type": "file",
"x-completion-glob": "main@(.js|.ts|.tsx)"
"x-completion-glob": "main@(.js|.ts|.tsx)",
"x-priority": "important"
},
"generateExportsField": {
"type": "boolean",
"alias": "exports",
"description": "Update the output package.json file's 'exports' field. This field is used by Node and bundles.",
"x-priority": "important",
"default": false
},
"additionalEntryPoints": {
"type": "array",
"description": "Additional entry-points to add to exports field in the package.json file.",
"items": {
"type": "string"
},
"x-priority": "important"
},
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
"x-completion-type": "directory",
"x-priority": "important"
},
"tsConfig": {
"type": "string",
"description": "The path to the Typescript configuration file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
"x-completion-glob": "tsconfig.*.json",
"x-priority": "important"
},
"swcrc": {
"type": "string",

View File

@ -1,6 +1,7 @@
import { ExecutorContext } from '@nx/devkit';
import { ExecutorContext, readJsonFile } from '@nx/devkit';
import { assetGlobsToFiles, FileInputOutput } from '../../utils/assets/assets';
import { removeSync } from 'fs-extra';
import { sync as globSync } from 'fast-glob';
import { dirname, join, relative, resolve } from 'path';
import { copyAssets } from '../../utils/assets';
import { checkDependencies } from '../../utils/check-dependencies';
@ -135,6 +136,13 @@ export async function* swcExecutor(
);
}
function determineModuleFormatFromSwcrc(
absolutePathToSwcrc: string
): 'cjs' | 'esm' {
const swcrc = readJsonFile(absolutePathToSwcrc);
return swcrc.module?.type?.startsWith('es') ? 'esm' : 'cjs';
}
if (options.watch) {
let disposeFn: () => void;
process.on('SIGINT', () => disposeFn());
@ -145,7 +153,13 @@ export async function* swcExecutor(
const packageJsonResult = await copyPackageJson(
{
...options,
skipTypings: !options.skipTypeCheck,
additionalEntryPoints: createEntryPoints(options, context),
format: [
determineModuleFormatFromSwcrc(options.swcCliOptions.swcrcPath),
],
// As long as d.ts files match their .js counterparts, we don't need to emit them.
// TSC can match them correctly based on file names.
skipTypings: true,
},
context
);
@ -161,8 +175,13 @@ export async function* swcExecutor(
await copyPackageJson(
{
...options,
generateExportsField: true,
skipTypings: !options.skipTypeCheck,
additionalEntryPoints: createEntryPoints(options, context),
format: [
determineModuleFormatFromSwcrc(options.swcCliOptions.swcrcPath),
],
// As long as d.ts files match their .js counterparts, we don't need to emit them.
// TSC can match them correctly based on file names.
skipTypings: true,
extraDependencies: swcHelperDependency ? [swcHelperDependency] : [],
},
context
@ -183,4 +202,14 @@ function removeTmpSwcrc(swcrcPath: string) {
}
}
function createEntryPoints(
options: { additionalEntryPoints?: string[] },
context: ExecutorContext
): string[] {
if (!options.additionalEntryPoints?.length) return [];
return globSync(options.additionalEntryPoints, {
cwd: context.root,
});
}
export default swcExecutor;

View File

@ -10,7 +10,23 @@
"type": "string",
"description": "The name of the main entry-point file.",
"x-completion-type": "file",
"x-completion-glob": "main@(.js|.ts|.jsx|.tsx)"
"x-completion-glob": "main@(.js|.ts|.jsx|.tsx)",
"x-priority": "important"
},
"generateExportsField": {
"type": "boolean",
"alias": "exports",
"description": "Update the output package.json file's 'exports' field. This field is used by Node and bundles.",
"default": false,
"x-priority": "important"
},
"additionalEntryPoints": {
"type": "array",
"description": "Additional entry-points to add to exports field in the package.json file.",
"items": {
"type": "string"
},
"x-priority": "important"
},
"rootDir": {
"type": "string",
@ -19,13 +35,15 @@
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
"x-completion-type": "directory",
"x-priority": "important"
},
"tsConfig": {
"type": "string",
"description": "The path to the Typescript configuration file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
"x-completion-glob": "tsconfig.*.json",
"x-priority": "important"
},
"assets": {
"type": "array",

View File

@ -4,6 +4,10 @@ import type { BatchExecutorTaskResult } from 'nx/src/config/misc-interfaces';
import { getLastValueFromAsyncIterableIterator } from 'nx/src/utils/async-iterator';
import { updatePackageJson } from '../../utils/package-json/update-package-json';
import type { ExecutorOptions } from '../../utils/schema';
import {
createEntryPoints,
determineModuleFormatFromTsConfig,
} from './tsc.impl';
import {
TypescripCompilationLogger,
TypescriptCompilationResult,
@ -81,7 +85,14 @@ export async function* tscBatchExecutor(
const taskInfo = tsConfigTaskInfoMap[tsConfig];
taskInfo.assetsHandler.processAllAssetsOnceSync();
updatePackageJson(
taskInfo.options,
{
...taskInfo.options,
additionalEntryPoints: createEntryPoints(taskInfo.options, context),
format: [determineModuleFormatFromTsConfig(tsConfig)],
// As long as d.ts files match their .js counterparts, we don't need to emit them.
// TSC can match them correctly based on file names.
skipTypings: true,
},
taskInfo.context,
taskInfo.projectGraphNode,
taskInfo.buildableProjectNodeDependencies
@ -114,7 +125,14 @@ export async function* tscBatchExecutor(
(changedTaskInfos: TaskInfo[]) => {
for (const t of changedTaskInfos) {
updatePackageJson(
t.options,
{
...t.options,
additionalEntryPoints: createEntryPoints(t.options, context),
format: [determineModuleFormatFromTsConfig(t.options.tsConfig)],
// As long as d.ts files match their .js counterparts, we don't need to emit them.
// TSC can match them correctly based on file names.
skipTypings: true,
},
t.context,
t.projectGraphNode,
t.buildableProjectNodeDependencies

View File

@ -1,3 +1,5 @@
import * as ts from 'typescript';
import { sync as globSync } from 'fast-glob';
import { ExecutorContext } from '@nx/devkit';
import type { TypeScriptCompilationOptions } from '@nx/workspace/src/utilities/typescript/compilation';
import { CopyAssetsHandler } from '../../utils/assets/copy-assets-handler';
@ -16,6 +18,23 @@ import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema';
import { compileTypeScriptFiles } from '../../utils/typescript/compile-typescript-files';
import { watchForSingleFileChanges } from '../../utils/watch-for-single-file-changes';
import { getCustomTrasformersFactory, normalizeOptions } from './lib';
import { readTsConfig } from '../../utils/typescript/ts-config';
export function determineModuleFormatFromTsConfig(
absolutePathToTsConfig: string
): 'cjs' | 'esm' {
const tsConfig = readTsConfig(absolutePathToTsConfig);
if (
tsConfig.options.module === ts.ModuleKind.ES2015 ||
tsConfig.options.module === ts.ModuleKind.ES2020 ||
tsConfig.options.module === ts.ModuleKind.ES2022 ||
tsConfig.options.module === ts.ModuleKind.ESNext
) {
return 'esm';
} else {
return 'cjs';
}
}
export function createTypeScriptCompilationOptions(
normalizedOptions: NormalizedExecutorOptions,
@ -90,7 +109,19 @@ export async function* tscExecutor(
tsCompilationOptions,
async () => {
await assetHandler.processAllAssetsOnce();
updatePackageJson(options, context, target, dependencies);
updatePackageJson(
{
...options,
additionalEntryPoints: createEntryPoints(options, context),
format: [determineModuleFormatFromTsConfig(options.tsConfig)],
// As long as d.ts files match their .js counterparts, we don't need to emit them.
// TSC can match them correctly based on file names.
skipTypings: true,
},
context,
target,
dependencies
);
postProcessInlinedDependencies(
tsCompilationOptions.outputPath,
tsCompilationOptions.projectRoot,
@ -106,7 +137,20 @@ export async function* tscExecutor(
context.projectName,
options.projectRoot,
'package.json',
() => updatePackageJson(options, context, target, dependencies)
() =>
updatePackageJson(
{
...options,
additionalEntryPoints: createEntryPoints(options, context),
// As long as d.ts files match their .js counterparts, we don't need to emit them.
// TSC can match them correctly based on file names.
skipTypings: true,
format: [determineModuleFormatFromTsConfig(options.tsConfig)],
},
context,
target,
dependencies
)
);
const handleTermination = async (exitCode: number) => {
await typescriptCompilation.close();
@ -121,4 +165,14 @@ export async function* tscExecutor(
return yield* typescriptCompilation.iterator;
}
export function createEntryPoints(
options: { additionalEntryPoints?: string[] },
context: ExecutorContext
): string[] {
if (!options.additionalEntryPoints?.length) return [];
return globSync(options.additionalEntryPoints, {
cwd: context.root,
});
}
export default tscExecutor;

View File

@ -1,11 +1,11 @@
import { dirname, relative } from 'path';
import { normalizePath } from 'nx/src/utils/path';
export function getMainFileDirRelativeToProjectRoot(
main: string,
export function getRelativeDirectoryToProjectRoot(
file: string,
projectRoot: string
): string {
const mainFileDir = dirname(main);
const relativeDir = normalizePath(relative(projectRoot, mainFileDir));
const dir = dirname(file);
const relativeDir = normalizePath(relative(projectRoot, dir));
return relativeDir === '' ? `./` : `./${relativeDir}/`;
}

View File

@ -30,6 +30,7 @@ describe('getUpdatedPackageJsonContent', () => {
expect(json).toEqual({
name: 'test',
main: './src/index.js',
type: 'commonjs',
types: './src/index.d.ts',
version: '0.0.1',
});
@ -99,66 +100,11 @@ describe('getUpdatedPackageJsonContent', () => {
expect(json).toEqual({
name: 'test',
main: './src/index.js',
type: 'commonjs',
version: '0.0.1',
});
});
it('should support generated exports field', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm'],
generateExportsField: true,
}
);
expect(json).toEqual({
name: 'test',
type: 'module',
main: './src/index.js',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': { import: './src/index.js' },
},
});
});
it('should support different CJS file extension', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm', 'cjs'],
outputFileExtensionForCjs: '.cjs',
generateExportsField: true,
}
);
expect(json).toEqual({
name: 'test',
main: './src/index.cjs',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': { require: './src/index.cjs', import: './src/index.js' },
},
});
});
it('should not set types when { skipTypings: true }', () => {
const json = getUpdatedPackageJsonContent(
{
@ -176,68 +122,181 @@ describe('getUpdatedPackageJsonContent', () => {
expect(json).toEqual({
name: 'test',
main: './src/index.js',
type: 'commonjs',
version: '0.0.1',
});
});
it('should support different exports field shape', () => {
// exports: string
expect(
getUpdatedPackageJsonContent(
describe('generateExportsField: true', () => {
it('should add ESM exports', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
exports: './custom.js',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm', 'cjs'],
outputFileExtensionForCjs: '.cjs',
format: ['esm'],
generateExportsField: true,
}
)
).toEqual({
name: 'test',
main: './src/index.cjs',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: './custom.js',
);
expect(json).toEqual({
name: 'test',
type: 'module',
main: './src/index.js',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './src/index.js',
'./package.json': './package.json',
},
});
});
// exports: { '.': string }
expect(
getUpdatedPackageJsonContent(
it('should add CJS exports', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
exports: {
'.': './custom.js',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['cjs'],
outputFileExtensionForCjs: '.cjs',
generateExportsField: true,
}
);
expect(json).toEqual({
name: 'test',
main: './src/index.cjs',
types: './src/index.d.ts',
version: '0.0.1',
type: 'commonjs',
exports: {
'.': './src/index.cjs',
'./package.json': './package.json',
},
});
});
it('should add additional entry-points into package.json', () => {
// CJS only
expect(
getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
additionalEntryPoints: [
'proj/src/foo.ts',
'proj/src/bar.ts',
'proj/migrations.json',
],
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['cjs'],
generateExportsField: true,
}
)
).toEqual({
name: 'test',
main: './src/index.js',
type: 'commonjs',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './src/index.js',
'./foo': './src/foo.js',
'./bar': './src/bar.js',
'./package.json': './package.json',
'./migrations.json': './migrations.json',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm', 'cjs'],
outputFileExtensionForCjs: '.cjs',
generateExportsField: true,
}
)
).toEqual({
name: 'test',
main: './src/index.cjs',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './custom.js',
},
});
});
// exports: { './custom': string }
// ESM only
expect(
getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
additionalEntryPoints: ['proj/src/foo.ts', 'proj/src/bar.ts'],
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm'],
generateExportsField: true,
}
)
).toEqual({
name: 'test',
type: 'module',
main: './src/index.js',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': './src/index.js',
'./foo': './src/foo.js',
'./bar': './src/bar.js',
'./package.json': './package.json',
},
});
// Dual format
expect(
getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
additionalEntryPoints: ['proj/src/foo.ts', 'proj/src/bar.ts'],
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['cjs', 'esm'],
outputFileExtensionForCjs: '.cjs',
generateExportsField: true,
}
)
).toEqual({
name: 'test',
main: './src/index.cjs',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': {
import: './src/index.js',
default: './src/index.cjs',
},
'./foo': {
import: './src/foo.js',
default: './src/foo.cjs',
},
'./bar': {
import: './src/bar.js',
default: './src/bar.cjs',
},
'./package.json': './package.json',
},
});
});
});
it('should support existing exports', () => {
// Merge additional exports from user
expect(
getUpdatedPackageJsonContent(
{
@ -265,8 +324,9 @@ describe('getUpdatedPackageJsonContent', () => {
exports: {
'.': {
import: './src/index.js',
require: './src/index.cjs',
default: './src/index.cjs',
},
'./package.json': './package.json',
'./custom': './custom.js',
},
});
@ -380,6 +440,7 @@ describe('updatePackageJson', () => {
{
"main": "./main.js",
"name": "@org/lib1",
"type": "commonjs",
"types": "./main.d.ts",
"version": "0.0.1",
}
@ -441,6 +502,7 @@ describe('updatePackageJson', () => {
},
"main": "./main.js",
"name": "@org/lib1",
"type": "commonjs",
"types": "./main.d.ts",
"version": "0.0.3",
}

View File

@ -5,6 +5,7 @@ import {
} from 'nx/src/plugins/js/lock-file/lock-file';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { createPackageJson } from 'nx/src/plugins/js/package-json/create-package-json';
import {
ExecutorContext,
getOutputsForTargetAndConfiguration,
@ -16,25 +17,28 @@ import {
writeJsonFile,
} from '@nx/devkit';
import { DependentBuildableProjectNode } from '../buildable-libs-utils';
import { basename, join, parse } from 'path';
import { basename, join, parse, relative } from 'path';
import { writeFileSync } from 'fs-extra';
import { isNpmProject } from 'nx/src/project-graph/operators';
import { fileExists } from 'nx/src/utils/fileutils';
import type { PackageJson } from 'nx/src/utils/package-json';
import { existsSync } from 'fs';
import { readProjectFileMapCache } from 'nx/src/project-graph/nx-deps-cache';
import * as fastGlob from 'fast-glob';
import { getMainFileDirRelativeToProjectRoot } from '../get-main-file-dir';
import { getRelativeDirectoryToProjectRoot } from '../get-main-file-dir';
export type SupportedFormat = 'cjs' | 'esm';
export interface UpdatePackageJsonOption {
projectRoot: string;
main: string;
additionalEntryPoints?: string[];
format?: SupportedFormat[];
outputPath: string;
outputFileName?: string;
outputFileExtensionForCjs?: `.${string}`;
outputFileExtensionForEsm?: `.${string}`;
skipTypings?: boolean;
generateExportsField?: boolean;
excludeLibsInPackageJson?: boolean;
@ -159,6 +163,50 @@ function addMissingDependencies(
});
}
interface Exports {
'.': string;
[name: string]: string;
}
export function getExports(
options: Pick<
UpdatePackageJsonOption,
'main' | 'projectRoot' | 'outputFileName' | 'additionalEntryPoints'
> & {
fileExt: string;
}
): Exports {
const mainFile = options.outputFileName
? options.outputFileName.replace(/\.[tj]s$/, '')
: basename(options.main).replace(/\.[tj]s$/, '');
const relativeMainFileDir = options.outputFileName
? './'
: getRelativeDirectoryToProjectRoot(options.main, options.projectRoot);
const exports: Exports = {
'.': relativeMainFileDir + mainFile + options.fileExt,
};
if (options.additionalEntryPoints) {
const jsRegex = /\.[jt]sx?$/;
for (const file of options.additionalEntryPoints) {
const { ext: fileExt, name: fileName } = parse(file);
const relativeDir = getRelativeDirectoryToProjectRoot(
file,
options.projectRoot
);
const sourceFilePath = relativeDir + fileName;
const entryFilepath = sourceFilePath.replace(/^\.\/src\//, './');
const isJsFile = jsRegex.test(fileExt);
exports[isJsFile ? entryFilepath : entryFilepath + fileExt] =
sourceFilePath + (isJsFile ? options.fileExt : fileExt);
}
}
return exports;
}
export function getUpdatedPackageJsonContent(
packageJson: PackageJson,
options: UpdatePackageJsonOption
@ -167,65 +215,66 @@ export function getUpdatedPackageJsonContent(
const hasCjsFormat = !options.format || options.format?.includes('cjs');
const hasEsmFormat = options.format?.includes('esm');
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const relativeMainFileDir = getMainFileDirRelativeToProjectRoot(
options.main,
options.projectRoot
);
const typingsFile = `${relativeMainFileDir}${mainFile}.d.ts`;
const exports =
typeof packageJson.exports === 'string'
? packageJson.exports
: {
'.': {},
...packageJson.exports,
};
const mainJsFile =
options.outputFileName ?? `${relativeMainFileDir}${mainFile}.js`;
if (options.generateExportsField) {
packageJson.exports =
typeof packageJson.exports === 'string' ? {} : { ...packageJson.exports };
packageJson.exports['./package.json'] = './package.json';
}
if (hasEsmFormat) {
// Unofficial field for backwards compat.
packageJson.module ??= mainJsFile;
const esmExports = getExports({
...options,
fileExt: options.outputFileExtensionForEsm ?? '.js',
});
packageJson.module = esmExports['.'];
if (!hasCjsFormat) {
packageJson.type = 'module';
packageJson.main ??= mainJsFile;
packageJson.main ??= esmExports['.'];
}
if (typeof exports !== 'string') {
if (typeof exports['.'] !== 'string') {
exports['.']['import'] ??= mainJsFile;
} else if (!hasCjsFormat) {
exports['.'] ??= mainJsFile;
if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(esmExports)) {
packageJson.exports[exportEntry] = hasCjsFormat
? { import: filePath }
: filePath;
}
}
}
// CJS output may have .cjs or .js file extensions.
// Bundlers like rollup and esbuild supports .cjs for CJS and .js for ESM.
// Bundlers/Compilers like webpack, tsc, swc do not have different file extensions.
// Bundlers/Compilers like webpack, tsc, swc do not have different file extensions (unless you use .mts or .cts in source).
if (hasCjsFormat) {
const { dir, name } = parse(mainJsFile);
const cjsMain = `${dir ? dir : '.'}/${name}${
options.outputFileExtensionForCjs ?? '.js'
}`;
packageJson.main ??= cjsMain;
if (typeof exports !== 'string') {
if (typeof exports['.'] !== 'string') {
exports['.']['require'] ??= cjsMain;
} else if (!hasEsmFormat) {
exports['.'] ??= cjsMain;
const cjsExports = getExports({
...options,
fileExt: options.outputFileExtensionForCjs ?? '.js',
});
packageJson.main = cjsExports['.'];
if (!hasEsmFormat) {
packageJson.type = 'commonjs';
}
if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(cjsExports)) {
if (hasEsmFormat) {
packageJson.exports[exportEntry]['default'] ??= filePath;
} else {
packageJson.exports[exportEntry] = filePath;
}
}
}
}
if (options.generateExportsField) {
packageJson.exports = exports;
}
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 = packageJson.types ?? typingsFile;
}

View File

@ -38,6 +38,8 @@ export interface ExecutorOptions {
rootDir?: string;
outputPath: string;
tsConfig: string;
generateExportsField?: boolean;
additionalEntryPoints?: string[];
swcrc?: string;
watch: boolean;
clean?: boolean;

View File

@ -20,7 +20,9 @@ function getSwcCmd(
// TODO(jack): clean this up when we remove inline module support
// Handle root project
srcPath === '.' ? 'src' : srcPath
} -d ${destPath} --config-file=${swcrcPath}`;
} -d ${
srcPath === '.' ? `${destPath}/src` : destPath
} --config-file=${swcrcPath}`;
return watch ? swcCmd.concat(' --watch') : swcCmd;
}

View File

@ -6,7 +6,7 @@ import { ensureTypescript } from './ensure-typescript';
let tsModule: typeof import('typescript');
export function readTsConfig(tsConfigPath: string) {
export function readTsConfig(tsConfigPath: string): ts.ParsedCommandLine {
if (!tsModule) {
tsModule = require('typescript');
}