@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 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: "route";
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
@@ -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,UAGT,QAAA;EACf,IAAA;EACA,SAAA,GAAY,aAAA,CAAc,MAAA;EAC1B,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;AAAA;;;iBClBc,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"}
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.11",
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.11",
21
+ "@canmi/seam-client": "0.4.18",
22
22
  "esbuild": "^0.27.3"
23
23
  },
24
24
  "devDependencies": {
25
- "@canmi/seam-engine": "0.4.11",
26
- "@canmi/seam-i18n": "0.4.11",
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 = `[seam] warning: ${w}`;
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
- /** Flatten routes, annotating each leaf with its parent layout id */
94
- function flattenRoutes(routes, currentLayout) {
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
- leaves.push(...flattenRoutes(route.children, toLayoutId(route.path)));
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
- leaves.push(...flattenRoutes(route.children, currentLayout));
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 = `[seam] warning: ${routePath}\n ${v.reason}`;
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 = `[seam] warning: ${w}`;
101
+ const msg = w;
102
102
  if (!ctx.seenWarnings.has(msg)) {
103
103
  ctx.seenWarnings.add(msg);
104
104
  ctx.buildWarnings.push(msg);