ts-procedures 6.2.0 → 7.0.0-beta.0

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 (109) hide show
  1. package/README.md +2 -0
  2. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +2 -1
  3. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +38 -1
  4. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +215 -3
  5. package/agent_config/claude-code/skills/ts-procedures/patterns.md +60 -2
  6. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +1 -1
  7. package/agent_config/copilot/copilot-instructions.md +3 -0
  8. package/agent_config/cursor/cursorrules +3 -0
  9. package/build/client/augment-error-map.test-d.d.ts +10 -0
  10. package/build/client/augment-error-map.test-d.js +14 -0
  11. package/build/client/augment-error-map.test-d.js.map +1 -0
  12. package/build/client/call.d.ts +14 -2
  13. package/build/client/call.js +96 -9
  14. package/build/client/call.js.map +1 -1
  15. package/build/client/call.test.js +50 -1
  16. package/build/client/call.test.js.map +1 -1
  17. package/build/client/classify-error.d.ts +11 -0
  18. package/build/client/classify-error.js +49 -0
  19. package/build/client/classify-error.js.map +1 -0
  20. package/build/client/classify-error.test.d.ts +1 -0
  21. package/build/client/classify-error.test.js +55 -0
  22. package/build/client/classify-error.test.js.map +1 -0
  23. package/build/client/error-dispatch.d.ts +1 -1
  24. package/build/client/error-dispatch.js +1 -1
  25. package/build/client/errors.d.ts +55 -4
  26. package/build/client/errors.js +54 -7
  27. package/build/client/errors.js.map +1 -1
  28. package/build/client/errors.test.js +89 -4
  29. package/build/client/errors.test.js.map +1 -1
  30. package/build/client/fetch-adapter.d.ts +2 -1
  31. package/build/client/fetch-adapter.js +2 -1
  32. package/build/client/fetch-adapter.js.map +1 -1
  33. package/build/client/fetch-adapter.test.js +12 -0
  34. package/build/client/fetch-adapter.test.js.map +1 -1
  35. package/build/client/index.d.ts +5 -3
  36. package/build/client/index.js +15 -3
  37. package/build/client/index.js.map +1 -1
  38. package/build/client/resolve-options.d.ts +32 -1
  39. package/build/client/resolve-options.js +32 -16
  40. package/build/client/resolve-options.js.map +1 -1
  41. package/build/client/resolve-options.test.js +67 -6
  42. package/build/client/resolve-options.test.js.map +1 -1
  43. package/build/client/result-type.test-d.d.ts +1 -0
  44. package/build/client/result-type.test-d.js +28 -0
  45. package/build/client/result-type.test-d.js.map +1 -0
  46. package/build/client/safe-call.test.d.ts +1 -0
  47. package/build/client/safe-call.test.js +137 -0
  48. package/build/client/safe-call.test.js.map +1 -0
  49. package/build/client/stream.d.ts +1 -1
  50. package/build/client/stream.js +22 -8
  51. package/build/client/stream.js.map +1 -1
  52. package/build/client/stream.test.js +11 -1
  53. package/build/client/stream.test.js.map +1 -1
  54. package/build/client/types.d.ts +96 -3
  55. package/build/codegen/bundle-size.test.d.ts +1 -0
  56. package/build/codegen/bundle-size.test.js +68 -0
  57. package/build/codegen/bundle-size.test.js.map +1 -0
  58. package/build/codegen/e2e.test.js +103 -1
  59. package/build/codegen/e2e.test.js.map +1 -1
  60. package/build/codegen/emit-client-runtime.js +7 -0
  61. package/build/codegen/emit-client-runtime.js.map +1 -1
  62. package/build/codegen/emit-client-runtime.test.js +6 -2
  63. package/build/codegen/emit-client-runtime.test.js.map +1 -1
  64. package/build/codegen/emit-client-types.d.ts +7 -2
  65. package/build/codegen/emit-client-types.js +29 -8
  66. package/build/codegen/emit-client-types.js.map +1 -1
  67. package/build/codegen/emit-client-types.test.js +20 -8
  68. package/build/codegen/emit-client-types.test.js.map +1 -1
  69. package/build/codegen/emit-errors.d.ts +1 -1
  70. package/build/codegen/emit-errors.js +1 -1
  71. package/build/codegen/emit-index.js +1 -1
  72. package/build/codegen/emit-index.js.map +1 -1
  73. package/build/codegen/emit-scope.js +94 -26
  74. package/build/codegen/emit-scope.js.map +1 -1
  75. package/build/codegen/emit-scope.test.js +297 -2
  76. package/build/codegen/emit-scope.test.js.map +1 -1
  77. package/docs/client-and-codegen.md +77 -7
  78. package/docs/client-error-handling.md +357 -0
  79. package/docs/superpowers/plans/2026-04-29-safe-result-api.md +2293 -0
  80. package/docs/superpowers/specs/2026-04-29-safe-result-api-design.md +324 -0
  81. package/package.json +1 -1
  82. package/src/client/augment-error-map.test-d.ts +22 -0
  83. package/src/client/call.test.ts +65 -1
  84. package/src/client/call.ts +111 -9
  85. package/src/client/classify-error.test.ts +65 -0
  86. package/src/client/classify-error.ts +59 -0
  87. package/src/client/error-dispatch.ts +1 -1
  88. package/src/client/errors.test.ts +108 -4
  89. package/src/client/errors.ts +70 -7
  90. package/src/client/fetch-adapter.test.ts +15 -0
  91. package/src/client/fetch-adapter.ts +5 -2
  92. package/src/client/index.ts +39 -3
  93. package/src/client/resolve-options.test.ts +83 -5
  94. package/src/client/resolve-options.ts +61 -16
  95. package/src/client/result-type.test-d.ts +51 -0
  96. package/src/client/safe-call.test.ts +157 -0
  97. package/src/client/stream.test.ts +13 -1
  98. package/src/client/stream.ts +25 -8
  99. package/src/client/types.ts +112 -3
  100. package/src/codegen/bundle-size.test.ts +74 -0
  101. package/src/codegen/e2e.test.ts +108 -1
  102. package/src/codegen/emit-client-runtime.test.ts +7 -2
  103. package/src/codegen/emit-client-runtime.ts +7 -0
  104. package/src/codegen/emit-client-types.test.ts +22 -7
  105. package/src/codegen/emit-client-types.ts +35 -10
  106. package/src/codegen/emit-errors.ts +1 -1
  107. package/src/codegen/emit-index.ts +1 -1
  108. package/src/codegen/emit-scope.test.ts +324 -2
  109. package/src/codegen/emit-scope.ts +98 -36
@@ -246,28 +246,57 @@ async function emitRpcRoute(route: RPCHttpRouteDoc, ctx: EmitRouteContext): Prom
246
246
  const responseTypeName = refs['Response'] ?? 'unknown'
247
247
  const scopeStr = Array.isArray(route.scope) ? route.scope.join('-') : route.scope
248
248
 
249
+ // Compute the error union once — used for both the .safe return type and
250
+ // injectRouteErrors. Use errorUnion !== null (not route.errors?.length) so
251
+ // we only emit Result<…, Errors> when keys were actually emitted in _errors.ts.
252
+ const errorUnion = buildErrorUnion(route.errors, ctx)
253
+ const hasErrors = errorUnion !== null
254
+ const errorsRef = ctx.namespaceTypes
255
+ ? `${ctx.scopePascal}.${pascal}.Errors`
256
+ : `${pascal}Errors`
257
+ const resultType = hasErrors
258
+ ? `Result<${responseTypeName}, ${errorsRef}>`
259
+ : `ResultNoTyped<${responseTypeName}>`
260
+
261
+ // When no typed errors, cast Result<T, never> → ResultNoTyped<T> so consumers
262
+ // see the simpler type in their IDE. The cast is safe: Result<T, never> has an
263
+ // unreachable `kind: 'typed'` arm that ResultNoTyped intentionally omits.
264
+ const safeReturn = hasErrors
265
+ ? `return client.safeCall<${responseTypeName}, ${errorsRef}>({`
266
+ : `return client.safeCall<${responseTypeName}>({`
267
+ const safeResultCast = hasErrors ? '' : ` as Promise<${resultType}>`
268
+
249
269
  const callable = [
250
270
  ` /** ${route.method.toUpperCase()} ${route.path} */`,
251
- ` ${pascal}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
252
- ` return client.call<${responseTypeName}>({`,
253
- ` name: '${pascal}',`,
254
- ` scope: '${scopeStr}',`,
255
- ` path: '${route.path}',`,
256
- ` method: '${route.method}',`,
257
- ` kind: 'rpc',`,
258
- ` params,`,
259
- ` }, options)`,
260
- ` },`,
271
+ ` ${pascal}: Object.assign(`,
272
+ ` function ${pascal}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
273
+ ` return client.call<${responseTypeName}>({`,
274
+ ` name: '${pascal}',`,
275
+ ` scope: '${scopeStr}',`,
276
+ ` path: '${route.path}',`,
277
+ ` method: '${route.method}',`,
278
+ ` kind: 'rpc',`,
279
+ ` params,`,
280
+ ` }, options)`,
281
+ ` },`,
282
+ ` {`,
283
+ ` safe(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${resultType}> {`,
284
+ ` ${safeReturn}`,
285
+ ` name: '${pascal}',`,
286
+ ` scope: '${scopeStr}',`,
287
+ ` path: '${route.path}',`,
288
+ ` method: '${route.method}',`,
289
+ ` kind: 'rpc',`,
290
+ ` params,`,
291
+ ` }, options)${safeResultCast}`,
292
+ ` },`,
293
+ ` },`,
294
+ ` ),`,
261
295
  ].join('\n')
262
296
 
263
- const hasErrors = injectRouteErrors(
264
- declarations,
265
- pascal,
266
- buildErrorUnion(route.errors, ctx),
267
- ctx.namespaceTypes
268
- )
297
+ const hasErrorsInjected = injectRouteErrors(declarations, pascal, errorUnion, ctx.namespaceTypes)
269
298
 
270
- return { typeDeclarations: declarations, callable, hasStream: false, hasErrors }
299
+ return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected }
271
300
  }
272
301
 
273
302
  async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
@@ -323,28 +352,61 @@ async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext): Prom
323
352
  const responseTypeName = refs['Response'] ?? 'unknown'
324
353
  const scopeStr = route.scope ?? 'default'
325
354
 
355
+ // Compute the error union once — used for both the .safe return type and
356
+ // injectRouteErrors. Use errorUnion !== null (not route.errors?.length) so
357
+ // we only emit Result<…, Errors> when keys were actually emitted in _errors.ts.
358
+ const errorUnion = buildErrorUnion(route.errors, ctx)
359
+ const hasErrors = errorUnion !== null
360
+ const errorsRef = ctx.namespaceTypes
361
+ ? `${ctx.scopePascal}.${pascal}.Errors`
362
+ : `${pascal}Errors`
363
+ const resultType = hasErrors
364
+ ? `Result<${responseTypeName}, ${errorsRef}>`
365
+ : `ResultNoTyped<${responseTypeName}>`
366
+
367
+ // When no typed errors, cast Result<T, never> → ResultNoTyped<T> so consumers
368
+ // see the simpler type in their IDE. The cast is safe: Result<T, never> has an
369
+ // unreachable `kind: 'typed'` arm that ResultNoTyped intentionally omits.
370
+ const safeReturn = hasErrors
371
+ ? `return client.safeCall<${responseTypeName}, ${errorsRef}>({`
372
+ : `return client.safeCall<${responseTypeName}>({`
373
+ const safeResultCast = hasErrors ? '' : ` as Promise<${resultType}>`
374
+
375
+ // Property key uses route.name verbatim (preserving the prior API
376
+ // emission contract). Function name uses route.name as well — JS function
377
+ // names accept any identifier, and matching the property key avoids any
378
+ // surprise asymmetry in stack traces.
326
379
  const callable = [
327
380
  ` /** ${route.method.toUpperCase()} ${route.fullPath} */`,
328
- ` ${route.name}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
329
- ` return client.call<${responseTypeName}>({`,
330
- ` name: '${route.name}',`,
331
- ` scope: '${scopeStr}',`,
332
- ` path: '${route.fullPath}',`,
333
- ` method: '${route.method}',`,
334
- ` kind: 'api',`,
335
- ` params,`,
336
- ` }, options)`,
337
- ` },`,
381
+ ` ${route.name}: Object.assign(`,
382
+ ` function ${route.name}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
383
+ ` return client.call<${responseTypeName}>({`,
384
+ ` name: '${route.name}',`,
385
+ ` scope: '${scopeStr}',`,
386
+ ` path: '${route.fullPath}',`,
387
+ ` method: '${route.method}',`,
388
+ ` kind: 'api',`,
389
+ ` params,`,
390
+ ` }, options)`,
391
+ ` },`,
392
+ ` {`,
393
+ ` safe(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${resultType}> {`,
394
+ ` ${safeReturn}`,
395
+ ` name: '${route.name}',`,
396
+ ` scope: '${scopeStr}',`,
397
+ ` path: '${route.fullPath}',`,
398
+ ` method: '${route.method}',`,
399
+ ` kind: 'api',`,
400
+ ` params,`,
401
+ ` }, options)${safeResultCast}`,
402
+ ` },`,
403
+ ` },`,
404
+ ` ),`,
338
405
  ].join('\n')
339
406
 
340
- const hasErrors = injectRouteErrors(
341
- declarations,
342
- pascal,
343
- buildErrorUnion(route.errors, ctx),
344
- ctx.namespaceTypes
345
- )
407
+ const hasErrorsInjected = injectRouteErrors(declarations, pascal, errorUnion, ctx.namespaceTypes)
346
408
 
347
- return { typeDeclarations: declarations, callable, hasStream: false, hasErrors }
409
+ return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected }
348
410
  }
349
411
 
350
412
  async function emitStreamRoute(route: StreamHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
@@ -458,8 +520,8 @@ export async function emitScopeFile(
458
520
 
459
521
  // Build client import line
460
522
  const clientImports = hasStream
461
- ? `import type { ClientInstance, ProcedureCallOptions, TypedStream } from '${clientImportPath}'`
462
- : `import type { ClientInstance, ProcedureCallOptions } from '${clientImportPath}'`
523
+ ? `import type { ClientInstance, ProcedureCallOptions, TypedStream, Result, ResultNoTyped } from '${clientImportPath}'`
524
+ : `import type { ClientInstance, ProcedureCallOptions, Result, ResultNoTyped } from '${clientImportPath}'`
463
525
 
464
526
  // Build _errors import line when at least one route emits an Errors union.
465
527
  // Namespace mode uses the qualified `${Service}Errors` namespace; flat mode