We currently rely on the TS behavior of matching `d.ts` files based on the `.js` file names. e.g. `foo.js` matches `foo.d.ts`. However, it isn't working for all tools so we should explicitly set it. Most modern packages are still setting it even though it is not technically needed. e.g. [Nuxt](https://unpkg.com/browse/nuxt@3.12.4/package.json) Note: If both ESM and CJS are present, then prefer `*.esm.d.ts` files since the generated types are in ESM format. ## Current Behavior `exports` entries are missing `types` field ## Expected Behavior `exports` entries have `types` field set ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #18835
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import { basename, join, parse } from 'path';
|
|
import { writeJsonFile } from 'nx/src/utils/fileutils';
|
|
import { writeFileSync } from 'fs';
|
|
import { PackageJson } from 'nx/src/utils/package-json';
|
|
import { stripIndents, workspaceRoot } from '@nx/devkit';
|
|
|
|
export function updatePackageJson(
|
|
options: {
|
|
outputPath: string;
|
|
main: string;
|
|
format: string[];
|
|
generateExportsField?: boolean;
|
|
skipTypeField?: boolean;
|
|
outputFileName?: string;
|
|
additionalEntryPoints?: string[];
|
|
},
|
|
packageJson: PackageJson
|
|
) {
|
|
const hasEsmFormat = options.format.includes('esm');
|
|
const hasCjsFormat = options.format.includes('cjs');
|
|
|
|
if (options.generateExportsField) {
|
|
packageJson.exports =
|
|
typeof packageJson.exports === 'string' ? {} : { ...packageJson.exports };
|
|
packageJson.exports['./package.json'] = './package.json';
|
|
}
|
|
|
|
if (hasEsmFormat) {
|
|
const esmExports = getExports({
|
|
...options,
|
|
fileExt: '.esm.js',
|
|
});
|
|
|
|
packageJson.module = esmExports['.'];
|
|
|
|
if (!hasCjsFormat) {
|
|
if (!options.skipTypeField) packageJson.type = 'module';
|
|
packageJson.main ??= esmExports['.'];
|
|
}
|
|
|
|
if (options.generateExportsField) {
|
|
for (const [exportEntry, filePath] of Object.entries(esmExports)) {
|
|
packageJson.exports[exportEntry] =
|
|
// If CJS format is used, make sure `import` (from Node) points to same instance of the package.
|
|
// Otherwise, packages that are required to be singletons (like React, RxJS, etc.) will break.
|
|
// Reserve `module` entry for bundlers to accommodate tree-shaking.
|
|
{
|
|
[hasCjsFormat ? 'module' : 'import']: filePath,
|
|
types: filePath.replace(/\.js$/, '.d.ts'),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasCjsFormat) {
|
|
const cjsExports = getExports({
|
|
...options,
|
|
fileExt: '.cjs.js',
|
|
});
|
|
|
|
packageJson.main = cjsExports['.'];
|
|
|
|
if (!hasEsmFormat && !options.skipTypeField) {
|
|
packageJson.type = 'commonjs';
|
|
}
|
|
|
|
if (options.generateExportsField) {
|
|
for (const [exportEntry, filePath] of Object.entries(cjsExports)) {
|
|
if (hasEsmFormat) {
|
|
// If ESM format used, make sure `import` (from Node) points to a wrapped
|
|
// version of CJS file to ensure the package remains a singleton.
|
|
// TODO(jack): This can be made into a rollup plugin to re-use in Vite.
|
|
const relativeFile = parse(filePath).base;
|
|
const fauxEsmFilePath = filePath.replace(/\.cjs\.js$/, '.cjs.mjs');
|
|
packageJson.exports[exportEntry]['import'] ??= fauxEsmFilePath;
|
|
packageJson.exports[exportEntry]['default'] ??= filePath;
|
|
// Set `types` field only if it's not already set as the preferred ESM Format.
|
|
packageJson.exports[exportEntry]['types'] ??= filePath.replace(
|
|
/\.js$/,
|
|
'.d.ts'
|
|
);
|
|
// Re-export from relative CJS file, and Node will synthetically export it as ESM.
|
|
// Make sure both ESM and CJS point to same instance of the package because libs like React, RxJS, etc. requires it.
|
|
// Also need a special .cjs.default.js file that re-exports the `default` from CJS, or else
|
|
// default import in Node will not work.
|
|
writeFileSync(
|
|
join(
|
|
workspaceRoot,
|
|
options.outputPath,
|
|
filePath.replace(/\.cjs\.js$/, '.cjs.default.js')
|
|
),
|
|
`exports._default = require('./${parse(filePath).base}').default;`
|
|
);
|
|
writeFileSync(
|
|
join(workspaceRoot, options.outputPath, fauxEsmFilePath),
|
|
// Re-export from relative CJS file, and Node will synthetically export it as ESM.
|
|
stripIndents`
|
|
export * from './${relativeFile}';
|
|
export { _default as default } from './${relativeFile.replace(
|
|
/\.cjs\.js$/,
|
|
'.cjs.default.js'
|
|
)}';
|
|
`
|
|
);
|
|
} else {
|
|
packageJson.exports[exportEntry] = filePath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (packageJson.module) {
|
|
packageJson.types ??= packageJson.module.replace(/\.js$/, '.d.ts');
|
|
} else {
|
|
packageJson.types ??= packageJson.main.replace(/\.js$/, '.d.ts');
|
|
}
|
|
|
|
writeJsonFile(
|
|
join(workspaceRoot, options.outputPath, 'package.json'),
|
|
packageJson
|
|
);
|
|
}
|
|
|
|
interface Exports {
|
|
[name: string]: string;
|
|
}
|
|
|
|
function getExports(options: {
|
|
main?: string;
|
|
fileExt: string;
|
|
outputFileName?: string;
|
|
additionalEntryPoints?: string[];
|
|
}): Exports {
|
|
const exports: Exports = {};
|
|
|
|
// Users may provide custom input option and skip the main field.
|
|
if (options.main) {
|
|
const mainFile = options.outputFileName
|
|
? options.outputFileName.replace(/\.[tj]s$/, '')
|
|
: basename(options.main).replace(/\.[tj]s$/, '');
|
|
exports['.'] = './' + mainFile + options.fileExt;
|
|
}
|
|
|
|
if (options.additionalEntryPoints) {
|
|
for (const file of options.additionalEntryPoints) {
|
|
const { name: fileName } = parse(file);
|
|
exports['./' + fileName] = './' + fileName + options.fileExt;
|
|
}
|
|
}
|
|
|
|
return exports;
|
|
}
|