@canmi/seam-react 0.4.11 → 0.4.18
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 +13 -8
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/scripts/build-skeletons.mjs +26 -2
- package/scripts/skeleton/layout.mjs +37 -9
- package/scripts/skeleton/render.mjs +1 -1
- package/scripts/skeleton/schema.mjs +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,19 @@ React bindings for SeamJS, providing hooks and components to consume server-inje
|
|
|
4
4
|
|
|
5
5
|
## Key Exports
|
|
6
6
|
|
|
7
|
-
| Export | Purpose
|
|
8
|
-
| --------------------- |
|
|
9
|
-
| `defineRoutes` | Define client-side route configuration
|
|
10
|
-
| `useSeamData` | Access server-injected data from `SeamDataProvider` context
|
|
11
|
-
| `SeamDataProvider` | Context provider for server data
|
|
12
|
-
| `parseSeamData` | Parse JSON from `<script id="__data">`
|
|
13
|
-
| `buildSentinelData` | Build sentinel data for skeleton rendering
|
|
14
|
-
| `useSeamSubscription` | Hook for SSE subscriptions, returns `{ data, error, status }`
|
|
7
|
+
| Export | Purpose |
|
|
8
|
+
| --------------------- | ---------------------------------------------------------------------- |
|
|
9
|
+
| `defineRoutes` | Define client-side route configuration |
|
|
10
|
+
| `useSeamData` | Access server-injected data from `SeamDataProvider` context |
|
|
11
|
+
| `SeamDataProvider` | Context provider for server data |
|
|
12
|
+
| `parseSeamData` | Parse JSON from `<script id="__data">` |
|
|
13
|
+
| `buildSentinelData` | Build sentinel data for skeleton rendering |
|
|
14
|
+
| `useSeamSubscription` | Hook for SSE subscriptions, returns `{ data, error, status }` |
|
|
15
|
+
| `LazyComponentLoader` | Type for dynamic `() => import(...)` page loaders (per-page splitting) |
|
|
16
|
+
|
|
17
|
+
## Types
|
|
18
|
+
|
|
19
|
+
`RouteDef.component` accepts either a `ComponentType` or a `LazyComponentLoader` (a function returning `Promise<{ default: ComponentType }>`). The lazy variant is produced by `@canmi/seam-vite` when per-page splitting is active.
|
|
15
20
|
|
|
16
21
|
## Structure
|
|
17
22
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,16 +4,21 @@ import { SeamClientError } from "@canmi/seam-client";
|
|
|
4
4
|
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
interface ParamMapping {
|
|
7
|
-
from:
|
|
7
|
+
from: string;
|
|
8
8
|
type?: "string" | "int";
|
|
9
9
|
}
|
|
10
10
|
interface LoaderDef {
|
|
11
11
|
procedure: string;
|
|
12
12
|
params?: Record<string, ParamMapping>;
|
|
13
13
|
}
|
|
14
|
+
/** Lazy component loader returned by dynamic import (per-page splitting) */
|
|
15
|
+
type LazyComponentLoader = () => Promise<{
|
|
16
|
+
default: ComponentType<Record<string, unknown>>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}>;
|
|
14
19
|
interface RouteDef {
|
|
15
20
|
path: string;
|
|
16
|
-
component?: ComponentType<Record<string, unknown
|
|
21
|
+
component?: ComponentType<Record<string, unknown>> | LazyComponentLoader;
|
|
17
22
|
layout?: ComponentType<{
|
|
18
23
|
children: ReactNode;
|
|
19
24
|
}>;
|
|
@@ -22,6 +27,8 @@ interface RouteDef {
|
|
|
22
27
|
mock?: Record<string, unknown>;
|
|
23
28
|
nullable?: string[];
|
|
24
29
|
staleTime?: number;
|
|
30
|
+
/** Internal: override layout ID for group layouts to avoid toLayoutId collision */
|
|
31
|
+
_layoutId?: string;
|
|
25
32
|
}
|
|
26
33
|
//#endregion
|
|
27
34
|
//#region src/define-routes.d.ts
|
|
@@ -56,5 +63,5 @@ declare function useSeamSubscription<T>(baseUrl: string, procedure: string, inpu
|
|
|
56
63
|
declare const SeamNavigateProvider: react.Provider<(url: string) => void>;
|
|
57
64
|
declare function useSeamNavigate(): (url: string) => void;
|
|
58
65
|
//#endregion
|
|
59
|
-
export { type LoaderDef, type ParamMapping, type RouteDef, SeamDataProvider, SeamNavigateProvider, type SubscriptionStatus, type UseSeamSubscriptionResult, buildSentinelData, defineRoutes, parseSeamData, useSeamData, useSeamNavigate, useSeamSubscription };
|
|
66
|
+
export { type LazyComponentLoader, type LoaderDef, type ParamMapping, type RouteDef, SeamDataProvider, SeamNavigateProvider, type SubscriptionStatus, type UseSeamSubscriptionResult, buildSentinelData, defineRoutes, parseSeamData, useSeamData, useSeamNavigate, useSeamSubscription };
|
|
60
67
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/define-routes.ts","../src/use-seam-data.ts","../src/sentinel.ts","../src/use-seam-subscription.ts","../src/use-seam-navigate.ts"],"mappings":";;;;;UAIiB,YAAA;EACf,IAAA;EACA,IAAA;AAAA;AAAA,UAGe,SAAA;EACf,SAAA;EACA,MAAA,GAAS,MAAA,SAAe,YAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/define-routes.ts","../src/use-seam-data.ts","../src/sentinel.ts","../src/use-seam-subscription.ts","../src/use-seam-navigate.ts"],"mappings":";;;;;UAIiB,YAAA;EACf,IAAA;EACA,IAAA;AAAA;AAAA,UAGe,SAAA;EACf,SAAA;EACA,MAAA,GAAS,MAAA,SAAe,YAAA;AAAA;;KAId,mBAAA,SAA4B,OAAA;EACtC,OAAA,EAAS,aAAA,CAAc,MAAA;EAAA,CACtB,GAAA;AAAA;AAAA,UAGc,QAAA;EACf,IAAA;EACA,SAAA,GAAY,aAAA,CAAc,MAAA,qBAA2B,mBAAA;EACrD,MAAA,GAAS,aAAA;IAAgB,QAAA,EAAU,SAAA;EAAA;EACnC,QAAA,GAAW,QAAA;EACX,OAAA,GAAU,MAAA,SAAe,SAAA;EACzB,IAAA,GAAO,MAAA;EACP,QAAA;EACA,SAAA;EAbsC;EAetC,SAAA;AAAA;;;iBC1Bc,YAAA,CAAa,MAAA,EAAQ,QAAA,KAAa,QAAA;;;cCErC,gBAAA,EAA2C,KAAA,CAA3B,QAAA;AAAA,iBAEb,WAAA,oBAA+B,MAAA,kBAAA,CAAA,GAA4B,CAAA;AAAA,iBAO3D,aAAA,CAAc,MAAA,YAAoB,MAAA;;;;;;;;AFXlD;;;iBGMgB,iBAAA,CACd,GAAA,EAAK,MAAA,mBACL,MAAA,WACA,SAAA,GAAY,GAAA,WACX,MAAA;;;KCTS,kBAAA;AAAA,UAEK,yBAAA;EACf,IAAA,EAAM,CAAA;EACN,KAAA,EAAO,eAAA;EACP,MAAA,EAAQ,kBAAA;AAAA;AAAA,iBAGM,mBAAA,GAAA,CACd,OAAA,UACA,SAAA,UACA,KAAA,YACC,yBAAA,CAA0B,CAAA;;;cCThB,oBAAA,EAAmD,KAAA,CAA/B,QAAA,EAAA,GAAA;AAAA,iBAEjB,eAAA,CAAA,IAAoB,GAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canmi/seam-react",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@canmi/seam-client": "0.4.
|
|
21
|
+
"@canmi/seam-client": "0.4.18",
|
|
22
22
|
"esbuild": "^0.27.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@canmi/seam-engine": "0.4.
|
|
26
|
-
"@canmi/seam-i18n": "0.4.
|
|
25
|
+
"@canmi/seam-engine": "0.4.18",
|
|
26
|
+
"@canmi/seam-i18n": "0.4.18",
|
|
27
27
|
"@types/react": "^19.2.14",
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^28.1.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* src/client/react/scripts/build-skeletons.mjs */
|
|
2
2
|
|
|
3
3
|
import { build } from "esbuild";
|
|
4
|
-
import { readFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
5
|
-
import { join, dirname, resolve } from "node:path";
|
|
4
|
+
import { readFileSync, mkdirSync, unlinkSync, existsSync } from "node:fs";
|
|
5
|
+
import { join, dirname, resolve, relative } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import { SeamBuildError } from "./skeleton/render.mjs";
|
|
@@ -37,6 +37,16 @@ function loadI18nConfig(i18nArg) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Resolve a source file path, probing for .tsx/.ts/.jsx/.js extensions */
|
|
41
|
+
function resolveSourcePath(p) {
|
|
42
|
+
if (existsSync(p)) return p;
|
|
43
|
+
const base = p.replace(/\.[jt]sx?$/, "");
|
|
44
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
45
|
+
if (existsSync(base + ext)) return base + ext;
|
|
46
|
+
}
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
async function main() {
|
|
41
51
|
const routesFile = process.argv[2];
|
|
42
52
|
if (!routesFile) {
|
|
@@ -126,12 +136,26 @@ async function main() {
|
|
|
126
136
|
stats: { hits: 0, misses: 0 },
|
|
127
137
|
};
|
|
128
138
|
|
|
139
|
+
// Build sourceFileMap: route path -> component source file (relative to cwd)
|
|
140
|
+
const sourceFileMap = {};
|
|
141
|
+
for (const route of flat) {
|
|
142
|
+
if (route.component?.name) {
|
|
143
|
+
const specifier = importMap.get(route.component.name);
|
|
144
|
+
if (specifier) {
|
|
145
|
+
const abs = resolve(routesDir, specifier);
|
|
146
|
+
const resolved = resolveSourcePath(abs);
|
|
147
|
+
sourceFileMap[route.path] = relative(process.cwd(), resolved);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
129
152
|
const layouts = await processLayoutsWithCache(layoutMap, ctx);
|
|
130
153
|
const renderedRoutes = await processRoutesWithCache(flat, ctx);
|
|
131
154
|
|
|
132
155
|
const output = {
|
|
133
156
|
layouts,
|
|
134
157
|
routes: renderedRoutes,
|
|
158
|
+
sourceFileMap,
|
|
135
159
|
warnings: buildWarnings,
|
|
136
160
|
cacheStats: ctx.stats,
|
|
137
161
|
};
|
|
@@ -19,18 +19,21 @@ function toLayoutId(path) {
|
|
|
19
19
|
: `_layout_${path.replace(/^\/|\/$/g, "").replace(/\//g, "-")}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Extract layout components and metadata from route tree
|
|
22
|
+
/** Extract layout components and metadata from route tree.
|
|
23
|
+
* When a node has both layout AND component, the loaders/mock belong to the
|
|
24
|
+
* page (component), not the layout — emit the layout with empty loaders. */
|
|
23
25
|
function extractLayouts(routes) {
|
|
24
26
|
const seen = new Map();
|
|
25
27
|
(function walk(defs, parentId) {
|
|
26
28
|
for (const def of defs) {
|
|
27
29
|
if (def.layout && def.children) {
|
|
28
|
-
const id = toLayoutId(def.path);
|
|
30
|
+
const id = def._layoutId || toLayoutId(def.path);
|
|
29
31
|
if (!seen.has(id)) {
|
|
32
|
+
const isPageRoute = !!def.component;
|
|
30
33
|
seen.set(id, {
|
|
31
34
|
component: def.layout,
|
|
32
|
-
loaders: def.loaders || {},
|
|
33
|
-
mock: def.mock || null,
|
|
35
|
+
loaders: isPageRoute ? {} : def.loaders || {},
|
|
36
|
+
mock: isPageRoute ? null : def.mock || null,
|
|
34
37
|
parentId: parentId || null,
|
|
35
38
|
});
|
|
36
39
|
}
|
|
@@ -80,7 +83,7 @@ function renderLayout(LayoutComponent, id, entry, manifest, i18nValue, ctx) {
|
|
|
80
83
|
|
|
81
84
|
const fieldWarnings = checkFieldAccess(accessed, schema, `layout:${id}`);
|
|
82
85
|
for (const w of fieldWarnings) {
|
|
83
|
-
const msg =
|
|
86
|
+
const msg = w;
|
|
84
87
|
if (!ctx.seenWarnings.has(msg)) {
|
|
85
88
|
ctx.seenWarnings.add(msg);
|
|
86
89
|
ctx.buildWarnings.push(msg);
|
|
@@ -90,15 +93,40 @@ function renderLayout(LayoutComponent, id, entry, manifest, i18nValue, ctx) {
|
|
|
90
93
|
return html;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
96
|
+
/** Join parent path prefix with a child path segment.
|
|
97
|
+
* Handles root ("/"), absolute child paths, and relative segments. */
|
|
98
|
+
function joinPaths(parent, child) {
|
|
99
|
+
if (child === "/") return parent || "/";
|
|
100
|
+
if (!parent || parent === "/") return child;
|
|
101
|
+
return parent + child;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Flatten routes, annotating each leaf with its parent layout id.
|
|
105
|
+
* Accumulates parent path segments so nested children get full paths
|
|
106
|
+
* (e.g. /blog + /:slug -> /blog/:slug). When a node has both layout
|
|
107
|
+
* and component, the component is emitted as a leaf route. */
|
|
108
|
+
function flattenRoutes(routes, currentLayout, parentPath) {
|
|
95
109
|
const leaves = [];
|
|
96
110
|
for (const route of routes) {
|
|
111
|
+
const fullPath = parentPath !== null ? joinPaths(parentPath, route.path) : route.path;
|
|
112
|
+
|
|
97
113
|
if (route.layout && route.children) {
|
|
98
|
-
|
|
114
|
+
const layoutId = route._layoutId || toLayoutId(route.path);
|
|
115
|
+
// Layout boundary with both component and layout: emit the page as a leaf
|
|
116
|
+
if (route.component) {
|
|
117
|
+
const leaf = { ...route, path: fullPath };
|
|
118
|
+
delete leaf.children;
|
|
119
|
+
delete leaf.layout;
|
|
120
|
+
leaf._layoutId = layoutId;
|
|
121
|
+
leaves.push(leaf);
|
|
122
|
+
}
|
|
123
|
+
leaves.push(...flattenRoutes(route.children, layoutId, fullPath));
|
|
99
124
|
} else if (route.children) {
|
|
100
|
-
|
|
125
|
+
// Container without layout: flatten children with accumulated path
|
|
126
|
+
leaves.push(...flattenRoutes(route.children, currentLayout, fullPath));
|
|
101
127
|
} else {
|
|
128
|
+
// Leaf route: assign full accumulated path
|
|
129
|
+
route.path = fullPath;
|
|
102
130
|
if (currentLayout) route._layoutId = currentLayout;
|
|
103
131
|
leaves.push(route);
|
|
104
132
|
}
|
|
@@ -144,7 +144,7 @@ function guardedRender(routePath, component, data, i18nValue, ctx) {
|
|
|
144
144
|
|
|
145
145
|
// After fatal check, only warnings remain — dedup per message
|
|
146
146
|
for (const v of violations) {
|
|
147
|
-
const msg =
|
|
147
|
+
const msg = `${routePath}\n ${v.reason}`;
|
|
148
148
|
if (!ctx.seenWarnings.has(msg)) {
|
|
149
149
|
ctx.seenWarnings.add(msg);
|
|
150
150
|
ctx.buildWarnings.push(msg);
|
|
@@ -98,7 +98,7 @@ function renderRoute(route, manifest, i18nValue, ctx) {
|
|
|
98
98
|
|
|
99
99
|
const fieldWarnings = checkFieldAccess(accessed, pageSchema, route.path);
|
|
100
100
|
for (const w of fieldWarnings) {
|
|
101
|
-
const msg =
|
|
101
|
+
const msg = w;
|
|
102
102
|
if (!ctx.seenWarnings.has(msg)) {
|
|
103
103
|
ctx.seenWarnings.add(msg);
|
|
104
104
|
ctx.buildWarnings.push(msg);
|