From 76fd23c0f0887a8e324a92b0cff705214e1883bc Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 24 Oct 2022 14:12:53 +0200 Subject: [PATCH] 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 --- .changeset/tricky-poems-juggle.md | 5 + src/commands/build.ts | 114 ++++++++++++------ src/commands/check.ts | 11 ++ .../simple-monorepo/packages/c/package.json | 15 +++ .../simple-monorepo/packages/c/src/index.ts | 3 + test/__fixtures__/simple-types-only/README.md | 1 + test/__fixtures__/simple-types-only/foo.json | 1 + .../simple-types-only/package.json | 26 ++++ .../simple-types-only/src/index.ts | 3 + .../simple-types-only/tsconfig.json | 8 ++ test/integration.spec.ts | 72 +++++++++++ 11 files changed, 222 insertions(+), 37 deletions(-) create mode 100644 .changeset/tricky-poems-juggle.md create mode 100644 test/__fixtures__/simple-monorepo/packages/c/package.json create mode 100644 test/__fixtures__/simple-monorepo/packages/c/src/index.ts create mode 100644 test/__fixtures__/simple-types-only/README.md create mode 100644 test/__fixtures__/simple-types-only/foo.json create mode 100644 test/__fixtures__/simple-types-only/package.json create mode 100644 test/__fixtures__/simple-types-only/src/index.ts create mode 100644 test/__fixtures__/simple-types-only/tsconfig.json diff --git a/.changeset/tricky-poems-juggle.md b/.changeset/tricky-poems-juggle.md new file mode 100644 index 00000000..321bd6dc --- /dev/null +++ b/.changeset/tricky-poems-juggle.md @@ -0,0 +1,5 @@ +--- +"bob-the-bundler": patch +--- + +Correct package.json for types-only packages diff --git a/src/commands/build.ts b/src/commands/build.ts index f644124b..7519a760 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -223,53 +223,69 @@ async function build({ return; } - validatePackageJson(pkg, config?.commonjs ?? true); - - // remove /dist - await fse.remove(distPath); - - // Copy type definitions - await fse.ensureDir(join(distPath, "typings")); - const declarations = await globby("**/*.d.ts", { cwd: getBuildPath("esm"), absolute: false, ignore: filesToExcludeFromDist, }); - await Promise.all( - declarations.map((filePath) => - limit(() => - fse.copy( - join(getBuildPath("esm"), filePath), - join(distPath, "typings", filePath) - ) - ) - ) - ); - - // Move ESM to dist/esm - await fse.ensureDir(join(distPath, "esm")); - const esmFiles = await globby("**/*.js", { cwd: getBuildPath("esm"), absolute: false, ignore: filesToExcludeFromDist, }); + // Check whether al esm files are empty, if not - probably a types only build + let emptyEsmFiles = true; + for (const file of esmFiles) { + const src = await fse.readFile(join(getBuildPath("esm"), file)); + if (src.toString().trim() !== "export {};") { + emptyEsmFiles = false; + break; + } + } + + // Empty ESM files with existing declarations is a types-only package + const typesOnly = emptyEsmFiles && declarations.length > 0; + + validatePackageJson(pkg, { + typesOnly, + includesCommonJS: config?.commonjs ?? true, + }); + + // remove /dist + await fse.remove(distPath); + + // Copy type definitions + await fse.ensureDir(join(distPath, "typings")); await Promise.all( - esmFiles.map((filePath) => + declarations.map((filePath) => limit(() => fse.copy( join(getBuildPath("esm"), filePath), - join(distPath, "esm", filePath) + join(distPath, "typings", filePath) ) ) ) ); - if (config?.commonjs === undefined) { - // Transpile ESM to CJS and move CJS to dist/cjs + // If ESM files are not empty, copy them to dist/esm + if (!emptyEsmFiles) { + await fse.ensureDir(join(distPath, "esm")); + await Promise.all( + esmFiles.map((filePath) => + limit(() => + fse.copy( + join(getBuildPath("esm"), filePath), + join(distPath, "esm", filePath) + ) + ) + ) + ); + } + + if (!emptyEsmFiles && config?.commonjs === undefined) { + // Transpile ESM to CJS and move CJS to dist/cjs only if there's something to transpile await fse.ensureDir(join(distPath, "cjs")); const cjsFiles = await globby("**/*.js", { @@ -323,8 +339,9 @@ async function build({ // move the package.json to dist await fse.writeFile( join(distPath, "package.json"), - JSON.stringify(rewritePackageJson(pkg), null, 2) + JSON.stringify(rewritePackageJson(pkg, typesOnly), null, 2) ); + // move README.md and LICENSE and other specified files await copyToDist( cwd, @@ -350,7 +367,7 @@ async function build({ reporter.success(`Built ${pkg.name}`); } -function rewritePackageJson(pkg: Record) { +function rewritePackageJson(pkg: Record, typesOnly: boolean) { const newPkg: Record = {}; const fields = [ "name", @@ -382,19 +399,26 @@ function rewritePackageJson(pkg: Record) { const distDirStr = `${DIST_DIR}/`; - newPkg.main = newPkg.main.replace(distDirStr, ""); - newPkg.module = newPkg.module.replace(distDirStr, ""); + if (typesOnly) { + newPkg.main = ""; + delete newPkg.module; + delete newPkg.type; + } else { + newPkg.main = newPkg.main.replace(distDirStr, ""); + newPkg.module = newPkg.module.replace(distDirStr, ""); + } newPkg.typings = newPkg.typings.replace(distDirStr, ""); newPkg.typescript = { definition: newPkg.typescript.definition.replace(distDirStr, ""), }; - if (!pkg.exports) { - newPkg.exports = presetFields.exports; + if (!typesOnly) { + if (!pkg.exports) { + newPkg.exports = presetFields.exports; + } + newPkg.exports = rewriteExports(pkg.exports, DIST_DIR); } - newPkg.exports = rewriteExports(pkg.exports, DIST_DIR); - if (pkg.bin) { newPkg.bin = {}; @@ -406,7 +430,13 @@ function rewritePackageJson(pkg: Record) { return newPkg; } -export function validatePackageJson(pkg: any, includesCommonJS: boolean) { +export function validatePackageJson( + pkg: any, + opts: { + typesOnly: boolean; + includesCommonJS: boolean; + } +) { function expect(key: string, expected: unknown) { const received = get(pkg, key); @@ -418,13 +448,23 @@ export function validatePackageJson(pkg: any, includesCommonJS: boolean) { ); } + // Type only packages have simpler rules (following the style of https://github.com/DefinitelyTyped/DefinitelyTyped packages) + if (opts.typesOnly) { + expect("main", ""); + expect("module", undefined); + expect("typings", presetFields.typings); + expect("typescript.definition", presetFields.typescript.definition); + expect("exports", undefined); + return; + } + // If the package has NO binary we need to check the exports map. // a package should either // 1. have a bin property // 2. have a exports property // 3. have an exports and bin property if (Object.keys(pkg.bin ?? {}).length > 0) { - if (includesCommonJS === true) { + if (opts.includesCommonJS === true) { expect("main", presetFields.main); expect("module", presetFields.module); expect("typings", presetFields.typings); @@ -442,7 +482,7 @@ export function validatePackageJson(pkg: any, includesCommonJS: boolean) { pkg.typings !== undefined || pkg.typescript !== undefined ) { - if (includesCommonJS === true) { + if (opts.includesCommonJS === true) { // if there is no bin property, we NEED to check the exports. expect("main", presetFields.main); expect("module", presetFields.module); diff --git a/src/commands/check.ts b/src/commands/check.ts index 23f4829c..4b469dd3 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -88,6 +88,17 @@ export const checkCommand = createCommand<{}, {}>((api) => { const distPackageJSONPath = path.join(cwd, "dist", "package.json"); const distPackageJSON = await fse.readJSON(distPackageJSONPath); + // a tell for a types-only build is the lack of main import and presence of typings + if ( + distPackageJSON.main === "" && + (distPackageJSON.typings || "").endsWith("d.ts") + ) { + api.reporter.warn( + `Skip check for '${packageJSON.name}' because it's a types-only package.` + ); + return; + } + try { await checkExportsMapIntegrity({ cwd: path.join(cwd, "dist"), diff --git a/test/__fixtures__/simple-monorepo/packages/c/package.json b/test/__fixtures__/simple-monorepo/packages/c/package.json new file mode 100644 index 00000000..3a020acc --- /dev/null +++ b/test/__fixtures__/simple-monorepo/packages/c/package.json @@ -0,0 +1,15 @@ +{ + "name": "c", + "main": "", + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + } +} diff --git a/test/__fixtures__/simple-monorepo/packages/c/src/index.ts b/test/__fixtures__/simple-monorepo/packages/c/src/index.ts new file mode 100644 index 00000000..d1d0c0d6 --- /dev/null +++ b/test/__fixtures__/simple-monorepo/packages/c/src/index.ts @@ -0,0 +1,3 @@ +export type SomeType = "type"; + +export interface SomeInterface {} diff --git a/test/__fixtures__/simple-types-only/README.md b/test/__fixtures__/simple-types-only/README.md new file mode 100644 index 00000000..a8dbe638 --- /dev/null +++ b/test/__fixtures__/simple-types-only/README.md @@ -0,0 +1 @@ +Hi types! diff --git a/test/__fixtures__/simple-types-only/foo.json b/test/__fixtures__/simple-types-only/foo.json new file mode 100644 index 00000000..40aad4dc --- /dev/null +++ b/test/__fixtures__/simple-types-only/foo.json @@ -0,0 +1 @@ +{ "hi": 1 } diff --git a/test/__fixtures__/simple-types-only/package.json b/test/__fixtures__/simple-types-only/package.json new file mode 100644 index 00000000..cde5c349 --- /dev/null +++ b/test/__fixtures__/simple-types-only/package.json @@ -0,0 +1,26 @@ +{ + "name": "simple-types-only", + "main": "", + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module", + "bob": { + "build": { + "copy": [ + "foo.json", + "src/style.css" + ] + }, + "check": { + "skip": [ + "./file-that-throws" + ] + } + } +} diff --git a/test/__fixtures__/simple-types-only/src/index.ts b/test/__fixtures__/simple-types-only/src/index.ts new file mode 100644 index 00000000..d1d0c0d6 --- /dev/null +++ b/test/__fixtures__/simple-types-only/src/index.ts @@ -0,0 +1,3 @@ +export type SomeType = "type"; + +export interface SomeInterface {} diff --git a/test/__fixtures__/simple-types-only/tsconfig.json b/test/__fixtures__/simple-types-only/tsconfig.json new file mode 100644 index 00000000..e58ac7a3 --- /dev/null +++ b/test/__fixtures__/simple-types-only/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext", + "skipLibCheck": true, + "declaration": true, + "outDir": "dist" + } +} diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 3bd70a19..59a10668 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -122,6 +122,13 @@ it("can build a monorepo project", async () => { "b", "dist" ); + const baseDistCPath = path.resolve( + fixturesFolder, + "simple-monorepo", + "packages", + "c", + "dist" + ); // prettier-ignore const files = { a: { @@ -136,6 +143,10 @@ it("can build a monorepo project", async () => { "esm/index.js": path.resolve(baseDistBPath, "esm", "index.js"), "package.json": path.resolve(baseDistBPath, "package.json"), }, + c: { + "typings/index.d.ts": path.resolve(baseDistCPath, "typings", "index.d.ts"), + "package.json": path.resolve(baseDistCPath, "package.json"), + }, } as const; expect(fse.readFileSync(files.a["cjs/index.js"], "utf8")) @@ -294,6 +305,27 @@ it("can build a monorepo project", async () => { }" `); + expect(fse.existsSync(path.resolve(baseDistCPath, "cjs"))).toBeFalsy(); + expect(fse.existsSync(path.resolve(baseDistCPath, "esm"))).toBeFalsy(); + expect(fse.readFileSync(files.c["typings/index.d.ts"], "utf8")) + .toMatchInlineSnapshot(` + "export declare type SomeType = \\"type\\"; + export interface SomeInterface { + } + " + `); + expect(fse.readFileSync(files.c["package.json"], "utf8")) + .toMatchInlineSnapshot(` + "{ + \\"name\\": \\"c\\", + \\"main\\": \\"\\", + \\"typings\\": \\"typings/index.d.ts\\", + \\"typescript\\": { + \\"definition\\": \\"typings/index.d.ts\\" + } + }" + `); + await execa("node", [binaryFolder, "check"], { cwd: path.resolve(fixturesFolder, "simple-monorepo"), }); @@ -345,3 +377,43 @@ it("can build an esm only project", async () => { " `); }); + +it("can build a types only project", async () => { + await fse.remove(path.resolve(fixturesFolder, "simple-types-only", "dist")); + const result = await execa("node", [binaryFolder, "build"], { + cwd: path.resolve(fixturesFolder, "simple-types-only"), + }); + expect(result.exitCode).toEqual(0); + + const baseDistPath = path.resolve( + fixturesFolder, + "simple-types-only", + "dist" + ); + + // types-only adjusted package.json + const packageJsonFilePath = path.resolve(baseDistPath, "package.json"); + expect(fse.readFileSync(packageJsonFilePath, "utf8")).toMatchInlineSnapshot(` + "{ + \\"name\\": \\"simple-types-only\\", + \\"main\\": \\"\\", + \\"typings\\": \\"typings/index.d.ts\\", + \\"typescript\\": { + \\"definition\\": \\"typings/index.d.ts\\" + } + }" + `); + + // no cjs or esm files + expect(fse.existsSync(path.resolve(baseDistPath, "cjs"))).toBeFalsy(); + expect(fse.existsSync(path.resolve(baseDistPath, "esm"))).toBeFalsy(); + + // only types + const indexDtsFilePath = path.resolve(baseDistPath, "typings", "index.d.ts"); + expect(fse.readFileSync(indexDtsFilePath, "utf8")).toMatchInlineSnapshot(` + "export declare type SomeType = \\"type\\"; + export interface SomeInterface { + } + " + `); +});