From 6ab618158b071e54e908a93dbc4082dcf3cea45c Mon Sep 17 00:00:00 2001 From: abrulic Date: Wed, 22 Jul 2026 00:52:07 +0200 Subject: [PATCH] imports per feature --- CLAUDE.md | 7 +++++-- README.md | 32 ++++++++++++++++++++++++++++++++ package.json | 4 ++++ src/generate/index.ts | 15 +++++++++++++++ tsup.config.ts | 21 +++++++++++++++------ 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a91068b..3a53849 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,9 +9,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co The package ships **two** tsup ESM builds ([tsup.config.ts](tsup.config.ts) exports an array): - `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` + `.d.ts` — the `.` entry, 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/generate/index.ts` → `dist/generate.js` + `.d.ts` — the `./generate` entry: `planFiles`/`writeFiles` plus the pure `generate*` functions, for callers building on top of deploykit. -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. +So [src/config.ts](src/config.ts) and [src/generate/index.ts](src/generate/index.ts) are **published public API**: anything exported there ships with types, a signature change is a breaking change, and any import added to them lands in consumers' dependency graph. Keep them pure — no `@clack/prompts`, no interactive stdio, no shelling out. The provider clients (`cloudflare`, `fly`, `provision`, `deploy`, `pr`) are deliberately *not* exported; they're shaped around the CLI's flow. + +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 import both entries at runtime **and** run `tsc` over a file that uses them. A missing `exports` entry fails only at install-time for real users, never in this repo's own tests. ## Commands diff --git a/README.md b/README.md index 57a2a0c..4b8a933 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ deploykit reads your workspace graph, figures out which apps are deployable, and - [Commands & flags](#commands--flags) - [What it generates](#what-it-generates) - [The config file](#the-config-file) +- [Programmatic use](#programmatic-use) - [Environments](#environments) - [Secrets & environment variables](#secrets--environment-variables) - [Health checks & automatic rollback](#health-checks--automatic-rollback) @@ -188,6 +189,37 @@ The full file — including the Prisma target, the `marketing` app, and the Clou The one rule is that every value must be **literal data**: strings, numbers, booleans, arrays, objects. Variables, imports, spreads and template placeholders are rejected (with the offending line) rather than guessed at. Everything else is fair game — quoted or bare keys, single or double quotes, `//` and `/* */` comments, trailing commas — so running Prettier, Biome, or your editor's formatter over the file is safe. +## Programmatic use + +deploykit is a CLI first, but the pieces the CLI is built from are published too, so you can generate the same files from your own tooling. Two entry points, both ESM and fully typed: + +```ts +import { defineConfig } from "@alminabrulic/deploykit"; +import { + planFiles, + writeFiles, + generateDockerfile, + generateFlyToml, + generateWorkflow, + generateSummary, + generateDockerignore, +} from "@alminabrulic/deploykit/generate"; + +const config = defineConfig({ /* ... */ }); + +// Every file deploykit would write, each tagged new / identical / modified: +const files = planFiles({ config, cwd: process.cwd() }); +files.forEach((f) => console.log(f.status, f.path)); +writeFiles({ files, cwd: process.cwd(), force: false }); + +// Or just one artifact, as a string — no disk access at all: +const dockerfile = generateDockerfile({ name: "web", app: config.apps.web, config }); +``` + +The individual `generate*` functions are pure: config in, string out, no filesystem or network. `planFiles` reads the target directory only to classify each file against what's already there, and `writeFiles` is the only one that writes (skipping existing files unless `force`). + +Provisioning, deploys and the Fly/Cloudflare/GitHub clients are deliberately **not** exported — they shell out to `flyctl`/`gh` and are shaped around the CLI's flow rather than a stable API. + ## Environments deploykit models three environments; you choose which with `--envs`. diff --git a/package.json b/package.json index b105e46..61bfbb4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "types": "./dist/config.d.ts", "default": "./dist/config.js" }, + "./generate": { + "types": "./dist/generate.d.ts", + "default": "./dist/generate.js" + }, "./package.json": "./package.json" }, "files": [ diff --git a/src/generate/index.ts b/src/generate/index.ts index 81a0302..f224bb8 100644 --- a/src/generate/index.ts +++ b/src/generate/index.ts @@ -10,6 +10,21 @@ import { generateFlyToml } from "./flytoml.js"; import { generateSummary } from "./summary.js"; import { generateWorkflow } from "./workflow.js"; +/** + * This module is the published `@alminabrulic/deploykit/generate` entry, so + * everything exported here is public API — changing a signature is a breaking + * change. `planFiles`/`writeFiles` are the high-level pair (every file for a + * config, then write them); the individual generators are re-exported for + * callers that just want one artifact as a string, with no disk access at all. + */ +export { generateConfigFile } from "./configfile.js"; +export { generateDockerfile } from "./dockerfile.js"; +export { generateDockerignore } from "./dockerignore.js"; +export { generateFlyToml } from "./flytoml.js"; +export { generateSummary } from "./summary.js"; +export type { GenerateAppFileInput } from "./types.js"; +export { generateWorkflow } from "./workflow.js"; + /** * How a generated file compares to what's already on disk: * - `new`: nothing there yet. diff --git a/tsup.config.ts b/tsup.config.ts index 2d6b54b..957013d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -11,13 +11,22 @@ export default defineConfig([ banner: { js: "#!/usr/bin/env node" }, }, { - // The library entry. Every generated `deploykit.config.ts` starts with - // `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, + // The library entries, built together so their shared internals land in one + // chunk instead of being duplicated into each file. + // + // `config` is load-bearing, not a nicety: every generated + // `deploykit.config.ts` starts with `import { defineConfig } from + // "@alminabrulic/deploykit"`, and without this build (plus the matching + // "exports" in package.json) that import resolves to nothing — the user's + // config then fails to typecheck. `dts` is what types it in their editor, // which is the whole reason the config is TypeScript rather than JSON. - entry: ["src/config.ts"], + // + // `generate` exposes the pure generators for callers building on top of + // deploykit. Keep these entries pure: no prompts, no interactive stdio. + entry: { + config: "src/config.ts", + generate: "src/generate/index.ts", + }, format: ["esm"], target: "node20", dts: true,