@spfn/core 0.3.0-beta.28 → 0.3.0-beta.29

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.
@@ -429,6 +429,64 @@ declare class ContractGeneratorError extends Error {
429
429
  constructor(message: string);
430
430
  }
431
431
 
432
+ /**
433
+ * States Generator
434
+ *
435
+ * Generates a file naming the states the app declared — their ids and their
436
+ * manifest — as values a browser root or a server component can execute.
437
+ *
438
+ * `defineStateRouter` declares the states, and the browser reads them through
439
+ * `createStateClient<typeof states>`: a type import, erased at build. So nothing
440
+ * that runs in the browser says which states the app declared, and importing
441
+ * the router to find out is not an option — each state's `read()` closes over
442
+ * repositories and the database. A root that compares the declared ids with the
443
+ * `manifest` of `GET /state` can report a deploy skew between the Next.js server
444
+ * and the API before rendering, instead of a screen meeting `state.<id>` as
445
+ * `undefined`.
446
+ *
447
+ * The router is loaded and walked, as the route map's is. A state router is told
448
+ * apart by the `_stateManifest` marker `defineStateRouter` sets on the router it
449
+ * derives — the object the snapshot route answers as `manifest` — so the ids
450
+ * written here and the manifest the server serves come from one value.
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * // .spfnrc.ts
455
+ * import { defineConfig, defineGenerator } from '@spfn/core/codegen';
456
+ *
457
+ * export default defineConfig({
458
+ * generators: [
459
+ * defineGenerator({
460
+ * name: '@spfn/core:states',
461
+ * routerPath: './src/server/router.ts',
462
+ * outputPath: './src/generated/states.ts',
463
+ * })
464
+ * ]
465
+ * });
466
+ * ```
467
+ */
468
+
469
+ interface StatesGeneratorConfig {
470
+ /**
471
+ * Generator name (required for package-based loading)
472
+ */
473
+ name: '@spfn/core:states';
474
+ /**
475
+ * Path to the router file (relative to project root) — the one the route map reads
476
+ * @example './src/server/router.ts'
477
+ */
478
+ routerPath: string;
479
+ /**
480
+ * Output path for the generated states file (relative to project root)
481
+ * @default './src/generated/states.ts'
482
+ */
483
+ outputPath?: string;
484
+ }
485
+ /** Thrown when the router cannot produce the app's declared states. */
486
+ declare class StatesGeneratorError extends Error {
487
+ constructor(message: string);
488
+ }
489
+
432
490
  /**
433
491
  * Router registration guard
434
492
  *
@@ -499,7 +557,7 @@ declare function assertUnconditionalRegistration(scan: RegistrationScan): void;
499
557
  * ```typescript
500
558
  * // .spfnrc.ts
501
559
  * import { defineConfig, defineGenerator } from '@spfn/core/codegen';
502
- * import type { RouteMapGeneratorConfig, ContractGeneratorConfig } from '@spfn/core/codegen';
560
+ * import type { RouteMapGeneratorConfig, ContractGeneratorConfig, StatesGeneratorConfig } from '@spfn/core/codegen';
503
561
  *
504
562
  * export default defineConfig({
505
563
  * generators: [
@@ -513,6 +571,11 @@ declare function assertUnconditionalRegistration(scan: RegistrationScan): void;
513
571
  * routerPath: './src/server/router.ts',
514
572
  * outputDir: './contracts',
515
573
  * }),
574
+ * defineGenerator<StatesGeneratorConfig>({
575
+ * name: '@spfn/core:states',
576
+ * routerPath: './src/server/router.ts',
577
+ * outputPath: './src/generated/states.ts',
578
+ * }),
516
579
  * ]
517
580
  * });
518
581
  * ```
@@ -525,4 +588,4 @@ declare function assertUnconditionalRegistration(scan: RegistrationScan): void;
525
588
  */
526
589
  declare const generators: Record<string, unknown>;
527
590
 
528
- export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, ConditionalRegistrationError, type ContractGeneratorConfig, ContractGeneratorError, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type RegistrationScan, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, RouteMapGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
591
+ export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, ConditionalRegistrationError, type ContractGeneratorConfig, ContractGeneratorError, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type RegistrationScan, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, RouteMapGeneratorError, type StatesGeneratorConfig, StatesGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
@@ -497,6 +497,21 @@ function conditionKind(compiler2, expression) {
497
497
  function spreadText(file, expression) {
498
498
  return expression.getText(file).replace(/\s+/g, " ");
499
499
  }
500
+
501
+ // src/codegen/generators/emit.ts
502
+ function toObjectKey(name) {
503
+ if (name === "__proto__") {
504
+ return `[${JSON.stringify(name)}]`;
505
+ }
506
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
507
+ }
508
+ var SINGLE_QUOTABLE = /^[\x20-\x26\x28-\x5B\x5D-\x7E]*$/;
509
+ function toStringLiteral(value) {
510
+ if (SINGLE_QUOTABLE.test(value)) {
511
+ return `'${value}'`;
512
+ }
513
+ return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
514
+ }
500
515
  var loadLogger = logger.child("@spfn/core:codegen");
501
516
  function pinNodeEnv() {
502
517
  if (process.env.NODE_ENV) {
@@ -833,19 +848,6 @@ function collectRouteMap(router) {
833
848
  assertNoPackageCollision(collection);
834
849
  return collection;
835
850
  }
836
- function toObjectKey(name) {
837
- if (name === "__proto__") {
838
- return `[${JSON.stringify(name)}]`;
839
- }
840
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
841
- }
842
- var SINGLE_QUOTABLE = /^[\x20-\x26\x28-\x5B\x5D-\x7E]*$/;
843
- function toStringLiteral(value) {
844
- if (SINGLE_QUOTABLE.test(value)) {
845
- return `'${value}'`;
846
- }
847
- return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
848
- }
849
851
  function describeMount(mount) {
850
852
  if (mount.count === 0) {
851
853
  return `${mount.label} \u2014 no routes${mount.skipped ? ` (${mount.skipped})` : ""}`;
@@ -1114,13 +1116,156 @@ A released client cannot be fixed by redeploying the server. Keep the promise, o
1114
1116
  }
1115
1117
  };
1116
1118
  }
1119
+ var genLogger3 = logger.child("@spfn/core:states-generator");
1120
+ var StatesGeneratorError = class extends Error {
1121
+ constructor(message) {
1122
+ super(message);
1123
+ this.name = "StatesGeneratorError";
1124
+ }
1125
+ };
1126
+ var ROUTER_EXPORTS2 = ["appRouter", "default", "router"];
1127
+ function loadRouter3(cwd, absoluteRouterPath) {
1128
+ const module = loadRouterModule({
1129
+ cwd,
1130
+ absoluteRouterPath,
1131
+ subject: "state manifest",
1132
+ fail: (message) => new StatesGeneratorError(message)
1133
+ });
1134
+ const resolved = resolveRouterExport(module, ROUTER_EXPORTS2);
1135
+ if (resolved) {
1136
+ return resolved;
1137
+ }
1138
+ throw new StatesGeneratorError(
1139
+ `No router found in ${relative(cwd, absoluteRouterPath)}. Looked for: ${ROUTER_EXPORTS2.join(", ")}. Export the defineRouter() result under one of those names.`
1140
+ );
1141
+ }
1142
+ function collectStates(router, trail, visited, into) {
1143
+ if (visited.has(router)) {
1144
+ return 0;
1145
+ }
1146
+ visited.add(router);
1147
+ let stateRouters = addStates(router, trail.join("."), into) ? 1 : 0;
1148
+ for (const [name, entry] of Object.entries(router.routes)) {
1149
+ if (isRouter(entry)) {
1150
+ stateRouters += collectStates(entry, [...trail, name], visited, into);
1151
+ }
1152
+ }
1153
+ for (const [index, nested] of (router._packageRouters ?? []).entries()) {
1154
+ stateRouters += collectStates(nested, [...trail, `packages[${index}]`], visited, into);
1155
+ }
1156
+ return stateRouters;
1157
+ }
1158
+ function addStates(router, trail, into) {
1159
+ if (!Object.hasOwn(router, "_stateManifest")) {
1160
+ return false;
1161
+ }
1162
+ for (const [id, entry] of Object.entries(router._stateManifest ?? {})) {
1163
+ const existing = into.get(id);
1164
+ if (existing) {
1165
+ throw new StatesGeneratorError(
1166
+ `Two state routers both declare a state "${id}" (${existing.trail} and ${trail}). The browser addresses a state by its id alone and the generated manifest holds one entry per id, so one of the two would be read through the other's routes. Rename one of the states, or mount one of the two routers only.`
1167
+ );
1168
+ }
1169
+ into.set(id, { entry, trail });
1170
+ }
1171
+ return true;
1172
+ }
1173
+ function collectDeclaredStates(router, routerPath) {
1174
+ const states = /* @__PURE__ */ new Map();
1175
+ if (collectStates(router, ["router"], /* @__PURE__ */ new Set(), states) === 0) {
1176
+ throw new StatesGeneratorError(
1177
+ `No state router is mounted by ${routerPath}. The @spfn/core:states generator is configured, but nothing it reads mounts a defineStateRouter(...).router, so the file it would write could not say which states the app declared, and every id the server serves would read as a skew. Mount the state router \u2014 \`appRouter.packages([states.router])\` \u2014 or remove the generator from .spfnrc.ts.`
1178
+ );
1179
+ }
1180
+ return states;
1181
+ }
1182
+ function entryLine2(id, entry) {
1183
+ return ` ${toObjectKey(id)}: { key: [${entry.key.map(toStringLiteral).join(", ")}], write: ${entry.write} },`;
1184
+ }
1185
+ function generateStatesContent(states, nodeEnv) {
1186
+ const ids = [...states.keys()];
1187
+ return [
1188
+ "/**",
1189
+ " * State Manifest (Auto-generated)",
1190
+ " *",
1191
+ " * DO NOT EDIT - This file is generated by @spfn/core:states generator",
1192
+ " *",
1193
+ ` * Generated with NODE_ENV=${nodeEnv}. A state router mounted only under one environment is`,
1194
+ " * named here only when the generator ran under it, so this line changing is that",
1195
+ " * difference rather than an edit.",
1196
+ " *",
1197
+ " * Compare `stateManifest` with the `manifest` of `GET <state path>` (or of the snapshot a",
1198
+ " * server component read): an id here that the server's manifest lacks, or an entry that",
1199
+ " * differs, is a deploy skew between this build and the API.",
1200
+ " */",
1201
+ "",
1202
+ `export const stateIds = [${ids.map(toStringLiteral).join(", ")}] as const;`,
1203
+ "",
1204
+ "export type StateId = (typeof stateIds)[number];",
1205
+ "",
1206
+ "export const stateManifest: Readonly<Record<StateId, { readonly key: readonly string[]; readonly write: boolean }>> = {",
1207
+ ...ids.map((id) => entryLine2(id, states.get(id).entry)),
1208
+ "};",
1209
+ ""
1210
+ ].join("\n");
1211
+ }
1212
+ function createStatesGenerator(config) {
1213
+ const { routerPath, outputPath = "./src/generated/states.ts" } = config;
1214
+ if (!routerPath) {
1215
+ throw new Error(
1216
+ `[@spfn/core:states] Missing required "routerPath" option.
1217
+
1218
+ Usage:
1219
+ defineGenerator<StatesGeneratorConfig>({
1220
+ name: '@spfn/core:states',
1221
+ routerPath: './src/server/router.ts',
1222
+ })`
1223
+ );
1224
+ }
1225
+ return {
1226
+ name: "@spfn/core:states",
1227
+ watchPatterns: [
1228
+ routerPath,
1229
+ "src/server/states.ts",
1230
+ "src/server/states/**/*.ts",
1231
+ "src/server/routes/**/*.ts"
1232
+ ],
1233
+ runOn: ["watch", "manual", "build"],
1234
+ async generate(options) {
1235
+ const { cwd, debug } = options;
1236
+ const absoluteRouterPath = join(cwd, routerPath);
1237
+ const absoluteOutputPath = join(cwd, outputPath);
1238
+ if (!existsSync(absoluteRouterPath)) {
1239
+ genLogger3.warn(`Router file not found: ${absoluteRouterPath}`);
1240
+ return;
1241
+ }
1242
+ pinNodeEnv();
1243
+ const { router, exportName } = loadRouter3(cwd, absoluteRouterPath);
1244
+ assertUnconditionalRegistration({
1245
+ routerPath,
1246
+ source: readFileSync(absoluteRouterPath, "utf-8"),
1247
+ exportName,
1248
+ subject: "state manifest"
1249
+ });
1250
+ const states = collectDeclaredStates(router, routerPath);
1251
+ if (debug) {
1252
+ genLogger3.info(`Found ${states.size} states`, { ids: [...states.keys()] });
1253
+ }
1254
+ mkdirSync(dirname(absoluteOutputPath), { recursive: true });
1255
+ const nodeEnv = process.env.NODE_ENV ?? "unset";
1256
+ writeFileSync(absoluteOutputPath, generateStatesContent(states, nodeEnv), "utf-8");
1257
+ genLogger3.info(`Generated states: ${relative(cwd, absoluteOutputPath)} (${states.size} states)`);
1258
+ }
1259
+ };
1260
+ }
1117
1261
 
1118
1262
  // src/codegen/generators/index.ts
1119
1263
  var generators = {
1120
1264
  "route-map": createRouteMapGenerator,
1121
- contract: createContractGenerator
1265
+ contract: createContractGenerator,
1266
+ states: createStatesGenerator
1122
1267
  };
1123
1268
 
1124
- export { CodegenOrchestrator, ConditionalRegistrationError, ContractGeneratorError, RouteMapGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
1269
+ export { CodegenOrchestrator, ConditionalRegistrationError, ContractGeneratorError, RouteMapGeneratorError, StatesGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
1125
1270
  //# sourceMappingURL=index.js.map
1126
1271
  //# sourceMappingURL=index.js.map