Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix types & typings fields #562

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-dodos-carry.md
Original file line number Diff line number Diff line change
@@ -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.
95 changes: 95 additions & 0 deletions packages/cli/src/__tests__/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/__tests__/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export class Entrypoint extends Item<{
module?: JSONValue;
"umd:main"?: JSONValue;
browser?: JSONValue;
types?: JSONValue;
typings?: JSONValue;
exports?: Record<string, ExportsConditions | string>;
preconstruct: {
source?: JSONValue;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ async function fixPackage(pkg: Package): Promise<() => Promise<boolean>> {
!!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") {
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -307,6 +310,8 @@ export const validFieldsForEntrypoint = {
...(entrypoint.hasModuleField && moduleBuild),
};
},
types: validTypesFieldForEntrypoint,
typings: validTypesFieldForEntrypoint,
};

export function flowTemplate(hasDefaultExport: boolean, relativePath: string) {
Expand Down
17 changes: 16 additions & 1 deletion packages/cli/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down