Skip to content

Commit ad9fb40

Browse files
authored
exports field should exist in types-only builds (#210)
1 parent 3b7efdc commit ad9fb40

File tree

9 files changed

+136
-37
lines changed

9 files changed

+136
-37
lines changed

.changeset/yellow-carrots-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'bob-the-bundler': patch
3+
---
4+
5+
exports field should exist in types-only builds

src/commands/build.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -373,12 +373,10 @@ function rewritePackageJson(pkg: Record<string, any>, typesOnly: boolean) {
373373
definition: newPkg.typescript.definition.replace(distDirStr, ''),
374374
};
375375

376-
if (!typesOnly) {
377-
if (!pkg.exports) {
378-
newPkg.exports = presetFields.exports;
379-
}
380-
newPkg.exports = rewriteExports(pkg.exports, DIST_DIR);
376+
if (!pkg.exports) {
377+
newPkg.exports = presetFields.exports;
381378
}
379+
newPkg.exports = rewriteExports(pkg.exports, DIST_DIR, typesOnly);
382380

383381
if (pkg.bin) {
384382
newPkg.bin = {};
@@ -415,7 +413,10 @@ export function validatePackageJson(
415413
expect('module', undefined);
416414
expect('typings', presetFields.typings);
417415
expect('typescript.definition', presetFields.typescript.definition);
418-
expect('exports', undefined);
416+
expect("exports['.'].default", {
417+
// only the "types" field is required
418+
types: presetFields.exports['.'].default.types,
419+
});
419420
return;
420421
}
421422

src/commands/check.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ const ExportsMapModel = zod.record(
2828
]),
2929
);
3030

31+
const TypesOnlyExportsMapEntry = zod.object({
32+
types: zod.string(),
33+
});
34+
35+
const TypesOnlyExportsMapModel = zod.record(
36+
zod.union([
37+
zod.string(),
38+
zod.object({
39+
default: TypesOnlyExportsMapEntry,
40+
}),
41+
]),
42+
);
43+
3144
const BinModel = zod.record(zod.string());
3245

3346
export const checkCommand = createCommand<{}, {}>(api => {
@@ -86,20 +99,15 @@ export const checkCommand = createCommand<{}, {}>(api => {
8699
const distPackageJSONPath = path.join(cwd, 'dist', 'package.json');
87100
const distPackageJSON = await fse.readJSON(distPackageJSONPath);
88101

89-
// a tell for a types-only build is the lack of main import and presence of typings
90-
if (distPackageJSON.main === '' && (distPackageJSON.typings || '').endsWith('d.ts')) {
91-
api.reporter.warn(
92-
`Skip check for '${packageJSON.name}' because it's a types-only package.`,
93-
);
94-
return;
95-
}
96-
97102
try {
98103
await checkExportsMapIntegrity({
99104
cwd: path.join(cwd, 'dist'),
100105
packageJSON: distPackageJSON,
101106
skipExports: new Set<string>(config?.check?.skip ?? []),
102107
includesCommonJS: config?.commonjs ?? true,
108+
// a tell for a types-only build is the lack of main import and presence of typings
109+
typesOnly:
110+
distPackageJSON.main === '' && (distPackageJSON.typings || '').endsWith('d.ts'),
103111
});
104112
} catch (err) {
105113
api.reporter.error(`Integrity check of '${packageJSON.name}' failed.`);
@@ -127,17 +135,39 @@ async function checkExportsMapIntegrity(args: {
127135
};
128136
skipExports: Set<string>;
129137
includesCommonJS: boolean;
138+
typesOnly: boolean;
130139
}) {
131-
const exportsMapResult = ExportsMapModel.safeParse(args.packageJSON['exports']);
140+
const exportsMapResult = (args.typesOnly ? TypesOnlyExportsMapModel : ExportsMapModel).safeParse(
141+
args.packageJSON['exports'],
142+
);
132143
if (exportsMapResult.success === false) {
133144
throw new Error(
134145
"Missing exports map within the 'package.json'.\n" +
135146
exportsMapResult.error.message +
136147
'\nCorrect Example:\n' +
137-
JSON.stringify(presetFields.exports, null, 2),
148+
JSON.stringify(
149+
args.typesOnly
150+
? {
151+
...presetFields.exports,
152+
'.': {
153+
default: {
154+
types: presetFields.exports['.'].default.types,
155+
},
156+
},
157+
}
158+
: presetFields.exports,
159+
null,
160+
2,
161+
),
138162
);
139163
}
140164

165+
// just check if the types under default exist
166+
if (args.typesOnly) {
167+
await fse.stat(path.join(args.cwd, (args.packageJSON.exports as any)?.['.']?.default?.types));
168+
return;
169+
}
170+
141171
const exportsMap = exportsMapResult['data'];
142172

143173
const cjsSkipExports = new Set<string>();

src/utils/rewrite-exports.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ type Exports =
66
default?: string | Record<string, string>;
77
};
88

9-
export function rewriteExports(exports: Record<string, Exports>, distDir: string) {
9+
export function rewriteExports(
10+
exports: Record<string, Exports>,
11+
distDir: string,
12+
typesOnly: boolean,
13+
) {
1014
const newExports = { ...exports };
1115

1216
for (const [key, value] of Object.entries(newExports)) {
@@ -24,17 +28,24 @@ export function rewriteExports(exports: Record<string, Exports>, distDir: string
2428
if (typeof value === 'object') {
2529
const newValue: Record<string, string> = {};
2630
for (const [key, path] of Object.entries(value)) {
27-
newValue[key] = path.replace(`${distDir}/`, '');
31+
if (!typesOnly || key === 'types') {
32+
// types-only builds need just the types field
33+
newValue[key] = path.replace(`${distDir}/`, '');
34+
}
2835
}
2936
return newValue;
3037
}
3138
return value.replace(`${distDir}/`, '');
3239
}
3340

3441
newValue = {
35-
require: transformValue(newValue.require),
36-
import: transformValue(newValue.import),
37-
default: transformValue(newValue.import),
42+
...(typesOnly
43+
? {}
44+
: {
45+
require: transformValue(newValue.require),
46+
import: transformValue(newValue.import),
47+
}),
48+
default: transformValue(typesOnly ? newValue.default : newValue.import),
3849
};
3950
}
4051
newExports[key.replace(`${distDir}/`, '')] = newValue;

test/__fixtures__/simple-monorepo-pnpm/packages/c/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
{
22
"name": "c",
33
"main": "",
4+
"exports": {
5+
".": {
6+
"default": {
7+
"types": "./dist/typings/index.d.ts"
8+
}
9+
},
10+
"./package.json": "./package.json"
11+
},
412
"typings": "dist/typings/index.d.ts",
513
"publishConfig": {
614
"directory": "dist",

test/__fixtures__/simple-monorepo/packages/c/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
{
22
"name": "c",
33
"main": "",
4+
"exports": {
5+
".": {
6+
"default": {
7+
"types": "./dist/typings/index.d.ts"
8+
}
9+
},
10+
"./package.json": "./package.json"
11+
},
412
"typings": "dist/typings/index.d.ts",
513
"publishConfig": {
614
"directory": "dist",

test/__fixtures__/simple-types-only/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
"name": "simple-types-only",
33
"type": "module",
44
"main": "",
5+
"exports": {
6+
".": {
7+
"default": {
8+
"types": "./dist/typings/index.d.ts"
9+
}
10+
},
11+
"./package.json": "./package.json"
12+
},
513
"typings": "dist/typings/index.d.ts",
614
"publishConfig": {
715
"directory": "dist",

test/exports.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ test('basic exports', () => {
2727
},
2828
},
2929
'dist',
30+
false,
3031
),
3132
).toStrictEqual({
3233
'.': {
@@ -96,6 +97,7 @@ test('with custom exports', () => {
9697
},
9798
},
9899
'dist',
100+
false,
99101
),
100102
).toStrictEqual({
101103
'.': {
@@ -142,3 +144,5 @@ test('with custom exports', () => {
142144
},
143145
});
144146
});
147+
148+
test.todo('with types-only exports');

test/integration.spec.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,23 @@ it('can build a monorepo project', async () => {
267267
}
268268
`);
269269
expect(await fse.readFile(files.c['package.json'], 'utf8')).toMatchInlineSnapshot(`
270-
{
271-
"name": "c",
272-
"main": "",
273-
"typings": "typings/index.d.ts",
274-
"typescript": {
275-
"definition": "typings/index.d.ts"
276-
}
270+
{
271+
"name": "c",
272+
"main": "",
273+
"typings": "typings/index.d.ts",
274+
"typescript": {
275+
"definition": "typings/index.d.ts"
276+
},
277+
"exports": {
278+
".": {
279+
"default": {
280+
"types": "./typings/index.d.ts"
281+
}
282+
},
283+
"./package.json": "./package.json"
277284
}
278-
`);
285+
}
286+
`);
279287

280288
await execa('node', [binaryFolder, 'check'], {
281289
cwd: path.resolve(fixturesFolder, 'simple-monorepo'),
@@ -345,6 +353,14 @@ it('can build a types only project', async () => {
345353
"typings": "typings/index.d.ts",
346354
"typescript": {
347355
"definition": "typings/index.d.ts"
356+
},
357+
"exports": {
358+
".": {
359+
"default": {
360+
"types": "./typings/index.d.ts"
361+
}
362+
},
363+
"./package.json": "./package.json"
348364
}
349365
}
350366
`);
@@ -560,15 +576,23 @@ it('can build a monorepo pnpm project', async () => {
560576
}
561577
`);
562578
expect(await fse.readFile(files.c['package.json'], 'utf8')).toMatchInlineSnapshot(`
563-
{
564-
"name": "c",
565-
"main": "",
566-
"typings": "typings/index.d.ts",
567-
"typescript": {
568-
"definition": "typings/index.d.ts"
569-
}
579+
{
580+
"name": "c",
581+
"main": "",
582+
"typings": "typings/index.d.ts",
583+
"typescript": {
584+
"definition": "typings/index.d.ts"
585+
},
586+
"exports": {
587+
".": {
588+
"default": {
589+
"types": "./typings/index.d.ts"
590+
}
591+
},
592+
"./package.json": "./package.json"
570593
}
571-
`);
594+
}
595+
`);
572596

573597
await execa('node', [binaryFolder, 'check'], {
574598
cwd: path.resolve(fixturesFolder, 'simple-monorepo-pnpm'),

0 commit comments

Comments
 (0)