@spfn/core 0.3.0-beta.12 → 0.3.0-beta.14
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 +7 -5
- package/dist/codegen/index.d.ts +9 -0
- package/dist/codegen/index.js +143 -37
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/event/index.d.ts +1 -2
- package/dist/event/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +13 -13
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +11 -6
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +2 -2
- package/dist/route/index.js.map +1 -1
- package/dist/{router-9L3El7nO.d.ts → router-BqyEARoY.d.ts} +25 -23
- package/dist/server/index.js.map +1 -1
- package/dist/{types-yv6z_mK7.d.ts → types-QoyjaJWw.d.ts} +6 -0
- package/package.json +1 -1
- package/src/codegen/README.md +29 -15
- package/src/nextjs/README.md +17 -10
- package/src/route/README.md +14 -9
package/README.md
CHANGED
|
@@ -122,8 +122,9 @@ the route map the RPC proxy needs — and nothing else.
|
|
|
122
122
|
5. **Mount the RPC proxy** in Next.js as the `app/api/rpc/[routeName]/route.ts`
|
|
123
123
|
catch-all. The browser only ever sends `GET` (no body) or `POST` (body or formData)
|
|
124
124
|
to `/api/rpc/{routeName}`; the proxy looks up `routeMap[routeName]`, substitutes
|
|
125
|
-
`:params`, and forwards with the real method.
|
|
126
|
-
|
|
125
|
+
`:params`, and forwards with the real method. The generated map already carries the
|
|
126
|
+
routes of the packages the app router mounts with `.packages()`; `eventRouteMap` is
|
|
127
|
+
merged by hand, being a constant rather than a mounted router.
|
|
127
128
|
6. **Call it** through `createApi<AppRouter>()`. The client is a `Proxy` over the
|
|
128
129
|
`AppRouter` type — typed in and out, with no runtime cost for the types. Errors
|
|
129
130
|
arrive as `ApiError`, or as the original error class when that class is registered in
|
|
@@ -791,9 +792,10 @@ way so the endpoint never reveals that someone else's record exists.
|
|
|
791
792
|
|
|
792
793
|
Other SPFN packages build on `@spfn/core`:
|
|
793
794
|
|
|
794
|
-
- [`@spfn/auth`](../auth/README.md) — sessions, social login and RBAC.
|
|
795
|
-
`
|
|
796
|
-
`
|
|
795
|
+
- [`@spfn/auth`](../auth/README.md) — sessions, social login and RBAC. Mount `authRouter`
|
|
796
|
+
with `.packages()` and its routes are in the app's generated map; the package registers
|
|
797
|
+
proxy interceptors automatically. (`authRouteMap` is still exported, and merging it is
|
|
798
|
+
now a no-op.)
|
|
797
799
|
- [`@spfn/mcp`](../mcp/README.md) — exposes your operations as MCP tools, so an agent can
|
|
798
800
|
run them instead of you building an admin dashboard.
|
|
799
801
|
- `@spfn/i18n`, `@spfn/storage`, `@spfn/notification`, `@spfn/cms`, `@spfn/monitor`,
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -302,6 +302,15 @@ interface GenerationStats {
|
|
|
302
302
|
* 404s. Walking the loaded router registers exactly what `registerRoutes`
|
|
303
303
|
* registers, whatever the source looks like.
|
|
304
304
|
*
|
|
305
|
+
* The map carries the routes of every package router the app mounts with
|
|
306
|
+
* `.packages()`, not only the app's own. A package ships a client that addresses
|
|
307
|
+
* its routes by name — `authApi.login`, `monitorApi.getStats` — and the app's
|
|
308
|
+
* `createRpcProxy` resolves a name in the single map it was constructed with, so
|
|
309
|
+
* a package route in nobody's map is a call that resolves nowhere. The app
|
|
310
|
+
* therefore merges nothing by hand: `{ ...routeMap, ...authRouteMap }` is a
|
|
311
|
+
* no-op today and `{ ...routeMap }` is the whole of it. (`eventRouteMap` is the
|
|
312
|
+
* exception that stays — it is a hand-written constant, not a mounted router.)
|
|
313
|
+
*
|
|
305
314
|
* @example
|
|
306
315
|
* ```typescript
|
|
307
316
|
* // .spfnrc.ts
|
package/dist/codegen/index.js
CHANGED
|
@@ -501,13 +501,24 @@ var NO_INPUTS = 18003;
|
|
|
501
501
|
function targetPath(target, base) {
|
|
502
502
|
return resolve(base, target.endsWith("/*") ? target.slice(0, -2) : target);
|
|
503
503
|
}
|
|
504
|
+
var UNREADABLE_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM"]);
|
|
505
|
+
function isUnreadable(error) {
|
|
506
|
+
return UNREADABLE_CODES.has(error?.code ?? "");
|
|
507
|
+
}
|
|
504
508
|
function targetResolves(target, base) {
|
|
505
509
|
const path = targetPath(target, base);
|
|
506
510
|
if (!target.endsWith("/*")) {
|
|
507
511
|
return existsSync(path);
|
|
508
512
|
}
|
|
509
|
-
|
|
510
|
-
|
|
513
|
+
try {
|
|
514
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
515
|
+
return stats?.isDirectory() === true && readdirSync(path).length > 0;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (!isUnreadable(error)) {
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
511
522
|
}
|
|
512
523
|
function chooseTarget(pattern, targets, base) {
|
|
513
524
|
const present = targets.filter((target2) => targetResolves(target2, base));
|
|
@@ -677,46 +688,99 @@ function loadRouter(cwd, absoluteRouterPath) {
|
|
|
677
688
|
);
|
|
678
689
|
}
|
|
679
690
|
var HTTP_METHODS = { GET: true, POST: true, PUT: true, PATCH: true, DELETE: true };
|
|
680
|
-
function
|
|
691
|
+
function duplicateNameError(origin, name, existing, where) {
|
|
692
|
+
if (origin === "app") {
|
|
693
|
+
return new RouteMapGeneratorError(
|
|
694
|
+
`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.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return new RouteMapGeneratorError(
|
|
698
|
+
`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.`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function addRoute(name, routeDef, trail, into, origin) {
|
|
681
702
|
const where = trail.join(".");
|
|
682
703
|
if (!routeDef.method || !routeDef.path) {
|
|
683
704
|
genLogger.warn(
|
|
684
705
|
`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.`
|
|
685
706
|
);
|
|
686
|
-
return;
|
|
707
|
+
return void 0;
|
|
687
708
|
}
|
|
688
709
|
if (!Object.hasOwn(HTTP_METHODS, routeDef.method)) {
|
|
689
710
|
throw new RouteMapGeneratorError(
|
|
690
711
|
`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.`
|
|
691
712
|
);
|
|
692
713
|
}
|
|
693
|
-
const existing =
|
|
714
|
+
const existing = into.get(name);
|
|
694
715
|
if (existing) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
716
|
+
if (origin === "package" && existing.def === routeDef) {
|
|
717
|
+
return void 0;
|
|
718
|
+
}
|
|
719
|
+
throw duplicateNameError(origin, name, existing, where);
|
|
698
720
|
}
|
|
699
|
-
|
|
721
|
+
const collected = {
|
|
722
|
+
method: routeDef.method,
|
|
723
|
+
path: routeDef.path,
|
|
724
|
+
trail: where,
|
|
725
|
+
def: routeDef
|
|
726
|
+
};
|
|
727
|
+
into.set(name, collected);
|
|
728
|
+
return collected;
|
|
700
729
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
730
|
+
function pathPrefix(path) {
|
|
731
|
+
const [, first] = path.split("/");
|
|
732
|
+
return first ? `/${first}` : path;
|
|
733
|
+
}
|
|
734
|
+
function collectPackageRoutes(router, trail, mount, collection) {
|
|
735
|
+
if (router._publishesRouteMap === false || collection.visited.has(router)) {
|
|
703
736
|
return;
|
|
704
737
|
}
|
|
738
|
+
collection.visited.set(router, trail.join("."));
|
|
705
739
|
for (const [name, entry] of Object.entries(router.routes)) {
|
|
706
740
|
if (isRouter(entry)) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
741
|
+
collectPackageRoutes(entry, [...trail, name], mount, collection);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!isRouteDef(entry)) {
|
|
745
|
+
genLogger.warn(
|
|
746
|
+
`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.`
|
|
747
|
+
);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const collected = addRoute(name, entry, [...trail, name], collection.packages, "package");
|
|
751
|
+
if (collected) {
|
|
752
|
+
mount.count += 1;
|
|
753
|
+
mount.prefixes.add(pathPrefix(collected.path));
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const already = collection.packages.get(name);
|
|
757
|
+
if (already?.def === entry) {
|
|
758
|
+
mount.sharedWith ??= already.trail;
|
|
710
759
|
}
|
|
711
760
|
}
|
|
712
|
-
for (const [index,
|
|
713
|
-
|
|
761
|
+
for (const [index, nested] of (router._packageRouters ?? []).entries()) {
|
|
762
|
+
collectPackageRoutes(nested, [...trail, `packages[${index}]`], mount, collection);
|
|
714
763
|
}
|
|
715
764
|
}
|
|
716
|
-
function
|
|
765
|
+
function mountPackageRouter(router, label, collection) {
|
|
766
|
+
const mount = { label, count: 0, prefixes: /* @__PURE__ */ new Set() };
|
|
767
|
+
const alreadyAt = collection.visited.get(router);
|
|
768
|
+
if (router._publishesRouteMap === false) {
|
|
769
|
+
mount.skipped = "publishes no route map";
|
|
770
|
+
} else if (alreadyAt) {
|
|
771
|
+
mount.skipped = `already reached through ${alreadyAt}`;
|
|
772
|
+
}
|
|
773
|
+
collectPackageRoutes(router, [label], mount, collection);
|
|
774
|
+
if (mount.count === 0 && !mount.skipped && mount.sharedWith) {
|
|
775
|
+
mount.skipped = `every route it holds was already collected through ${mount.sharedWith}`;
|
|
776
|
+
}
|
|
777
|
+
collection.mounts.push(mount);
|
|
778
|
+
return mount;
|
|
779
|
+
}
|
|
780
|
+
function collectRoutes(router, trail, collection) {
|
|
717
781
|
for (const [name, entry] of Object.entries(router.routes)) {
|
|
718
782
|
if (isRouter(entry)) {
|
|
719
|
-
collectRoutes(entry, [...trail, name],
|
|
783
|
+
collectRoutes(entry, [...trail, name], collection);
|
|
720
784
|
continue;
|
|
721
785
|
}
|
|
722
786
|
if (!isRouteDef(entry)) {
|
|
@@ -725,29 +789,33 @@ function collectRoutes(router, trail, found, packageNames) {
|
|
|
725
789
|
);
|
|
726
790
|
continue;
|
|
727
791
|
}
|
|
728
|
-
addRoute(name, entry, [...trail, name],
|
|
792
|
+
addRoute(name, entry, [...trail, name], collection.app, "app");
|
|
729
793
|
}
|
|
730
794
|
for (const [index, packageRouter] of (router._packageRouters ?? []).entries()) {
|
|
731
|
-
|
|
795
|
+
mountPackageRouter(packageRouter, `${trail.join(".")}.packages[${index}]`, collection);
|
|
732
796
|
}
|
|
733
797
|
}
|
|
734
|
-
function assertNoPackageCollision(
|
|
735
|
-
for (const [name,
|
|
736
|
-
const
|
|
737
|
-
if (!
|
|
798
|
+
function assertNoPackageCollision(collection) {
|
|
799
|
+
for (const [name, route] of collection.packages) {
|
|
800
|
+
const appRoute = collection.app.get(name);
|
|
801
|
+
if (!appRoute) {
|
|
738
802
|
continue;
|
|
739
803
|
}
|
|
740
804
|
throw new RouteMapGeneratorError(
|
|
741
|
-
`The app route "${name}" (${
|
|
805
|
+
`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.`
|
|
742
806
|
);
|
|
743
807
|
}
|
|
744
808
|
}
|
|
745
809
|
function collectRouteMap(router) {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
810
|
+
const collection = {
|
|
811
|
+
app: /* @__PURE__ */ new Map(),
|
|
812
|
+
packages: /* @__PURE__ */ new Map(),
|
|
813
|
+
mounts: [],
|
|
814
|
+
visited: /* @__PURE__ */ new Map()
|
|
815
|
+
};
|
|
816
|
+
collectRoutes(router, ["router"], collection);
|
|
817
|
+
assertNoPackageCollision(collection);
|
|
818
|
+
return collection;
|
|
751
819
|
}
|
|
752
820
|
function toObjectKey(name) {
|
|
753
821
|
if (name === "__proto__") {
|
|
@@ -762,7 +830,29 @@ function toStringLiteral(value) {
|
|
|
762
830
|
}
|
|
763
831
|
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
764
832
|
}
|
|
765
|
-
function
|
|
833
|
+
function describeMount(mount) {
|
|
834
|
+
if (mount.count === 0) {
|
|
835
|
+
return `${mount.label} \u2014 no routes${mount.skipped ? ` (${mount.skipped})` : ""}`;
|
|
836
|
+
}
|
|
837
|
+
const routes = `${mount.count} route${mount.count === 1 ? "" : "s"}`;
|
|
838
|
+
return `${mount.label} \u2014 ${routes} under ${[...mount.prefixes].join(", ")}`;
|
|
839
|
+
}
|
|
840
|
+
function packageHeaderLines(collection) {
|
|
841
|
+
if (collection.packages.size === 0) {
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
return [
|
|
845
|
+
" *",
|
|
846
|
+
" * The entries under the .packages() line below are not this app's. They belong",
|
|
847
|
+
" * to the package routers it mounts, copied in as they were when this file was",
|
|
848
|
+
" * generated:",
|
|
849
|
+
...collection.mounts.map((mount) => ` * ${describeMount(mount)}`),
|
|
850
|
+
" *",
|
|
851
|
+
" * A package upgrade reaches them only through a regeneration: `spfn build`",
|
|
852
|
+
" * regenerates before it compiles, a bare `next build` uses what is committed here."
|
|
853
|
+
];
|
|
854
|
+
}
|
|
855
|
+
function generateRouteMapContent(collection, nodeEnv) {
|
|
766
856
|
const lines = [
|
|
767
857
|
"/**",
|
|
768
858
|
" * Route Map (Auto-generated)",
|
|
@@ -772,6 +862,7 @@ function generateRouteMapContent(routes, nodeEnv) {
|
|
|
772
862
|
` * Generated with NODE_ENV=${nodeEnv}. A route registered only under one environment is`,
|
|
773
863
|
" * named here only when the generator ran under it, so this line changing is that",
|
|
774
864
|
" * difference rather than an edit.",
|
|
865
|
+
...packageHeaderLines(collection),
|
|
775
866
|
" */",
|
|
776
867
|
"",
|
|
777
868
|
"import type { HttpMethod } from '@spfn/core/route';",
|
|
@@ -784,8 +875,14 @@ function generateRouteMapContent(routes, nodeEnv) {
|
|
|
784
875
|
"",
|
|
785
876
|
"export const routeMap: Record<string, RouteInfo> = {"
|
|
786
877
|
];
|
|
787
|
-
for (const [name, route] of
|
|
788
|
-
lines.push(
|
|
878
|
+
for (const [name, route] of collection.app) {
|
|
879
|
+
lines.push(entryLine(name, route));
|
|
880
|
+
}
|
|
881
|
+
if (collection.packages.size > 0) {
|
|
882
|
+
lines.push(" // From the package routers this app mounts with .packages(), not its own:");
|
|
883
|
+
for (const [name, route] of collection.packages) {
|
|
884
|
+
lines.push(entryLine(name, route));
|
|
885
|
+
}
|
|
789
886
|
}
|
|
790
887
|
lines.push("};");
|
|
791
888
|
lines.push("");
|
|
@@ -795,6 +892,9 @@ function generateRouteMapContent(routes, nodeEnv) {
|
|
|
795
892
|
lines.push("");
|
|
796
893
|
return lines.join("\n");
|
|
797
894
|
}
|
|
895
|
+
function entryLine(name, route) {
|
|
896
|
+
return ` ${toObjectKey(name)}: { method: '${route.method}', path: ${toStringLiteral(route.path)} },`;
|
|
897
|
+
}
|
|
798
898
|
function createRouteMapGenerator(config) {
|
|
799
899
|
const {
|
|
800
900
|
routerPath,
|
|
@@ -842,17 +942,23 @@ Usage:
|
|
|
842
942
|
exportName,
|
|
843
943
|
subject: "route map"
|
|
844
944
|
});
|
|
845
|
-
const
|
|
945
|
+
const collection = collectRouteMap(router);
|
|
946
|
+
const total = collection.app.size + collection.packages.size;
|
|
846
947
|
if (debug) {
|
|
847
|
-
genLogger.info(`Found ${
|
|
948
|
+
genLogger.info(`Found ${total} routes`, {
|
|
949
|
+
app: [...collection.app.keys()],
|
|
950
|
+
packages: [...collection.packages.keys()]
|
|
951
|
+
});
|
|
848
952
|
}
|
|
849
953
|
const outputDir = dirname(absoluteOutputPath);
|
|
850
954
|
if (!existsSync(outputDir)) {
|
|
851
955
|
mkdirSync(outputDir, { recursive: true });
|
|
852
956
|
}
|
|
853
957
|
const nodeEnv = process.env.NODE_ENV ?? "unset";
|
|
854
|
-
writeFileSync(absoluteOutputPath, generateRouteMapContent(
|
|
855
|
-
genLogger.info(
|
|
958
|
+
writeFileSync(absoluteOutputPath, generateRouteMapContent(collection, nodeEnv), "utf-8");
|
|
959
|
+
genLogger.info(
|
|
960
|
+
`Generated route map: ${relative(cwd, absoluteOutputPath)} (${collection.app.size} app routes, ${collection.packages.size} from mounted packages)`
|
|
961
|
+
);
|
|
856
962
|
}
|
|
857
963
|
};
|
|
858
964
|
}
|