@spfn/core 0.3.0-beta.3 → 0.3.0-beta.30
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/README.md +301 -10
- package/dist/app-config/index.d.ts +39 -1
- package/dist/app-config/index.js.map +1 -1
- package/dist/authz/index.js +47 -391
- package/dist/authz/index.js.map +1 -1
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/codegen/index.d.ts +157 -16
- package/dist/codegen/index.js +772 -166
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js +4 -4
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +39 -16
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/db/index.d.ts +230 -35
- package/dist/db/index.js +316 -160
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/error-CIZlYJlL.d.ts +157 -0
- package/dist/errors/index.d.ts +18 -1
- package/dist/errors/index.js +47 -391
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +202 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +94 -18
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +18 -5
- package/dist/event/sse/index.js +452 -114
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +131 -52
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js +52 -397
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +75 -14
- package/dist/nextjs/index.js +200 -84
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +46 -8
- package/dist/nextjs/server.js +15 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +61 -42
- package/dist/ops/index.js +648 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +5 -2
- package/dist/route/index.js +107 -28
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{router-Cy7rAfmj.d.ts → route-builder-DRW-MSCJ.d.ts} +51 -126
- package/dist/router-C57y-vz1.d.ts +184 -0
- package/dist/router-CX5uVjmU.d.ts +406 -0
- package/dist/server/index.d.ts +130 -7
- package/dist/server/index.js +1174 -164
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +206 -0
- package/dist/state/client.js +678 -0
- package/dist/state/client.js.map +1 -0
- package/dist/state/index.d.ts +443 -0
- package/dist/state/index.js +644 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/react.d.ts +49 -0
- package/dist/state/react.js +50 -0
- package/dist/state/react.js.map +1 -0
- package/dist/surface/index.d.ts +115 -0
- package/dist/surface/index.js +1013 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-CfYVhIQ9.d.ts → types-5Rl11_Nb.d.ts} +43 -4
- package/dist/types-CKH0D3Dy.d.ts +441 -0
- package/dist/{types-ZQODsBft.d.ts → types-CurMab2Q.d.ts} +52 -2
- package/dist/{types-2AbaW4Ie.d.ts → types-DncqXfWN.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +39 -9
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +721 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +442 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +845 -0
- package/src/env/README.md +787 -0
- package/src/errors/README.md +445 -0
- package/src/event/README.md +994 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +756 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/src/state/README.md +806 -0
- package/dist/token-manager-BT5EnUAR.d.ts +0 -278
- package/dist/types-9oszaJqp.d.ts +0 -250
package/dist/codegen/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { watch } from 'chokidar';
|
|
2
|
-
import { join, relative, sep, dirname, resolve } from 'path';
|
|
2
|
+
import { join, relative, sep, dirname, resolve, isAbsolute } from 'path';
|
|
3
3
|
import mm from 'micromatch';
|
|
4
4
|
import { logger } from '@spfn/core/logger';
|
|
5
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync } from 'fs';
|
|
6
6
|
import { createJiti } from 'jiti';
|
|
7
|
+
import { createRequire } from 'module';
|
|
7
8
|
import { collectContractDocument, writeCurrentDocument, checkContract, formatViolations } from '@spfn/core/contract';
|
|
8
9
|
|
|
9
10
|
// src/codegen/core/orchestrator.ts
|
|
@@ -334,70 +335,552 @@ async function createGeneratorsFromConfig(config, cwd) {
|
|
|
334
335
|
}
|
|
335
336
|
return generators2;
|
|
336
337
|
}
|
|
337
|
-
var
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
338
|
+
var compiler;
|
|
339
|
+
function typescript() {
|
|
340
|
+
compiler ??= createRequire(import.meta.url)("typescript");
|
|
341
|
+
return compiler;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/codegen/generators/contract-guard.ts
|
|
345
|
+
var ConditionalRegistrationError = class extends Error {
|
|
346
|
+
constructor(message) {
|
|
347
|
+
super(message);
|
|
348
|
+
this.name = "ConditionalRegistrationError";
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var REFUSAL_WORDING = {
|
|
352
|
+
defineRouter: {
|
|
353
|
+
noun: "routes",
|
|
354
|
+
consequence: "a route production registers is left out, and the client that addresses it by name gets a 404",
|
|
355
|
+
remedy: "Register the route unconditionally and gate its behaviour inside the handler instead."
|
|
356
|
+
},
|
|
357
|
+
defineEventRouter: {
|
|
358
|
+
noun: "events",
|
|
359
|
+
consequence: "an event production streams is left out, and the client that subscribes to it by name gets a 400",
|
|
360
|
+
remedy: "Register the event unconditionally and decide who receives it with the stream's authorize hook instead."
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function assertUnconditionalRegistration(scan) {
|
|
364
|
+
const compiler2 = typescript();
|
|
365
|
+
const file = compiler2.createSourceFile(
|
|
366
|
+
scan.routerPath,
|
|
367
|
+
scan.source,
|
|
368
|
+
compiler2.ScriptTarget.Latest,
|
|
369
|
+
/* setParentNodes */
|
|
370
|
+
false
|
|
371
|
+
);
|
|
372
|
+
const declarations = topLevelInitialisers(compiler2, file);
|
|
373
|
+
const routes = routesLiteral(compiler2, declarations.get(scan.exportName), declarations, /* @__PURE__ */ new Set(), factoryOf(scan));
|
|
374
|
+
if (routes) {
|
|
375
|
+
assertLiteral(compiler2, file, routes, declarations, /* @__PURE__ */ new Set(), scan);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function factoryOf(scan) {
|
|
379
|
+
return scan.factory ?? "defineRouter";
|
|
380
|
+
}
|
|
381
|
+
function topLevelInitialisers(compiler2, file) {
|
|
382
|
+
const found = /* @__PURE__ */ new Map();
|
|
383
|
+
for (const statement of file.statements) {
|
|
384
|
+
if (compiler2.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
385
|
+
found.set("default", statement.expression);
|
|
386
|
+
}
|
|
387
|
+
if (compiler2.isVariableStatement(statement)) {
|
|
388
|
+
collectDeclarations(compiler2, statement, found);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const statement of file.statements) {
|
|
392
|
+
collectRenamedExports(compiler2, statement, found);
|
|
393
|
+
}
|
|
394
|
+
return found;
|
|
395
|
+
}
|
|
396
|
+
function collectDeclarations(compiler2, statement, found) {
|
|
397
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
398
|
+
if (compiler2.isIdentifier(declaration.name) && declaration.initializer) {
|
|
399
|
+
found.set(declaration.name.text, declaration.initializer);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function collectRenamedExports(compiler2, statement, found) {
|
|
404
|
+
if (!compiler2.isExportDeclaration(statement) || !statement.exportClause || !compiler2.isNamedExports(statement.exportClause)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
for (const specifier of statement.exportClause.elements) {
|
|
408
|
+
if (specifier.propertyName && !found.has(specifier.name.text)) {
|
|
409
|
+
found.set(specifier.name.text, specifier.propertyName);
|
|
352
410
|
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
genLogger.warn(`Failed to parse route file: ${filePath}`, error);
|
|
355
411
|
}
|
|
356
|
-
return routes;
|
|
357
412
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
413
|
+
function unwrap(compiler2, expression) {
|
|
414
|
+
if (!expression) {
|
|
415
|
+
return void 0;
|
|
416
|
+
}
|
|
417
|
+
if (compiler2.isParenthesizedExpression(expression) || compiler2.isAsExpression(expression) || compiler2.isSatisfiesExpression(expression) || compiler2.isNonNullExpression(expression) || compiler2.isTypeAssertionExpression(expression)) {
|
|
418
|
+
return unwrap(compiler2, expression.expression);
|
|
419
|
+
}
|
|
420
|
+
return expression;
|
|
421
|
+
}
|
|
422
|
+
function routesLiteral(compiler2, expression, declarations, seen, factory) {
|
|
423
|
+
const node = unwrap(compiler2, expression);
|
|
424
|
+
if (!node) {
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
427
|
+
if (compiler2.isIdentifier(node)) {
|
|
428
|
+
if (seen.has(node.text)) {
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
431
|
+
seen.add(node.text);
|
|
432
|
+
return routesLiteral(compiler2, declarations.get(node.text), declarations, seen, factory);
|
|
433
|
+
}
|
|
434
|
+
if (!compiler2.isCallExpression(node)) {
|
|
435
|
+
return void 0;
|
|
436
|
+
}
|
|
437
|
+
const callee = unwrap(compiler2, node.expression);
|
|
438
|
+
if (callee && compiler2.isIdentifier(callee) && callee.text === factory) {
|
|
439
|
+
const routes = unwrap(compiler2, node.arguments[0]);
|
|
440
|
+
return routes && compiler2.isObjectLiteralExpression(routes) ? routes : void 0;
|
|
441
|
+
}
|
|
442
|
+
return callee && compiler2.isPropertyAccessExpression(callee) ? routesLiteral(compiler2, callee.expression, declarations, seen, factory) : void 0;
|
|
443
|
+
}
|
|
444
|
+
function assertLiteral(compiler2, file, literal, declarations, seen, scan) {
|
|
445
|
+
for (const property of literal.properties) {
|
|
446
|
+
if (compiler2.isSpreadAssignment(property)) {
|
|
447
|
+
assertUnconditionalSpread(compiler2, file, property.expression, scan);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const nested = routesLiteral(compiler2, mountedRouter(compiler2, property), declarations, seen, factoryOf(scan));
|
|
451
|
+
if (nested) {
|
|
452
|
+
assertLiteral(compiler2, file, nested, declarations, seen, scan);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function mountedRouter(compiler2, property) {
|
|
457
|
+
if (compiler2.isPropertyAssignment(property)) {
|
|
458
|
+
return property.initializer;
|
|
459
|
+
}
|
|
460
|
+
return compiler2.isShorthandPropertyAssignment(property) ? property.name : void 0;
|
|
461
|
+
}
|
|
462
|
+
function assertUnconditionalSpread(compiler2, file, expression, scan) {
|
|
463
|
+
const condition = conditionKind(compiler2, expression);
|
|
464
|
+
if (!condition) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const wording = REFUSAL_WORDING[factoryOf(scan)];
|
|
468
|
+
throw new ConditionalRegistrationError(
|
|
469
|
+
`${scan.routerPath} registers ${wording.noun} conditionally: "...${spreadText(file, expression)}" (${condition}).
|
|
470
|
+
|
|
471
|
+
The ${scan.subject} is generated from the router as it loads, so a set of ${wording.noun} that depends on a flag or an environment describes whichever way the generator happened to run: ${wording.consequence}.
|
|
472
|
+
` + wording.remedy
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
function conditionKind(compiler2, expression) {
|
|
476
|
+
const node = unwrap(compiler2, expression);
|
|
477
|
+
if (!node) {
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
if (compiler2.isConditionalExpression(node)) {
|
|
481
|
+
return "a ternary";
|
|
482
|
+
}
|
|
483
|
+
if (!compiler2.isBinaryExpression(node)) {
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
switch (node.operatorToken.kind) {
|
|
487
|
+
case compiler2.SyntaxKind.AmpersandAmpersandToken:
|
|
488
|
+
return "&&";
|
|
489
|
+
case compiler2.SyntaxKind.BarBarToken:
|
|
490
|
+
return "||";
|
|
491
|
+
case compiler2.SyntaxKind.QuestionQuestionToken:
|
|
492
|
+
return "??";
|
|
493
|
+
default:
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function spreadText(file, expression) {
|
|
498
|
+
return expression.getText(file).replace(/\s+/g, " ");
|
|
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
|
+
}
|
|
515
|
+
var loadLogger = logger.child("@spfn/core:codegen");
|
|
516
|
+
function pinNodeEnv() {
|
|
517
|
+
if (process.env.NODE_ENV) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
process.env.NODE_ENV = "production";
|
|
521
|
+
loadLogger.info('NODE_ENV was unset; pinned to "production" so generated output does not depend on the shell');
|
|
522
|
+
}
|
|
523
|
+
function parseHost(compiler2) {
|
|
524
|
+
return {
|
|
525
|
+
useCaseSensitiveFileNames: compiler2.sys.useCaseSensitiveFileNames,
|
|
526
|
+
readDirectory: () => [],
|
|
527
|
+
fileExists: compiler2.sys.fileExists,
|
|
528
|
+
readFile: compiler2.sys.readFile
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
var NO_INPUTS = 18003;
|
|
532
|
+
function targetPath(target, base) {
|
|
533
|
+
return resolve(base, target.endsWith("/*") ? target.slice(0, -2) : target);
|
|
534
|
+
}
|
|
535
|
+
var UNREADABLE_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM"]);
|
|
536
|
+
function isUnreadable(error) {
|
|
537
|
+
return UNREADABLE_CODES.has(error?.code ?? "");
|
|
538
|
+
}
|
|
539
|
+
function targetResolves(target, base) {
|
|
540
|
+
const path = targetPath(target, base);
|
|
541
|
+
if (!target.endsWith("/*")) {
|
|
542
|
+
return existsSync(path);
|
|
543
|
+
}
|
|
361
544
|
try {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (importPath.startsWith(".")) {
|
|
368
|
-
importPaths.push(importPath);
|
|
369
|
-
}
|
|
545
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
546
|
+
return stats?.isDirectory() === true && readdirSync(path).length > 0;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (!isUnreadable(error)) {
|
|
549
|
+
throw error;
|
|
370
550
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function chooseTarget(pattern, targets, base) {
|
|
555
|
+
const present = targets.filter((target2) => targetResolves(target2, base));
|
|
556
|
+
const [target] = present.length > 0 ? present : targets;
|
|
557
|
+
if (!target) {
|
|
558
|
+
return void 0;
|
|
559
|
+
}
|
|
560
|
+
if (present.length > 1) {
|
|
561
|
+
loadLogger.warn(
|
|
562
|
+
`tsconfig "paths" maps ${pattern} to ${present.length} targets that could each resolve a module; while the router loads, only the first (${target}) does`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (present.length === 0 && targets.length > 1) {
|
|
566
|
+
loadLogger.warn(
|
|
567
|
+
`tsconfig "paths" maps ${pattern} to ${targets.length} targets and none of them exists with anything in it; ${target} is used, and an import through it will name itself as unresolved`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return { target, others: targets.filter((candidate) => candidate !== target) };
|
|
571
|
+
}
|
|
572
|
+
function toAlias(pattern, targets, base) {
|
|
573
|
+
const choice = chooseTarget(pattern, targets, base);
|
|
574
|
+
if (!choice) {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
const { target, others } = choice;
|
|
578
|
+
if (!pattern.includes("*")) {
|
|
579
|
+
return { from: pattern, to: resolve(base, target), pattern, target, others };
|
|
580
|
+
}
|
|
581
|
+
if (!pattern.endsWith("/*") || !target.endsWith("/*")) {
|
|
582
|
+
loadLogger.warn(
|
|
583
|
+
`tsconfig "paths" maps ${pattern} to ${target}, which is not a directory prefix mapping; it will not resolve while the router loads`
|
|
584
|
+
);
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
return { from: pattern.slice(0, -1), to: resolve(base, target.slice(0, -2)), pattern, target, others };
|
|
588
|
+
}
|
|
589
|
+
function readCompilerOptions(configPath, cwd) {
|
|
590
|
+
if (!existsSync(configPath)) {
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
const compiler2 = typescript();
|
|
594
|
+
const { config, error } = compiler2.readConfigFile(configPath, compiler2.sys.readFile);
|
|
595
|
+
if (error || !config) {
|
|
596
|
+
loadLogger.warn(`Could not read ${relative(cwd, configPath)}; its path aliases will not resolve`);
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
const parsed = compiler2.parseJsonConfigFileContent(config, parseHost(compiler2), dirname(configPath));
|
|
600
|
+
for (const diagnostic of parsed.errors) {
|
|
601
|
+
if (diagnostic.code !== NO_INPUTS) {
|
|
602
|
+
loadLogger.warn(
|
|
603
|
+
`${relative(cwd, configPath)}: ${compiler2.flattenDiagnosticMessageText(diagnostic.messageText, " ")} \u2014 path aliases it should have declared may be missing while the router loads`
|
|
604
|
+
);
|
|
389
605
|
}
|
|
606
|
+
}
|
|
607
|
+
return parsed.options;
|
|
608
|
+
}
|
|
609
|
+
function isWithin(root, directory) {
|
|
610
|
+
const step = relative(root, directory);
|
|
611
|
+
return step === "" || !step.startsWith("..") && !isAbsolute(step);
|
|
612
|
+
}
|
|
613
|
+
function searchDirectories(cwd, startDir) {
|
|
614
|
+
const root = resolve(cwd);
|
|
615
|
+
const directories = [];
|
|
616
|
+
for (let directory = resolve(startDir); isWithin(root, directory); directory = dirname(directory)) {
|
|
617
|
+
directories.push(directory);
|
|
618
|
+
if (directory === root) {
|
|
619
|
+
return directories;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return [root];
|
|
623
|
+
}
|
|
624
|
+
function aliasesOf(paths, base) {
|
|
625
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
627
|
+
const alias = toAlias(pattern, targets, base);
|
|
628
|
+
if (alias && !aliases.has(alias.from)) {
|
|
629
|
+
aliases.set(alias.from, alias);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return [...aliases.values()];
|
|
633
|
+
}
|
|
634
|
+
function projectAliases(cwd, startDir) {
|
|
635
|
+
for (const directory of searchDirectories(cwd, startDir)) {
|
|
636
|
+
const options = readCompilerOptions(join(directory, "tsconfig.json"), cwd);
|
|
637
|
+
if (options?.paths) {
|
|
638
|
+
const declaredIn = options.pathsBasePath;
|
|
639
|
+
return aliasesOf(options.paths, options.baseUrl ?? declaredIn ?? directory);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
function describeFailure(error, subject, aliases) {
|
|
645
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
646
|
+
const unresolved = /Cannot find module '([^']+)'/.exec(message);
|
|
647
|
+
if (!unresolved) {
|
|
648
|
+
return `${message}
|
|
649
|
+
|
|
650
|
+
The ${subject} 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.`;
|
|
651
|
+
}
|
|
652
|
+
return `${message.split("\n")[0]}
|
|
653
|
+
|
|
654
|
+
"${unresolved[1]}" did not resolve while the router was being imported. Imports resolve through Node and through the "paths" of the nearest tsconfig.json that declares them, searching from the router file up to the project root. Check that the specifier is mapped in one of those, and that what it points at exists: a workspace package resolves to a dist/ that has to have been built.` + describePassedOverTargets(unresolved[1], aliases);
|
|
655
|
+
}
|
|
656
|
+
function describePassedOverTargets(specifier, aliases) {
|
|
657
|
+
const alias = aliases.find((entry) => specifier.startsWith(entry.from) || specifier.startsWith(entry.to));
|
|
658
|
+
if (!alias || alias.others.length === 0) {
|
|
659
|
+
return "";
|
|
660
|
+
}
|
|
661
|
+
return `
|
|
662
|
+
|
|
663
|
+
It was resolved through "${alias.target}", the first target of the "paths" entry "${alias.pattern}" that names a directory holding anything. That entry also lists ${alias.others.map((other) => `"${other}"`).join(", ")}, which the router load never tried: one alias per key is all the loader can express, so the whole load goes through a single target.`;
|
|
664
|
+
}
|
|
665
|
+
function loadRouterModule(load) {
|
|
666
|
+
const { cwd, absoluteRouterPath, subject, fail } = load;
|
|
667
|
+
const aliases = projectAliases(cwd, dirname(absoluteRouterPath));
|
|
668
|
+
const jiti = createJiti(cwd, {
|
|
669
|
+
interopDefault: true,
|
|
670
|
+
moduleCache: false,
|
|
671
|
+
alias: Object.fromEntries(aliases.map((alias) => [alias.from, alias.to]))
|
|
672
|
+
});
|
|
673
|
+
try {
|
|
674
|
+
return jiti(absoluteRouterPath);
|
|
390
675
|
} catch (error) {
|
|
391
|
-
|
|
676
|
+
throw fail(
|
|
677
|
+
`Failed to load ${relative(cwd, absoluteRouterPath)}: ${describeFailure(error, subject, aliases)}`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function isRouter(value) {
|
|
682
|
+
return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
|
|
683
|
+
}
|
|
684
|
+
function resolveRouterExport(module, candidates) {
|
|
685
|
+
for (const exportName of candidates) {
|
|
686
|
+
const candidate = module[exportName];
|
|
687
|
+
if (isRouter(candidate)) {
|
|
688
|
+
return { router: candidate, exportName };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return void 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/codegen/generators/route-map.ts
|
|
695
|
+
var genLogger = logger.child("@spfn/core:route-map-generator");
|
|
696
|
+
var RouteMapGeneratorError = class extends Error {
|
|
697
|
+
constructor(message) {
|
|
698
|
+
super(message);
|
|
699
|
+
this.name = "RouteMapGeneratorError";
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
function isRouteDef(value) {
|
|
703
|
+
return value !== null && typeof value === "object" && "handler" in value;
|
|
704
|
+
}
|
|
705
|
+
var ROUTER_EXPORTS = ["appRouter", "default", "router"];
|
|
706
|
+
function loadRouter(cwd, absoluteRouterPath) {
|
|
707
|
+
const module = loadRouterModule({
|
|
708
|
+
cwd,
|
|
709
|
+
absoluteRouterPath,
|
|
710
|
+
subject: "route map",
|
|
711
|
+
fail: (message) => new RouteMapGeneratorError(message)
|
|
712
|
+
});
|
|
713
|
+
const resolved = resolveRouterExport(module, ROUTER_EXPORTS);
|
|
714
|
+
if (resolved) {
|
|
715
|
+
return resolved;
|
|
716
|
+
}
|
|
717
|
+
throw new RouteMapGeneratorError(
|
|
718
|
+
`No router found in ${relative(cwd, absoluteRouterPath)}. Looked for: ${ROUTER_EXPORTS.join(", ")}. Export the defineRouter() result under one of those names.`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
var HTTP_METHODS = { GET: true, POST: true, PUT: true, PATCH: true, DELETE: true };
|
|
722
|
+
function duplicateNameError(origin, name, existing, where) {
|
|
723
|
+
if (origin === "app") {
|
|
724
|
+
return new RouteMapGeneratorError(
|
|
725
|
+
`Two routes are both named "${name}" (${existing.trail} and ${where}). The RPC client addresses a route by its name alone, and a nested route registers under its own key, so a name must be unique across the whole router.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return new RouteMapGeneratorError(
|
|
729
|
+
`Two package routers both register a route named "${name}" (${existing.trail} and ${where}). Both are written into this app's route map, which holds one entry per name, so one of the two packages would have its client call the other package's path. Nothing of this app's can be renamed to fix it: drop one of the two mounts, or raise the name with the package that took it second.`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
function addRoute(name, routeDef, trail, into, origin) {
|
|
733
|
+
const where = trail.join(".");
|
|
734
|
+
if (!routeDef.method || !routeDef.path) {
|
|
735
|
+
genLogger.warn(
|
|
736
|
+
`Route "${where}" has no method or path and is left out of the map. The RPC client resolves a name to a method and a path, so both are required. A route reaches this state by being registered before .handler() was called \u2014 the server skips it too.`
|
|
737
|
+
);
|
|
738
|
+
return void 0;
|
|
739
|
+
}
|
|
740
|
+
if (!Object.hasOwn(HTTP_METHODS, routeDef.method)) {
|
|
741
|
+
throw new RouteMapGeneratorError(
|
|
742
|
+
`Route "${where}" declares the method "${routeDef.method}", which is not an HttpMethod (${Object.keys(HTTP_METHODS).join(", ")}). The generated map is typed HttpMethod, so naming it would write a file that does not compile.`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const existing = into.get(name);
|
|
746
|
+
if (existing) {
|
|
747
|
+
if (origin === "package" && existing.def === routeDef) {
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
throw duplicateNameError(origin, name, existing, where);
|
|
751
|
+
}
|
|
752
|
+
const collected = {
|
|
753
|
+
method: routeDef.method,
|
|
754
|
+
path: routeDef.path,
|
|
755
|
+
trail: where,
|
|
756
|
+
def: routeDef
|
|
757
|
+
};
|
|
758
|
+
into.set(name, collected);
|
|
759
|
+
return collected;
|
|
760
|
+
}
|
|
761
|
+
function pathPrefix(path) {
|
|
762
|
+
const [, first] = path.split("/");
|
|
763
|
+
return first ? `/${first}` : path;
|
|
764
|
+
}
|
|
765
|
+
function collectPackageRoutes(router, trail, mount, collection) {
|
|
766
|
+
if (router._publishesRouteMap === false || collection.visited.has(router)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
collection.visited.set(router, trail.join("."));
|
|
770
|
+
for (const [name, entry] of Object.entries(router.routes)) {
|
|
771
|
+
if (isRouter(entry)) {
|
|
772
|
+
collectPackageRoutes(entry, [...trail, name], mount, collection);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (!isRouteDef(entry)) {
|
|
776
|
+
genLogger.warn(
|
|
777
|
+
`Package router entry "${[...trail, name].join(".")}" is neither a route nor a router (got ${typeof entry}) and is left out of the map. \`registerRoutes\` skips it too.`
|
|
778
|
+
);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const collected = addRoute(name, entry, [...trail, name], collection.packages, "package");
|
|
782
|
+
if (collected) {
|
|
783
|
+
mount.count += 1;
|
|
784
|
+
mount.prefixes.add(pathPrefix(collected.path));
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const already = collection.packages.get(name);
|
|
788
|
+
if (already?.def === entry) {
|
|
789
|
+
mount.sharedWith ??= already.trail;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const [index, nested] of (router._packageRouters ?? []).entries()) {
|
|
793
|
+
collectPackageRoutes(nested, [...trail, `packages[${index}]`], mount, collection);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function mountPackageRouter(router, label, collection) {
|
|
797
|
+
const mount = { label, count: 0, prefixes: /* @__PURE__ */ new Set() };
|
|
798
|
+
const alreadyAt = collection.visited.get(router);
|
|
799
|
+
if (router._publishesRouteMap === false) {
|
|
800
|
+
mount.skipped = "publishes no route map";
|
|
801
|
+
} else if (alreadyAt) {
|
|
802
|
+
mount.skipped = `already reached through ${alreadyAt}`;
|
|
392
803
|
}
|
|
393
|
-
|
|
804
|
+
collectPackageRoutes(router, [label], mount, collection);
|
|
805
|
+
if (mount.count === 0 && !mount.skipped && mount.sharedWith) {
|
|
806
|
+
mount.skipped = `every route it holds was already collected through ${mount.sharedWith}`;
|
|
807
|
+
}
|
|
808
|
+
collection.mounts.push(mount);
|
|
809
|
+
return mount;
|
|
394
810
|
}
|
|
395
|
-
function
|
|
811
|
+
function collectRoutes(router, trail, collection) {
|
|
812
|
+
for (const [name, entry] of Object.entries(router.routes)) {
|
|
813
|
+
if (isRouter(entry)) {
|
|
814
|
+
collectRoutes(entry, [...trail, name], collection);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (!isRouteDef(entry)) {
|
|
818
|
+
genLogger.warn(
|
|
819
|
+
`Router entry "${[...trail, name].join(".")}" is neither a route nor a router (got ${typeof entry}) and is left out of the map. \`registerRoutes\` skips it too, so no name the client can call goes unanswered.`
|
|
820
|
+
);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
addRoute(name, entry, [...trail, name], collection.app, "app");
|
|
824
|
+
}
|
|
825
|
+
for (const [index, packageRouter] of (router._packageRouters ?? []).entries()) {
|
|
826
|
+
mountPackageRouter(packageRouter, `${trail.join(".")}.packages[${index}]`, collection);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function assertNoPackageCollision(collection) {
|
|
830
|
+
for (const [name, route] of collection.packages) {
|
|
831
|
+
const appRoute = collection.app.get(name);
|
|
832
|
+
if (!appRoute) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
throw new RouteMapGeneratorError(
|
|
836
|
+
`The app route "${name}" (${appRoute.trail}) is also registered by a package router (${route.trail}). Both go into this app's generated route map, which holds one entry per name, so one of the two paths becomes unreachable while the typed client still describes the app's route. Rename the app route.`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function collectRouteMap(router) {
|
|
841
|
+
const collection = {
|
|
842
|
+
app: /* @__PURE__ */ new Map(),
|
|
843
|
+
packages: /* @__PURE__ */ new Map(),
|
|
844
|
+
mounts: [],
|
|
845
|
+
visited: /* @__PURE__ */ new Map()
|
|
846
|
+
};
|
|
847
|
+
collectRoutes(router, ["router"], collection);
|
|
848
|
+
assertNoPackageCollision(collection);
|
|
849
|
+
return collection;
|
|
850
|
+
}
|
|
851
|
+
function describeMount(mount) {
|
|
852
|
+
if (mount.count === 0) {
|
|
853
|
+
return `${mount.label} \u2014 no routes${mount.skipped ? ` (${mount.skipped})` : ""}`;
|
|
854
|
+
}
|
|
855
|
+
const routes = `${mount.count} route${mount.count === 1 ? "" : "s"}`;
|
|
856
|
+
return `${mount.label} \u2014 ${routes} under ${[...mount.prefixes].join(", ")}`;
|
|
857
|
+
}
|
|
858
|
+
function packageHeaderLines(collection) {
|
|
859
|
+
if (collection.packages.size === 0) {
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
return [
|
|
863
|
+
" *",
|
|
864
|
+
" * The entries under the .packages() line below are not this app's. They belong",
|
|
865
|
+
" * to the package routers it mounts, copied in as they were when this file was",
|
|
866
|
+
" * generated:",
|
|
867
|
+
...collection.mounts.map((mount) => ` * ${describeMount(mount)}`),
|
|
868
|
+
" *",
|
|
869
|
+
" * A package upgrade reaches them only through a regeneration: `spfn build`",
|
|
870
|
+
" * regenerates before it compiles, a bare `next build` uses what is committed here."
|
|
871
|
+
];
|
|
872
|
+
}
|
|
873
|
+
function generateRouteMapContent(collection, nodeEnv) {
|
|
396
874
|
const lines = [
|
|
397
875
|
"/**",
|
|
398
876
|
" * Route Map (Auto-generated)",
|
|
399
877
|
" *",
|
|
400
878
|
" * DO NOT EDIT - This file is generated by @spfn/core:route-map generator",
|
|
879
|
+
" *",
|
|
880
|
+
` * Generated with NODE_ENV=${nodeEnv}. A route registered only under one environment is`,
|
|
881
|
+
" * named here only when the generator ran under it, so this line changing is that",
|
|
882
|
+
" * difference rather than an edit.",
|
|
883
|
+
...packageHeaderLines(collection),
|
|
401
884
|
" */",
|
|
402
885
|
"",
|
|
403
886
|
"import type { HttpMethod } from '@spfn/core/route';",
|
|
@@ -410,8 +893,14 @@ function generateRouteMapContent(routes) {
|
|
|
410
893
|
"",
|
|
411
894
|
"export const routeMap: Record<string, RouteInfo> = {"
|
|
412
895
|
];
|
|
413
|
-
for (const route of
|
|
414
|
-
lines.push(
|
|
896
|
+
for (const [name, route] of collection.app) {
|
|
897
|
+
lines.push(entryLine(name, route));
|
|
898
|
+
}
|
|
899
|
+
if (collection.packages.size > 0) {
|
|
900
|
+
lines.push(" // From the package routers this app mounts with .packages(), not its own:");
|
|
901
|
+
for (const [name, route] of collection.packages) {
|
|
902
|
+
lines.push(entryLine(name, route));
|
|
903
|
+
}
|
|
415
904
|
}
|
|
416
905
|
lines.push("};");
|
|
417
906
|
lines.push("");
|
|
@@ -421,6 +910,9 @@ function generateRouteMapContent(routes) {
|
|
|
421
910
|
lines.push("");
|
|
422
911
|
return lines.join("\n");
|
|
423
912
|
}
|
|
913
|
+
function entryLine(name, route) {
|
|
914
|
+
return ` ${toObjectKey(name)}: { method: '${route.method}', path: ${toStringLiteral(route.path)} },`;
|
|
915
|
+
}
|
|
424
916
|
function createRouteMapGenerator(config) {
|
|
425
917
|
const {
|
|
426
918
|
routerPath,
|
|
@@ -440,9 +932,11 @@ Usage:
|
|
|
440
932
|
}
|
|
441
933
|
return {
|
|
442
934
|
name: "@spfn/core:route-map",
|
|
935
|
+
// Unchanged, including the deprecated dirs: what the map is *built* from
|
|
936
|
+
// moved to the loaded router, but what a rebuild should be *triggered* by
|
|
937
|
+
// is still every file a route can live in.
|
|
443
938
|
watchPatterns: [
|
|
444
939
|
routerPath,
|
|
445
|
-
// Watch route directories derived from router imports
|
|
446
940
|
"src/server/routes/**/*.ts",
|
|
447
941
|
...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
|
|
448
942
|
],
|
|
@@ -456,97 +950,36 @@ Usage:
|
|
|
456
950
|
return;
|
|
457
951
|
}
|
|
458
952
|
if (debug) {
|
|
459
|
-
genLogger.info("
|
|
460
|
-
}
|
|
461
|
-
const { importPaths, routeNames } = parseRouterFile(absoluteRouterPath);
|
|
462
|
-
if (debug) {
|
|
463
|
-
genLogger.info("Found route imports", { count: importPaths.length, names: routeNames });
|
|
464
|
-
}
|
|
465
|
-
const routerDir = dirname(absoluteRouterPath);
|
|
466
|
-
const allRoutes = [];
|
|
467
|
-
for (const importPath of importPaths) {
|
|
468
|
-
let resolvedPath = resolve(routerDir, importPath);
|
|
469
|
-
if (!resolvedPath.endsWith(".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
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (existsSync(resolvedPath)) {
|
|
483
|
-
const routes = parseRouteFile(resolvedPath);
|
|
484
|
-
allRoutes.push(...routes);
|
|
485
|
-
if (debug) {
|
|
486
|
-
genLogger.info(`Parsed ${routes.length} routes from ${relative(cwd, resolvedPath)}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
953
|
+
genLogger.info("Loading router", { path: absoluteRouterPath });
|
|
489
954
|
}
|
|
490
|
-
|
|
955
|
+
pinNodeEnv();
|
|
956
|
+
const { router, exportName } = loadRouter(cwd, absoluteRouterPath);
|
|
957
|
+
assertUnconditionalRegistration({
|
|
958
|
+
routerPath,
|
|
959
|
+
source: readFileSync(absoluteRouterPath, "utf-8"),
|
|
960
|
+
exportName,
|
|
961
|
+
subject: "route map"
|
|
962
|
+
});
|
|
963
|
+
const collection = collectRouteMap(router);
|
|
964
|
+
const total = collection.app.size + collection.packages.size;
|
|
491
965
|
if (debug) {
|
|
492
|
-
genLogger.info(`Found ${
|
|
966
|
+
genLogger.info(`Found ${total} routes`, {
|
|
967
|
+
app: [...collection.app.keys()],
|
|
968
|
+
packages: [...collection.packages.keys()]
|
|
969
|
+
});
|
|
493
970
|
}
|
|
494
|
-
const content = generateRouteMapContent(exportedRoutes);
|
|
495
971
|
const outputDir = dirname(absoluteOutputPath);
|
|
496
972
|
if (!existsSync(outputDir)) {
|
|
497
973
|
mkdirSync(outputDir, { recursive: true });
|
|
498
974
|
}
|
|
499
|
-
|
|
500
|
-
|
|
975
|
+
const nodeEnv = process.env.NODE_ENV ?? "unset";
|
|
976
|
+
writeFileSync(absoluteOutputPath, generateRouteMapContent(collection, nodeEnv), "utf-8");
|
|
977
|
+
genLogger.info(
|
|
978
|
+
`Generated route map: ${relative(cwd, absoluteOutputPath)} (${collection.app.size} app routes, ${collection.packages.size} from mounted packages)`
|
|
979
|
+
);
|
|
501
980
|
}
|
|
502
981
|
};
|
|
503
982
|
}
|
|
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
983
|
var genLogger2 = logger.child("@spfn/core:contract-generator");
|
|
551
984
|
var ContractGeneratorError = class extends Error {
|
|
552
985
|
constructor(message) {
|
|
@@ -554,43 +987,67 @@ var ContractGeneratorError = class extends Error {
|
|
|
554
987
|
this.name = "ContractGeneratorError";
|
|
555
988
|
}
|
|
556
989
|
};
|
|
557
|
-
function
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
}
|
|
990
|
+
function loadRouter2(cwd, absoluteRouterPath, routerExport) {
|
|
991
|
+
const module = loadRouterModule({
|
|
992
|
+
cwd,
|
|
993
|
+
absoluteRouterPath,
|
|
994
|
+
subject: "contract",
|
|
995
|
+
fail: (message) => new ContractGeneratorError(message)
|
|
996
|
+
});
|
|
573
997
|
const candidates = routerExport ? [routerExport] : ["appRouter", "default", "router"];
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return candidate;
|
|
578
|
-
}
|
|
998
|
+
const resolved = resolveRouterExport(module, candidates);
|
|
999
|
+
if (resolved) {
|
|
1000
|
+
return resolved;
|
|
579
1001
|
}
|
|
580
1002
|
throw new ContractGeneratorError(
|
|
581
1003
|
`No router found in ${relative(cwd, absoluteRouterPath)}. Looked for: ${candidates.join(", ")}. Set "routerExport" to the export holding the defineRouter() result.`
|
|
582
1004
|
);
|
|
583
1005
|
}
|
|
584
|
-
function
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1006
|
+
function isEventRouter(value) {
|
|
1007
|
+
return value !== null && typeof value === "object" && "events" in value && "eventNames" in value;
|
|
1008
|
+
}
|
|
1009
|
+
function loadEventRouter(cwd, absolutePath, eventRouterExport) {
|
|
1010
|
+
const module = loadRouterModule({
|
|
1011
|
+
cwd,
|
|
1012
|
+
absoluteRouterPath: absolutePath,
|
|
1013
|
+
subject: "contract",
|
|
1014
|
+
fail: (message) => new ContractGeneratorError(message)
|
|
1015
|
+
});
|
|
1016
|
+
const candidates = eventRouterExport ? [eventRouterExport] : ["eventRouter", "default"];
|
|
1017
|
+
const exportName = candidates.find((candidate) => isEventRouter(module[candidate]));
|
|
1018
|
+
if (exportName) {
|
|
1019
|
+
return { eventRouter: module[exportName], exportName };
|
|
1020
|
+
}
|
|
1021
|
+
throw new ContractGeneratorError(
|
|
1022
|
+
`No event router found in ${relative(cwd, absolutePath)}. Looked for: ${candidates.join(", ")}. Set "eventRouterExport" to the export holding the defineEventRouter() result.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
function readEventRouter(cwd, eventRouterPath, eventRouterExport) {
|
|
1026
|
+
if (!eventRouterPath) {
|
|
1027
|
+
return void 0;
|
|
588
1028
|
}
|
|
1029
|
+
const absolutePath = join(cwd, eventRouterPath);
|
|
1030
|
+
if (!existsSync(absolutePath)) {
|
|
1031
|
+
throw new ContractGeneratorError(
|
|
1032
|
+
`Event router file not found: ${eventRouterPath}. The contract generator is configured with "eventRouterPath" but has nothing to read there.`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
const { eventRouter, exportName } = loadEventRouter(cwd, absolutePath, eventRouterExport);
|
|
1036
|
+
assertUnconditionalRegistration({
|
|
1037
|
+
routerPath: eventRouterPath,
|
|
1038
|
+
source: readFileSync(absolutePath, "utf-8"),
|
|
1039
|
+
exportName,
|
|
1040
|
+
subject: "contract",
|
|
1041
|
+
factory: "defineEventRouter"
|
|
1042
|
+
});
|
|
1043
|
+
return eventRouter;
|
|
589
1044
|
}
|
|
590
1045
|
function createContractGenerator(config) {
|
|
591
1046
|
const {
|
|
592
1047
|
routerPath,
|
|
593
1048
|
routerExport,
|
|
1049
|
+
eventRouterPath,
|
|
1050
|
+
eventRouterExport,
|
|
594
1051
|
outputDir = "./contracts",
|
|
595
1052
|
additionalRouteDirs = []
|
|
596
1053
|
} = config;
|
|
@@ -609,6 +1066,7 @@ Usage:
|
|
|
609
1066
|
name: "@spfn/core:contract",
|
|
610
1067
|
watchPatterns: [
|
|
611
1068
|
routerPath,
|
|
1069
|
+
...eventRouterPath ? [eventRouterPath] : [],
|
|
612
1070
|
"src/server/routes/**/*.ts",
|
|
613
1071
|
...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
|
|
614
1072
|
],
|
|
@@ -623,9 +1081,14 @@ Usage:
|
|
|
623
1081
|
);
|
|
624
1082
|
}
|
|
625
1083
|
pinNodeEnv();
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1084
|
+
const { router, exportName } = loadRouter2(cwd, absoluteRouterPath, routerExport);
|
|
1085
|
+
assertUnconditionalRegistration({
|
|
1086
|
+
routerPath,
|
|
1087
|
+
source: readFileSync(absoluteRouterPath, "utf-8"),
|
|
1088
|
+
exportName,
|
|
1089
|
+
subject: "contract"
|
|
1090
|
+
});
|
|
1091
|
+
const document = collectContractDocument(router, readEventRouter(cwd, eventRouterPath, eventRouterExport));
|
|
629
1092
|
const changed = writeCurrentDocument(contractsDir, document);
|
|
630
1093
|
genLogger2.info(
|
|
631
1094
|
`${changed ? "Wrote" : "Verified"} ${relative(cwd, join(contractsDir, "current.json"))} (${document.operations.length} contracted operation(s))`
|
|
@@ -653,13 +1116,156 @@ A released client cannot be fixed by redeploying the server. Keep the promise, o
|
|
|
653
1116
|
}
|
|
654
1117
|
};
|
|
655
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
|
+
}
|
|
656
1261
|
|
|
657
1262
|
// src/codegen/generators/index.ts
|
|
658
1263
|
var generators = {
|
|
659
1264
|
"route-map": createRouteMapGenerator,
|
|
660
|
-
contract: createContractGenerator
|
|
1265
|
+
contract: createContractGenerator,
|
|
1266
|
+
states: createStatesGenerator
|
|
661
1267
|
};
|
|
662
1268
|
|
|
663
|
-
export { CodegenOrchestrator, ConditionalRegistrationError, ContractGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
|
|
1269
|
+
export { CodegenOrchestrator, ConditionalRegistrationError, ContractGeneratorError, RouteMapGeneratorError, StatesGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
|
|
664
1270
|
//# sourceMappingURL=index.js.map
|
|
665
1271
|
//# sourceMappingURL=index.js.map
|