diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9a27cd..6f0b42f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ # Default owners for this repository. # GitHub: openmirai/mirai-openapi-codegen -# npm: @openmirai/openapi-codegen +# npm: @openmirai/typeforge * @openmirai/platform-foundations @openmirai/openmirai-engineer diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5ba6936..f9f6e59 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -94,8 +94,8 @@ jobs: - name: Publish package run: | set -euo pipefail - if npm view "@openmirai/openapi-codegen@${RELEASE_VERSION}" version >/dev/null 2>&1; then - echo "@openmirai/openapi-codegen@${RELEASE_VERSION} is already published" + if npm view "@openmirai/typeforge@${RELEASE_VERSION}" version >/dev/null 2>&1; then + echo "@openmirai/typeforge@${RELEASE_VERSION} is already published" else npm publish --access public fi diff --git a/.gitignore b/.gitignore index 67743f0..d8e3c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ dist/ node_modules/ coverage/ +.typeforge/ .openapi-codegen/ test/fixtures/layouts/ *.tsbuildinfo .tmp/ +typeforge.local.json openapi-codegen.local.json .env diff --git a/README.md b/README.md index 41ef85d..5226406 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ -# @openmirai/openapi-codegen +

+ Typeforge logo +

-Headless **OpenAPI / Swagger → TypeScript** codegen. The CLI is `openapi-codegen`. It reads a spec, writes typed route enums, request types, and HTTP caller functions, and never talks to a network. +# @openmirai/typeforge -- **npm:** [`@openmirai/openapi-codegen`](https://www.npmjs.com/package/@openmirai/openapi-codegen) +Headless **OpenAPI / Swagger → TypeScript** codegen. The CLI is `typeforge`. It reads a spec, writes typed route enums, request types, and HTTP caller functions, and never talks to a network. + +- **npm:** [`@openmirai/typeforge`](https://www.npmjs.com/package/@openmirai/typeforge) - **GitHub:** [openmirai/mirai-openapi-codegen](https://github.com/openmirai/mirai-openapi-codegen) You own `http.ts` (the `HTTPFetch` adapter). Generated files import that adapter — they do not invent axios/fetch calls inline. +## Migrating from `@openmirai/openapi-codegen` + +Install `@openmirai/typeforge` and update package imports and scripts to use the canonical `typeforge` name. During migration, the package also exposes the legacy `openapi-codegen` binary and reads `openapi-codegen.json`, `openapi-codegen.local.json`, and the `openapiCodegen` package.json key. New projects created by `typeforge init` use the Typeforge names. + ## What it generates For each **source** (a named API, e.g. `atlas`), under `//generated/`: @@ -22,7 +30,7 @@ For each **source** (a named API, e.g. `atlas`), under `//gener Optional: - **TanStack Query** — set `tanstackQuery: true` in `source.ts` **and** add `/query-scope.ts`. -- **Zod** — wrap a schema with `createZodValidator` from `@openmirai/openapi-codegen/validation/zod` and pass it as `config.validateResponse`. +- **Zod** — wrap a schema with `createZodValidator` from `@openmirai/typeforge/validation/zod` and pass it as `config.validateResponse`. ## Install @@ -30,10 +38,10 @@ Requires **Node.js 20.11+** (LTS). Use any package manager. | Package manager | Install | | --- | --- | -| npm | `npm install --save-dev @openmirai/openapi-codegen` | -| pnpm | `pnpm add -D @openmirai/openapi-codegen` | -| yarn | `yarn add -D @openmirai/openapi-codegen` | -| bun | `bun add -d @openmirai/openapi-codegen` | +| npm | `npm install --save-dev @openmirai/typeforge` | +| pnpm | `pnpm add -D @openmirai/typeforge` | +| yarn | `yarn add -D @openmirai/typeforge` | +| bun | `bun add -d @openmirai/typeforge` | Axios is an **optional peer**. Install `axios` only if you use `--client axios`. @@ -42,7 +50,7 @@ Add a script so every package manager resolves the CLI from `node_modules/.bin`: ```json { "scripts": { - "generate:types": "openapi-codegen generate --all" + "generate:types": "typeforge generate --all" } } ``` @@ -55,10 +63,10 @@ Prefer the `package.json` script above. To invoke the binary directly: | Command | npm | pnpm | yarn | bun | | --- | --- | --- | --- | --- | -| Init a source | `npx openapi-codegen init --source atlas --client axios` | `pnpm exec openapi-codegen init --source atlas --client axios` | `yarn openapi-codegen init --source atlas --client axios` | `bunx openapi-codegen init --source atlas --client axios` | -| Generate one source | `npx openapi-codegen generate --source atlas` | `pnpm exec openapi-codegen generate --source atlas` | `yarn openapi-codegen generate --source atlas` | `bunx openapi-codegen generate --source atlas` | -| Generate all sources | `npx openapi-codegen generate --all` | `pnpm exec openapi-codegen generate --all` | `yarn openapi-codegen generate --all` | `bunx openapi-codegen generate --all` | -| Drift check (CI) | `npx openapi-codegen generate --all --check` | `pnpm exec openapi-codegen generate --all --check` | `yarn openapi-codegen generate --all --check` | `bunx openapi-codegen generate --all --check` | +| Init a source | `npx typeforge init --source atlas --client axios` | `pnpm exec typeforge init --source atlas --client axios` | `yarn typeforge init --source atlas --client axios` | `bunx typeforge init --source atlas --client axios` | +| Generate one source | `npx typeforge generate --source atlas` | `pnpm exec typeforge generate --source atlas` | `yarn typeforge generate --source atlas` | `bunx typeforge generate --source atlas` | +| Generate all sources | `npx typeforge generate --all` | `pnpm exec typeforge generate --all` | `yarn typeforge generate --all` | `bunx typeforge generate --all` | +| Drift check (CI) | `npx typeforge generate --all --check` | `pnpm exec typeforge generate --all --check` | `yarn typeforge generate --all --check` | `bunx typeforge generate --all --check` | | Subcommand | Purpose | | --- | --- | @@ -78,15 +86,15 @@ init → source.ts + http.ts → resolve spec → generate → typed cal ### 1. Init a source ```bash -openapi-codegen init --source atlas --client axios -openapi-codegen init --source orbit --client fetch --layout packages +typeforge init --source atlas --client axios +typeforge init --source orbit --client fetch --layout packages ``` `--client` is `axios` | `fetch` | `custom`. `--layout` is `monolith` (default, `apiRoot` = `src/api`) or `packages` (`apiRoot` = `packages/utils/src/api`). Init creates (if missing): -- `openapi-codegen.json` with `apiRoot` +- `typeforge.json` with `apiRoot` - `/http.ts` — your `HTTPFetch` implementation - `/known-types.ts` — optional schema → local type mapping - `//source.ts` — per-API config (type-safe template) @@ -99,7 +107,7 @@ Existing files are skipped. Use `defineSourceConfig` for autocomplete and compile-time checks: ```ts -import { defineSourceConfig } from "@openmirai/openapi-codegen"; +import { defineSourceConfig } from "@openmirai/typeforge"; export default defineSourceConfig({ spec: "./specs/acme.json", @@ -156,7 +164,7 @@ Re-exported types from the package root: 1. `--spec ` 2. Env `OPENAPI_SPEC_` — source key uppercased, hyphens → underscores 3. `spec` in that source’s `source.ts` -4. `openapi-codegen.local.json` (gitignored) map of `{ "": "" }` +4. `typeforge.local.json` (gitignored) map of `{ "": "" }` 5. Committed snapshot `//spec.json` ### 4. Envelope modes @@ -181,7 +189,7 @@ Axios and Fetch adapters. ### 5. HTTPFetch (`http.ts`) -Adapters implement `HTTPFetch` from `@openmirai/openapi-codegen/http` (or the axios/fetch adapter packages). Methods return `Promise<{ data: TResponse }>`. +Adapters implement `HTTPFetch` from `@openmirai/typeforge/http` (or the axios/fetch adapter packages). Methods return `Promise<{ data: TResponse }>`. - If `http.ts` **exports `httpFetch`**, generated functions call that singleton. - Otherwise they take `props.http: HTTPFetch` (injected). @@ -198,13 +206,13 @@ types import the generated base declaration (`base.ts` in monolith output, or ## Where files go -`openapi-codegen.json`: +`typeforge.json`: ```json { "apiRoot": "packages/utils/src/api" } ``` -You can also set `"openapiCodegen": { "apiRoot": "..." }` in `package.json`. The JSON file wins. +You can also set `"typeforge": { "apiRoot": "..." }` in `package.json`. The JSON file wins. **Monolith** (`--layout monolith`, default): @@ -228,7 +236,7 @@ aliases automatically. ## Zod (optional) ```ts -import { createZodValidator } from "@openmirai/openapi-codegen/validation/zod"; +import { createZodValidator } from "@openmirai/typeforge/validation/zod"; import { widgetListSchema } from "./widget-list"; await getWidgets({ @@ -243,7 +251,7 @@ Publishes go through [npm Trusted Publishing](https://docs.npmjs.com/trusted-pub | | Value | | --- | --- | -| npm package | `@openmirai/openapi-codegen` | +| npm package | `@openmirai/typeforge` | | GitHub repo | `openmirai/mirai-openapi-codegen` | | Workflow | `.github/workflows/publish.yml` | | Tag | `v*` (e.g. `v0.1.3`) | diff --git a/assets/typeforge-logo.png b/assets/typeforge-logo.png new file mode 100644 index 0000000..ae3aaff Binary files /dev/null and b/assets/typeforge-logo.png differ diff --git a/docs/cli.md b/docs/cli.md index 045389a..5513e32 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,11 +1,13 @@ # CLI reference -Install `@openmirai/openapi-codegen` from [npmjs](https://www.npmjs.com/package/@openmirai/openapi-codegen). Add a script so the binary resolves from `node_modules/.bin`: +Install `@openmirai/typeforge` from [npmjs](https://www.npmjs.com/package/@openmirai/typeforge). Add a script so the binary resolves from `node_modules/.bin`: + +The legacy `openapi-codegen` binary and configuration filenames remain readable during migration, but all new usage should use `typeforge`. ```json { "scripts": { - "generate:types": "openapi-codegen generate --all" + "generate:types": "typeforge generate --all" } } ``` @@ -14,10 +16,10 @@ Install `@openmirai/openapi-codegen` from [npmjs](https://www.npmjs.com/package/ | Package manager | Command | | --- | --- | -| npm | `npm install --save-dev @openmirai/openapi-codegen` | -| pnpm | `pnpm add -D @openmirai/openapi-codegen` | -| yarn | `yarn add -D @openmirai/openapi-codegen` | -| bun | `bun add -d @openmirai/openapi-codegen` | +| npm | `npm install --save-dev @openmirai/typeforge` | +| pnpm | `pnpm add -D @openmirai/typeforge` | +| yarn | `yarn add -D @openmirai/typeforge` | +| bun | `bun add -d @openmirai/typeforge` | ## Commands × package managers @@ -25,10 +27,10 @@ Replace `` with the flags for that subcommand (see below). | Subcommand | npm | pnpm | yarn | bun | | --- | --- | --- | --- | --- | -| `init ` | `npx openapi-codegen init ` | `pnpm exec openapi-codegen init ` | `yarn openapi-codegen init ` | `bunx openapi-codegen init ` | -| `generate ` | `npx openapi-codegen generate ` | `pnpm exec openapi-codegen generate ` | `yarn openapi-codegen generate ` | `bunx openapi-codegen generate ` | -| `check ` | `npx openapi-codegen check ` | `pnpm exec openapi-codegen check ` | `yarn openapi-codegen check ` | `bunx openapi-codegen check ` | -| `accept-base ` | `npx openapi-codegen accept-base ` | `pnpm exec openapi-codegen accept-base ` | `yarn openapi-codegen accept-base ` | `bunx openapi-codegen accept-base ` | +| `init ` | `npx typeforge init ` | `pnpm exec typeforge init ` | `yarn typeforge init ` | `bunx typeforge init ` | +| `generate ` | `npx typeforge generate ` | `pnpm exec typeforge generate ` | `yarn typeforge generate ` | `bunx typeforge generate ` | +| `check ` | `npx typeforge check ` | `pnpm exec typeforge check ` | `yarn typeforge check ` | `bunx typeforge check ` | +| `accept-base ` | `npx typeforge accept-base ` | `pnpm exec typeforge accept-base ` | `yarn typeforge accept-base ` | `bunx typeforge accept-base ` | Recommended day-to-day: `npm run generate:types` (or the equivalent for your package manager). @@ -37,7 +39,7 @@ Recommended day-to-day: `npm run generate:types` (or the equivalent for your pac ### `init` ```bash -openapi-codegen init --source --client axios|fetch|custom [--layout monolith|packages] +typeforge init --source --client axios|fetch|custom [--layout monolith|packages] ``` Creates `http.ts`, `known-types.ts`, `source.ts`, and `generated/` under `apiRoot`. @@ -45,8 +47,8 @@ Creates `http.ts`, `known-types.ts`, `source.ts`, and `generated/` under `apiRoo ### `generate` ```bash -openapi-codegen generate --source [--source ...] [--spec ] [--check] [--accept-base] -openapi-codegen generate --all [--check] [--accept-base] +typeforge generate --source [--source ...] [--spec ] [--check] [--accept-base] +typeforge generate --all [--check] [--accept-base] ``` `--all` walks every directory under `apiRoot` that contains `source.ts`. @@ -78,13 +80,13 @@ Updates `generated/base.ts` and patches `models.ts` `BaseResponse` to match the 1. `--spec ` 2. Environment variable `OPENAPI_SPEC_` (key uppercased, `-` → `_`) 3. `spec` field in `//source.ts` -4. `openapi-codegen.local.json` (gitignored) +4. `typeforge.local.json` (gitignored) 5. `//spec.json` snapshot ## Type-safe `source.ts` ```ts -import { defineSourceConfig } from "@openmirai/openapi-codegen"; +import { defineSourceConfig } from "@openmirai/typeforge"; export default defineSourceConfig({ spec: "./specs/acme.json", diff --git a/docs/envelope.md b/docs/envelope.md index 9fd1013..93d07c6 100644 --- a/docs/envelope.md +++ b/docs/envelope.md @@ -52,4 +52,4 @@ recognized without changing their source schemas. ## accept-base -`openapi-codegen accept-base --source atlas` regenerates `generated/base.ts` and rewrites `BaseResponse` in `models.ts` to match the spec. Use it when the envelope shape in the spec is the source of truth and `models.ts` is stale. Do not combine with `--check`. +`typeforge accept-base --source atlas` regenerates `generated/base.ts` and rewrites `BaseResponse` in `models.ts` to match the spec. Use it when the envelope shape in the spec is the source of truth and `models.ts` is stale. Do not combine with `--check`. diff --git a/package.json b/package.json index 452d635..964ab74 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@openmirai/openapi-codegen", + "name": "@openmirai/typeforge", "version": "0.1.7", - "description": "Headless OpenAPI to TypeScript codegen CLI and HTTPFetch runtime", + "description": "Typeforge: headless OpenAPI to TypeScript codegen CLI and HTTPFetch runtime", "homepage": "https://github.com/openmirai/mirai-openapi-codegen#readme", "bugs": { "url": "https://github.com/openmirai/mirai-openapi-codegen/issues" @@ -12,9 +12,11 @@ "url": "git+https://github.com/openmirai/mirai-openapi-codegen.git" }, "bin": { - "openapi-codegen": "./dist/cli.js" + "openapi-codegen": "./dist/cli.js", + "typeforge": "./dist/cli.js" }, "files": [ + "assets/typeforge-logo.png", "dist" ], "type": "module", diff --git a/src/cli.ts b/src/cli.ts index 5adbc6e..80bc993 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -117,19 +117,19 @@ export function parseArgs(argv: Array): ParsedArgs { } function printHelp(): void { - process.stdout.write(`openapi-codegen — headless OpenAPI TypeScript codegen + process.stdout.write(`typeforge — headless OpenAPI TypeScript codegen Usage: - openapi-codegen init --source --client axios|fetch|custom [--layout monolith|packages] - openapi-codegen generate --source [--source ...] [--spec ] [--check] [--accept-base] - openapi-codegen generate --all [--check] [--accept-base] - openapi-codegen check --source [--spec ] - openapi-codegen accept-base --source [--spec ] + typeforge init --source --client axios|fetch|custom [--layout monolith|packages] + typeforge generate --source [--source ...] [--spec ] [--check] [--accept-base] + typeforge generate --all [--check] [--accept-base] + typeforge check --source [--spec ] + typeforge accept-base --source [--spec ] Multi-source generate: Provide multiple --source flags, or use --all to generate every source under apiRoot. Configure per-source spec paths in each source.ts: - import { defineSourceConfig } from "@openmirai/openapi-codegen"; + import { defineSourceConfig } from "@openmirai/typeforge"; export default defineSourceConfig({ spec: "./specs/acme.json", ... }); `); } @@ -137,7 +137,7 @@ Multi-source generate: async function runGenerate(args: ParsedArgs): Promise { if (args.check === true && args.acceptBase === true) { process.stderr.write( - "openapi-codegen: --accept-base is not allowed with --check\n" + "typeforge: --accept-base is not allowed with --check\n" ); return 1; } @@ -155,9 +155,7 @@ async function runGenerate(args: ParsedArgs): Promise { } if (sources.length === 0) { - process.stderr.write( - "openapi-codegen: --source or --all is required\n" - ); + process.stderr.write("typeforge: --source or --all is required\n"); return 1; } @@ -180,20 +178,18 @@ async function runGenerate(args: ParsedArgs): Promise { if (args.check === true) { if (result.changed.length > 0) { process.stderr.write( - `openapi-codegen: stale generated files for "${sourceKey}":\n` + `typeforge: stale generated files for "${sourceKey}":\n` ); for (const file of result.changed) { process.stderr.write(` ${file}\n`); } exitCode = 1; } else { - process.stdout.write( - `openapi-codegen: "${sourceKey}" is up to date\n` - ); + process.stdout.write(`typeforge: "${sourceKey}" is up to date\n`); } } else { process.stdout.write( - `openapi-codegen: generated ${result.files} files for "${sourceKey}" (${result.changed.length} changed)\n` + `typeforge: generated ${result.files} files for "${sourceKey}" (${result.changed.length} changed)\n` ); } } catch (error) { @@ -221,9 +217,7 @@ async function main(): Promise { if (args.command === "init") { if (args.source === undefined || args.client === undefined) { - process.stderr.write( - "openapi-codegen: init requires --source and --client\n" - ); + process.stderr.write("typeforge: init requires --source and --client\n"); process.exit(1); } @@ -260,7 +254,7 @@ async function main(): Promise { process.exit(await runGenerate(args)); } - process.stderr.write(`openapi-codegen: unknown command "${args.command}"\n`); + process.stderr.write(`typeforge: unknown command "${args.command}"\n`); printHelp(); process.exit(1); } diff --git a/src/color/__tests__/diagnostic.test.ts b/src/color/__tests__/diagnostic.test.ts index bec1b8f..ced8010 100644 --- a/src/color/__tests__/diagnostic.test.ts +++ b/src/color/__tests__/diagnostic.test.ts @@ -31,13 +31,13 @@ describe("formatDiagnostic", () => { it("formats an error with code, message, and help like oxlint", () => { const output = formatDiagnostic({ - code: "openapi-codegen/spec-not-found", + code: "typeforge/spec-not-found", help: "Provide --spec or set OPENAPI_SPEC_CORE_V2.", message: 'No OpenAPI spec found for source "core-v2"', }); const plain = stripVTControlCharacters(output); - expect(plain).toContain("openapi-codegen/spec-not-found"); + expect(plain).toContain("typeforge/spec-not-found"); expect(plain).toContain('No OpenAPI spec found for source "core-v2"'); expect(plain).toContain("help:"); expect(plain).toContain("Provide --spec or set OPENAPI_SPEC_CORE_V2."); @@ -45,7 +45,7 @@ describe("formatDiagnostic", () => { it("formats a snippet with file location and caret", () => { const output = formatDiagnostic({ - code: "openapi-codegen/base-response-drift", + code: "typeforge/base-response-drift", help: "Update models.ts or run with --accept-base.", message: "BaseResponse does not match the OpenAPI envelope", snippet: { @@ -68,7 +68,7 @@ describe("formatDiagnostic", () => { it("suppresses ANSI codes under NO_COLOR", () => { process.env["NO_COLOR"] = "1"; const output = formatDiagnostic({ - code: "openapi-codegen/spec-not-found", + code: "typeforge/spec-not-found", message: "missing spec", }); expect(output).toBe(output.replaceAll("\u001b", "")); diff --git a/src/config/__tests__/load-edges.test.ts b/src/config/__tests__/load-edges.test.ts index 34c845d..2832c79 100644 --- a/src/config/__tests__/load-edges.test.ts +++ b/src/config/__tests__/load-edges.test.ts @@ -28,10 +28,10 @@ describe("config/load edge cases", () => { ); tempRoots.push(cwd); mkdirSync(cwd, { recursive: true }); - writeFileSync(join(cwd, "openapi-codegen.json"), "{not json", "utf8"); + writeFileSync(join(cwd, "typeforge.json"), "{not json", "utf8"); writeFileSync( join(cwd, "package.json"), - JSON.stringify({ openapiCodegen: "invalid" }), + JSON.stringify({ typeforge: "invalid" }), "utf8" ); diff --git a/src/config/__tests__/load.test.ts b/src/config/__tests__/load.test.ts index f1d7d3f..7a95317 100644 --- a/src/config/__tests__/load.test.ts +++ b/src/config/__tests__/load.test.ts @@ -15,7 +15,7 @@ function sortedStrings(values: Array): Array { } describe("config/load", () => { - it("loads apiRoot from openapi-codegen.json and package.json", () => { + it("prefers typeforge.json over legacy and package config", () => { const cwd = join( process.cwd(), "test/fixtures/layouts", @@ -23,19 +23,52 @@ describe("config/load", () => { ); mkdirSync(cwd, { recursive: true }); writeFileSync( - join(cwd, "openapi-codegen.json"), + join(cwd, "typeforge.json"), JSON.stringify({ apiRoot: "packages/utils/src/api" }), "utf8" ); + writeFileSync( + join(cwd, "openapi-codegen.json"), + JSON.stringify({ apiRoot: "legacy" }), + "utf8" + ); writeFileSync( join(cwd, "package.json"), - JSON.stringify({ openapiCodegen: { apiRoot: "ignored" } }), + JSON.stringify({ typeforge: { apiRoot: "ignored" } }), "utf8" ); expect(loadProjectConfig(cwd).apiRoot).toBe("packages/utils/src/api"); }); + it("reads legacy project config during migration", () => { + const jsonCwd = join( + process.cwd(), + "test/fixtures/layouts", + `config-legacy-json-${Date.now()}` + ); + mkdirSync(jsonCwd, { recursive: true }); + writeFileSync( + join(jsonCwd, "openapi-codegen.json"), + JSON.stringify({ apiRoot: "legacy/json" }), + "utf8" + ); + expect(loadProjectConfig(jsonCwd).apiRoot).toBe("legacy/json"); + + const packageCwd = join( + process.cwd(), + "test/fixtures/layouts", + `config-legacy-package-${Date.now()}` + ); + mkdirSync(packageCwd, { recursive: true }); + writeFileSync( + join(packageCwd, "package.json"), + JSON.stringify({ openapiCodegen: { apiRoot: "legacy/package" } }), + "utf8" + ); + expect(loadProjectConfig(packageCwd).apiRoot).toBe("legacy/package"); + }); + it("parses source.ts config fields", () => { const cwd = join( process.cwd(), @@ -101,7 +134,7 @@ describe("config/load", () => { mkdirSync(join(cwd, "src/api/atlas"), { recursive: true }); writeFileSync( join(cwd, "src/api/atlas/source.ts"), - `import { defineSourceConfig } from "@openmirai/openapi-codegen"; + `import { defineSourceConfig } from "@openmirai/typeforge"; export default defineSourceConfig({ spec: "./specs/acme.json", diff --git a/src/config/load.ts b/src/config/load.ts index f70c06e..0e4b657 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -3,20 +3,20 @@ import { join, resolve } from "node:path"; import { readJsonObject } from "../json/types"; import type { - OpenApiCodegenConfig, + TypeforgeConfig, QueryExtendsConfig, SourceConfig, } from "./types"; import { DEFAULT_API_ROOT } from "./types"; -function readOptionalJson(path: string): OpenApiCodegenConfig { +function readOptionalJson(path: string): TypeforgeConfig { if (!existsSync(path)) { return {}; } try { const raw = readJsonObject(readFileSync(path, "utf8")); - const config: OpenApiCodegenConfig = {}; + const config: TypeforgeConfig = {}; if (typeof raw["apiRoot"] === "string") { config.apiRoot = raw["apiRoot"]; } @@ -26,25 +26,25 @@ function readOptionalJson(path: string): OpenApiCodegenConfig { } } -function readPackageConfig(path: string): OpenApiCodegenConfig { +function readPackageConfig(path: string): TypeforgeConfig { if (!existsSync(path)) { return {}; } try { const raw = readJsonObject(readFileSync(path, "utf8")); - const openapiCodegen = raw["openapiCodegen"]; + const typeforge = raw["typeforge"] ?? raw["openapiCodegen"]; if ( - typeof openapiCodegen !== "object" || - openapiCodegen === null || - Array.isArray(openapiCodegen) + typeof typeforge !== "object" || + typeforge === null || + Array.isArray(typeforge) ) { return {}; } - const config: OpenApiCodegenConfig = {}; - if (typeof openapiCodegen["apiRoot"] === "string") { - config.apiRoot = openapiCodegen["apiRoot"]; + const config: TypeforgeConfig = {}; + if (typeof typeforge["apiRoot"] === "string") { + config.apiRoot = typeforge["apiRoot"]; } return config; } catch { @@ -199,12 +199,17 @@ function parseQueryExtends(content: string): QueryExtendsConfig | undefined { return Object.keys(config).length > 0 ? config : undefined; } -export function loadProjectConfig(cwd: string): OpenApiCodegenConfig { - const fromJson = readOptionalJson(resolve(cwd, "openapi-codegen.json")); +export function loadProjectConfig(cwd: string): TypeforgeConfig { + const fromJson = readOptionalJson(resolve(cwd, "typeforge.json")); + const fromLegacyJson = readOptionalJson(resolve(cwd, "openapi-codegen.json")); const fromPackage = readPackageConfig(resolve(cwd, "package.json")); return { - apiRoot: fromJson.apiRoot ?? fromPackage.apiRoot ?? DEFAULT_API_ROOT, + apiRoot: + fromJson.apiRoot ?? + fromLegacyJson.apiRoot ?? + fromPackage.apiRoot ?? + DEFAULT_API_ROOT, }; } diff --git a/src/config/types.ts b/src/config/types.ts index 814bf77..3757bb0 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,7 +1,10 @@ -export interface OpenApiCodegenConfig { +export interface TypeforgeConfig { apiRoot?: string; } +/** @deprecated Use `TypeforgeConfig`. */ +export type OpenApiCodegenConfig = TypeforgeConfig; + export type GenerationMode = "authoritative" | "merge"; export type NamingStrategy = "path" | "operationId"; diff --git a/src/emitters/recursive-ref-error.ts b/src/emitters/recursive-ref-error.ts index 877072a..17ec131 100644 --- a/src/emitters/recursive-ref-error.ts +++ b/src/emitters/recursive-ref-error.ts @@ -20,11 +20,7 @@ export function formatRecursiveRefError( schemaPath: string ): string { const lines = [ - bold( - red( - `openapi-codegen: recursive schema reference in source "${sourceKey}"` - ) - ), + bold(red(`typeforge: recursive schema reference in source "${sourceKey}"`)), "", bold("Cycle:"), ` ${cycle.join(" → ")}`, @@ -35,7 +31,7 @@ export function formatRecursiveRefError( bold("Fix:"), " • Add a known-type override in known-types.ts for this shape, or", " • Simplify the OpenAPI schema to remove the circular reference", - cyan(` • openapi-codegen generate --source ${sourceKey} --spec `), + cyan(` • typeforge generate --source ${sourceKey} --spec `), ]; return lines.join("\n"); } diff --git a/src/emitters/routes/index.ts b/src/emitters/routes/index.ts index aedb6fe..890f4ff 100644 --- a/src/emitters/routes/index.ts +++ b/src/emitters/routes/index.ts @@ -77,7 +77,7 @@ export function emitRoutesFile(options: RoutesEmitterOptions): string { "import {", " buildRouteFromHandlers,", " createRouteHandlers,", - '} from "@openmirai/openapi-codegen/routes";', + '} from "@openmirai/typeforge/routes";', "", ...renderRouteParamsType(entries), `export enum ${options.routeEnumName} {`, diff --git a/src/envelope-guard/diagnostic.ts b/src/envelope-guard/diagnostic.ts index 655be65..1458272 100644 --- a/src/envelope-guard/diagnostic.ts +++ b/src/envelope-guard/diagnostic.ts @@ -10,7 +10,7 @@ export function formatMixedEnvelopeError( groups: Map> ): string { const header = bold( - red(`openapi-codegen: mixed envelope shapes in source "${sourceKey}"`) + red(`typeforge: mixed envelope shapes in source "${sourceKey}"`) ); const lines = [header, ""]; @@ -39,7 +39,7 @@ export function formatMixedEnvelopeError( lines.push(bold("Fix:")); lines.push(" • Narrow pathPrefix or add ignorePaths in source.ts"); lines.push( - cyan(` • openapi-codegen generate --source ${sourceKey} --spec `) + cyan(` • typeforge generate --source ${sourceKey} --spec `) ); return lines.join("\n"); @@ -54,7 +54,7 @@ export function formatDriftError( outliers: Array = [] ): string { const header = bold( - red(`openapi-codegen: base response mismatch in source "${sourceKey}"`) + red(`typeforge: base response mismatch in source "${sourceKey}"`) ); const lines = [header, ""]; @@ -100,7 +100,7 @@ export function formatDriftError( lines.push(" • Update models.ts to match the spec, or"); lines.push( cyan( - ` • openapi-codegen generate --source ${sourceKey} --spec --accept-base` + ` • typeforge generate --source ${sourceKey} --spec --accept-base` ) ); diff --git a/src/index.ts b/src/index.ts index 8696a48..5d686bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export type { GenerationMode, NamingStrategy, OpenApiCodegenConfig, + TypeforgeConfig, QueryExtendsConfig, SourceConfig, } from "./config/types"; diff --git a/src/init/index.ts b/src/init/index.ts index 998169a..6f422e7 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { loadProjectConfig } from "../config/load"; -import type { OpenApiCodegenConfig } from "../config/types"; +import type { TypeforgeConfig } from "../config/types"; import { DEFAULT_API_ROOT } from "../config/types"; export type HttpClient = "axios" | "fetch" | "custom"; @@ -16,7 +16,7 @@ export interface InitOptions { } const AXIOS_HTTP_TEMPLATE = `import axiosBase from "axios"; -import { createAxiosAdapter } from "@openmirai/openapi-codegen/adapters/axios"; +import { createAxiosAdapter } from "@openmirai/typeforge/adapters/axios"; const axios = axiosBase.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -37,19 +37,19 @@ axios.interceptors.response.use( export const httpFetch = createAxiosAdapter(axios); export { axios }; -export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/openapi-codegen/adapters/axios"; +export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/adapters/axios"; `; -const FETCH_HTTP_TEMPLATE = `import { createFetchAdapter } from "@openmirai/openapi-codegen/adapters/fetch"; +const FETCH_HTTP_TEMPLATE = `import { createFetchAdapter } from "@openmirai/typeforge/adapters/fetch"; export const httpFetch = createFetchAdapter({ baseURL: process.env.NEXT_PUBLIC_API_URL, }); -export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/openapi-codegen/adapters/fetch"; +export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/adapters/fetch"; `; -const CUSTOM_HTTP_TEMPLATE = `import type { HTTPFetch, HTTPFetchConfig } from "@openmirai/openapi-codegen/http"; +const CUSTOM_HTTP_TEMPLATE = `import type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/http"; export type { HTTPFetch, HTTPFetchConfig }; @@ -90,11 +90,11 @@ export const httpFetch: HTTPFetch = { }; `; -const SOURCE_TEMPLATE = `import { defineSourceConfig } from "@openmirai/openapi-codegen"; +const SOURCE_TEMPLATE = `import { defineSourceConfig } from "@openmirai/typeforge"; export default defineSourceConfig({ // Path to the OpenAPI spec file, relative to the project root. - // Set this so \`openapi-codegen generate --source \` (or --all) works + // Set this so \`typeforge generate --source \` (or --all) works // without a per-invocation --spec flag. // spec: "./specs/acme.json", pathPrefix: "/api/acme/v3", @@ -155,11 +155,13 @@ export function initProject(options: InitOptions): { } { const cwd = options.cwd ?? process.cwd(); const layout = options.layout ?? "monolith"; - const configPath = resolve(cwd, "openapi-codegen.json"); + const configPath = resolve(cwd, "typeforge.json"); + const legacyConfigPath = resolve(cwd, "openapi-codegen.json"); const projectConfig = loadProjectConfig(cwd); - const apiRoot = existsSync(configPath) - ? (projectConfig.apiRoot ?? DEFAULT_API_ROOT) - : defaultApiRoot(layout); + const apiRoot = + existsSync(configPath) || existsSync(legacyConfigPath) + ? (projectConfig.apiRoot ?? DEFAULT_API_ROOT) + : defaultApiRoot(layout); const apiRootPath = resolve(cwd, apiRoot); const sourceDir = join(apiRootPath, options.sourceKey); @@ -197,9 +199,9 @@ export function initProject(options: InitOptions): { skipped.push(knownTypesPath); } - const configWritePath = resolve(cwd, "openapi-codegen.json"); - if (!existsSync(configWritePath)) { - const config: OpenApiCodegenConfig = { apiRoot }; + const configWritePath = resolve(cwd, "typeforge.json"); + if (!existsSync(configWritePath) && !existsSync(legacyConfigPath)) { + const config: TypeforgeConfig = { apiRoot }; writeFileSync( configWritePath, `${JSON.stringify(config, null, 2)}\n`, diff --git a/src/parser/__tests__/loader.test.ts b/src/parser/__tests__/loader.test.ts index ecf797b..1b01773 100644 --- a/src/parser/__tests__/loader.test.ts +++ b/src/parser/__tests__/loader.test.ts @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loadSpec, resolveSpecSource } from "../loader"; import type { JsonValue } from "../../json/types"; -const testDir = join(tmpdir(), `openapi-codegen-loader-${Date.now()}`); +const testDir = join(tmpdir(), `typeforge-loader-${Date.now()}`); function getErrorMessage(error: unknown): string { if (error instanceof Error) { @@ -106,7 +106,7 @@ describe("resolveSpecSource - priority", () => { it("env var takes priority over local override and snapshot", () => { const envSpecPath = writeSpec("env-spec.json", {}), - localPath = join(testDir, "openapi-codegen.local.json"), + localPath = join(testDir, "typeforge.local.json"), snapshotPath = writeSpec("snapshot.json", {}); writeFileSync(localPath, JSON.stringify({ "core-v2": snapshotPath })); vi.stubEnv("OPENAPI_SPEC_CORE_V2", envSpecPath); @@ -124,7 +124,7 @@ describe("resolveSpecSource - priority", () => { it("local override takes priority over snapshot", () => { const specPath = writeSpec("local-spec.json", {}), snapshotPath = writeSpec("snapshot.json", {}), - localPath = join(testDir, "openapi-codegen.local.json"); + localPath = join(testDir, "typeforge.local.json"); writeFileSync(localPath, JSON.stringify({ "core-v2": specPath })); const source = resolveSpecSource("core-v2", { @@ -148,7 +148,7 @@ describe("resolveSpecSource - priority", () => { it("sourceConfigSpec takes priority over local override and snapshot", () => { const specPath = writeSpec("source-config-spec.json", {}), - localPath = join(testDir, "openapi-codegen.local.json"), + localPath = join(testDir, "typeforge.local.json"), snapshotPath = writeSpec("snapshot.json", {}); writeFileSync(localPath, JSON.stringify({ "core-v2": snapshotPath })); @@ -178,7 +178,7 @@ describe("resolveSpecSource - priority", () => { }); it("local override file without entry for source key falls through to snapshot", () => { - const localPath = join(testDir, "openapi-codegen.local.json"), + const localPath = join(testDir, "typeforge.local.json"), snapshotPath = writeSpec("snapshot.json", {}); // Local file exists but has different source key writeFileSync( @@ -237,7 +237,7 @@ describe("resolveSpecSource - error when no spec found", () => { const message = getErrorMessage(error); expect(message).toContain("--spec flag"); expect(message).toContain("OPENAPI_SPEC_CORE_V2"); - expect(message).toContain("openapi-codegen.local.json"); + expect(message).toContain("typeforge.local.json"); expect(message).toContain("committed snapshot"); } }); @@ -248,7 +248,7 @@ describe("resolveSpecSource - error when no spec found", () => { expect.fail("should have thrown"); } catch (error) { const message = getErrorMessage(error); - expect(message).toContain("openapi-codegen/spec-not-found"); + expect(message).toContain("typeforge/spec-not-found"); expect(message).toContain("help:"); } }); diff --git a/src/parser/loader.ts b/src/parser/loader.ts index fd8d72e..38b1c13 100644 --- a/src/parser/loader.ts +++ b/src/parser/loader.ts @@ -42,7 +42,7 @@ export async function loadSpec(source: SpecSource): Promise { * 1. --spec CLI flag * 2. OPENAPI_SPEC_ env var * 3. source.ts `spec` property (project-relative path from config) - * 4. openapi-codegen.local.json (gitignored per-machine override) + * 4. typeforge.local.json (gitignored per-machine override) * 5. committed snapshot at snapshotPath * * Throws a human-readable error (no stack trace as first line) when nothing is found. @@ -74,7 +74,13 @@ export function resolveSpecSource( } // 4. Local override file - const localPath = opts.localOverridePath ?? "./openapi-codegen.local.json"; + const defaultLocalPath = "./typeforge.local.json"; + const legacyLocalPath = "./openapi-codegen.local.json"; + const localPath = + opts.localOverridePath ?? + (existsSync(defaultLocalPath) || !existsSync(legacyLocalPath) + ? defaultLocalPath + : legacyLocalPath); if (existsSync(localPath)) { try { const localData = readJsonObject(readFileSync(localPath, "utf8")), @@ -134,16 +140,16 @@ function buildNotFoundMessage( ` --spec flag: ${specFlagNote}`, ` ${envVarName} env: ${envNote}`, ` source.ts spec: ${sourceConfigNote}`, - ` openapi-codegen.local.json: ${localNote}`, + ` local override: ${localNote}`, ` committed snapshot: ${snapshotNote}`, ].join("\n"), fixCommands = [ - `openapi-codegen generate --source ${sourceKey} --spec ./path/to/swagger.json`, + `typeforge generate --source ${sourceKey} --spec ./path/to/swagger.json`, `export ${envVarName}=./path/to/swagger.json`, ]; const diagnostic = formatDiagnostic({ - code: "openapi-codegen/spec-not-found", + code: "typeforge/spec-not-found", help: "Provide one of the resolution paths above, for example with --spec or an env var.", message: `No OpenAPI spec found for source "${sourceKey}"`, severity: "error", diff --git a/test/e2e/cli-accept-base.test.ts b/test/e2e/cli-accept-base.test.ts index 7f3965a..703c3ad 100644 --- a/test/e2e/cli-accept-base.test.ts +++ b/test/e2e/cli-accept-base.test.ts @@ -64,7 +64,7 @@ export type ItemList = BaseResponse>;`, mkdirSync(join(root, "src/api/alpha"), { recursive: true }); mkdirSync(join(root, "src/api/beta"), { recursive: true }); writeFileSync( - join(root, "openapi-codegen.json"), + join(root, "typeforge.json"), JSON.stringify({ apiRoot: "src/api" }), "utf8" ); diff --git a/test/e2e/cli.test.ts b/test/e2e/cli.test.ts index 75292b4..f52b331 100644 --- a/test/e2e/cli.test.ts +++ b/test/e2e/cli.test.ts @@ -33,7 +33,7 @@ describe("e2e cli init", () => { tempRoots.push(root); mkdirSync(join(root, "src/api"), { recursive: true }); writeFileSync( - join(root, "openapi-codegen.json"), + join(root, "typeforge.json"), JSON.stringify({ apiRoot: "src/api" }), "utf8" ); @@ -79,6 +79,29 @@ describe("e2e cli init", () => { "// keep me" ); }); + + it("uses legacy project config without writing a duplicate", () => { + const root = join(fixtureRoot, "layouts", `cli-init-legacy-${Date.now()}`); + tempRoots.push(root); + mkdirSync(root, { recursive: true }); + writeFileSync( + join(root, "openapi-codegen.json"), + JSON.stringify({ apiRoot: "legacy/api" }), + "utf8" + ); + + const result = runCli(root, [ + "init", + "--source", + "atlas", + "--client", + "fetch", + ]); + + expect(result.exitCode).toBe(0); + expect(existsSync(join(root, "legacy/api/http.ts"))).toBe(true); + expect(existsSync(join(root, "typeforge.json"))).toBe(false); + }); }); describe("e2e cli generate", () => { diff --git a/test/helpers/project.ts b/test/helpers/project.ts index 73923bb..81ed2fe 100644 --- a/test/helpers/project.ts +++ b/test/helpers/project.ts @@ -37,7 +37,7 @@ export function createMonolithProject(options: MonolithProjectOptions): { mkdirSync(join(options.root, apiRoot), { recursive: true }); writeFileSync( - join(options.root, "openapi-codegen.json"), + join(options.root, "typeforge.json"), JSON.stringify({ apiRoot }), "utf8" ); diff --git a/test/integration/generate.test.ts b/test/integration/generate.test.ts index 3a4ec5e..9c857bc 100644 --- a/test/integration/generate.test.ts +++ b/test/integration/generate.test.ts @@ -399,7 +399,7 @@ describe("integration: multi-source", () => { mkdirSync(join(root, "src/api/wrapped"), { recursive: true }); mkdirSync(join(root, "src/api/raw"), { recursive: true }); writeFileSync( - join(root, "openapi-codegen.json"), + join(root, "typeforge.json"), JSON.stringify({ apiRoot: "src/api" }), "utf8" ); diff --git a/test/integration/spec-resolution.test.ts b/test/integration/spec-resolution.test.ts index aa7f251..ce9a0e1 100644 --- a/test/integration/spec-resolution.test.ts +++ b/test/integration/spec-resolution.test.ts @@ -48,19 +48,19 @@ describe("integration: spec resolution", () => { } }); - it("resolves openapi-codegen.local.json override", () => { + it("resolves typeforge.local.json override", () => { const root = join(fixtureRoot, "layouts", `local-spec-${Date.now()}`); tempRoots.push(root); mkdirSync(root, { recursive: true }); const specPath = join(fixtureRoot, "specs/envelope-list.json"); writeFileSync( - join(root, "openapi-codegen.local.json"), + join(root, "typeforge.local.json"), JSON.stringify({ atlas: specPath }), "utf8" ); const source = resolveSpecSource("atlas", { - localOverridePath: join(root, "openapi-codegen.local.json"), + localOverridePath: join(root, "typeforge.local.json"), }); expect(source.kind).toBe("local-override"); }); diff --git a/test/integration/type-safety.test.ts b/test/integration/type-safety.test.ts index 1d8e0d8..7456c9f 100644 --- a/test/integration/type-safety.test.ts +++ b/test/integration/type-safety.test.ts @@ -116,7 +116,8 @@ describe("integration: end-to-end type safety", () => { } catch (error) { const execError = error as { stdout?: string; stderr?: string }; throw new Error( - [execError.stdout, execError.stderr].filter(Boolean).join("\n") + [execError.stdout, execError.stderr].filter(Boolean).join("\n"), + { cause: error } ); } expect(output).toBeDefined(); @@ -137,9 +138,9 @@ describe("integration: monolith layouts", () => { const root = join(fixtureRoot, "layouts", `fetch-${Date.now()}`); tempRoots.push(root); createMonolithProject({ - httpContent: `import { createFetchAdapter } from "@openmirai/openapi-codegen/adapters/fetch"; + httpContent: `import { createFetchAdapter } from "@openmirai/typeforge/adapters/fetch"; export const httpFetch = createFetchAdapter({ baseURL: "https://example.com" }); -export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/openapi-codegen/adapters/fetch"; +export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/adapters/fetch"; `, root, sourceKey: "atlas", @@ -178,7 +179,7 @@ export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/openapi-codegen/adap `import { buildRouteFromHandlers, createRouteHandlers, -} from "@openmirai/openapi-codegen/routes"; +} from "@openmirai/typeforge/routes"; export enum RouteTargets { LEGACY_PING = "/v1/legacy/ping", diff --git a/tsdown.config.ts b/tsdown.config.ts index ee06b71..42bd404 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -20,41 +20,41 @@ export default defineConfig([ { ...shared, entry: { cli: "src/cli.ts", index: "src/index.ts" }, - name: "@openmirai/openapi-codegen", + name: "@openmirai/typeforge", platform: "node", }, { ...shared, entry: { index: "src/adapters/axios/index.ts" }, - name: "@openmirai/openapi-codegen/adapters/axios", + name: "@openmirai/typeforge/adapters/axios", outDir: "dist/adapters/axios", platform: "node", }, { ...shared, entry: { index: "src/adapters/fetch/index.ts" }, - name: "@openmirai/openapi-codegen/adapters/fetch", + name: "@openmirai/typeforge/adapters/fetch", outDir: "dist/adapters/fetch", platform: "neutral", }, { ...shared, entry: { types: "src/http/types.ts", validate: "src/http/validate.ts" }, - name: "@openmirai/openapi-codegen/http", + name: "@openmirai/typeforge/http", outDir: "dist/http", platform: "neutral", }, { ...shared, entry: { zod: "src/validation/zod.ts" }, - name: "@openmirai/openapi-codegen/validation/zod", + name: "@openmirai/typeforge/validation/zod", outDir: "dist/validation", platform: "neutral", }, { ...shared, entry: { index: "src/routes/index.ts" }, - name: "@openmirai/openapi-codegen/routes", + name: "@openmirai/typeforge/routes", outDir: "dist/routes", platform: "neutral", },