feat(js): add generateExportsField and additionalEntryPoints options to update exports field when building with tsc/swc (#18319)
This commit is contained in:
parent
90e4e7e7de
commit
d63d3573c4
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}/`;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
2
packages/js/src/utils/schema.d.ts
vendored
2
packages/js/src/utils/schema.d.ts
vendored
@ -38,6 +38,8 @@ export interface ExecutorOptions {
|
||||
rootDir?: string;
|
||||
outputPath: string;
|
||||
tsConfig: string;
|
||||
generateExportsField?: boolean;
|
||||
additionalEntryPoints?: string[];
|
||||
swcrc?: string;
|
||||
watch: boolean;
|
||||
clean?: boolean;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user