Skip to content

Commit 76fd23c

Browse files
authored
Type only packages (#144)
* types only build for single package * format * monorepo fixture with types only * refactor build cmd for efficiency * correct package.json for types only * test c in simple-monorepo * skip exports integrity check for types-only * changeset
1 parent 4697b5f commit 76fd23c

File tree

11 files changed

+222
-37
lines changed

11 files changed

+222
-37
lines changed

.changeset/tricky-poems-juggle.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+
Correct package.json for types-only packages

src/commands/build.ts

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -223,53 +223,69 @@ async function build({
223223
return;
224224
}
225225

226-
validatePackageJson(pkg, config?.commonjs ?? true);
227-
228-
// remove <project>/dist
229-
await fse.remove(distPath);
230-
231-
// Copy type definitions
232-
await fse.ensureDir(join(distPath, "typings"));
233-
234226
const declarations = await globby("**/*.d.ts", {
235227
cwd: getBuildPath("esm"),
236228
absolute: false,
237229
ignore: filesToExcludeFromDist,
238230
});
239231

240-
await Promise.all(
241-
declarations.map((filePath) =>
242-
limit(() =>
243-
fse.copy(
244-
join(getBuildPath("esm"), filePath),
245-
join(distPath, "typings", filePath)
246-
)
247-
)
248-
)
249-
);
250-
251-
// Move ESM to dist/esm
252-
await fse.ensureDir(join(distPath, "esm"));
253-
254232
const esmFiles = await globby("**/*.js", {
255233
cwd: getBuildPath("esm"),
256234
absolute: false,
257235
ignore: filesToExcludeFromDist,
258236
});
259237

238+
// Check whether al esm files are empty, if not - probably a types only build
239+
let emptyEsmFiles = true;
240+
for (const file of esmFiles) {
241+
const src = await fse.readFile(join(getBuildPath("esm"), file));
242+
if (src.toString().trim() !== "export {};") {
243+
emptyEsmFiles = false;
244+
break;
245+
}
246+
}
247+
248+
// Empty ESM files with existing declarations is a types-only package
249+
const typesOnly = emptyEsmFiles && declarations.length > 0;
250+
251+
validatePackageJson(pkg, {
252+
typesOnly,
253+
includesCommonJS: config?.commonjs ?? true,
254+
});
255+
256+
// remove <project>/dist
257+
await fse.remove(distPath);
258+
259+
// Copy type definitions
260+
await fse.ensureDir(join(distPath, "typings"));
260261
await Promise.all(
261-
esmFiles.map((filePath) =>
262+
declarations.map((filePath) =>
262263
limit(() =>
263264
fse.copy(
264265
join(getBuildPath("esm"), filePath),
265-
join(distPath, "esm", filePath)
266+
join(distPath, "typings", filePath)
266267
)
267268
)
268269
)
269270
);
270271

271-
if (config?.commonjs === undefined) {
272-
// Transpile ESM to CJS and move CJS to dist/cjs
272+
// If ESM files are not empty, copy them to dist/esm
273+
if (!emptyEsmFiles) {
274+
await fse.ensureDir(join(distPath, "esm"));
275+
await Promise.all(
276+
esmFiles.map((filePath) =>
277+
limit(() =>
278+
fse.copy(
279+
join(getBuildPath("esm"), filePath),
280+
join(distPath, "esm", filePath)
281+
)
282+
)
283+
)
284+
);
285+
}
286+
287+
if (!emptyEsmFiles && config?.commonjs === undefined) {
288+
// Transpile ESM to CJS and move CJS to dist/cjs only if there's something to transpile
273289
await fse.ensureDir(join(distPath, "cjs"));
274290

275291
const cjsFiles = await globby("**/*.js", {
@@ -323,8 +339,9 @@ async function build({
323339
// move the package.json to dist
324340
await fse.writeFile(
325341
join(distPath, "package.json"),
326-
JSON.stringify(rewritePackageJson(pkg), null, 2)
342+
JSON.stringify(rewritePackageJson(pkg, typesOnly), null, 2)
327343
);
344+
328345
// move README.md and LICENSE and other specified files
329346
await copyToDist(
330347
cwd,
@@ -350,7 +367,7 @@ async function build({
350367
reporter.success(`Built ${pkg.name}`);
351368
}
352369

353-
function rewritePackageJson(pkg: Record<string, any>) {
370+
function rewritePackageJson(pkg: Record<string, any>, typesOnly: boolean) {
354371
const newPkg: Record<string, any> = {};
355372
const fields = [
356373
"name",
@@ -382,19 +399,26 @@ function rewritePackageJson(pkg: Record<string, any>) {
382399

383400
const distDirStr = `${DIST_DIR}/`;
384401

385-
newPkg.main = newPkg.main.replace(distDirStr, "");
386-
newPkg.module = newPkg.module.replace(distDirStr, "");
402+
if (typesOnly) {
403+
newPkg.main = "";
404+
delete newPkg.module;
405+
delete newPkg.type;
406+
} else {
407+
newPkg.main = newPkg.main.replace(distDirStr, "");
408+
newPkg.module = newPkg.module.replace(distDirStr, "");
409+
}
387410
newPkg.typings = newPkg.typings.replace(distDirStr, "");
388411
newPkg.typescript = {
389412
definition: newPkg.typescript.definition.replace(distDirStr, ""),
390413
};
391414

392-
if (!pkg.exports) {
393-
newPkg.exports = presetFields.exports;
415+
if (!typesOnly) {
416+
if (!pkg.exports) {
417+
newPkg.exports = presetFields.exports;
418+
}
419+
newPkg.exports = rewriteExports(pkg.exports, DIST_DIR);
394420
}
395421

396-
newPkg.exports = rewriteExports(pkg.exports, DIST_DIR);
397-
398422
if (pkg.bin) {
399423
newPkg.bin = {};
400424

@@ -406,7 +430,13 @@ function rewritePackageJson(pkg: Record<string, any>) {
406430
return newPkg;
407431
}
408432

409-
export function validatePackageJson(pkg: any, includesCommonJS: boolean) {
433+
export function validatePackageJson(
434+
pkg: any,
435+
opts: {
436+
typesOnly: boolean;
437+
includesCommonJS: boolean;
438+
}
439+
) {
410440
function expect(key: string, expected: unknown) {
411441
const received = get(pkg, key);
412442

@@ -418,13 +448,23 @@ export function validatePackageJson(pkg: any, includesCommonJS: boolean) {
418448
);
419449
}
420450

451+
// Type only packages have simpler rules (following the style of https://github.com/DefinitelyTyped/DefinitelyTyped packages)
452+
if (opts.typesOnly) {
453+
expect("main", "");
454+
expect("module", undefined);
455+
expect("typings", presetFields.typings);
456+
expect("typescript.definition", presetFields.typescript.definition);
457+
expect("exports", undefined);
458+
return;
459+
}
460+
421461
// If the package has NO binary we need to check the exports map.
422462
// a package should either
423463
// 1. have a bin property
424464
// 2. have a exports property
425465
// 3. have an exports and bin property
426466
if (Object.keys(pkg.bin ?? {}).length > 0) {
427-
if (includesCommonJS === true) {
467+
if (opts.includesCommonJS === true) {
428468
expect("main", presetFields.main);
429469
expect("module", presetFields.module);
430470
expect("typings", presetFields.typings);
@@ -442,7 +482,7 @@ export function validatePackageJson(pkg: any, includesCommonJS: boolean) {
442482
pkg.typings !== undefined ||
443483
pkg.typescript !== undefined
444484
) {
445-
if (includesCommonJS === true) {
485+
if (opts.includesCommonJS === true) {
446486
// if there is no bin property, we NEED to check the exports.
447487
expect("main", presetFields.main);
448488
expect("module", presetFields.module);

src/commands/check.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ export const checkCommand = createCommand<{}, {}>((api) => {
8888
const distPackageJSONPath = path.join(cwd, "dist", "package.json");
8989
const distPackageJSON = await fse.readJSON(distPackageJSONPath);
9090

91+
// a tell for a types-only build is the lack of main import and presence of typings
92+
if (
93+
distPackageJSON.main === "" &&
94+
(distPackageJSON.typings || "").endsWith("d.ts")
95+
) {
96+
api.reporter.warn(
97+
`Skip check for '${packageJSON.name}' because it's a types-only package.`
98+
);
99+
return;
100+
}
101+
91102
try {
92103
await checkExportsMapIntegrity({
93104
cwd: path.join(cwd, "dist"),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "c",
3+
"main": "",
4+
"typings": "dist/typings/index.d.ts",
5+
"typescript": {
6+
"definition": "dist/typings/index.d.ts"
7+
},
8+
"buildOptions": {
9+
"input": "./src/index.ts"
10+
},
11+
"publishConfig": {
12+
"directory": "dist",
13+
"access": "public"
14+
}
15+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type SomeType = "type";
2+
3+
export interface SomeInterface {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hi types!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "hi": 1 }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "simple-types-only",
3+
"main": "",
4+
"typings": "dist/typings/index.d.ts",
5+
"typescript": {
6+
"definition": "dist/typings/index.d.ts"
7+
},
8+
"publishConfig": {
9+
"directory": "dist",
10+
"access": "public"
11+
},
12+
"type": "module",
13+
"bob": {
14+
"build": {
15+
"copy": [
16+
"foo.json",
17+
"src/style.css"
18+
]
19+
},
20+
"check": {
21+
"skip": [
22+
"./file-that-throws"
23+
]
24+
}
25+
}
26+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type SomeType = "type";
2+
3+
export interface SomeInterface {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"module": "ESNext",
4+
"skipLibCheck": true,
5+
"declaration": true,
6+
"outDir": "dist"
7+
}
8+
}

0 commit comments

Comments
 (0)