@swimmesberger/elarion-jsonrpc-client-generator 0.2.5-preview.98.1 → 0.2.5

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.
@@ -296,7 +296,8 @@ function createTopicTreeNode() {
296
296
  function emitProperties(node, indent) {
297
297
  const pad = ' '.repeat(indent);
298
298
  return Array.from(node.children.entries())
299
- .sort(([left], [right]) => left.localeCompare(right))
299
+ // Code-unit comparison, never localeCompare: output must be byte-identical regardless of host locale.
300
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
300
301
  .map(([segment, child]) => `${pad}readonly ${JSON.stringify(segment)}: ${emitNodeType(child, indent)}`);
301
302
  }
302
303
  function emitNodeType(node, indent) {
@@ -1,6 +1,6 @@
1
- import { baseType, childContext, formatPropertyName, isNullable, resolveSchema, stripNullable, } from './json-schema.js';
1
+ import { baseType, childContext, formatPropertyName, isNullable, resolveSchema, } from './json-schema.js';
2
2
  export function jsonSchemaToTypeScript(schema, ctx, indent = 0) {
3
- const resolved = resolveSchema(schema, ctx);
3
+ const { schema: resolved, ctx: resolvedCtx } = resolveSchema(schema, ctx);
4
4
  const pad = ' '.repeat(indent);
5
5
  const nullable = isNullable(resolved);
6
6
  const base = baseType(resolved);
@@ -26,14 +26,17 @@ export function jsonSchemaToTypeScript(schema, ctx, indent = 0) {
26
26
  return nullable ? 'boolean | null | undefined' : 'boolean';
27
27
  }
28
28
  if (base === 'array' && resolved.items) {
29
- const itemType = jsonSchemaToTypeScript(stripNullable(resolved.items), childContext(ctx, 'items'), indent);
30
- return nullable ? `(${itemType})[] | null | undefined` : `${itemType}[]`;
29
+ // Unstripped items: the recursion emits the item's own nullability (`string | null | undefined`).
30
+ const itemType = jsonSchemaToTypeScript(resolved.items, childContext(resolvedCtx, 'items'), indent);
31
+ // A union item type must be parenthesized — `"red" | "green"[]` is `"red" | ("green"[])`.
32
+ const element = itemType.includes('|') ? `(${itemType})` : itemType;
33
+ return nullable ? `${element}[] | null | undefined` : `${element}[]`;
31
34
  }
32
35
  if (base === 'object' && resolved.properties) {
33
36
  const required = new Set(resolved.required ?? []);
34
37
  const lines = Object.entries(resolved.properties).map(([key, property]) => {
35
38
  const optional = required.has(key) ? '' : '?';
36
- const propertyType = jsonSchemaToTypeScript(property, childContext(ctx, `properties.${key}`), indent + 1);
39
+ const propertyType = jsonSchemaToTypeScript(property, childContext(resolvedCtx, `properties.${key}`), indent + 1);
37
40
  return `${pad} ${formatPropertyName(key)}${optional}: ${propertyType}`;
38
41
  });
39
42
  const objectType = `{\n${lines.join('\n')}\n${pad}}`;
@@ -1,6 +1,6 @@
1
- import { baseType, childContext, formatPropertyName, isNullable, resolveSchema, stripNullable, } from './json-schema.js';
1
+ import { baseType, childContext, formatPropertyName, isNullable, resolveSchema, } from './json-schema.js';
2
2
  export function jsonSchemaToZod(schema, ctx, indent = 0) {
3
- const resolved = resolveSchema(schema, ctx);
3
+ const { schema: resolved, ctx: resolvedCtx } = resolveSchema(schema, ctx);
4
4
  const nullable = isNullable(resolved);
5
5
  const base = baseType(resolved);
6
6
  // Schemas validate the user-facing shape: a file field is a native File on both sides of a call (the
@@ -33,14 +33,17 @@ export function jsonSchemaToZod(schema, ctx, indent = 0) {
33
33
  return nullish('z.boolean()', nullable);
34
34
  }
35
35
  if (base === 'array' && resolved.items) {
36
- const itemSchema = jsonSchemaToZod(stripNullable(resolved.items), childContext(ctx, 'items'), indent);
36
+ // The item schema goes in unstripped: the recursion handles item-level nullability, so a
37
+ // `["string","null"]` item emits `z.string().nullish()` and a legal `["a", null]` element validates.
38
+ // Nullability is only ever stripped at the top-level params/result envelope.
39
+ const itemSchema = jsonSchemaToZod(resolved.items, childContext(resolvedCtx, 'items'), indent);
37
40
  return nullish(arraySchema(`z.array(${itemSchema})`, resolved), nullable);
38
41
  }
39
42
  if (base === 'object' && resolved.properties) {
40
43
  const required = new Set(resolved.required ?? []);
41
44
  const pad = ' '.repeat(indent + 1);
42
45
  const fields = Object.entries(resolved.properties).map(([key, property]) => {
43
- let fieldSchema = jsonSchemaToZod(property, childContext(ctx, `properties.${key}`), indent + 1);
46
+ let fieldSchema = jsonSchemaToZod(property, childContext(resolvedCtx, `properties.${key}`), indent + 1);
44
47
  if (!required.has(key)) {
45
48
  fieldSchema += '.optional()';
46
49
  }
@@ -11,7 +11,17 @@ export interface SchemaContext {
11
11
  export declare function isNullable(schema: JsonSchema): boolean;
12
12
  export declare function baseType(schema: JsonSchema): string | undefined;
13
13
  export declare function stripNullable(schema: JsonSchema): JsonSchema;
14
- export declare function resolveSchema(schema: JsonSchema, ctx: SchemaContext): JsonSchema;
14
+ export interface ResolvedSchema {
15
+ schema: JsonSchema;
16
+ /**
17
+ * The context after resolution, carrying every `$ref` traversed to reach the schema. Recursion into
18
+ * properties/items must continue from this context (not the caller's), so a cycle that re-enters one of
19
+ * those refs through a property (the shape STJ emits for self-referential DTOs) is detected instead of
20
+ * overflowing the stack.
21
+ */
22
+ ctx: SchemaContext;
23
+ }
24
+ export declare function resolveSchema(schema: JsonSchema, ctx: SchemaContext): ResolvedSchema;
15
25
  export declare function childContext(ctx: SchemaContext, segment: string): SchemaContext;
16
26
  export declare function formatPropertyName(key: string): string;
17
27
  /**
@@ -35,7 +35,7 @@ export function stripNullable(schema) {
35
35
  export function resolveSchema(schema, ctx) {
36
36
  assertSupportedComposition(schema, ctx.path);
37
37
  if (!schema.$ref) {
38
- return schema;
38
+ return { schema, ctx };
39
39
  }
40
40
  if (!schema.$ref.startsWith('#/')) {
41
41
  throw new UnsupportedJsonSchemaError(ctx.path, `only local JSON Pointer $ref values are supported (${schema.$ref})`);
@@ -75,17 +75,17 @@ export function collectFilePaths(schema, ctx) {
75
75
  return paths;
76
76
  }
77
77
  function visitFileNodes(schema, ctx, prefix, paths) {
78
- const resolved = resolveSchema(schema, ctx);
78
+ const { schema: resolved, ctx: resolvedCtx } = resolveSchema(schema, ctx);
79
79
  if (resolved['x-elarion-file'] === true) {
80
80
  paths.push(prefix);
81
81
  return;
82
82
  }
83
83
  if (resolved.items) {
84
- visitFileNodes(stripNullable(resolved.items), childContext(ctx, 'items'), [...prefix, '*'], paths);
84
+ visitFileNodes(stripNullable(resolved.items), childContext(resolvedCtx, 'items'), [...prefix, '*'], paths);
85
85
  }
86
86
  if (resolved.properties) {
87
87
  for (const [key, property] of Object.entries(resolved.properties)) {
88
- visitFileNodes(property, childContext(ctx, `properties.${key}`), [...prefix, key], paths);
88
+ visitFileNodes(property, childContext(resolvedCtx, `properties.${key}`), [...prefix, key], paths);
89
89
  }
90
90
  }
91
91
  }
@@ -326,7 +326,8 @@ export function generateRpcClientSource(options) {
326
326
  ' }',
327
327
  '',
328
328
  ' function parseResult<M extends RpcMethod>(method: M, result: unknown): RpcResult<M> {',
329
- ' const transformed = options.transformResult?.(method, result) ?? result',
329
+ ' // A transform returning null/undefined is a legitimate result — never ??-fallback to the raw value.',
330
+ ' const transformed = options.transformResult ? options.transformResult(method, result) : result',
330
331
  ' if (!validateResults) {',
331
332
  ' return transformed as RpcResult<M>',
332
333
  ' }',
@@ -351,6 +352,12 @@ export function generateRpcClientSource(options) {
351
352
  ' try {',
352
353
  ' const raw = await post(request, context, requestOptions, span)',
353
354
  ' const response = parseResponseEnvelope(raw)',
355
+ ' // A spec-compliant server reports request-level failures (e.g. parse errors) with "id": null —',
356
+ ' // surface the server error instead of a generic id-mismatch protocol error.',
357
+ ' if (response.error && (response.id === null || response.id === undefined)) {',
358
+ ' throw toRpcError(response.error)',
359
+ ' }',
360
+ '',
354
361
  ' if (!idsEqual(response.id, request.id)) {',
355
362
  " throw new RpcProtocolError('JSON-RPC response id does not match request id.')",
356
363
  ' }',
@@ -412,6 +419,11 @@ export function generateRpcClientSource(options) {
412
419
  ' const raw = await post(requests, context, requestOptions, span)',
413
420
  '',
414
421
  ' if (!Array.isArray(raw)) {',
422
+ ' // JSON-RPC 2.0: a whole-batch failure (invalid JSON, batch too large, ...) is a single error',
423
+ ' // response object — surface the server error instead of a generic protocol error.',
424
+ ' if (isRecord(raw) && isJsonRpcErrorObject(raw.error)) {',
425
+ ' throw toRpcError(raw.error)',
426
+ ' }',
415
427
  " throw new RpcProtocolError('Expected JSON-RPC batch response array.')",
416
428
  ' }',
417
429
  '',
@@ -652,7 +664,8 @@ function generateApiTypeLines(methodTree) {
652
664
  function emitProperties(node, endpointType, indent) {
653
665
  const pad = ' '.repeat(indent);
654
666
  return Array.from(node.children.entries())
655
- .sort(([left], [right]) => left.localeCompare(right))
667
+ // Code-unit comparison, never localeCompare: output must be byte-identical regardless of host locale.
668
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
656
669
  .map(([segment, child]) => {
657
670
  return `${pad}readonly ${JSON.stringify(segment)}: ${emitNodeType(child, endpointType, indent)}`;
658
671
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-jsonrpc-client-generator",
3
- "version": "0.2.5-preview.98.1",
3
+ "version": "0.2.5",
4
4
  "description": "Generate TypeScript RPC contracts, Zod schemas, and a fetch client from Elarion JSON-RPC schema exports.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",