fix(bundling): explicitly set types for exports entries in package.json (#27152)

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
This commit is contained in:
Jack Hsu 2024-07-26 12:23:46 -04:00 committed by GitHub
parent 7b9ee39c22
commit 2d2c0b5acb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 45 additions and 14 deletions

View File

@ -44,6 +44,7 @@ describe('Rollup Plugin', () => {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
types: './index.esm.d.ts',
},
'./package.json': './package.json',
});
@ -111,16 +112,19 @@ describe('Rollup Plugin', () => {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
types: './index.esm.d.ts',
},
'./bar': {
module: './bar.esm.js',
import: './bar.cjs.mjs',
default: './bar.cjs.js',
types: './bar.esm.d.ts',
},
'./foo': {
module: './foo.esm.js',
import: './foo.cjs.mjs',
default: './foo.cjs.js',
types: './foo.esm.d.ts',
},
});
});

View File

@ -47,6 +47,7 @@ describe('Rollup Plugin', () => {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
types: './index.esm.d.ts',
},
'./package.json': './package.json',
});
@ -142,16 +143,19 @@ describe('Rollup Plugin', () => {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
types: './index.esm.d.ts',
},
'./bar': {
module: './bar.esm.js',
import: './bar.cjs.mjs',
default: './bar.cjs.js',
types: './bar.esm.d.ts',
},
'./foo': {
module: './foo.esm.js',
import: './foo.cjs.mjs',
default: './foo.cjs.js',
types: './foo.esm.d.ts',
},
});
});

View File

@ -30,7 +30,10 @@ describe('updatePackageJson', () => {
expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'./package.json': './package.json',
'.': './index.esm.js',
'.': {
import: './index.esm.js',
types: './index.esm.d.ts',
},
},
main: './index.esm.js',
module: './index.esm.js',
@ -85,11 +88,12 @@ describe('updatePackageJson', () => {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
types: './index.esm.d.ts',
},
},
main: './index.cjs.js',
module: './index.esm.js',
types: './index.cjs.d.ts',
types: './index.esm.d.ts',
});
spy.mockRestore();
@ -106,7 +110,10 @@ describe('updatePackageJson', () => {
},
{
exports: {
'./foo': './foo.esm.js',
'./foo': {
import: './some/custom/path/foo.esm.js',
types: './some/custom/path/foo.d.ts',
},
},
} as unknown as PackageJson
);
@ -114,9 +121,14 @@ describe('updatePackageJson', () => {
expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'./package.json': './package.json',
'.': './index.esm.js',
'./foo': './foo.esm.js',
'.': {
import: './index.esm.js',
types: './index.esm.d.ts',
},
'./foo': {
import: './some/custom/path/foo.esm.js',
types: './some/custom/path/foo.d.ts',
},
},
main: './index.esm.js',
module: './index.esm.js',
@ -184,7 +196,7 @@ describe('updatePackageJson', () => {
expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
main: './index.cjs.js',
module: './index.esm.js',
types: './index.cjs.d.ts',
types: './index.esm.d.ts',
});
spy.mockRestore();

View File

@ -40,12 +40,14 @@ export function updatePackageJson(
if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(esmExports)) {
packageJson.exports[exportEntry] = hasCjsFormat
? // If CJS format is used, make sure `import` (from Node) points to same instance of the package.
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 }
: filePath;
{
[hasCjsFormat ? 'module' : 'import']: filePath,
types: filePath.replace(/\.js$/, '.d.ts'),
};
}
}
}
@ -72,6 +74,11 @@ export function updatePackageJson(
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
@ -102,7 +109,11 @@ export function updatePackageJson(
}
}
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'),