@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.
- package/LICENSE +1 -1
- package/README.md +467 -305
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +810 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +121 -13
- package/dist/codegen/index.js +212 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +615 -6
- package/dist/config/index.js +124 -5
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +220 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +528 -85
- package/dist/db/index.js +831 -122
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +418 -5
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +280 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +155 -42
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +347 -9
- package/dist/middleware/index.js +1462 -15
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +42 -28
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +35 -51
- package/dist/nextjs/server.js +126 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +152 -0
- package/dist/ops/index.js +500 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +8 -694
- package/dist/route/index.js +111 -22
- package/dist/route/index.js.map +1 -1
- package/dist/router-Qbssr11H.d.ts +676 -0
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +491 -22
- package/dist/server/index.js +1887 -308
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-BT5EnUAR.d.ts +278 -0
- package/dist/types-2AbaW4Ie.d.ts +205 -0
- package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
- package/dist/types-D1c57Ko-.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/package.json +244 -208
- package/dist/router-Di7ENoah.d.ts +0 -151
- package/dist/types-B-e_f2dQ.d.ts +0 -121
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -346
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -477
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -116
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -241
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -307
package/dist/codegen/index.d.ts
CHANGED
|
@@ -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'
|
|
42
|
-
* - ['
|
|
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
|
-
*
|
|
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
|
-
*
|
|
320
|
-
*
|
|
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({
|
|
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
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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 };
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { watch } from 'chokidar';
|
|
2
|
-
import { join, dirname, resolve
|
|
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
|
-
|
|
115
|
-
// ignore
|
|
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.
|
|
367
|
+
if (importPath.startsWith(".")) {
|
|
355
368
|
importPaths.push(importPath);
|
|
356
369
|
}
|
|
357
370
|
}
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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", "
|
|
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
|
|
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
|