diff --git a/.changeset/proud-dodos-carry.md b/.changeset/proud-dodos-carry.md new file mode 100644 index 00000000..c4607630 --- /dev/null +++ b/.changeset/proud-dodos-carry.md @@ -0,0 +1,5 @@ +--- +"@preconstruct/cli": minor +--- + +`package.json#types`/`package.json#typings` fields will now be validated and fixed. They are not needed at all for TypeScript to consume packages built using Preconstruct but when they are present it's best to fix them to avoid confusion. diff --git a/packages/cli/src/__tests__/fix.ts b/packages/cli/src/__tests__/fix.ts index 8bebc573..84e6d413 100644 --- a/packages/cli/src/__tests__/fix.ts +++ b/packages/cli/src/__tests__/fix.ts @@ -72,6 +72,101 @@ test("set main and module field", async () => { `); }); +test("set types with no extra entrypoints", async () => { + let tmpPath = await testdir({ + "package.json": JSON.stringify({ + name: "@blah/something", + main: "index.js", + types: "invalid.d.ts", + }), + "src/index.js": "", + }); + + await fix(tmpPath); + + expect(await getFiles(tmpPath, ["**/package.json"])).toMatchInlineSnapshot(` + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ package.json ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + { + "name": "@blah/something", + "main": "dist/blah-something.cjs.js", + "types": "dist/blah-something.cjs.d.ts" + } + + `); +}); + +test("set typings with no extra entrypoints", async () => { + let tmpPath = await testdir({ + "package.json": JSON.stringify({ + name: "@blah/something", + main: "index.js", + typings: "invalid.d.ts", + }), + "src/index.js": "", + }); + + await fix(tmpPath); + + expect(await getFiles(tmpPath, ["**/package.json"])).toMatchInlineSnapshot(` + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ package.json ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + { + "name": "@blah/something", + "main": "dist/blah-something.cjs.js", + "typings": "dist/blah-something.cjs.d.ts" + } + + `); +}); + +test("set types field with multiple entrypoints", async () => { + let tmpPath = await testdir({ + "package.json": JSON.stringify({ + name: "@blah/something", + main: "index.js", + types: "invalid.d.ts", + preconstruct: { + entrypoints: ["index.js", "other.js", "deep/something.js"], + }, + }), + "other/package.json": JSON.stringify({}), + "deep/something/package.json": JSON.stringify({}), + "src/index.js": "", + "src/other.js": "", + "src/deep/something.js": "", + }); + + await fix(tmpPath); + + expect(await getFiles(tmpPath, ["**/package.json"])).toMatchInlineSnapshot(` + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ deep/something/package.json ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + { + "main": "dist/blah-something-deep-something.cjs.js", + "types": "dist/blah-something-deep-something.cjs.d.ts" + } + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ other/package.json ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + { + "main": "dist/blah-something-other.cjs.js", + "types": "dist/blah-something-other.cjs.d.ts" + } + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ package.json ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + { + "name": "@blah/something", + "main": "dist/blah-something.cjs.js", + "types": "dist/blah-something.cjs.d.ts", + "preconstruct": { + "entrypoints": [ + "index.js", + "other.js", + "deep/something.js" + ] + } + } + + `); +}); + test("set exports field when opt-in", async () => { let tmpPath = await testdir({ "package.json": JSON.stringify({ diff --git a/packages/cli/src/__tests__/validate.ts b/packages/cli/src/__tests__/validate.ts index 67ffadc0..b80533c3 100644 --- a/packages/cli/src/__tests__/validate.ts +++ b/packages/cli/src/__tests__/validate.ts @@ -139,6 +139,36 @@ test("valid browser", async () => { `); }); +test("invalid types field", async () => { + let tmpPath = await testdir({ + "package.json": JSON.stringify({ + name: "@blah/something", + main: "dist/blah-something.cjs.js", + types: "invalid.d.ts", + }), + "src/index.js": "", + }); + + await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot( + `[Error: types field is invalid, found \`"invalid.d.ts"\`, expected \`"dist/blah-something.cjs.d.ts"\`]` + ); +}); + +test("invalid typings field", async () => { + let tmpPath = await testdir({ + "package.json": JSON.stringify({ + name: "@blah/something", + main: "dist/blah-something.cjs.js", + typings: "invalid.d.ts", + }), + "src/index.js": "", + }); + + await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot( + `[Error: typings field is invalid, found \`"invalid.d.ts"\`, expected \`"dist/blah-something.cjs.d.ts"\`]` + ); +}); + test("monorepo single package", async () => { let tmpPath = f.copy("monorepo-single-package"); diff --git a/packages/cli/src/entrypoint.ts b/packages/cli/src/entrypoint.ts index 7deb43db..c24846df 100644 --- a/packages/cli/src/entrypoint.ts +++ b/packages/cli/src/entrypoint.ts @@ -9,6 +9,8 @@ export class Entrypoint extends Item<{ module?: JSONValue; "umd:main"?: JSONValue; browser?: JSONValue; + types?: JSONValue; + typings?: JSONValue; exports?: Record; preconstruct: { source?: JSONValue; diff --git a/packages/cli/src/fix.ts b/packages/cli/src/fix.ts index 95371f47..7e1d0964 100644 --- a/packages/cli/src/fix.ts +++ b/packages/cli/src/fix.ts @@ -27,6 +27,8 @@ async function fixPackage(pkg: Package): Promise<() => Promise> { !!exportsFieldConfig, "umd:main": pkg.entrypoints.some((x) => x.json["umd:main"] !== undefined), browser: pkg.entrypoints.some((x) => x.json.browser !== undefined), + types: pkg.entrypoints.some((x) => x.json.types !== undefined), + typings: pkg.entrypoints.some((x) => x.json.typings !== undefined), }; if (exportsFieldConfig?.conditions.kind === "legacy") { diff --git a/packages/cli/src/messages.ts b/packages/cli/src/messages.ts index 0d2e2ce6..fb7bb6b4 100644 --- a/packages/cli/src/messages.ts +++ b/packages/cli/src/messages.ts @@ -2,7 +2,14 @@ import { PKG_JSON_CONFIG_FIELD } from "./constants"; import { createPromptConfirmLoader } from "./prompt"; import chalk from "chalk"; -type Field = "main" | "module" | "browser" | "umd:main" | "exports"; +type Field = + | "main" + | "module" + | "browser" + | "umd:main" + | "exports" + | "types" + | "typings"; export let errors = { noSource: (source: string) => diff --git a/packages/cli/src/package.ts b/packages/cli/src/package.ts index b24bc45b..cef76a53 100644 --- a/packages/cli/src/package.ts +++ b/packages/cli/src/package.ts @@ -289,7 +289,7 @@ export class Package extends Item<{ return pkg; } - setFieldOnEntrypoints(field: "main" | "browser" | "module" | "umd:main") { + setFieldOnEntrypoints(field: keyof typeof validFieldsForEntrypoint) { this.entrypoints.forEach((entrypoint) => { entrypoint.json = setFieldInOrder( entrypoint.json, diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 7f6e8766..bd35ac06 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -21,7 +21,7 @@ let fields = [ export function setFieldInOrder< Obj extends { [key: string]: any }, - Key extends "main" | "module" | "umd:main" | "browser" | "exports", + Key extends keyof typeof validFieldsForEntrypoint | "exports", Val extends any >(obj: Obj, field: Key, value: Val): Obj & { [k in Key]: Val } { if (field in obj) { @@ -277,6 +277,9 @@ export function getExportsImportUnwrappingDefaultOutputPath( return getExportsFieldOutputPath(entrypoint, "cjs").replace(/\.js$/, ".mjs"); } +const validTypesFieldForEntrypoint = (entrypoint: MinimalEntrypoint) => + validFieldsForEntrypoint.main(entrypoint).replace(/\.js$/, ".d.ts"); + export const validFieldsForEntrypoint = { main(entrypoint: MinimalEntrypoint) { return getDistFilename(entrypoint, "cjs"); @@ -307,6 +310,8 @@ export const validFieldsForEntrypoint = { ...(entrypoint.hasModuleField && moduleBuild), }; }, + types: validTypesFieldForEntrypoint, + typings: validTypesFieldForEntrypoint, }; export function flowTemplate(hasDefaultExport: boolean, relativePath: string) { diff --git a/packages/cli/src/validate.ts b/packages/cli/src/validate.ts index 8e8cdb0f..99be653a 100644 --- a/packages/cli/src/validate.ts +++ b/packages/cli/src/validate.ts @@ -42,6 +42,14 @@ export const isFieldValid = { // JSON.stringify to make sure conditions are in proper order return JSON.stringify(pkg.json.exports) === JSON.stringify(generated); }, + types(entrypoint: Entrypoint) { + return entrypoint.json.types === validFieldsForEntrypoint.types(entrypoint); + }, + typings(entrypoint: Entrypoint) { + return ( + entrypoint.json.typings === validFieldsForEntrypoint.typings(entrypoint) + ); + }, }; export function isUmdNameSpecified(entrypoint: Entrypoint) { @@ -55,7 +63,14 @@ function validateEntrypoint(entrypoint: Entrypoint, log: boolean) { logger.info(infos.validEntrypoint, entrypoint.name); } const fatalErrors: FatalError[] = []; - for (const field of ["main", "module", "umd:main", "browser"] as const) { + for (const field of [ + "main", + "module", + "umd:main", + "browser", + "types", + "typings", + ] as const) { if (field !== "main" && entrypoint.json[field] === undefined) { continue; }