diff --git a/CLAUDE.md b/CLAUDE.md index a91068b..326772c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co `@alminabrulic/deploykit` — a local-only TypeScript CLI (ESM, Node ≥ 20) that reads a Turbo or Nx monorepo and emits the CI/CD files to deploy it to Fly.io: Dockerfiles, `fly.toml`, a GitHub Actions workflow, `.dockerignore`, `DEPLOYMENTS.md`, and `deploykit.config.ts`. There is no backend, no runtime, and no telemetry — the output is files the user owns. -The package ships **two** tsup ESM builds ([tsup.config.ts](tsup.config.ts) exports an array): +The `build` script is `tsup && tsc -p tsconfig.dts.json`. tsup ([tsup.config.ts](tsup.config.ts) exports an array) emits the two ESM bundles; `tsc` emits the one `.d.ts`: - `src/index.ts` → `dist/index.js` — the CLI (`bin`, shebang banner, owns `clean`). -- `src/config.ts` → `dist/config.js` + `.d.ts` — the library entry behind `package.json` `exports`, because every generated `deploykit.config.ts` opens with `import { defineConfig } from "@alminabrulic/deploykit"`. Without it that import resolves to nothing and the user's config fails to typecheck — which is the entire reason the config is TypeScript instead of JSON. +- `src/config.ts` → `dist/config.js` (tsup) + `dist/config.d.ts` (tsc, via [tsconfig.dts.json](tsconfig.dts.json)) — the library entry behind `package.json` `exports`, because every generated `deploykit.config.ts` opens with `import { defineConfig } from "@alminabrulic/deploykit"`. Without it that import resolves to nothing and the user's config fails to typecheck — which is the entire reason the config is TypeScript instead of JSON. + +The declaration is emitted by `tsc`, not tsup's `dts`, because the TS7 native compiler doesn't expose the classic programmatic API that tsup's `dts` (rollup-plugin-dts) drives — enabling it crashes the build. `config.ts` is self-contained (no imports), so a plain single-file `tsc --emitDeclarationOnly` produces the same bundled declaration. Keeping `config.ts` import-free is therefore load-bearing: an import would split the emit across files and pull that module into the published types. So [src/config.ts](src/config.ts) is **published public API**: anything exported there ships with types, and any import added to it lands in consumers' dependency graph. Keep it types + `defineConfig` + pure constants. After touching either build or the `exports` map, verify like a consumer, not just with `pnpm build`: `npm pack`, install the tarball in a temp project, then `node -e "import('@alminabrulic/deploykit')"` and `tsc` over a generated config. diff --git a/README.md b/README.md index 57a2a0c..c4a3fba 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ To report a vulnerability, please use [GitHub's private vulnerability reporting] ```bash pnpm install pnpm dev init --dry-run # run the CLI from source (tsx) -pnpm build # bundle to dist/ (tsup) +pnpm build # bundle to dist/ (tsup) + emit config.d.ts (tsc) pnpm test # vitest pnpm typecheck # tsc --noEmit pnpm lint # biome check diff --git a/package.json b/package.json index ebf6f9f..460aea3 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ ] }, "scripts": { - "build": "tsup", + "build": "tsup && tsc -p tsconfig.dts.json", "dev": "tsx src/index.ts", "start": "node dist/index.js", "typecheck": "tsc --noEmit", diff --git a/tsconfig.dts.json b/tsconfig.dts.json new file mode 100644 index 0000000..d47e091 --- /dev/null +++ b/tsconfig.dts.json @@ -0,0 +1,16 @@ +{ + // Emits only `dist/config.d.ts` — the published types behind the package + // `exports`. Kept separate from the base config (which is typecheck-only, + // `declaration: false`) and run after `tsup` in the `build` script. `tsup`'s + // own `dts` can't be used: the TS7 native compiler doesn't expose the + // programmatic API rollup-plugin-dts needs. Only `src/config.ts` is emitted; + // it has no imports, so a single-file declaration emit is self-contained. + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/config.ts"] +} diff --git a/tsup.config.ts b/tsup.config.ts index 2d6b54b..ab8ab66 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -15,12 +15,17 @@ export default defineConfig([ // `import { defineConfig } from "@alminabrulic/deploykit"` — without this // build (and the matching "exports" in package.json) that import resolves // to nothing, so the user's config fails to typecheck and can't be loaded - // by their own tooling. `dts` is what makes the config typed in an editor, - // which is the whole reason the config is TypeScript rather than JSON. + // by their own tooling. + // + // The matching `dist/config.d.ts` — what makes the config typed in an + // editor, the whole reason the config is TypeScript rather than JSON — is + // emitted by `tsc -p tsconfig.dts.json` in the `build` script, not here: + // the TS7 native compiler doesn't expose the classic programmatic API that + // tsup's `dts` (rollup-plugin-dts) drives. `config.ts` is self-contained + // (no imports), so a plain single-file declaration emit is equivalent. entry: ["src/config.ts"], format: ["esm"], target: "node20", - dts: true, minify: false, }, ]);