@octalmesh/seagull-core 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ import { renderArtifactTag } from "../config/publishing";
2
+ import type {
3
+ ResolvedArtifact,
4
+ ResolvedContract,
5
+ VarsTree,
6
+ } from "../config/types";
7
+
8
+ export interface DefaultReadmeArgs {
9
+ contract: ResolvedContract;
10
+ artifact: ResolvedArtifact;
11
+ version: string;
12
+ github: { owner: string; repo: string };
13
+ vars: VarsTree;
14
+ }
15
+
16
+ /**
17
+ * Renders a sensible default `README.md` for a generated SDK package, used
18
+ * whenever the artifact has no custom `readme:` template configured. Covers
19
+ * every built-in `lang`/`kind` combination; anything more specific (house
20
+ * install instructions, extra usage notes) belongs in a custom template
21
+ * instead - see `readme-renderer.ts`. Registry URLs and branch/tag names
22
+ * come from the artifact's resolved `publishing:` config, so a repo that
23
+ * overrides its registry sees that reflected here automatically.
24
+ *
25
+ * @param args - The contract, artifact, version, and github/vars coordinates
26
+ * to render for.
27
+ * @returns The rendered README content.
28
+ */
29
+ export function renderDefaultReadme({
30
+ contract,
31
+ artifact,
32
+ version,
33
+ github,
34
+ vars,
35
+ }: DefaultReadmeArgs): string {
36
+ const header = `# ${contract.title} - ${label(artifact)}
37
+
38
+ > Generated from \`${contract.entrypointRelative}\` in [${github.owner}/${github.repo}](https://github.com/${github.owner}/${github.repo}).
39
+ > Do not edit by hand - this package is regenerated and republished on every release.
40
+
41
+ Version: \`${version}\`
42
+ Source branch: \`${artifact.branch}\`
43
+ `;
44
+
45
+ return `${header}\n${body(contract, artifact, version, github, vars)}\n`;
46
+ }
47
+
48
+ //<editor-fold desc="README Template Helpers" defaultstate="collapsed">
49
+
50
+ function label(artifact: ResolvedArtifact): string {
51
+ const lang = { typescript: "TypeScript", go: "Go", java: "Java" }[
52
+ artifact.lang
53
+ ];
54
+ const kind =
55
+ artifact.kind === "client"
56
+ ? "Client SDK"
57
+ : artifact.lang === "typescript"
58
+ ? "Server Types"
59
+ : "Server Stubs";
60
+
61
+ return `${lang} ${kind}`;
62
+ }
63
+
64
+ function body(
65
+ contract: ResolvedContract,
66
+ artifact: ResolvedArtifact,
67
+ version: string,
68
+ github: { owner: string; repo: string },
69
+ vars: VarsTree,
70
+ ): string {
71
+ switch (`${artifact.lang}-${artifact.kind}`) {
72
+ case "typescript-client":
73
+ return tsClient(artifact, version);
74
+ case "typescript-server":
75
+ return tsServer(artifact, version);
76
+ case "go-client":
77
+ return goClient(contract, artifact, github, vars);
78
+ case "go-server":
79
+ return goServer(artifact);
80
+ case "java-client":
81
+ return javaClient(artifact, version);
82
+ case "java-server":
83
+ return javaServer(artifact, version);
84
+ default:
85
+ return "";
86
+ }
87
+ }
88
+
89
+ //</editor-fold>
90
+
91
+ //<editor-fold desc="README Body Templates" defaultstate="collapsed">
92
+
93
+ function tsClient(artifact: ResolvedArtifact, version: string): string {
94
+ return `## Install
95
+
96
+ \`\`\`bash
97
+ npm config set <scope>:registry ${artifact.publishing.npmRegistry}
98
+ npm install ${artifact.package}@${version}
99
+ \`\`\`
100
+
101
+ (Requires an authenticated \`.npmrc\` with a token for that registry.)
102
+
103
+ ## Usage
104
+
105
+ \`\`\`ts
106
+ import { Configuration, DefaultApi } from "${artifact.package}";
107
+
108
+ const api = new DefaultApi(
109
+ new Configuration({ basePath: "https://api.your-domain.com" }),
110
+ );
111
+
112
+ const result = await api.someOperation();
113
+ \`\`\`
114
+ `;
115
+ }
116
+
117
+ function tsServer(artifact: ResolvedArtifact, version: string): string {
118
+ return `## Install
119
+
120
+ \`\`\`bash
121
+ npm config set <scope>:registry ${artifact.publishing.npmRegistry}
122
+ npm install --save-dev ${artifact.package}@${version}
123
+ \`\`\`
124
+
125
+ (Requires an authenticated \`.npmrc\` with a token for that registry.)
126
+
127
+ ## Usage
128
+
129
+ \`\`\`ts
130
+ import type { components, operations } from "${artifact.package}";
131
+
132
+ type LoginResponses = operations["login"]["responses"];
133
+
134
+ // Example: an Express handler typed against the contract
135
+ app.post("/login", (req, res) => {
136
+ const body = req.body as components["schemas"]["LoginRequest"];
137
+ const response: LoginResponses[200]["content"]["application/json"] = {
138
+ // ...
139
+ };
140
+ res.json(response);
141
+ });
142
+ \`\`\`
143
+ `;
144
+ }
145
+
146
+ function goClient(
147
+ contract: ResolvedContract,
148
+ artifact: ResolvedArtifact,
149
+ github: { owner: string; repo: string },
150
+ vars: VarsTree,
151
+ ): string {
152
+ // Illustrative only - renders the tag template with a literal "<version>"
153
+ // placeholder rather than an actual version, since this is documentation
154
+ // text showing the *pattern*, not a specific release.
155
+ const exampleTag = renderArtifactTag(
156
+ artifact,
157
+ contract.name,
158
+ "<version>",
159
+ github,
160
+ vars,
161
+ );
162
+
163
+ return `## Install
164
+
165
+ Go has no package registry, so this module is pulled directly from its
166
+ publishing branch:
167
+
168
+ \`\`\`bash
169
+ go get ${artifact.goModule}@${artifact.branch}
170
+ \`\`\`
171
+
172
+ To pin an exact release instead of the branch head, use the matching tag:
173
+
174
+ \`\`\`bash
175
+ go get ${artifact.goModule}@${exampleTag}
176
+ \`\`\`
177
+
178
+ ## Usage
179
+
180
+ \`\`\`go
181
+ import (
182
+ "context"
183
+
184
+ ${artifact.goPackageName} "${artifact.goModule}"
185
+ )
186
+
187
+ func main() {
188
+ cfg := ${artifact.goPackageName}.NewConfiguration()
189
+ client := ${artifact.goPackageName}.NewAPIClient(cfg)
190
+
191
+ resp, _, err := client.DefaultAPI.SomeOperation(context.Background()).Execute()
192
+ _ = resp
193
+ _ = err
194
+ }
195
+ \`\`\`
196
+ `;
197
+ }
198
+
199
+ function goServer(artifact: ResolvedArtifact): string {
200
+ return `## Install
201
+
202
+ \`\`\`bash
203
+ go get ${artifact.goModule}@${artifact.branch}
204
+ \`\`\`
205
+
206
+ ## Usage
207
+
208
+ Implement the generated \`${artifact.goPackageName}.*ApiServicer\` interfaces and
209
+ wire them into the generated router:
210
+
211
+ \`\`\`go
212
+ router := ${artifact.goPackageName}.NewRouter(
213
+ ${artifact.goPackageName}.NewSomeApiController(yourServiceImpl),
214
+ )
215
+ \`\`\`
216
+ `;
217
+ }
218
+
219
+ function javaClient(artifact: ResolvedArtifact, version: string): string {
220
+ return `## Install (Maven)
221
+
222
+ \`\`\`xml
223
+ <dependency>
224
+ <groupId>${artifact.maven?.groupId}</groupId>
225
+ <artifactId>${artifact.maven?.artifactId}</artifactId>
226
+ <version>${version}</version>
227
+ </dependency>
228
+ \`\`\`
229
+
230
+ Add the repository to your \`settings.xml\` (or \`pom.xml\`) with credentials
231
+ for that repository:
232
+
233
+ \`\`\`xml
234
+ <repository>
235
+ <id>${artifact.publishing.mavenRepositoryId}</id>
236
+ <url>${artifact.publishing.mavenRepositoryUrl}</url>
237
+ </repository>
238
+ \`\`\`
239
+
240
+ ## Usage
241
+
242
+ Generated with \`library=restclient\` - Spring's \`RestClient\`, the current
243
+ recommended synchronous HTTP client for Spring apps:
244
+
245
+ \`\`\`java
246
+ @Configuration
247
+ public class SomeServiceClientConfig {
248
+
249
+ @Bean
250
+ public ApiClient someServiceApiClient(RestClient.Builder builder) {
251
+ ApiClient client = new ApiClient(builder.build());
252
+ client.setBasePath("https://api.your-domain.com");
253
+ return client;
254
+ }
255
+
256
+ @Bean
257
+ public DefaultApi someServiceApi(ApiClient someServiceApiClient) {
258
+ return new DefaultApi(someServiceApiClient);
259
+ }
260
+ }
261
+ \`\`\`
262
+ `;
263
+ }
264
+
265
+ function javaServer(artifact: ResolvedArtifact, version: string): string {
266
+ return `## Install (Maven)
267
+
268
+ \`\`\`xml
269
+ <dependency>
270
+ <groupId>${artifact.maven?.groupId}</groupId>
271
+ <artifactId>${artifact.maven?.artifactId}</artifactId>
272
+ <version>${version}</version>
273
+ </dependency>
274
+ \`\`\`
275
+
276
+ Add the repository to your \`settings.xml\` (or \`pom.xml\`) with credentials
277
+ for that repository:
278
+
279
+ \`\`\`xml
280
+ <repository>
281
+ <id>${artifact.publishing.mavenRepositoryId}</id>
282
+ <url>${artifact.publishing.mavenRepositoryUrl}</url>
283
+ </repository>
284
+ \`\`\`
285
+
286
+ ## Usage
287
+
288
+ This artifact only contains the generated Spring \`@RestController\` interfaces
289
+ (\`interfaceOnly=true\`) - implement them in your service:
290
+
291
+ \`\`\`java
292
+ @RestController
293
+ public class SomeController implements SomeApi {
294
+ // interface methods generated from the OpenAPI contract
295
+ }
296
+ \`\`\`
297
+ `;
298
+ }
299
+
300
+ //</editor-fold>
@@ -0,0 +1,69 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { renderArtifactTag } from "../config/publishing";
4
+ import { buildTemplateContext, interpolate } from "../config/template";
5
+ import type {
6
+ ResolvedArtifact,
7
+ ResolvedContract,
8
+ VarsTree,
9
+ } from "../config/types";
10
+ import { renderDefaultReadme } from "./default-templates";
11
+
12
+ export interface RenderReadmeArgs {
13
+ contract: ResolvedContract;
14
+ artifact: ResolvedArtifact;
15
+ version: string;
16
+ github: { owner: string; repo: string };
17
+ vars: VarsTree;
18
+ }
19
+
20
+ /**
21
+ * Renders the root-level `README.md` for a generated SDK package.
22
+ *
23
+ * If the artifact has a `readme:` path configured (resolved at config-load time
24
+ * to `artifact.readmeTemplate`), that file is read and interpolated with the
25
+ * same `{...}` placeholder engine naming templates use - `{service}`,
26
+ * `{title}`, `{version}`, `{vars.*}`, `{github.owner}`, `{github.repo}`, plus
27
+ * `{artifact.*}` (id/lang/kind/package/goModule/goPackageName/maven.groupId/
28
+ * maven.artifactId/branch/tag/npmRegistry/mavenRepositoryUrl). Otherwise,
29
+ * falls back to a built-in default template for the artifact's language/kind.
30
+ *
31
+ * @param args - The contract, artifact, version, and github/vars context to
32
+ * render for.
33
+ * @returns The rendered README content.
34
+ */
35
+ export async function renderReadme(args: RenderReadmeArgs): Promise<string> {
36
+ if (!args.artifact.readmeTemplate) {
37
+ return renderDefaultReadme(args);
38
+ }
39
+
40
+ const raw = await readFile(args.artifact.readmeTemplate, "utf8");
41
+ const context = buildTemplateContext({
42
+ service: args.contract.name,
43
+ title: args.contract.title,
44
+ version: args.version,
45
+ github: args.github,
46
+ vars: args.vars,
47
+ artifact: {
48
+ id: args.artifact.id,
49
+ lang: args.artifact.lang,
50
+ kind: args.artifact.kind,
51
+ package: args.artifact.package,
52
+ goModule: args.artifact.goModule,
53
+ goPackageName: args.artifact.goPackageName,
54
+ maven: args.artifact.maven,
55
+ branch: args.artifact.branch,
56
+ tag: renderArtifactTag(
57
+ args.artifact,
58
+ args.contract.name,
59
+ args.version,
60
+ args.github,
61
+ args.vars,
62
+ ),
63
+ npmRegistry: args.artifact.publishing.npmRegistry,
64
+ mavenRepositoryUrl: args.artifact.publishing.mavenRepositoryUrl,
65
+ },
66
+ });
67
+
68
+ return interpolate(raw, context);
69
+ }
@@ -0,0 +1,67 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+
6
+ import type { ResolvedConfig } from "../config/types";
7
+
8
+ const GENERATED_HEADER =
9
+ "# AUTO-GENERATED by Seagull from its config and 'redocly.base.yaml'.\n" +
10
+ "# Do not edit directly, edit those two files instead (this file is\n" +
11
+ "# regenerated on every lint/bundle/generate run).\n\n";
12
+
13
+ interface RedoclyBase {
14
+ extends?: string[];
15
+ rules?: Record<string, string>;
16
+ }
17
+
18
+ /**
19
+ * Regenerates `redocly.yaml`'s `apis:` section from the resolved CLI config,
20
+ * merging it with the hand-authored `extends`/`rules` in `redocly.base.yaml`.
21
+ *
22
+ * The CLI config stays the single source of truth for which APIs exist and
23
+ * where their TypeScript server types land, instead of that being duplicated
24
+ * by hand into `redocly.yaml`.
25
+ *
26
+ * Called at the start of every command that shells out to `redocly` or
27
+ * `openapi-typescript` (both read `redocly.yaml` directly), so it's always
28
+ * up to date before those tools run.
29
+ *
30
+ * @param config - The resolved CLI config.
31
+ */
32
+ export async function syncRedoclyConfig(config: ResolvedConfig): Promise<void> {
33
+ const basePath = path.join(config.rootDir, "redocly.base.yaml");
34
+ const base = parseYaml(await readFile(basePath, "utf8")) as RedoclyBase;
35
+
36
+ const apis = Object.fromEntries(
37
+ config.contracts.map((contract) => {
38
+ const typesArtifact = contract.artifacts.find(
39
+ (artifact) => artifact.tool === "openapi-typescript",
40
+ );
41
+
42
+ return [
43
+ `${contract.name}@v1`,
44
+ {
45
+ root: contract.entrypointRelative,
46
+ ...(typesArtifact
47
+ ? {
48
+ "x-openapi-ts": {
49
+ output: path.join(
50
+ path.relative(config.rootDir, typesArtifact.outputDir),
51
+ "index.d.ts",
52
+ ),
53
+ },
54
+ }
55
+ : {}),
56
+ },
57
+ ];
58
+ }),
59
+ );
60
+
61
+ const final = { extends: base.extends, apis, rules: base.rules };
62
+
63
+ await writeFile(
64
+ path.join(config.rootDir, "redocly.yaml"),
65
+ GENERATED_HEADER + stringifyYaml(final),
66
+ );
67
+ }
@@ -0,0 +1,67 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /**
4
+ * Shape of a bundled OpenAPI document, narrowed to the one field this module
5
+ * cares about.
6
+ */
7
+ export interface BundledSpec {
8
+ info?: {
9
+ version?: string;
10
+ };
11
+ }
12
+
13
+ /**
14
+ * Resolves the version to stamp onto a single contract's generated SDK
15
+ * artifacts (npm/Maven packages, Go module tags, git branches, etc.).
16
+ *
17
+ * The single source of truth is that contract's own `info.version` field in its
18
+ * `openapi.yaml` - bump it there and every artifact, package, and git tag for
19
+ * that contract picks up the new version on the next release. Versions are
20
+ * resolved independently per contract: two services can be at different
21
+ * versions at the same time.
22
+ *
23
+ * `SDK_VERSION_OVERRIDE` (wired up from the release workflow's manual `version`
24
+ * input) bypasses the spec entirely and stamps every contract with the same
25
+ * given value. It exists for one-off emergency republishes, not routine
26
+ * releases. Routine releases should always go through `info.version`.
27
+ *
28
+ * @param spec - The parsed, bundled OpenAPI document for the contract.
29
+ * @param contractName - The contract name, used only for the error message.
30
+ * @returns The resolved version string (no leading `v`).
31
+ * @throws Error if no override is set and the spec has no `info.version`.
32
+ */
33
+ export function resolveVersion(
34
+ spec: BundledSpec,
35
+ contractName: string,
36
+ ): string {
37
+ const override = process.env.SDK_VERSION_OVERRIDE?.trim();
38
+
39
+ if (override) {
40
+ return override.replace(/^v/, "");
41
+ }
42
+
43
+ const specVersion = spec.info?.version?.trim();
44
+
45
+ if (!specVersion) {
46
+ throw new Error(
47
+ `specs/${contractName}/openapi.yaml is missing "info.version" - set it ` +
48
+ `to a semver value. This field is the single source of truth for ` +
49
+ `the ${contractName} contract's SDK version.`,
50
+ );
51
+ }
52
+
53
+ return specVersion.replace(/^v/, "");
54
+ }
55
+
56
+ /**
57
+ * Computes a stable content hash of a bundled OpenAPI document's raw JSON text.
58
+ * Stamped alongside `VERSION` into every generated SDK package so
59
+ * `publish-sdk.ts` can tell a genuine no-op republish (same spec, same version)
60
+ * apart from a spec that changed without its `info.version` being bumped.
61
+ *
62
+ * @param raw - The raw bundled spec file contents (JSON text).
63
+ * @returns A `sha256` hex digest of the raw contents.
64
+ */
65
+ export function hashSpec(raw: string): string {
66
+ return createHash("sha256").update(raw).digest("hex");
67
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+
4
+ "compilerOptions": {
5
+ /* Environment */
6
+ "target": "es2024",
7
+ "lib": ["esnext"],
8
+ "types": ["node"],
9
+ "allowJs": false,
10
+
11
+ /* Modules */
12
+ "module": "esnext",
13
+ "moduleResolution": "bundler",
14
+ "moduleDetection": "force",
15
+ "allowImportingTsExtensions": false,
16
+ "erasableSyntaxOnly": true,
17
+ "esModuleInterop": true,
18
+ "resolveJsonModule": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "isolatedModules": true,
21
+ "noEmit": true,
22
+
23
+ /* Strict Type Checking */
24
+ "strict": true,
25
+ "skipLibCheck": true,
26
+ "noFallthroughCasesInSwitch": true,
27
+ "noImplicitOverride": true,
28
+ "noUnusedLocals": true,
29
+ "noUnusedParameters": true,
30
+ "noUncheckedIndexedAccess": true
31
+ },
32
+
33
+ "include": ["src/**/*.ts", "*.config.ts"]
34
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ /**
4
+ * Tsdown configuration
5
+ *
6
+ * @see {@link https://tsdown.dev Tsdown documentation}
7
+ */
8
+ // noinspection JSUnusedGlobalSymbols
9
+ export default defineConfig({
10
+ entry: {
11
+ index: "src/index.ts",
12
+ },
13
+ platform: "node",
14
+ format: ["esm"],
15
+ target: "node22",
16
+ dts: {
17
+ entry: "src/index.ts",
18
+ },
19
+ clean: true,
20
+ sourcemap: true,
21
+ });