@spfn/core 0.2.0-beta.9 → 0.3.0-beta.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 (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +467 -305
  3. package/dist/authz/index.d.ts +34 -0
  4. package/dist/authz/index.js +810 -0
  5. package/dist/authz/index.js.map +1 -0
  6. package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
  7. package/dist/cache/index.js +42 -30
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/codegen/index.d.ts +121 -13
  10. package/dist/codegen/index.js +212 -15
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/config/index.d.ts +615 -6
  13. package/dist/config/index.js +124 -5
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/contract/index.d.ts +220 -0
  16. package/dist/contract/index.js +558 -0
  17. package/dist/contract/index.js.map +1 -0
  18. package/dist/db/index.d.ts +528 -85
  19. package/dist/db/index.js +831 -122
  20. package/dist/db/index.js.map +1 -1
  21. package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
  22. package/dist/env/index.d.ts +26 -2
  23. package/dist/env/index.js +15 -5
  24. package/dist/env/index.js.map +1 -1
  25. package/dist/env/loader.d.ts +26 -19
  26. package/dist/env/loader.js +32 -25
  27. package/dist/env/loader.js.map +1 -1
  28. package/dist/errors/index.d.ts +10 -0
  29. package/dist/errors/index.js +418 -5
  30. package/dist/errors/index.js.map +1 -1
  31. package/dist/event/index.d.ts +33 -3
  32. package/dist/event/index.js +24 -3
  33. package/dist/event/index.js.map +1 -1
  34. package/dist/event/sse/client.d.ts +42 -3
  35. package/dist/event/sse/client.js +128 -45
  36. package/dist/event/sse/client.js.map +1 -1
  37. package/dist/event/sse/index.d.ts +12 -5
  38. package/dist/event/sse/index.js +280 -32
  39. package/dist/event/sse/index.js.map +1 -1
  40. package/dist/event/ws/client.d.ts +59 -0
  41. package/dist/event/ws/client.js +273 -0
  42. package/dist/event/ws/client.js.map +1 -0
  43. package/dist/event/ws/index.d.ts +94 -0
  44. package/dist/event/ws/index.js +272 -0
  45. package/dist/event/ws/index.js.map +1 -0
  46. package/dist/job/index.d.ts +2 -2
  47. package/dist/job/index.js +155 -42
  48. package/dist/job/index.js.map +1 -1
  49. package/dist/logger/index.d.ts +5 -0
  50. package/dist/logger/index.js +14 -0
  51. package/dist/logger/index.js.map +1 -1
  52. package/dist/middleware/index.d.ts +347 -9
  53. package/dist/middleware/index.js +1462 -15
  54. package/dist/middleware/index.js.map +1 -1
  55. package/dist/nextjs/index.d.ts +2 -2
  56. package/dist/nextjs/index.js +42 -28
  57. package/dist/nextjs/index.js.map +1 -1
  58. package/dist/nextjs/server.d.ts +35 -51
  59. package/dist/nextjs/server.js +126 -60
  60. package/dist/nextjs/server.js.map +1 -1
  61. package/dist/ops/index.d.ts +152 -0
  62. package/dist/ops/index.js +500 -0
  63. package/dist/ops/index.js.map +1 -0
  64. package/dist/route/index.d.ts +8 -694
  65. package/dist/route/index.js +111 -22
  66. package/dist/route/index.js.map +1 -1
  67. package/dist/router-Qbssr11H.d.ts +676 -0
  68. package/dist/security/index.d.ts +83 -0
  69. package/dist/security/index.js +173 -0
  70. package/dist/security/index.js.map +1 -0
  71. package/dist/server/index.d.ts +491 -22
  72. package/dist/server/index.js +1887 -308
  73. package/dist/server/index.js.map +1 -1
  74. package/dist/token-manager-BT5EnUAR.d.ts +278 -0
  75. package/dist/types-2AbaW4Ie.d.ts +205 -0
  76. package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
  77. package/dist/types-D1c57Ko-.d.ts +115 -0
  78. package/dist/types-ZQODsBft.d.ts +282 -0
  79. package/package.json +244 -208
  80. package/dist/router-Di7ENoah.d.ts +0 -151
  81. package/dist/types-B-e_f2dQ.d.ts +0 -121
  82. package/docs/cache.md +0 -133
  83. package/docs/codegen.md +0 -74
  84. package/docs/database.md +0 -346
  85. package/docs/entity.md +0 -539
  86. package/docs/env.md +0 -477
  87. package/docs/errors.md +0 -319
  88. package/docs/event.md +0 -116
  89. package/docs/job.md +0 -131
  90. package/docs/logger.md +0 -108
  91. package/docs/middleware.md +0 -337
  92. package/docs/nextjs.md +0 -241
  93. package/docs/repository.md +0 -496
  94. package/docs/route.md +0 -497
  95. package/docs/server.md +0 -307
@@ -7,6 +7,13 @@ import { HttpMethod } from '@spfn/core/route';
7
7
  */
8
8
  /**
9
9
  * Generator execution trigger types
10
+ *
11
+ * Who fires each one:
12
+ * - `watch` — `spfn dev`, on the initial pass and every file change
13
+ * - `manual` — `spfn codegen run` and `spfn contract`
14
+ * - `build` — `spfn build`
15
+ * - `start` — nothing in the shipped CLI; reserved for a programmatic caller that
16
+ * dispatches it through `orchestrator.generateAll('start')`
10
17
  */
11
18
  type GeneratorTrigger = 'watch' | 'manual' | 'build' | 'start';
12
19
  interface GeneratorOptions {
@@ -35,13 +42,14 @@ interface Generator {
35
42
  /**
36
43
  * When this generator should run
37
44
  *
38
- * @default ['watch', 'manual', 'build']
45
+ * @default ['watch', 'manual', 'build'] — every trigger the CLI fires
46
+ *
47
+ * Narrowing this is the easy mistake: drop `build` and the generator is silently
48
+ * skipped during `spfn build`, so whatever it produces ships stale.
39
49
  *
40
50
  * Examples:
41
- * - ['watch', 'build']: Run during development and build (e.g., admin-nav-generator)
42
- * - ['build', 'start']: Run during build and server start (e.g., db-migration)
43
- * - ['watch', 'manual']: Run during development and manual CLI (e.g., contract-generator)
44
- * - ['start']: Run only on server start (e.g., runtime config generator)
51
+ * - ['watch']: only while `spfn dev` is watching, never from `spfn codegen run`
52
+ * - ['manual']: only on an explicit run, never mid-edit — for something slow
45
53
  */
46
54
  runOn?: GeneratorTrigger[];
47
55
  /**
@@ -89,11 +97,21 @@ interface OrchestratorOptions {
89
97
  cwd?: string;
90
98
  /** Enable debug logging */
91
99
  debug?: boolean;
100
+ /**
101
+ * Rethrow a generator failure instead of only logging it
102
+ *
103
+ * Off by default, which keeps watch mode alive through a half-edited file.
104
+ * A build turns it on: a generator that refuses — a broken contract, a
105
+ * router that will not load — has to reach the exit code, or the refusal
106
+ * scrolls past and the build ships anyway.
107
+ */
108
+ throwOnError?: boolean;
92
109
  }
93
110
  declare class CodegenOrchestrator {
94
111
  private readonly generators;
95
112
  private readonly cwd;
96
113
  private readonly debug;
114
+ private readonly throwOnError;
97
115
  private isGenerating;
98
116
  private pendingRegenerations;
99
117
  private watcher?;
@@ -314,10 +332,15 @@ interface RouteMapGeneratorConfig {
314
332
  }
315
333
 
316
334
  /**
317
- * Built-in Generators Export
335
+ * Contract Generator
336
+ *
337
+ * Reads the router, writes `contracts/current.json`, and on a build compares it
338
+ * against the newest released snapshot.
318
339
  *
319
- * Provides a registry of all built-in generators.
320
- * Custom generators can be added via .spfnrc.ts configuration.
340
+ * It is a codegen plugin for the same reason the route map is one: hanging off
341
+ * `spfn build` and `spfn dev` removes "forgot to regenerate the contract" as a
342
+ * failure mode. On `dev` it only regenerates — refusing a half-finished route
343
+ * mid-edit would make the feature unusable — and on `build` it also gates.
321
344
  *
322
345
  * @example
323
346
  * ```typescript
@@ -325,18 +348,103 @@ interface RouteMapGeneratorConfig {
325
348
  * import { defineConfig, defineGenerator } from '@spfn/core/codegen';
326
349
  *
327
350
  * export default defineConfig({
351
+ * generators: [
352
+ * defineGenerator({
353
+ * name: '@spfn/core:contract',
354
+ * routerPath: './src/server/router.ts',
355
+ * outputDir: './contracts',
356
+ * })
357
+ * ]
358
+ * });
359
+ * ```
360
+ */
361
+
362
+ interface ContractGeneratorConfig {
363
+ /**
364
+ * Generator name (required for package-based loading)
365
+ */
366
+ name: '@spfn/core:contract';
367
+ /**
368
+ * Path to the router file (relative to project root)
369
+ * @example './src/server/router.ts'
370
+ */
371
+ routerPath: string;
372
+ /**
373
+ * Named export holding the router.
374
+ * @default 'appRouter', falling back to the default export
375
+ */
376
+ routerExport?: string;
377
+ /**
378
+ * Directory holding current.json, released/ and usage/ (relative to project root)
379
+ * @default './contracts'
380
+ */
381
+ outputDir?: string;
382
+ /**
383
+ * Extra file patterns to watch, for routes outside src/server/routes
384
+ */
385
+ additionalRouteDirs?: string[];
386
+ }
387
+ /** Thrown when the build must stop. */
388
+ declare class ContractGeneratorError extends Error {
389
+ constructor(message: string);
390
+ }
391
+
392
+ /**
393
+ * Router registration guard
394
+ *
395
+ * A contract has to describe what production serves. When a route is registered
396
+ * only under a feature flag or an environment check, the generated contract
397
+ * describes whichever way the generator happened to run — and the gate then
398
+ * compares a promise nobody made.
399
+ *
400
+ * Object spread is the one way a key can be conditionally present in the object
401
+ * `defineRouter()` receives, so that is what this reads. A spread of a plain
402
+ * identifier (`...baseRoutes`) is unconditional and passes; anything computed
403
+ * inside the spread does not.
404
+ */
405
+ /** Thrown when the router registers contracted routes conditionally. */
406
+ declare class ConditionalRegistrationError extends Error {
407
+ constructor(message: string);
408
+ }
409
+ /**
410
+ * Refuse a router whose route set depends on a condition.
411
+ *
412
+ * @param routerPath - path named in the error message
413
+ * @param source - contents of the router file
414
+ */
415
+ declare function assertUnconditionalRegistration(routerPath: string, source: string): void;
416
+
417
+ /**
418
+ * Built-in Generators Export
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * // .spfnrc.ts
423
+ * import { defineConfig, defineGenerator } from '@spfn/core/codegen';
424
+ * import type { RouteMapGeneratorConfig, ContractGeneratorConfig } from '@spfn/core/codegen';
425
+ *
426
+ * export default defineConfig({
328
427
  * generators: [
329
- * defineGenerator({ path: './my-generator.ts' })
428
+ * defineGenerator<RouteMapGeneratorConfig>({
429
+ * name: '@spfn/core:route-map',
430
+ * routerPath: './src/server/router.ts',
431
+ * outputPath: './src/generated/route-map.ts',
432
+ * }),
433
+ * defineGenerator<ContractGeneratorConfig>({
434
+ * name: '@spfn/core:contract',
435
+ * routerPath: './src/server/router.ts',
436
+ * outputDir: './contracts',
437
+ * }),
330
438
  * ]
331
439
  * });
332
440
  * ```
333
441
  */
334
442
 
335
443
  /**
336
- * Registry of available generators
337
- *
338
- * Used by package-based generator loading (e.g., "@spfn/core:route-map")
444
+ * @internal
445
+ * Registry of available generators for package-based loading.
446
+ * DO NOT use directly - use defineGenerator({ name: '@spfn/core:route-map', ... }) instead.
339
447
  */
340
448
  declare const generators: Record<string, unknown>;
341
449
 
342
- export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
450
+ export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, ConditionalRegistrationError, type ContractGeneratorConfig, ContractGeneratorError, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
@@ -1,9 +1,10 @@
1
1
  import { watch } from 'chokidar';
2
- import { join, dirname, resolve, relative } from 'path';
2
+ import { join, relative, sep, dirname, resolve } from 'path';
3
3
  import mm from 'micromatch';
4
4
  import { logger } from '@spfn/core/logger';
5
5
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
6
6
  import { createJiti } from 'jiti';
7
+ import { collectContractDocument, writeCurrentDocument, checkContract, formatViolations } from '@spfn/core/contract';
7
8
 
8
9
  // src/codegen/core/orchestrator.ts
9
10
  var orchestratorLogger = logger.child("@spfn/core:orchestrator");
@@ -11,6 +12,7 @@ var CodegenOrchestrator = class {
11
12
  generators;
12
13
  cwd;
13
14
  debug;
15
+ throwOnError;
14
16
  isGenerating = false;
15
17
  pendingRegenerations = /* @__PURE__ */ new Set();
16
18
  watcher;
@@ -19,6 +21,7 @@ var CodegenOrchestrator = class {
19
21
  this.generators = options.generators;
20
22
  this.cwd = options.cwd ?? process.cwd();
21
23
  this.debug = options.debug ?? false;
24
+ this.throwOnError = options.throwOnError ?? false;
22
25
  }
23
26
  /**
24
27
  * Close watcher and cleanup resources
@@ -80,6 +83,9 @@ var CodegenOrchestrator = class {
80
83
  } catch (error) {
81
84
  const err = error instanceof Error ? error : new Error(String(error));
82
85
  orchestratorLogger.error(`[${generator.name}] \u2717 Generation failed`, err);
86
+ if (this.throwOnError) {
87
+ throw err;
88
+ }
83
89
  }
84
90
  }
85
91
  }
@@ -111,8 +117,10 @@ var CodegenOrchestrator = class {
111
117
  });
112
118
  }
113
119
  this.watcher = watch(watchDirs, {
114
- ignored: /(^|[\/\\])\../,
115
- // ignore dotfiles
120
+ // Ignore dotfiles by path segments relative to cwd — matching the absolute
121
+ // path would ignore everything when the checkout itself lives under a dot
122
+ // directory (e.g. .claude/worktrees/<name>), leaving zero watched files
123
+ ignored: (watchedPath) => relative(this.cwd, watchedPath).split(sep).some((segment) => segment.startsWith(".") && segment !== "." && segment !== ".."),
116
124
  persistent: true,
117
125
  ignoreInitial: true,
118
126
  awaitWriteFinish: {
@@ -276,6 +284,11 @@ async function createGeneratorsFromConfig(config, cwd) {
276
284
  }
277
285
  for (const generatorConfig of config.generators) {
278
286
  try {
287
+ if ("generate" in generatorConfig && typeof generatorConfig.generate === "function") {
288
+ generators2.push(generatorConfig);
289
+ configLogger.info(`Generator instance added: ${generatorConfig.name}`);
290
+ continue;
291
+ }
279
292
  if ("path" in generatorConfig) {
280
293
  const generatorPath = generatorConfig.path.startsWith(".") ? join(cwd, generatorConfig.path) : generatorConfig.path;
281
294
  configLogger.info(`Loading custom generator: ${generatorPath}`);
@@ -351,17 +364,27 @@ function parseRouterFile(routerPath) {
351
364
  let match;
352
365
  while ((match = importPattern.exec(content)) !== null) {
353
366
  const importPath = match[1];
354
- if (importPath.includes("route")) {
367
+ if (importPath.startsWith(".")) {
355
368
  importPaths.push(importPath);
356
369
  }
357
370
  }
358
- const routerPattern = /defineRouter\s*\(\s*\{([^}]+)\}/s;
359
- const routerMatch = routerPattern.exec(content);
360
- if (routerMatch) {
361
- const routerContent = routerMatch[1];
362
- const namePattern = /(\w+)\s*[,}]/g;
363
- while ((match = namePattern.exec(routerContent)) !== null) {
364
- routeNames.push(match[1]);
371
+ const defineRouterStart = content.indexOf("defineRouter(");
372
+ if (defineRouterStart !== -1) {
373
+ const braceStart = content.indexOf("{", defineRouterStart);
374
+ if (braceStart !== -1) {
375
+ let depth = 1;
376
+ let braceEnd = braceStart + 1;
377
+ while (depth > 0 && braceEnd < content.length) {
378
+ if (content[braceEnd] === "{") depth++;
379
+ else if (content[braceEnd] === "}") depth--;
380
+ braceEnd++;
381
+ }
382
+ const routerContent = content.slice(braceStart + 1, braceEnd - 1);
383
+ const withoutComments = routerContent.replace(/\/\/[^\n]*/g, "");
384
+ const namePattern = /^\s*(\w+)\s*[,\n]/gm;
385
+ while ((match = namePattern.exec(withoutComments)) !== null) {
386
+ routeNames.push(match[1]);
387
+ }
365
388
  }
366
389
  }
367
390
  } catch (error) {
@@ -404,6 +427,17 @@ function createRouteMapGenerator(config) {
404
427
  outputPath = "./src/generated/route-map.ts",
405
428
  additionalRouteDirs = []
406
429
  } = config;
430
+ if (!routerPath) {
431
+ throw new Error(
432
+ `[@spfn/core:route-map] Missing required "routerPath" option.
433
+
434
+ Usage:
435
+ defineGenerator<RouteMapGeneratorConfig>({
436
+ name: '@spfn/core:route-map',
437
+ routerPath: './src/server/router.ts',
438
+ })`
439
+ );
440
+ }
407
441
  return {
408
442
  name: "@spfn/core:route-map",
409
443
  watchPatterns: [
@@ -412,7 +446,7 @@ function createRouteMapGenerator(config) {
412
446
  "src/server/routes/**/*.ts",
413
447
  ...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
414
448
  ],
415
- runOn: ["watch", "build", "start", "manual"],
449
+ runOn: ["watch", "manual", "build"],
416
450
  async generate(options) {
417
451
  const { cwd, debug } = options;
418
452
  const absoluteRouterPath = join(cwd, routerPath);
@@ -433,7 +467,17 @@ function createRouteMapGenerator(config) {
433
467
  for (const importPath of importPaths) {
434
468
  let resolvedPath = resolve(routerDir, importPath);
435
469
  if (!resolvedPath.endsWith(".ts")) {
436
- resolvedPath += ".ts";
470
+ const withTs = resolvedPath + ".ts";
471
+ if (existsSync(withTs)) {
472
+ resolvedPath = withTs;
473
+ } else {
474
+ const indexPath = join(resolvedPath, "index.ts");
475
+ if (existsSync(indexPath)) {
476
+ resolvedPath = indexPath;
477
+ } else {
478
+ resolvedPath = withTs;
479
+ }
480
+ }
437
481
  }
438
482
  if (existsSync(resolvedPath)) {
439
483
  const routes = parseRouteFile(resolvedPath);
@@ -458,11 +502,164 @@ function createRouteMapGenerator(config) {
458
502
  };
459
503
  }
460
504
 
505
+ // src/codegen/generators/contract-guard.ts
506
+ var ConditionalRegistrationError = class extends Error {
507
+ constructor(message) {
508
+ super(message);
509
+ this.name = "ConditionalRegistrationError";
510
+ }
511
+ };
512
+ function defineRouterBlock(source) {
513
+ const start = source.indexOf("defineRouter(");
514
+ if (start === -1) {
515
+ return void 0;
516
+ }
517
+ const open = source.indexOf("{", start);
518
+ if (open === -1) {
519
+ return void 0;
520
+ }
521
+ let depth = 1;
522
+ let cursor = open + 1;
523
+ while (depth > 0 && cursor < source.length) {
524
+ if (source[cursor] === "{") depth++;
525
+ else if (source[cursor] === "}") depth--;
526
+ cursor++;
527
+ }
528
+ return source.slice(open + 1, cursor - 1).replace(/\/\/[^\n]*/g, "");
529
+ }
530
+ var PLAIN_REFERENCE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*\s*$/;
531
+ function assertUnconditionalRegistration(routerPath, source) {
532
+ const block = defineRouterBlock(source);
533
+ if (!block) {
534
+ return;
535
+ }
536
+ for (const [, expression] of block.matchAll(/\.\.\.\s*([^,\n]+)/g)) {
537
+ if (PLAIN_REFERENCE.test(expression)) {
538
+ continue;
539
+ }
540
+ throw new ConditionalRegistrationError(
541
+ `${routerPath} registers routes conditionally: "...${expression.trim()}".
542
+
543
+ A contract has to describe what production serves. When the route set depends on a flag or an environment, the generated contract describes whichever way the generator happened to run.
544
+ Register contracted routes unconditionally, and gate behaviour inside the handler instead.`
545
+ );
546
+ }
547
+ }
548
+
549
+ // src/codegen/generators/contract.ts
550
+ var genLogger2 = logger.child("@spfn/core:contract-generator");
551
+ var ContractGeneratorError = class extends Error {
552
+ constructor(message) {
553
+ super(message);
554
+ this.name = "ContractGeneratorError";
555
+ }
556
+ };
557
+ function isRouter(value) {
558
+ return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
559
+ }
560
+ function loadRouter(cwd, absoluteRouterPath, routerExport) {
561
+ let module;
562
+ try {
563
+ const jiti = createJiti(cwd, { interopDefault: true, moduleCache: false });
564
+ module = jiti(absoluteRouterPath);
565
+ } catch (error) {
566
+ const message = error instanceof Error ? error.message : String(error);
567
+ throw new ContractGeneratorError(
568
+ `Failed to load ${relative(cwd, absoluteRouterPath)}: ${message}
569
+
570
+ The contract is read from the loaded router, so a route module must be importable without side effects. Check that nothing at module scope opens a connection or reads a missing environment value.`
571
+ );
572
+ }
573
+ const candidates = routerExport ? [routerExport] : ["appRouter", "default", "router"];
574
+ for (const name of candidates) {
575
+ const candidate = module[name];
576
+ if (isRouter(candidate)) {
577
+ return candidate;
578
+ }
579
+ }
580
+ throw new ContractGeneratorError(
581
+ `No router found in ${relative(cwd, absoluteRouterPath)}. Looked for: ${candidates.join(", ")}. Set "routerExport" to the export holding the defineRouter() result.`
582
+ );
583
+ }
584
+ function pinNodeEnv() {
585
+ if (!process.env.NODE_ENV) {
586
+ process.env.NODE_ENV = "production";
587
+ genLogger2.info('NODE_ENV was unset; pinned to "production" so the contract does not depend on the shell');
588
+ }
589
+ }
590
+ function createContractGenerator(config) {
591
+ const {
592
+ routerPath,
593
+ routerExport,
594
+ outputDir = "./contracts",
595
+ additionalRouteDirs = []
596
+ } = config;
597
+ if (!routerPath) {
598
+ throw new Error(
599
+ `[@spfn/core:contract] Missing required "routerPath" option.
600
+
601
+ Usage:
602
+ defineGenerator<ContractGeneratorConfig>({
603
+ name: '@spfn/core:contract',
604
+ routerPath: './src/server/router.ts',
605
+ })`
606
+ );
607
+ }
608
+ return {
609
+ name: "@spfn/core:contract",
610
+ watchPatterns: [
611
+ routerPath,
612
+ "src/server/routes/**/*.ts",
613
+ ...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
614
+ ],
615
+ runOn: ["watch", "build", "manual"],
616
+ async generate(options) {
617
+ const { cwd } = options;
618
+ const absoluteRouterPath = join(cwd, routerPath);
619
+ const contractsDir = join(cwd, outputDir);
620
+ if (!existsSync(absoluteRouterPath)) {
621
+ throw new ContractGeneratorError(
622
+ `Router file not found: ${routerPath}. The contract generator is configured but has nothing to read.`
623
+ );
624
+ }
625
+ pinNodeEnv();
626
+ assertUnconditionalRegistration(routerPath, readFileSync(absoluteRouterPath, "utf-8"));
627
+ const router = loadRouter(cwd, absoluteRouterPath, routerExport);
628
+ const document = collectContractDocument(router);
629
+ const changed = writeCurrentDocument(contractsDir, document);
630
+ genLogger2.info(
631
+ `${changed ? "Wrote" : "Verified"} ${relative(cwd, join(contractsDir, "current.json"))} (${document.operations.length} contracted operation(s))`
632
+ );
633
+ if (options.trigger?.type !== "build") {
634
+ return;
635
+ }
636
+ const result = checkContract(contractsDir, document);
637
+ for (const warning of result.warnings) {
638
+ genLogger2.warn(warning);
639
+ }
640
+ if (result.violations.length === 0) {
641
+ if (result.baselineVersion) {
642
+ genLogger2.info(`Contract is backward compatible with released ${result.baselineVersion}`);
643
+ }
644
+ return;
645
+ }
646
+ throw new ContractGeneratorError(
647
+ `This build breaks the contract released as ${result.baselineVersion}:
648
+
649
+ ${formatViolations(result.violations)}
650
+
651
+ A released client cannot be fixed by redeploying the server. Keep the promise, or cut a new contract version and let the old operation stay until no released app calls it.`
652
+ );
653
+ }
654
+ };
655
+ }
656
+
461
657
  // src/codegen/generators/index.ts
462
658
  var generators = {
463
- "route-map": createRouteMapGenerator
659
+ "route-map": createRouteMapGenerator,
660
+ contract: createContractGenerator
464
661
  };
465
662
 
466
- export { CodegenOrchestrator, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
663
+ export { CodegenOrchestrator, ConditionalRegistrationError, ContractGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
467
664
  //# sourceMappingURL=index.js.map
468
665
  //# sourceMappingURL=index.js.map