diff --git a/.releaserc.json b/.releaserc.json index 52496a6..2c405b0 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -15,14 +15,11 @@ "npmPublish": true } ], - [ - "@semantic-release/github", - {} - ], + ["@semantic-release/github", {}], [ "@semantic-release/git", { - "assets": ["CHANGELOG.md", "package.json"], + "assets": ["CHANGELOG.md", "package.json", "package-lock.json", "src/cli-core.ts"], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ] diff --git a/package-lock.json b/package-lock.json index ea0aebd..6809793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgedatajs", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgedatajs", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "bin": { "forgedata": "dist/cli.js" diff --git a/package.json b/package.json index ba1e595..02d5e95 100644 --- a/package.json +++ b/package.json @@ -29,19 +29,34 @@ }, "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } }, "./react": { - "types": "./dist/react/index.d.ts", - "import": "./dist/react/index.js", - "require": "./dist/react/index.cjs" + "import": { + "types": "./dist/react/index.d.ts", + "default": "./dist/react/index.js" + }, + "require": { + "types": "./dist/react/index.d.cts", + "default": "./dist/react/index.cjs" + } }, "./zod": { - "types": "./dist/zod/index.d.ts", - "import": "./dist/zod/index.js", - "require": "./dist/zod/index.cjs" + "import": { + "types": "./dist/zod/index.d.ts", + "default": "./dist/zod/index.js" + }, + "require": { + "types": "./dist/zod/index.d.cts", + "default": "./dist/zod/index.cjs" + } }, "./package.json": "./package.json" }, @@ -66,7 +81,7 @@ }, "packageManager": "npm@10.0.0", "scripts": { - "build": "tsup && node scripts/chmod-cli.mjs", + "build": "node scripts/clean-dist.mjs && tsup && node scripts/chmod-cli.mjs", "dev": "tsup --watch", "test": "vitest run", "test:watch": "vitest", @@ -82,7 +97,8 @@ "prepublishOnly": "npm run build", "preflight": "npm run lint && npm run typecheck && npm run test:coverage && npm run build", "release:dry-run": "semantic-release --dry-run --no-ci", - "semantic-release": "semantic-release" + "semantic-release": "semantic-release", + "version": "node scripts/sync-version.mjs" }, "peerDependencies": { "react": ">=17.0.0", diff --git a/scripts/clean-dist.mjs b/scripts/clean-dist.mjs new file mode 100644 index 0000000..77a26d4 --- /dev/null +++ b/scripts/clean-dist.mjs @@ -0,0 +1,8 @@ +// Removes dist/ before a build. tsup runs the configs in tsup.config.ts in +// parallel, so letting one of them `clean` could delete another's output. +import { rmSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +rmSync(path.join(__dirname, "..", "dist"), { recursive: true, force: true }); diff --git a/scripts/sync-version.mjs b/scripts/sync-version.mjs new file mode 100644 index 0000000..ee0de74 --- /dev/null +++ b/scripts/sync-version.mjs @@ -0,0 +1,24 @@ +// Keeps CLI_VERSION in src/cli-core.ts in sync with package.json. +// +// Runs as the npm `version` lifecycle script, i.e. whenever `npm version` +// bumps package.json (semantic-release's @semantic-release/npm does exactly +// that in its prepare step). The publish step then rebuilds dist via +// `prepublishOnly`, so `forgedata --version` reports the released version, +// and @semantic-release/git commits the updated source file. +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const { version } = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")); +const file = path.join(root, "src", "cli-core.ts"); +const source = readFileSync(file, "utf8"); +const pattern = /export const CLI_VERSION = "[^"]*";/; + +if (!pattern.test(source)) { + console.error(`sync-version: could not find CLI_VERSION in ${file}`); + process.exit(1); +} + +writeFileSync(file, source.replace(pattern, `export const CLI_VERSION = "${version}";`)); +console.log(`sync-version: CLI_VERSION set to ${version}`); diff --git a/src/cli-core.ts b/src/cli-core.ts index 4b6b308..2591b79 100644 --- a/src/cli-core.ts +++ b/src/cli-core.ts @@ -1,10 +1,11 @@ import { ForgeData } from "./ForgeData.js"; // Kept as a plain constant rather than read from package.json at runtime: -// JSON import assertions are still inconsistent across Node/Bun/Deno/bundlers, -// and this is purely cosmetic output for `--version`. Bump it alongside the -// version field in package.json on release. -export const CLI_VERSION = "0.2.0"; +// JSON import assertions are still inconsistent across Node/Bun/Deno/bundlers. +// It is rewritten automatically by scripts/sync-version.mjs (the npm `version` +// lifecycle script, run by semantic-release when it bumps package.json), and +// tests/cli.test.ts asserts it matches package.json. +export const CLI_VERSION = "0.2.1"; export interface ParsedArgs { command?: string; diff --git a/tests/cli.test.ts b/tests/cli.test.ts index f84b4aa..d53214d 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { parseArgs, runCli, CLI_VERSION, HELP_TEXT, type CliIO } from "../src/cli-core.js"; @@ -42,6 +43,15 @@ describe("parseArgs", () => { }); }); +describe("CLI_VERSION", () => { + it("matches the version in package.json", () => { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { + version: string; + }; + expect(CLI_VERSION).toBe(pkg.version); + }); +}); + describe("runCli", () => { it("prints the version for --version, -v, or the version command", () => { for (const argv of [["--version"], ["-v"], ["version"]]) { diff --git a/tsup.config.ts b/tsup.config.ts index 61fafb2..e1ac484 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,43 +1,29 @@ import { defineConfig } from "tsup"; +// dist/ is wiped by scripts/clean-dist.mjs before tsup runs: tsup builds these +// configs in parallel, so `clean: true` in one could delete another's output. + export default defineConfig([ { - entry: { index: "src/index.ts" }, - format: ["esm", "cjs"], - dts: true, - sourcemap: true, - clean: true, - splitting: false, - treeshake: true, - target: "es2020", - outExtension({ format }) { - return { js: format === "cjs" ? ".cjs" : ".js" }; - }, - }, - { - entry: { "react/index": "src/react/index.tsx" }, - format: ["esm", "cjs"], - dts: true, - sourcemap: true, - clean: false, - splitting: false, - treeshake: true, - target: "es2020", - external: ["react"], - outExtension({ format }) { - return { js: format === "cjs" ? ".cjs" : ".js" }; + // The library entry points are built together (with code splitting) so + // that `forgedatajs`, `forgedatajs/react` and `forgedatajs/zod` share one + // copy of the ForgeData class at runtime *and* in the emitted .d.ts files. + // Building them separately inlined a private copy of ForgeData into each + // subpath, so e.g. `fromZodSchema(new ForgeData(), schema)` failed to + // type-check (and `instanceof ForgeData` differed between entries). + entry: { + index: "src/index.ts", + "react/index": "src/react/index.tsx", + "zod/index": "src/zod/index.ts", }, - }, - { - entry: { "zod/index": "src/zod/index.ts" }, format: ["esm", "cjs"], dts: true, sourcemap: true, clean: false, - splitting: false, + splitting: true, treeshake: true, target: "es2020", - external: ["zod"], + external: ["react", "zod"], outExtension({ format }) { return { js: format === "cjs" ? ".cjs" : ".js" }; },