ts-procedures 6.0.1 → 6.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.
Files changed (36) hide show
  1. package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +24 -0
  2. package/build/codegen/targets/kotlin/ajsc-adapter.js +33 -0
  3. package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -0
  4. package/build/codegen/targets/kotlin/ajsc-adapter.test.d.ts +1 -0
  5. package/build/codegen/targets/kotlin/ajsc-adapter.test.js +19 -0
  6. package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -0
  7. package/build/codegen/targets/kotlin/e2e-compile.test.d.ts +1 -0
  8. package/build/codegen/targets/kotlin/e2e-compile.test.js +43 -0
  9. package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -0
  10. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +11 -0
  11. package/build/codegen/targets/kotlin/emit-route-kotlin.js +73 -0
  12. package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -0
  13. package/build/codegen/targets/kotlin/emit-route-kotlin.test.d.ts +1 -0
  14. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -0
  15. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -0
  16. package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +11 -0
  17. package/build/codegen/targets/kotlin/emit-scope-kotlin.js +35 -0
  18. package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -0
  19. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.d.ts +1 -0
  20. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +52 -0
  21. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -0
  22. package/build/codegen/targets/kotlin/format-kotlin.d.ts +4 -0
  23. package/build/codegen/targets/kotlin/format-kotlin.js +20 -0
  24. package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -0
  25. package/build/codegen/targets/kotlin/format-kotlin.test.d.ts +1 -0
  26. package/build/codegen/targets/kotlin/format-kotlin.test.js +24 -0
  27. package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -0
  28. package/build/codegen/targets/kotlin/integration.test.d.ts +1 -0
  29. package/build/codegen/targets/kotlin/integration.test.js +34 -0
  30. package/build/codegen/targets/kotlin/integration.test.js.map +1 -0
  31. package/docs/http-integrations.md +32 -0
  32. package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +1265 -0
  33. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +401 -0
  34. package/package.json +1 -1
  35. package/src/implementations/http/README.md +2 -0
  36. package/src/implementations/http/hono-stream/README.md +15 -0
@@ -0,0 +1,24 @@
1
+ export interface KotlinEmitResult {
2
+ code: string;
3
+ rootTypeName: string;
4
+ extractedTypeNames: string[];
5
+ imports: string[];
6
+ }
7
+ export interface KotlinEmitOptions {
8
+ rootTypeName: string;
9
+ inlineTypes?: boolean;
10
+ serializer?: 'kotlinx';
11
+ enumStyle?: string;
12
+ depluralize?: boolean;
13
+ arrayItemNaming?: string;
14
+ uncountableWords?: string[];
15
+ }
16
+ export interface KotlinEmitter {
17
+ emit(schema: Record<string, unknown>, opts: KotlinEmitOptions): KotlinEmitResult;
18
+ }
19
+ export declare function createStubKotlinEmitter(results: Record<string, KotlinEmitResult>): KotlinEmitter;
20
+ /**
21
+ * Resolves the production Kotlin emitter from `ajsc`. Throws a clear error
22
+ * if the ajsc package does not yet expose `emitKotlin` (Phase A pending).
23
+ */
24
+ export declare function resolveProductionKotlinEmitter(): Promise<KotlinEmitter>;
@@ -0,0 +1,33 @@
1
+ export function createStubKotlinEmitter(results) {
2
+ return {
3
+ emit(_schema, opts) {
4
+ const result = results[opts.rootTypeName];
5
+ if (result == null) {
6
+ throw new Error(`[stub-kotlin-emitter] No stubbed result for rootTypeName "${opts.rootTypeName}". ` +
7
+ `Provide one in the results map.`);
8
+ }
9
+ return result;
10
+ },
11
+ };
12
+ }
13
+ /**
14
+ * Resolves the production Kotlin emitter from `ajsc`. Throws a clear error
15
+ * if the ajsc package does not yet expose `emitKotlin` (Phase A pending).
16
+ */
17
+ export async function resolveProductionKotlinEmitter() {
18
+ // TODO(ajsc-phase-a): replace dynamic import with a static import once ajsc ships emitKotlin.
19
+ const ajsc = await import('ajsc').catch(() => null);
20
+ const emitKotlin = ajsc?.emitKotlin;
21
+ if (typeof emitKotlin !== 'function') {
22
+ throw new Error('[ts-procedures-codegen] ajsc.emitKotlin is not available. ' +
23
+ 'Kotlin codegen requires ajsc Phase A. See docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md.');
24
+ }
25
+ return {
26
+ emit(schema, opts) {
27
+ // ajsc's return shape is normalized to KotlinEmitResult here.
28
+ const r = emitKotlin(schema, opts);
29
+ return r;
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=ajsc-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ajsc-adapter.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/ajsc-adapter.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,uBAAuB,CACrC,OAAyC;IAEzC,OAAO;QACL,IAAI,CAAC,OAAO,EAAE,IAAI;YAChB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YACzC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CACb,6DAA6D,IAAI,CAAC,YAAY,KAAK;oBACjF,iCAAiC,CACpC,CAAA;YACH,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B;IAClD,8FAA8F;IAC9F,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,UAAU,GAAI,IAAwC,EAAE,UAAU,CAAA;IACxE,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,4DAA4D;YAC5D,6GAA6G,CAC9G,CAAA;IACH,CAAC;IACD,OAAO;QACL,IAAI,CAAC,MAAM,EAAE,IAAI;YACf,8DAA8D;YAC9D,MAAM,CAAC,GAAI,UAA2D,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YACpF,OAAO,CAAC,CAAA;QACV,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
3
+ describe('createStubKotlinEmitter', () => {
4
+ it('returns the configured EmitResult for the matching root name', () => {
5
+ const expected = {
6
+ code: '@Serializable data class User(val id: String)',
7
+ rootTypeName: 'User',
8
+ extractedTypeNames: [],
9
+ imports: ['kotlinx.serialization.Serializable'],
10
+ };
11
+ const emitter = createStubKotlinEmitter({ User: expected });
12
+ expect(emitter.emit({ type: 'object' }, { rootTypeName: 'User' })).toEqual(expected);
13
+ });
14
+ it('throws when asked to emit a name not in the stub map', () => {
15
+ const emitter = createStubKotlinEmitter({});
16
+ expect(() => emitter.emit({}, { rootTypeName: 'Missing' })).toThrow(/Missing/);
17
+ });
18
+ });
19
+ //# sourceMappingURL=ajsc-adapter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ajsc-adapter.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/ajsc-adapter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAyB,MAAM,mBAAmB,CAAA;AAElF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,QAAQ,GAAqB;YACjC,IAAI,EAAE,+CAA+C;YACrD,YAAY,EAAE,MAAM;YACpB,kBAAkB,EAAE,EAAE;YACtB,OAAO,EAAE,CAAC,oCAAoC,CAAC;SAChD,CAAA;QACD,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC3D,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,CAAC,CAAA;QAC3C,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAChF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'node:child_process';
3
+ import { mkdtempSync, writeFileSync } from 'node:fs';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { runPipeline } from '../../pipeline.js';
9
+ import { resolveProductionKotlinEmitter } from './ajsc-adapter.js';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ function kotlincAvailable() {
13
+ try {
14
+ execSync('kotlinc -version', { stdio: 'ignore' });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ describe('kotlin codegen — kotlinc compile (gated)', () => {
22
+ it.skipIf(!kotlincAvailable() || process.env.TS_PROCEDURES_KOTLIN_E2E !== '1')('compiles generated output without errors', async () => {
23
+ const emitter = await resolveProductionKotlinEmitter();
24
+ const envelope = JSON.parse(await readFile(join(__dirname, '__fixtures__/users-envelope.json'), 'utf8'));
25
+ const files = await runPipeline({
26
+ envelope,
27
+ outDir: 'out',
28
+ dryRun: true,
29
+ target: 'kotlin',
30
+ kotlinPackage: 'com.example.api',
31
+ kotlinEmitter: emitter,
32
+ });
33
+ const dir = mkdtempSync(join(tmpdir(), 'tsp-kotlin-e2e-'));
34
+ for (const f of files) {
35
+ writeFileSync(join(dir, f.path.split('/').pop()), f.code);
36
+ }
37
+ // Compile against the kotlinx-serialization runtime jar present in the test env.
38
+ // If the jar isn't on the classpath, this fails; CI must provide it.
39
+ execSync(`kotlinc ${dir}/*.kt -d ${dir}/out.jar`, { stdio: 'inherit' });
40
+ expect(true).toBe(true); // reaching here means compile succeeded
41
+ });
42
+ });
43
+ //# sourceMappingURL=e2e-compile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e-compile.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/e2e-compile.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAA;AAElE,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,SAAS,gBAAgB;IACvB,IAAI,CAAC;QACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,EAAE,CAAC,MAAM,CAAC,CAAC,gBAAgB,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,KAAK,GAAG,CAAC,CAC5E,0CAA0C,EAC1C,KAAK,IAAI,EAAE;QACT,MAAM,OAAO,GAAG,MAAM,8BAA8B,EAAE,CAAA;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CACzB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,kCAAkC,CAAC,EAAE,MAAM,CAAC,CAC5E,CAAA;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ;YACR,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,QAAQ;YAChB,aAAa,EAAE,iBAAiB;YAChC,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;QACF,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAA;QAC1D,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;QAC5D,CAAC;QACD,iFAAiF;QACjF,qEAAqE;QACrE,QAAQ,CAAC,WAAW,GAAG,YAAY,GAAG,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;QACvE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,wCAAwC;IAClE,CAAC,CACF,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { AnyHttpRouteDoc } from '../../../implementations/types.js';
2
+ import type { KotlinEmitter } from './ajsc-adapter.js';
3
+ export interface EmitRouteResult {
4
+ /** Inner body of the `object RouteName { ... }` block — already indented one level. */
5
+ code: string;
6
+ /** Imports collected from every ajsc emit + any helpers this route used. */
7
+ imports: string[];
8
+ /** Outer route name used as the `object RouteName` identifier. */
9
+ routeName: string;
10
+ }
11
+ export declare function emitKotlinRoute(route: AnyHttpRouteDoc, emitter: KotlinEmitter, errorSchemas: Map<string, unknown>): EmitRouteResult;
@@ -0,0 +1,73 @@
1
+ import { indent } from './format-kotlin.js';
2
+ const COLON_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g;
3
+ function toBracePath(template) {
4
+ return template.replace(COLON_PARAM_RE, '{$1}');
5
+ }
6
+ function pathParamNames(template) {
7
+ const names = [];
8
+ for (const match of template.matchAll(COLON_PARAM_RE))
9
+ names.push(match[1]);
10
+ return names;
11
+ }
12
+ function buildPathFn(bracePath, params) {
13
+ if (params.length === 0)
14
+ return `const val path = "${bracePath}"`;
15
+ let body = bracePath;
16
+ for (const name of params)
17
+ body = body.replace(`{${name}}`, `\${p.${name}}`);
18
+ return `fun path(p: PathParams): String = "${body}"`;
19
+ }
20
+ export function emitKotlinRoute(route, emitter, errorSchemas) {
21
+ const kind = route.kind;
22
+ if (kind === 'stream') {
23
+ console.warn(`[ts-procedures-codegen] Skipping stream route "${route.name}" — streams are out of scope for kotlin target.`);
24
+ return { code: '', imports: [], routeName: route.name };
25
+ }
26
+ const isApi = kind === 'api' || 'fullPath' in route;
27
+ const rawPath = isApi ? route.fullPath : route.path;
28
+ const method = String(route.method).toUpperCase();
29
+ const bracePath = toBracePath(rawPath);
30
+ const params = pathParamNames(rawPath);
31
+ const lines = [
32
+ `const val method = "${method}"`,
33
+ `const val pathTemplate = "${bracePath}"`,
34
+ buildPathFn(bracePath, params),
35
+ ];
36
+ const imports = [];
37
+ const schema = route.schema ?? {};
38
+ const input = (schema.input ?? {});
39
+ // Per-slot emission. Order is fixed for deterministic output.
40
+ const slots = [
41
+ { key: 'pathParams', rootName: 'PathParams', source: input.pathParams },
42
+ { key: 'query', rootName: 'Query', source: input.query },
43
+ { key: 'body', rootName: 'Body', source: input.body },
44
+ { key: 'response', rootName: 'Response', source: schema.returnType },
45
+ ];
46
+ for (const slot of slots) {
47
+ if (slot.source == null)
48
+ continue;
49
+ const result = emitter.emit(slot.source, { rootTypeName: slot.rootName });
50
+ lines.push('');
51
+ lines.push(result.code);
52
+ imports.push(...result.imports);
53
+ }
54
+ // Errors namespace — route.errors is `string[]` of taxonomy keys; look up each schema
55
+ // from the envelope-level errors map. Keys without schemas are skipped silently
56
+ // (matching the existing TS scope emitter's `errorKeys` filter).
57
+ const routeErrorKeys = (route.errors ?? [])
58
+ .filter((key) => errorSchemas.has(key));
59
+ if (routeErrorKeys.length > 0) {
60
+ const inner = [];
61
+ for (const key of routeErrorKeys) {
62
+ const r = emitter.emit(errorSchemas.get(key), { rootTypeName: key });
63
+ inner.push(r.code);
64
+ imports.push(...r.imports);
65
+ }
66
+ lines.push('');
67
+ lines.push('object Errors {');
68
+ lines.push(indent(inner.join('\n\n'), 1));
69
+ lines.push('}');
70
+ }
71
+ return { code: lines.join('\n'), imports, routeName: route.name };
72
+ }
73
+ //# sourceMappingURL=emit-route-kotlin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-route-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-route-kotlin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { emitKotlinRoute } from './emit-route-kotlin.js';
3
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
4
+ const ok = (code, rootTypeName) => ({
5
+ code,
6
+ rootTypeName,
7
+ extractedTypeNames: [],
8
+ imports: ['kotlinx.serialization.Serializable'],
9
+ });
10
+ const noErrors = new Map();
11
+ describe('emitKotlinRoute', () => {
12
+ it('emits an api-kind route with path params and a response', () => {
13
+ const route = {
14
+ kind: 'api',
15
+ name: 'GetUser',
16
+ method: 'GET',
17
+ fullPath: '/users/:id',
18
+ schema: {
19
+ input: {
20
+ pathParams: { type: 'object' },
21
+ },
22
+ returnType: { type: 'object' },
23
+ },
24
+ errors: [],
25
+ };
26
+ const emitter = createStubKotlinEmitter({
27
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
28
+ Response: ok('@Serializable data class Response(val id: String, val name: String)', 'Response'),
29
+ });
30
+ const result = emitKotlinRoute(route, emitter, noErrors);
31
+ expect(result.imports).toContain('kotlinx.serialization.Serializable');
32
+ expect(result.code).toContain('const val method = "GET"');
33
+ expect(result.code).toContain('const val pathTemplate = "/users/{id}"');
34
+ expect(result.code).toContain('fun path(p: PathParams): String = "/users/${p.id}"');
35
+ expect(result.code).toContain('@Serializable data class PathParams(val id: String)');
36
+ expect(result.code).toContain('@Serializable data class Response(val id: String, val name: String)');
37
+ });
38
+ it('emits a route with no path params using a path constant', () => {
39
+ const route = {
40
+ kind: 'api',
41
+ name: 'CreateUser',
42
+ method: 'POST',
43
+ fullPath: '/users',
44
+ schema: { input: { body: { type: 'object' } }, returnType: { type: 'object' } },
45
+ errors: [],
46
+ };
47
+ const emitter = createStubKotlinEmitter({
48
+ Body: ok('@Serializable data class Body(val name: String)', 'Body'),
49
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
50
+ });
51
+ const result = emitKotlinRoute(route, emitter, noErrors);
52
+ expect(result.code).toContain('const val path = "/users"');
53
+ expect(result.code).not.toContain('fun path(');
54
+ });
55
+ it('emits an Errors namespace for routes whose error keys have schemas in the envelope', () => {
56
+ const route = {
57
+ kind: 'api',
58
+ name: 'GetUser',
59
+ method: 'GET',
60
+ fullPath: '/users/:id',
61
+ schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
62
+ errors: ['NotFound'],
63
+ };
64
+ const emitter = createStubKotlinEmitter({
65
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
66
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
67
+ NotFound: ok('@Serializable data class NotFound(val name: String, val message: String)', 'NotFound'),
68
+ });
69
+ const errorSchemas = new Map([['NotFound', { type: 'object' }]]);
70
+ const result = emitKotlinRoute(route, emitter, errorSchemas);
71
+ expect(result.code).toContain('object Errors {');
72
+ expect(result.code).toContain('@Serializable data class NotFound(val name: String, val message: String)');
73
+ });
74
+ it('silently skips error keys with no schema in the envelope map', () => {
75
+ const route = {
76
+ kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
77
+ schema: {}, errors: ['UnknownTaxonomyKey'],
78
+ };
79
+ const result = emitKotlinRoute(route, createStubKotlinEmitter({}), new Map());
80
+ expect(result.code).not.toContain('object Errors {');
81
+ });
82
+ it('skips stream routes with a warning', () => {
83
+ const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', schema: {}, errors: [] };
84
+ const result = emitKotlinRoute(route, createStubKotlinEmitter({}), noErrors);
85
+ expect(result.code).toBe('');
86
+ });
87
+ });
88
+ //# sourceMappingURL=emit-route-kotlin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-route-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-route-kotlin.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ScopeGroup } from '../../group-routes.js';
2
+ import type { KotlinEmitter } from './ajsc-adapter.js';
3
+ export interface EmitScopeOptions {
4
+ kotlinPackage: string;
5
+ sourceHash: string;
6
+ }
7
+ export interface EmittedKotlinFile {
8
+ filename: string;
9
+ code: string;
10
+ }
11
+ export declare function emitKotlinScope(group: ScopeGroup, opts: EmitScopeOptions, emitter: KotlinEmitter, errorSchemas: Map<string, unknown>): EmittedKotlinFile;
@@ -0,0 +1,35 @@
1
+ import { emitKotlinRoute } from './emit-route-kotlin.js';
2
+ import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent } from './format-kotlin.js';
3
+ function pascalCase(scope) {
4
+ return scope
5
+ .split('-')
6
+ .filter((p) => p.length > 0)
7
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
8
+ .join('');
9
+ }
10
+ export function emitKotlinScope(group, opts, emitter, errorSchemas) {
11
+ const scopeName = pascalCase(group.scopeKey);
12
+ const allImports = [];
13
+ const routeBlocks = [];
14
+ for (const route of group.routes) {
15
+ const r = emitKotlinRoute(route, emitter, errorSchemas);
16
+ if (r.code === '')
17
+ continue;
18
+ allImports.push(...r.imports);
19
+ const wrapped = `object ${r.routeName} {\n${indent(r.code, 1)}\n}`;
20
+ routeBlocks.push(wrapped);
21
+ }
22
+ const innerScope = routeBlocks.length === 0 ? '' : indent(routeBlocks.join('\n\n'), 1);
23
+ const scopeBlock = innerScope === ''
24
+ ? `object ${scopeName} {\n}`
25
+ : `object ${scopeName} {\n${innerScope}\n}`;
26
+ const importsBlock = kotlinImports(allImports);
27
+ const parts = [
28
+ kotlinPackageDecl(opts.kotlinPackage),
29
+ kotlinSourceHashHeader(opts.sourceHash),
30
+ importsBlock,
31
+ scopeBlock,
32
+ ].filter((p) => p.length > 0);
33
+ return { filename: `${scopeName}.kt`, code: parts.join('\n\n') + '\n' };
34
+ }
35
+ //# sourceMappingURL=emit-scope-kotlin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-scope-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-scope-kotlin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAYrG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,EAAE,CAAC,CAAA;AACb,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAiB,EACjB,IAAsB,EACtB,OAAsB,EACtB,YAAkC;IAElC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAa,EAAE,CAAA;IAC/B,MAAM,WAAW,GAAa,EAAE,CAAA;IAEhC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;QACvD,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE;YAAE,SAAQ;QAC3B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAA;QAC7B,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,SAAS,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAA;QAClE,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3B,CAAC;IAED,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACtF,MAAM,UAAU,GAAG,UAAU,KAAK,EAAE;QAClC,CAAC,CAAC,UAAU,SAAS,OAAO;QAC5B,CAAC,CAAC,UAAU,SAAS,OAAO,UAAU,KAAK,CAAA;IAE7C,MAAM,YAAY,GAAG,aAAa,CAAC,UAAU,CAAC,CAAA;IAC9C,MAAM,KAAK,GAAG;QACZ,iBAAiB,CAAC,IAAI,CAAC,aAAa,CAAC;QACrC,sBAAsB,CAAC,IAAI,CAAC,UAAU,CAAC;QACvC,YAAY;QACZ,UAAU;KACX,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAE7B,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;AACzE,CAAC"}
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { emitKotlinScope } from './emit-scope-kotlin.js';
3
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
4
+ const ok = (code, rootTypeName) => ({
5
+ code,
6
+ rootTypeName,
7
+ extractedTypeNames: [],
8
+ imports: ['kotlinx.serialization.Serializable'],
9
+ });
10
+ describe('emitKotlinScope', () => {
11
+ it('produces a complete kotlin source file for a single-route scope', () => {
12
+ const route = {
13
+ kind: 'api',
14
+ name: 'GetUser',
15
+ method: 'GET',
16
+ fullPath: '/users/:id',
17
+ schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
18
+ errors: [],
19
+ };
20
+ const group = { scopeKey: 'users', camelCase: 'users', routes: [route] };
21
+ const emitter = createStubKotlinEmitter({
22
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
23
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
24
+ });
25
+ const file = emitKotlinScope(group, { kotlinPackage: 'com.example.api', sourceHash: 'abc123' }, emitter, new Map());
26
+ expect(file.filename).toBe('Users.kt');
27
+ expect(file.code).toContain('package com.example.api');
28
+ expect(file.code).toContain('// Source hash: abc123');
29
+ expect(file.code).toContain('import kotlinx.serialization.Serializable');
30
+ expect(file.code).toContain('object Users {');
31
+ expect(file.code).toContain('object GetUser {');
32
+ expect(file.code).toContain('const val method = "GET"');
33
+ });
34
+ it('joins multiple routes inside one scope object', () => {
35
+ const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', schema: {}, errors: [] };
36
+ const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', schema: {}, errors: [] };
37
+ const group = { scopeKey: 'users', camelCase: 'users', routes: [route1, route2] };
38
+ const emitter = createStubKotlinEmitter({});
39
+ const file = emitKotlinScope(group, { kotlinPackage: 'com.example.api', sourceHash: 'h' }, emitter, new Map());
40
+ expect(file.code).toContain('object GetUser {');
41
+ expect(file.code).toContain('object CreateUser {');
42
+ // exactly one outer scope object
43
+ expect((file.code.match(/^object Users \{/gm) ?? []).length).toBe(1);
44
+ });
45
+ it('uses PascalCase scope name for the filename and outer object', () => {
46
+ const group = { scopeKey: 'admin-users', camelCase: 'adminUsers', routes: [] };
47
+ const file = emitKotlinScope(group, { kotlinPackage: 'p', sourceHash: 'h' }, createStubKotlinEmitter({}), new Map());
48
+ expect(file.filename).toBe('AdminUsers.kt');
49
+ expect(file.code).toContain('object AdminUsers {');
50
+ });
51
+ });
52
+ //# sourceMappingURL=emit-scope-kotlin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-scope-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-scope-kotlin.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare function kotlinPackageDecl(pkg: string): string;
2
+ export declare function kotlinSourceHashHeader(hash: string): string;
3
+ export declare function kotlinImports(imports: string[]): string;
4
+ export declare function indent(text: string, level: number): string;
@@ -0,0 +1,20 @@
1
+ export function kotlinPackageDecl(pkg) {
2
+ return `package ${pkg}`;
3
+ }
4
+ export function kotlinSourceHashHeader(hash) {
5
+ return `// Source hash: ${hash}`;
6
+ }
7
+ export function kotlinImports(imports) {
8
+ if (imports.length === 0)
9
+ return '';
10
+ const unique = Array.from(new Set(imports)).sort();
11
+ return unique.map((i) => `import ${i}`).join('\n');
12
+ }
13
+ export function indent(text, level) {
14
+ const prefix = ' '.repeat(level);
15
+ return text
16
+ .split('\n')
17
+ .map((line) => (line.length === 0 ? line : `${prefix}${line}`))
18
+ .join('\n');
19
+ }
20
+ //# sourceMappingURL=format-kotlin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/format-kotlin.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,WAAW,GAAG,EAAE,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,OAAO,mBAAmB,IAAI,EAAE,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAiB;IAC7C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAClD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAa;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACnC,OAAO,IAAI;SACR,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC;SAC9D,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent, } from './format-kotlin.js';
3
+ describe('format-kotlin', () => {
4
+ it('emits a package declaration', () => {
5
+ expect(kotlinPackageDecl('com.example.api')).toBe('package com.example.api');
6
+ });
7
+ it('emits a source-hash header line', () => {
8
+ expect(kotlinSourceHashHeader('abc123')).toBe('// Source hash: abc123');
9
+ });
10
+ it('dedupes and sorts imports', () => {
11
+ expect(kotlinImports(['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.Serializable'])).toBe('import kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable');
12
+ });
13
+ it('returns empty string when no imports', () => {
14
+ expect(kotlinImports([])).toBe('');
15
+ });
16
+ it('indents every line by 4 spaces per level', () => {
17
+ expect(indent('a\nb', 1)).toBe(' a\n b');
18
+ expect(indent('a', 2)).toBe(' a');
19
+ });
20
+ it('preserves blank lines without trailing whitespace when indenting', () => {
21
+ expect(indent('a\n\nb', 1)).toBe(' a\n\n b');
22
+ });
23
+ });
24
+ //# sourceMappingURL=format-kotlin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/format-kotlin.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,MAAM,GACP,MAAM,oBAAoB,CAAA;AAE3B,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;IACzE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,aAAa,CAAC,CAAC,oCAAoC,EAAE,kCAAkC,EAAE,oCAAoC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC1I,oFAAoF,CACrF,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAC9C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runPipeline } from '../../pipeline.js';
6
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ describe('kotlin codegen — integration', () => {
10
+ it('produces byte-identical output against the golden fixture', async () => {
11
+ const envelopePath = join(__dirname, '__fixtures__/users-envelope.json');
12
+ const goldenPath = join(__dirname, '__fixtures__/users-golden.kt');
13
+ const envelope = JSON.parse(await readFile(envelopePath, 'utf8'));
14
+ const goldenTemplate = await readFile(goldenPath, 'utf8');
15
+ const emitter = createStubKotlinEmitter({
16
+ PathParams: { code: '@Serializable\ndata class PathParams(val id: String)', rootTypeName: 'PathParams', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
17
+ Response: { code: '@Serializable\ndata class Response(val id: String, val name: String)', rootTypeName: 'Response', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
18
+ Body: { code: '@Serializable\ndata class Body(val name: String, val email: String)', rootTypeName: 'Body', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
19
+ NotFound: { code: '@Serializable\ndata class NotFound(val name: String = "NotFound", val message: String)', rootTypeName: 'NotFound', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
20
+ });
21
+ const files = await runPipeline({
22
+ envelope, outDir: 'out', dryRun: true,
23
+ target: 'kotlin', kotlinPackage: 'com.example.api',
24
+ kotlinEmitter: emitter,
25
+ });
26
+ expect(files).toHaveLength(1);
27
+ expect(files[0].path).toBe(join('out', 'Users.kt'));
28
+ // Hash is deterministic given envelope contents; splice into golden template.
29
+ const sourceHashLine = files[0].code.split('\n').find((l) => l.startsWith('// Source hash:')) ?? '';
30
+ const goldenWithHash = goldenTemplate.replace('// Source hash: <PLACEHOLDER>', sourceHashLine);
31
+ expect(files[0].code).toBe(goldenWithHash);
32
+ });
33
+ });
34
+ //# sourceMappingURL=integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/integration.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,kCAAkC,CAAC,CAAA;QACxE,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAA;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;QACjE,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAEzD,MAAM,OAAO,GAAG,uBAAuB,CAAC;YACtC,UAAU,EAAE,EAAE,IAAI,EAAE,sDAAsD,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YACjL,QAAQ,EAAE,EAAE,IAAI,EAAE,sEAAsE,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YAC7L,IAAI,EAAE,EAAE,IAAI,EAAE,qEAAqE,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YACpL,QAAQ,EAAE,EAAE,IAAI,EAAE,wFAAwF,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;SAChN,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI;YACrC,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,iBAAiB;YAClD,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;QAEF,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAA;QAEpD,8EAA8E;QAC9E,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAA;QACpG,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,+BAA+B,EAAE,cAAc,CAAC,CAAA;QAC9F,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -271,6 +271,38 @@ The observer is awaited before the response is sent, and any error it throws is
271
271
 
272
272
  Configuring only the taxonomy, only `onError`, both, or neither are all valid. When neither is configured the builder goes straight from step 1 to step 4.
273
273
 
274
+ ## One Hono Server, Multiple Builders
275
+
276
+ You don't need a separate Hono instance per builder. `HonoRPCAppBuilder`, `HonoAPIAppBuilder`, and `HonoStreamAppBuilder` each accept an optional `app?: Hono` in their config — pass the same instance and they all mount their routes onto the same server:
277
+
278
+ ```typescript
279
+ import { Hono } from 'hono'
280
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
281
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
282
+ import { HonoStreamAppBuilder } from 'ts-procedures/hono-stream'
283
+ import { DocRegistry } from 'ts-procedures/http-docs'
284
+
285
+ const app = new Hono()
286
+
287
+ const rpcBuilder = new HonoRPCAppBuilder({ app, pathPrefix: '/rpc' })
288
+ .register(RPC, ctxResolver)
289
+ const apiBuilder = new HonoAPIAppBuilder({ app, pathPrefix: '/api' })
290
+ .register(API, ctxResolver)
291
+ const streamBuilder = new HonoStreamAppBuilder({ app })
292
+ .register(Stream, ctxResolver)
293
+
294
+ rpcBuilder.build()
295
+ apiBuilder.build()
296
+ streamBuilder.build()
297
+
298
+ const docs = new DocRegistry().from(rpcBuilder).from(apiBuilder).from(streamBuilder)
299
+ app.get('/docs', (c) => c.json(docs.toJSON()))
300
+ ```
301
+
302
+ One server, one Hono instance, one `/docs` endpoint. The three builders are **registration scopes**, not separate servers — each owns the routes it mounts (RPC vs API vs Stream) but all of them coexist on the same listener. Mix in custom middleware (`app.use(...)`), health checks, or static routes on the shared `app` before or after `.build()`.
303
+
304
+ If you omit `app`, each builder constructs its own internal `Hono` instance — useful when you really do want isolated apps (multi-tenant routing, separate ports, etc.).
305
+
274
306
  ## DocRegistry — Composing Docs from Multiple Builders
275
307
 
276
308
  Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope: