Skip to content

Commit 1567b4d

Browse files
authored
Include empty cjs/esm entry points for types-only packages (#214)
1 parent a1df531 commit 1567b4d

File tree

8 files changed

+142
-135
lines changed

8 files changed

+142
-135
lines changed

.changeset/bright-ducks-agree.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+
Include empty cjs/esm entry points for types-only packages

src/commands/build.ts

Lines changed: 41 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -207,60 +207,49 @@ async function build({
207207
return;
208208
}
209209

210+
validatePackageJson(pkg, {
211+
includesCommonJS: config?.commonjs ?? true,
212+
});
213+
210214
const declarations = await globby('**/*.d.ts', {
211215
cwd: getBuildPath('esm'),
212216
absolute: false,
213217
ignore: filesToExcludeFromDist,
214218
});
215219

220+
await fse.ensureDir(join(distPath, 'typings'));
221+
await Promise.all(
222+
declarations.map(filePath =>
223+
limit(() =>
224+
fse.copy(join(getBuildPath('esm'), filePath), join(distPath, 'typings', filePath)),
225+
),
226+
),
227+
);
228+
216229
const esmFiles = await globby('**/*.js', {
217230
cwd: getBuildPath('esm'),
218231
absolute: false,
219232
ignore: filesToExcludeFromDist,
220233
});
221234

222-
// Check whether al esm files are empty, if not - probably a types only build
223-
let emptyEsmFiles = true;
235+
// all files that export nothing, should be completely empty
236+
// this way we wont have issues with linters: <link to issue>
237+
// and we will also make all type-only packages happy
224238
for (const file of esmFiles) {
225239
const src = await fse.readFile(join(getBuildPath('esm'), file));
226-
if (src.toString().trim() !== 'export {};') {
227-
emptyEsmFiles = false;
228-
break;
240+
if (src.toString().trim() === 'export {};') {
241+
await fse.writeFile(join(getBuildPath('esm'), file), '');
229242
}
230243
}
231244

232-
// Empty ESM files with existing declarations is a types-only package
233-
const typesOnly = emptyEsmFiles && declarations.length > 0;
234-
235-
validatePackageJson(pkg, {
236-
typesOnly,
237-
includesCommonJS: config?.commonjs ?? true,
238-
});
239-
240-
// remove <project>/dist
241-
await fse.remove(distPath);
242-
243-
// Copy type definitions
244-
await fse.ensureDir(join(distPath, 'typings'));
245+
await fse.ensureDir(join(distPath, 'esm'));
245246
await Promise.all(
246-
declarations.map(filePath =>
247-
limit(() =>
248-
fse.copy(join(getBuildPath('esm'), filePath), join(distPath, 'typings', filePath)),
249-
),
247+
esmFiles.map(filePath =>
248+
limit(() => fse.copy(join(getBuildPath('esm'), filePath), join(distPath, 'esm', filePath))),
250249
),
251250
);
252251

253-
// If ESM files are not empty, copy them to dist/esm
254-
if (!emptyEsmFiles) {
255-
await fse.ensureDir(join(distPath, 'esm'));
256-
await Promise.all(
257-
esmFiles.map(filePath =>
258-
limit(() => fse.copy(join(getBuildPath('esm'), filePath), join(distPath, 'esm', filePath))),
259-
),
260-
);
261-
}
262-
263-
if (!emptyEsmFiles && config?.commonjs === undefined) {
252+
if (config?.commonjs === undefined) {
264253
// Transpile ESM to CJS and move CJS to dist/cjs only if there's something to transpile
265254
await fse.ensureDir(join(distPath, 'cjs'));
266255

@@ -270,6 +259,20 @@ async function build({
270259
ignore: filesToExcludeFromDist,
271260
});
272261

262+
// all files that export nothing, should be completely empty
263+
// this way we wont have issues with linters: <link to issue>
264+
// and we will also make all type-only packages happy
265+
for (const file of cjsFiles) {
266+
const src = await fse.readFile(join(getBuildPath('cjs'), file));
267+
if (
268+
// TODO: will this always be the case with empty cjs files
269+
src.toString().trim() ===
270+
'"use strict";\nObject.defineProperty(exports, "__esModule", { value: true });'
271+
) {
272+
await fse.writeFile(join(getBuildPath('cjs'), file), '');
273+
}
274+
}
275+
273276
await Promise.all(
274277
cjsFiles.map(filePath =>
275278
limit(() => fse.copy(join(getBuildPath('cjs'), filePath), join(distPath, 'cjs', filePath))),
@@ -305,7 +308,7 @@ async function build({
305308
// move the package.json to dist
306309
await fse.writeFile(
307310
join(distPath, 'package.json'),
308-
JSON.stringify(rewritePackageJson(pkg, typesOnly), null, 2),
311+
JSON.stringify(rewritePackageJson(pkg), null, 2),
309312
);
310313

311314
// move README.md and LICENSE and other specified files
@@ -327,7 +330,7 @@ async function build({
327330
reporter.success(`Built ${pkg.name}`);
328331
}
329332

330-
function rewritePackageJson(pkg: Record<string, any>, typesOnly: boolean) {
333+
function rewritePackageJson(pkg: Record<string, any>) {
331334
const newPkg: Record<string, any> = {};
332335
const fields = [
333336
'name',
@@ -360,14 +363,8 @@ function rewritePackageJson(pkg: Record<string, any>, typesOnly: boolean) {
360363

361364
const distDirStr = `${DIST_DIR}/`;
362365

363-
if (typesOnly) {
364-
newPkg.main = '';
365-
delete newPkg.module;
366-
delete newPkg.type;
367-
} else {
368-
newPkg.main = newPkg.main.replace(distDirStr, '');
369-
newPkg.module = newPkg.module.replace(distDirStr, '');
370-
}
366+
newPkg.main = newPkg.main.replace(distDirStr, '');
367+
newPkg.module = newPkg.module.replace(distDirStr, '');
371368
newPkg.typings = newPkg.typings.replace(distDirStr, '');
372369
newPkg.typescript = {
373370
definition: newPkg.typescript.definition.replace(distDirStr, ''),
@@ -376,7 +373,7 @@ function rewritePackageJson(pkg: Record<string, any>, typesOnly: boolean) {
376373
if (!pkg.exports) {
377374
newPkg.exports = presetFields.exports;
378375
}
379-
newPkg.exports = rewriteExports(pkg.exports, DIST_DIR, typesOnly);
376+
newPkg.exports = rewriteExports(pkg.exports, DIST_DIR);
380377

381378
if (pkg.bin) {
382379
newPkg.bin = {};
@@ -392,7 +389,6 @@ function rewritePackageJson(pkg: Record<string, any>, typesOnly: boolean) {
392389
export function validatePackageJson(
393390
pkg: any,
394391
opts: {
395-
typesOnly: boolean;
396392
includesCommonJS: boolean;
397393
},
398394
) {
@@ -407,19 +403,6 @@ export function validatePackageJson(
407403
);
408404
}
409405

410-
// Type only packages have simpler rules (following the style of https://github.com/DefinitelyTyped/DefinitelyTyped packages)
411-
if (opts.typesOnly) {
412-
expect('main', '');
413-
expect('module', undefined);
414-
expect('typings', presetFields.typings);
415-
expect('typescript.definition', presetFields.typescript.definition);
416-
expect("exports['.'].default", {
417-
// only the "types" field is required
418-
types: presetFields.exports['.'].default.types,
419-
});
420-
return;
421-
}
422-
423406
// If the package has NO binary we need to check the exports map.
424407
// a package should either
425408
// 1. have a bin property

src/commands/check.ts

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,6 @@ 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-
4431
const BinModel = zod.record(zod.string());
4532

4633
export const checkCommand = createCommand<{}, {}>(api => {
@@ -105,9 +92,6 @@ export const checkCommand = createCommand<{}, {}>(api => {
10592
packageJSON: distPackageJSON,
10693
skipExports: new Set<string>(config?.check?.skip ?? []),
10794
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'),
11195
});
11296
} catch (err) {
11397
api.reporter.error(`Integrity check of '${packageJSON.name}' failed.`);
@@ -135,39 +119,17 @@ async function checkExportsMapIntegrity(args: {
135119
};
136120
skipExports: Set<string>;
137121
includesCommonJS: boolean;
138-
typesOnly: boolean;
139122
}) {
140-
const exportsMapResult = (args.typesOnly ? TypesOnlyExportsMapModel : ExportsMapModel).safeParse(
141-
args.packageJSON['exports'],
142-
);
123+
const exportsMapResult = ExportsMapModel.safeParse(args.packageJSON['exports']);
143124
if (exportsMapResult.success === false) {
144125
throw new Error(
145126
"Missing exports map within the 'package.json'.\n" +
146127
exportsMapResult.error.message +
147128
'\nCorrect Example:\n' +
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-
),
129+
JSON.stringify(presetFields.exports, null, 2),
162130
);
163131
}
164132

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-
171133
const exportsMap = exportsMapResult['data'];
172134

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

src/utils/rewrite-exports.ts

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

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

1612
for (const [key, value] of Object.entries(newExports)) {
@@ -28,24 +24,17 @@ export function rewriteExports(
2824
if (typeof value === 'object') {
2925
const newValue: Record<string, string> = {};
3026
for (const [key, path] of Object.entries(value)) {
31-
if (!typesOnly || key === 'types') {
32-
// types-only builds need just the types field
33-
newValue[key] = path.replace(`${distDir}/`, '');
34-
}
27+
newValue[key] = path.replace(`${distDir}/`, '');
3528
}
3629
return newValue;
3730
}
3831
return value.replace(`${distDir}/`, '');
3932
}
4033

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

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
{
22
"name": "c",
3-
"main": "",
3+
"main": "dist/cjs/index.js",
4+
"module": "dist/esm/index.js",
45
"exports": {
56
".": {
7+
"require": {
8+
"types": "./dist/typings/index.d.cts",
9+
"default": "./dist/cjs/index.js"
10+
},
11+
"import": {
12+
"types": "./dist/typings/index.d.ts",
13+
"default": "./dist/esm/index.js"
14+
},
615
"default": {
7-
"types": "./dist/typings/index.d.ts"
16+
"types": "./dist/typings/index.d.ts",
17+
"default": "./dist/esm/index.js"
818
}
919
},
1020
"./package.json": "./package.json"

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
{
22
"name": "c",
3-
"main": "",
3+
"main": "dist/cjs/index.js",
4+
"module": "dist/esm/index.js",
45
"exports": {
56
".": {
7+
"require": {
8+
"types": "./dist/typings/index.d.cts",
9+
"default": "./dist/cjs/index.js"
10+
},
11+
"import": {
12+
"types": "./dist/typings/index.d.ts",
13+
"default": "./dist/esm/index.js"
14+
},
615
"default": {
7-
"types": "./dist/typings/index.d.ts"
16+
"types": "./dist/typings/index.d.ts",
17+
"default": "./dist/esm/index.js"
818
}
919
},
1020
"./package.json": "./package.json"

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
{
22
"name": "simple-types-only",
33
"type": "module",
4-
"main": "",
4+
"main": "dist/cjs/index.js",
5+
"module": "dist/esm/index.js",
56
"exports": {
67
".": {
8+
"require": {
9+
"types": "./dist/typings/index.d.cts",
10+
"default": "./dist/cjs/index.js"
11+
},
12+
"import": {
13+
"types": "./dist/typings/index.d.ts",
14+
"default": "./dist/esm/index.js"
15+
},
716
"default": {
8-
"types": "./dist/typings/index.d.ts"
17+
"types": "./dist/typings/index.d.ts",
18+
"default": "./dist/esm/index.js"
919
}
1020
},
1121
"./package.json": "./package.json"

0 commit comments

Comments
 (0)