Skip to content

Commit 5eb4821

Browse files
feat(openapi-client): publish to npm as a standalone library (#84)
* feat(openapi-client): publish to npm as a standalone library * ci(release): bump publish jobs from Node 20 (EOL) to Node 22
1 parent 82e9d48 commit 5eb4821

11 files changed

Lines changed: 497 additions & 21 deletions

File tree

.github/workflows/release.yml

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,20 @@ jobs:
3838
outputs:
3939
cli-version: ${{ steps.check-cli.outputs.version }}
4040
database-shell-version: ${{ steps.check-database-shell.outputs.version }}
41+
openapi-client-version: ${{ steps.check-openapi-client.outputs.version }}
4142
steps:
4243
- uses: actions/checkout@v5
44+
- name: Check openapi-client version
45+
id: check-openapi-client
46+
run: |
47+
VERSION=$(node -p "require('./packages/openapi-client/package.json').version")
48+
PUBLISHED=$(npm view @bunny.net/openapi-client version 2>/dev/null || echo "0.0.0")
49+
if [ "$VERSION" != "$PUBLISHED" ]; then
50+
echo "version=$VERSION" >> $GITHUB_OUTPUT
51+
echo "New version detected: $VERSION (published: $PUBLISHED)"
52+
else
53+
echo "No version change: $VERSION"
54+
fi
4355
- name: Check CLI version
4456
id: check-cli
4557
run: |
@@ -177,7 +189,7 @@ jobs:
177189

178190
- uses: actions/setup-node@v5
179191
with:
180-
node-version: "20"
192+
node-version: "22"
181193
registry-url: "https://registry.npmjs.org"
182194

183195
- name: Download all platform artifacts
@@ -248,7 +260,7 @@ jobs:
248260

249261
- uses: actions/setup-node@v5
250262
with:
251-
node-version: "20"
263+
node-version: "22"
252264
registry-url: "https://registry.npmjs.org"
253265

254266
- name: Download all platform artifacts
@@ -285,3 +297,30 @@ jobs:
285297
npm publish --access public
286298
env:
287299
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
300+
301+
publish-openapi-client:
302+
name: Publish openapi-client
303+
runs-on: ubuntu-latest
304+
needs: version
305+
if: needs.version.outputs.openapi-client-version
306+
steps:
307+
- uses: actions/checkout@v5
308+
- uses: oven-sh/setup-bun@v2
309+
with:
310+
bun-version: "1.3.11"
311+
- run: bun install
312+
313+
- name: Build package
314+
run: bun run --filter @bunny.net/openapi-client build
315+
316+
- uses: actions/setup-node@v5
317+
with:
318+
node-version: "22"
319+
registry-url: "https://registry.npmjs.org"
320+
321+
- name: Publish @bunny.net/openapi-client
322+
run: |
323+
cd packages/openapi-client
324+
npm publish --access public
325+
env:
326+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,16 @@ Each release includes prebuilt binaries as release assets, created automatically
726726
3. Merge the Release PR — changesets bumps versions for `@bunny.net/cli` and all platform packages (kept in sync via `fixed`)
727727
4. The release workflow detects the version change, builds binaries for all platforms, publishes platform packages then `@bunny.net/cli` to npm, and creates a GitHub release with binaries attached
728728

729+
### Publishing `@bunny.net/openapi-client`
730+
731+
Unlike the CLI and `database-shell` (which ship as compiled binaries), `@bunny.net/openapi-client` is published as a plain TypeScript library — compiled JS plus `.d.ts` declarations.
732+
733+
Its `package.json` `exports`/`main`/`types` point at `dist/`, so npm consumers get the compiled output. **In-repo tooling resolves it from source instead** — the root `tsconfig.json` has a `paths` mapping for `@bunny.net/openapi-client``src/`, and `bun run`, `bun build --compile`, `bun test`, and `tsc` all honor `paths` over the package's `exports`. So the CLI build and dev loop consume live source with no prebuild step, while only the publish step needs `dist/` built. (Published consumers never see the repo `tsconfig.json`, so they fall back to `exports`.)
734+
735+
- `bun run --filter @bunny.net/openapi-client build` runs `generate` (the `src/generated/` types are gitignored, so they are regenerated from the committed specs), then `scripts/build.ts`.
736+
- `scripts/build.ts` drives the TypeScript compiler API (using `tsconfig.build.json`) to emit JS + declarations, then copies the generated `.d.ts` files into `dist/generated/` (tsc never emits its inputs, and those files back the `./generated/*` subpath export). `rewriteRelativeImportExtensions` rewrites `./x.ts``./x.js` in the emitted **JS**; TypeScript has no equivalent for declaration emit, so an `afterDeclarations` transformer rewrites the `.ts`/`.d.ts` specifiers in the emitted **`.d.ts`** files on the AST.
737+
- The `publish-openapi-client` job in `release.yml` (gated on a version bump detected via `npm view`) builds, then runs `cd packages/openapi-client && npm publish` (`files` ships `dist` + `README.md`). The package versions independently of the CLI — it is not part of any `fixed` group in `.changeset/config.json`.
738+
729739
### CI
730740

731741
Tests and type-checking run on every pull request via `.github/workflows/ci.yml` (`bun run typecheck` and `bun test`).

bun.lock

Lines changed: 13 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/openapi-client/package.json

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
"name": "@bunny.net/openapi-client",
33
"version": "0.1.0",
44
"type": "module",
5-
"module": "src/index.ts",
5+
"main": "./dist/index.js",
6+
"module": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
68
"scripts": {
79
"generate": "openapi-typescript",
8-
"update-specs": "bun run scripts/update-specs.ts && bun run generate"
10+
"update-specs": "bun run scripts/update-specs.ts && bun run generate",
11+
"build": "rm -rf dist && bun run generate && bun run scripts/build.ts"
912
},
1013
"exports": {
11-
".": "./src/index.ts",
12-
"./generated/*": "./src/generated/*"
14+
".": {
15+
"types": "./dist/index.d.ts",
16+
"import": "./dist/index.js"
17+
},
18+
"./generated/*": "./dist/generated/*"
1319
},
1420
"files": [
15-
"src",
21+
"dist",
1622
"README.md"
1723
],
1824
"publishConfig": {
@@ -22,6 +28,7 @@
2228
"openapi-fetch": "^0.17.0"
2329
},
2430
"devDependencies": {
25-
"openapi-typescript": "^7.13.0"
31+
"openapi-typescript": "^7.13.0",
32+
"typescript": "^5"
2633
}
2734
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Compiles src/ to dist/ for publishing (run only at publish time; see AGENTS.md "Publishing").
2+
3+
import { cp } from "node:fs/promises";
4+
import ts from "typescript";
5+
6+
const root = new URL("..", import.meta.url).pathname;
7+
const dist = `${root}dist`;
8+
const configPath = `${root}tsconfig.build.json`;
9+
10+
// Rewrite a relative `.ts`/`.d.ts` specifier to its emitted `.js` sibling (TS resolves it against the `.d.ts`).
11+
function rewriteSpecifier(specifier: string): string {
12+
if (!specifier.startsWith(".")) return specifier;
13+
for (const ext of [".d.ts", ".ts"]) {
14+
if (specifier.endsWith(ext)) return `${specifier.slice(0, -ext.length)}.js`;
15+
}
16+
return specifier;
17+
}
18+
19+
// afterDeclarations transformer: fix specifiers on import/export-from and inline import("...") type nodes.
20+
const rewriteDeclarationSpecifiers: ts.TransformerFactory<
21+
ts.SourceFile | ts.Bundle
22+
> = (context) => {
23+
const { factory } = context;
24+
const visit = (node: ts.Node): ts.Node => {
25+
if (
26+
ts.isImportDeclaration(node) &&
27+
ts.isStringLiteral(node.moduleSpecifier)
28+
) {
29+
return factory.updateImportDeclaration(
30+
node,
31+
node.modifiers,
32+
node.importClause,
33+
factory.createStringLiteral(
34+
rewriteSpecifier(node.moduleSpecifier.text),
35+
),
36+
node.attributes,
37+
);
38+
}
39+
if (
40+
ts.isExportDeclaration(node) &&
41+
node.moduleSpecifier &&
42+
ts.isStringLiteral(node.moduleSpecifier)
43+
) {
44+
return factory.updateExportDeclaration(
45+
node,
46+
node.modifiers,
47+
node.isTypeOnly,
48+
node.exportClause,
49+
factory.createStringLiteral(
50+
rewriteSpecifier(node.moduleSpecifier.text),
51+
),
52+
node.attributes,
53+
);
54+
}
55+
if (
56+
ts.isImportTypeNode(node) &&
57+
ts.isLiteralTypeNode(node.argument) &&
58+
ts.isStringLiteral(node.argument.literal)
59+
) {
60+
return factory.updateImportTypeNode(
61+
node,
62+
factory.createLiteralTypeNode(
63+
factory.createStringLiteral(
64+
rewriteSpecifier(node.argument.literal.text),
65+
),
66+
),
67+
node.attributes,
68+
node.qualifier,
69+
node.typeArguments,
70+
node.isTypeOf,
71+
);
72+
}
73+
return ts.visitEachChild(node, visit, context);
74+
};
75+
return (sourceFile) =>
76+
ts.visitNode(sourceFile, visit) as ts.SourceFile | ts.Bundle;
77+
};
78+
79+
function reportDiagnostics(diagnostics: readonly ts.Diagnostic[]): void {
80+
if (diagnostics.length === 0) return;
81+
const host: ts.FormatDiagnosticsHost = {
82+
getCanonicalFileName: (f) => f,
83+
getCurrentDirectory: ts.sys.getCurrentDirectory,
84+
getNewLine: () => ts.sys.newLine,
85+
};
86+
console.error(ts.formatDiagnosticsWithColorAndContext(diagnostics, host));
87+
process.exit(1);
88+
}
89+
90+
// 1. Emit JS + declarations.
91+
const parsed = ts.getParsedCommandLineOfConfigFile(
92+
configPath,
93+
{},
94+
{
95+
...ts.sys,
96+
onUnRecoverableConfigFileDiagnostic: (d) => reportDiagnostics([d]),
97+
},
98+
);
99+
if (!parsed) {
100+
console.error(`Could not read ${configPath}`);
101+
process.exit(1);
102+
}
103+
104+
const program = ts.createProgram({
105+
rootNames: parsed.fileNames,
106+
options: parsed.options,
107+
});
108+
reportDiagnostics(ts.getPreEmitDiagnostics(program));
109+
110+
const emitResult = program.emit(undefined, undefined, undefined, false, {
111+
afterDeclarations: [rewriteDeclarationSpecifiers],
112+
});
113+
reportDiagnostics(emitResult.diagnostics);
114+
115+
// 2. Ship the generated declaration files (tsc does not emit its inputs).
116+
await cp(`${root}src/generated`, `${dist}/generated`, { recursive: true });
117+
118+
console.log("build: emitted dist/, copied generated declarations");

0 commit comments

Comments
 (0)