@decocms/start 1.5.0 → 1.6.0
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/package.json +2 -1
- package/src/admin/render.ts +16 -6
- package/src/apps/autoconfig.ts +106 -60
- package/src/apps/index.ts +24 -0
- package/src/cms/applySectionConventions.ts +6 -3
- package/src/cms/index.ts +2 -0
- package/src/cms/resolve.ts +10 -0
- package/src/cms/sectionLoaders.ts +1 -1
- package/src/hooks/PreviewProviders.tsx +4 -1
- package/src/sdk/setupApps.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/start",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"./sdk/createInvoke": "./src/sdk/createInvoke.ts",
|
|
49
49
|
"./sdk/router": "./src/sdk/router.ts",
|
|
50
50
|
"./matchers/posthog": "./src/matchers/posthog.ts",
|
|
51
|
+
"./apps": "./src/apps/index.ts",
|
|
51
52
|
"./apps/autoconfig": "./src/apps/autoconfig.ts",
|
|
52
53
|
"./sdk/setupApps": "./src/sdk/setupApps.ts",
|
|
53
54
|
"./matchers/builtins": "./src/matchers/builtins.ts",
|
package/src/admin/render.ts
CHANGED
|
@@ -15,6 +15,16 @@ import { getPreviewWrapper } from "./setup";
|
|
|
15
15
|
|
|
16
16
|
export { setRenderShell, setPreviewWrapper } from "./setup";
|
|
17
17
|
|
|
18
|
+
/** Escape user-controlled strings before interpolating into HTML. */
|
|
19
|
+
function escapeHtml(str: string): string {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/&/g, "&")
|
|
22
|
+
.replace(/</g, "<")
|
|
23
|
+
.replace(/>/g, ">")
|
|
24
|
+
.replace(/"/g, """)
|
|
25
|
+
.replace(/'/g, "'");
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
// Cache the dynamic import — avoids re-importing per section render
|
|
19
29
|
let _renderToString: ((element: any) => string) | null = null;
|
|
20
30
|
async function getRenderToString() {
|
|
@@ -36,7 +46,7 @@ function wrapInHtmlShell(sectionHtml: string): string {
|
|
|
36
46
|
async function renderResolvedSection(section: ResolvedSection): Promise<string> {
|
|
37
47
|
const sectionLoader = getSection(section.component);
|
|
38
48
|
if (!sectionLoader) {
|
|
39
|
-
return `<div style="padding:8px;color:orange;font-size:12px;border:1px dashed orange;margin:4px 0;">Unsupported: ${section.component}</div>`;
|
|
49
|
+
return `<div style="padding:8px;color:orange;font-size:12px;border:1px dashed orange;margin:4px 0;">Unsupported: ${escapeHtml(section.component)}</div>`;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
try {
|
|
@@ -47,7 +57,7 @@ async function renderResolvedSection(section: ResolvedSection): Promise<string>
|
|
|
47
57
|
const wrapped = Wrapper ? createElement(Wrapper, null, element) : element;
|
|
48
58
|
return renderToString(wrapped);
|
|
49
59
|
} catch (error) {
|
|
50
|
-
return `<div style="padding:8px;color:red;font-size:12px;">Error rendering ${section.component}: ${(error as Error).message}</div>`;
|
|
60
|
+
return `<div style="padding:8px;color:red;font-size:12px;">Error rendering ${escapeHtml(section.component)}: ${escapeHtml((error as Error).message)}</div>`;
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
63
|
|
|
@@ -62,7 +72,7 @@ async function renderOneSection(section: Record<string, unknown>): Promise<strin
|
|
|
62
72
|
|
|
63
73
|
const sectionLoader = getSection(resolveType);
|
|
64
74
|
if (!sectionLoader) {
|
|
65
|
-
return `<div style="padding:8px;color:orange;font-size:12px;border:1px dashed orange;margin:4px 0;">Unsupported: ${resolveType}</div>`;
|
|
75
|
+
return `<div style="padding:8px;color:orange;font-size:12px;border:1px dashed orange;margin:4px 0;">Unsupported: ${escapeHtml(resolveType)}</div>`;
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
try {
|
|
@@ -74,7 +84,7 @@ async function renderOneSection(section: Record<string, unknown>): Promise<strin
|
|
|
74
84
|
const wrapped = Wrapper ? createElement(Wrapper, null, element) : element;
|
|
75
85
|
return renderToString(wrapped);
|
|
76
86
|
} catch (error) {
|
|
77
|
-
return `<div style="padding:8px;color:red;font-size:12px;">Error rendering ${resolveType}: ${(error as Error).message}</div>`;
|
|
87
|
+
return `<div style="padding:8px;color:red;font-size:12px;">Error rendering ${escapeHtml(resolveType)}: ${escapeHtml((error as Error).message)}</div>`;
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -234,7 +244,7 @@ export async function handleRender(request: Request): Promise<Response> {
|
|
|
234
244
|
const sectionLoader = getSection(component);
|
|
235
245
|
if (!sectionLoader) {
|
|
236
246
|
const unknownHtml = wrapInHtmlShell(
|
|
237
|
-
`<div style="padding:20px;color:red;">Unknown section: ${component}</div>`,
|
|
247
|
+
`<div style="padding:20px;color:red;">Unknown section: ${escapeHtml(component)}</div>`,
|
|
238
248
|
);
|
|
239
249
|
return new Response(unknownHtml, {
|
|
240
250
|
status: 200,
|
|
@@ -257,7 +267,7 @@ export async function handleRender(request: Request): Promise<Response> {
|
|
|
257
267
|
});
|
|
258
268
|
} catch (error) {
|
|
259
269
|
const errorHtml = wrapInHtmlShell(
|
|
260
|
-
`<div style="padding:20px;color:red;">Render error: ${(error as Error).message}</div>`,
|
|
270
|
+
`<div style="padding:20px;color:red;">Render error: ${escapeHtml((error as Error).message)}</div>`,
|
|
261
271
|
);
|
|
262
272
|
return new Response(errorHtml, {
|
|
263
273
|
status: 200,
|
package/src/apps/autoconfig.ts
CHANGED
|
@@ -1,89 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Registry-driven auto-configuration of known apps from CMS blocks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A site passes an `AppRegistry` (declarative list of known app blockKeys +
|
|
5
|
+
* lazy module imports). For every registry entry whose `blockKey` exists in
|
|
6
|
+
* the decofile, this calls `mod.configure(block, resolveSecret)` and then
|
|
7
|
+
* hands the resulting AppDefinitions off to `setupApps()`.
|
|
8
|
+
*
|
|
9
|
+
* The canonical registry lives in `@decocms/apps/registry` so the framework
|
|
10
|
+
* itself has zero knowledge of which apps exist.
|
|
7
11
|
*
|
|
8
12
|
* Usage in setup.ts:
|
|
9
13
|
* import { autoconfigApps } from "@decocms/start/apps/autoconfig";
|
|
10
|
-
*
|
|
11
|
-
* await autoconfigApps(generatedBlocks);
|
|
14
|
+
* import { APP_REGISTRY } from "@decocms/apps/registry";
|
|
15
|
+
* await autoconfigApps(generatedBlocks, APP_REGISTRY);
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { onChange } from "../cms/loader";
|
|
15
19
|
import { resolveSecret } from "../sdk/crypto";
|
|
16
20
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
setupApps,
|
|
22
|
+
type AppDefinition,
|
|
23
|
+
type AppDefinitionWithHandlers,
|
|
20
24
|
} from "../sdk/setupApps";
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Shape of the secret resolver passed to each app's `configure()`. Matches
|
|
28
|
+
* `resolveSecret` in `src/sdk/crypto.ts`. Apps typically narrow the return
|
|
29
|
+
* type inside their own `configure()` by throwing on null.
|
|
30
|
+
*/
|
|
31
|
+
export type ResolveSecret = (
|
|
32
|
+
value: unknown,
|
|
33
|
+
envVarName?: string,
|
|
34
|
+
) => Promise<string | null>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One entry in the app registry — describes a single installable app.
|
|
38
|
+
* Sites pass an array of these to `autoconfigApps()` (typically imported
|
|
39
|
+
* from `@decocms/apps/registry`).
|
|
40
|
+
*/
|
|
41
|
+
export interface AppRegistryEntry {
|
|
42
|
+
/** Block key in the decofile, e.g. "deco-shopify". */
|
|
43
|
+
blockKey: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lazy import of the app's mod module. Must return an object exposing
|
|
47
|
+
* `configure(block, resolveSecret)` and optionally `handlers`.
|
|
48
|
+
*
|
|
49
|
+
* Use a string-literal dynamic import so bundlers (Vite/Rollup) can
|
|
50
|
+
* statically trace the chunk. E.g.
|
|
51
|
+
* () => import("@decocms/apps/shopify/mod")
|
|
52
|
+
*/
|
|
53
|
+
module: () => Promise<{
|
|
54
|
+
configure: (
|
|
55
|
+
block: unknown,
|
|
56
|
+
resolveSecret: ResolveSecret,
|
|
57
|
+
) => Promise<AppDefinition | null>;
|
|
58
|
+
handlers?: Record<string, (props: any, req: Request) => Promise<any>>;
|
|
59
|
+
}>;
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
/** Human-readable name shown in admin install UI. */
|
|
62
|
+
displayName?: string;
|
|
63
|
+
/** Icon URL (absolute or site-relative) shown in admin install UI. */
|
|
64
|
+
icon?: string;
|
|
65
|
+
/** Grouping label, e.g. "commerce", "email", "analytics". */
|
|
66
|
+
category?: string;
|
|
67
|
+
/** Short summary (one sentence) shown in admin install UI. */
|
|
68
|
+
description?: string;
|
|
69
|
+
}
|
|
31
70
|
|
|
32
|
-
|
|
33
|
-
// Main
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
71
|
+
export type AppRegistry = readonly AppRegistryEntry[];
|
|
35
72
|
|
|
36
73
|
async function configureAllApps(
|
|
37
|
-
|
|
74
|
+
blocks: Record<string, unknown>,
|
|
75
|
+
registry: AppRegistry,
|
|
38
76
|
): Promise<AppDefinitionWithHandlers[]> {
|
|
39
|
-
|
|
77
|
+
const apps: AppDefinitionWithHandlers[] = [];
|
|
40
78
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
for (const entry of registry) {
|
|
80
|
+
const block = blocks[entry.blockKey];
|
|
81
|
+
if (!block) continue;
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
83
|
+
try {
|
|
84
|
+
const mod = await entry.module();
|
|
85
|
+
if (typeof mod?.configure !== "function") continue;
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
const appDef: AppDefinition | null = await mod.configure(
|
|
88
|
+
block,
|
|
89
|
+
resolveSecret,
|
|
90
|
+
);
|
|
91
|
+
if (!appDef) continue;
|
|
54
92
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
93
|
+
const withHandlers: AppDefinitionWithHandlers = {
|
|
94
|
+
...appDef,
|
|
95
|
+
handlers: mod.handlers,
|
|
96
|
+
};
|
|
97
|
+
apps.push(withHandlers);
|
|
98
|
+
} catch {
|
|
99
|
+
// App module missing, configure threw, or block was malformed — skip.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
65
102
|
|
|
66
|
-
|
|
103
|
+
return apps;
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
/**
|
|
70
|
-
* Auto-configure apps from CMS blocks.
|
|
71
|
-
*
|
|
107
|
+
* Auto-configure apps from CMS blocks against a declarative registry.
|
|
108
|
+
*
|
|
109
|
+
* Call once in setup.ts after setBlocks(). Re-runs on admin hot-reload.
|
|
110
|
+
*
|
|
111
|
+
* @param blocks Decofile blocks (from blocks.gen or loadBlocks()).
|
|
112
|
+
* @param registry List of installable apps — typically
|
|
113
|
+
* `import { APP_REGISTRY } from "@decocms/apps/registry"`.
|
|
72
114
|
*/
|
|
73
|
-
export async function autoconfigApps(
|
|
74
|
-
|
|
115
|
+
export async function autoconfigApps(
|
|
116
|
+
blocks: Record<string, unknown>,
|
|
117
|
+
registry: AppRegistry,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
if (typeof document !== "undefined") return; // server-only
|
|
120
|
+
if (!registry || registry.length === 0) return;
|
|
75
121
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
const apps = await configureAllApps(blocks, registry);
|
|
123
|
+
if (apps.length > 0) {
|
|
124
|
+
await setupApps(apps);
|
|
125
|
+
}
|
|
80
126
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
// Re-configure on admin hot-reload
|
|
128
|
+
onChange(async (newBlocks) => {
|
|
129
|
+
if (typeof document !== "undefined") return;
|
|
130
|
+
const updatedApps = await configureAllApps(newBlocks, registry);
|
|
131
|
+
if (updatedApps.length > 0) {
|
|
132
|
+
await setupApps(updatedApps);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
89
135
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for app-install primitives.
|
|
3
|
+
*
|
|
4
|
+
* Sites import from `@decocms/start/apps`:
|
|
5
|
+
* - `autoconfigApps(blocks, registry)` — main bootstrap call
|
|
6
|
+
* - `AppRegistry`, `AppRegistryEntry` — registry types
|
|
7
|
+
* - `setupApps`, `AppDefinition` — lower-level primitives when composing custom registries
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
autoconfigApps,
|
|
12
|
+
type AppRegistry,
|
|
13
|
+
type AppRegistryEntry,
|
|
14
|
+
} from "./autoconfig";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
setupApps,
|
|
18
|
+
registerAppMiddleware,
|
|
19
|
+
getAppMiddleware,
|
|
20
|
+
type AppDefinition,
|
|
21
|
+
type AppDefinitionWithHandlers,
|
|
22
|
+
type AppManifest,
|
|
23
|
+
type AppMiddleware,
|
|
24
|
+
} from "../sdk/setupApps";
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
registerSectionsSync,
|
|
12
12
|
} from "./registry";
|
|
13
13
|
import {
|
|
14
|
+
type CacheableSectionInput,
|
|
14
15
|
registerCacheableSections,
|
|
15
16
|
registerLayoutSections,
|
|
16
17
|
} from "./sectionLoaders";
|
|
17
18
|
import {
|
|
19
|
+
type AsyncRenderingConfig,
|
|
18
20
|
registerEagerSections,
|
|
19
21
|
registerSeoSections,
|
|
20
22
|
setAsyncRenderingConfig,
|
|
@@ -48,13 +50,13 @@ export function applySectionConventions(input: ApplySectionConventionsInput): vo
|
|
|
48
50
|
const eagerSections: string[] = [];
|
|
49
51
|
const layoutSections: string[] = [];
|
|
50
52
|
const seoSections: string[] = [];
|
|
51
|
-
const cacheableSections: Record<string,
|
|
53
|
+
const cacheableSections: Record<string, CacheableSectionInput> = {};
|
|
52
54
|
|
|
53
55
|
for (const [key, entry] of Object.entries(meta)) {
|
|
54
56
|
if (entry.eager) eagerSections.push(key);
|
|
55
57
|
if (entry.layout) layoutSections.push(key);
|
|
56
58
|
if (entry.seo) seoSections.push(key);
|
|
57
|
-
if (entry.cache) cacheableSections[key] = entry.cache;
|
|
59
|
+
if (entry.cache) cacheableSections[key] = entry.cache as CacheableSectionInput;
|
|
58
60
|
|
|
59
61
|
if (entry.clientOnly && sectionGlob) {
|
|
60
62
|
const globKey = sectionGlobKey(key, sectionGlob);
|
|
@@ -81,7 +83,8 @@ export function applySectionConventions(input: ApplySectionConventionsInput): vo
|
|
|
81
83
|
// Permanent registry — survives subsequent setAsyncRenderingConfig() calls
|
|
82
84
|
registerEagerSections(eagerSections);
|
|
83
85
|
// Also add to alwaysEager for backward compat with code that reads the config
|
|
84
|
-
const existing =
|
|
86
|
+
const existing: Partial<AsyncRenderingConfig> =
|
|
87
|
+
getAsyncRenderingConfig() ?? {};
|
|
85
88
|
setAsyncRenderingConfig({
|
|
86
89
|
...existing,
|
|
87
90
|
alwaysEager: [...(existing.alwaysEager ?? []), ...eagerSections],
|
package/src/cms/index.ts
CHANGED
package/src/cms/resolve.ts
CHANGED
|
@@ -298,6 +298,16 @@ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>)
|
|
|
298
298
|
Object.assign(commerceLoaders, loaders);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** Delete a single commerce loader by key. No-op if key is absent. */
|
|
302
|
+
export function unregisterCommerceLoader(key: string): void {
|
|
303
|
+
delete commerceLoaders[key];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Clear all commerce loaders. Use with care — wipes site-registered entries too. */
|
|
307
|
+
export function clearCommerceLoaders(): void {
|
|
308
|
+
for (const key of Object.keys(commerceLoaders)) delete commerceLoaders[key];
|
|
309
|
+
}
|
|
310
|
+
|
|
301
311
|
// ---------------------------------------------------------------------------
|
|
302
312
|
// Custom matchers
|
|
303
313
|
// ---------------------------------------------------------------------------
|
|
@@ -34,7 +34,7 @@ interface CacheableSectionConfig {
|
|
|
34
34
|
maxAge: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
type CacheableSectionInput = CacheableSectionConfig | import("../sdk/cacheHeaders").CacheProfileName;
|
|
37
|
+
export type CacheableSectionInput = CacheableSectionConfig | import("../sdk/cacheHeaders").CacheProfileName;
|
|
38
38
|
|
|
39
39
|
function resolveSectionCacheConfig(input: CacheableSectionInput): CacheableSectionConfig {
|
|
40
40
|
if (typeof input === "string") {
|
|
@@ -9,7 +9,10 @@ import type { ReactNode } from "react";
|
|
|
9
9
|
const rootRoute = createRootRoute();
|
|
10
10
|
|
|
11
11
|
const previewRouter = createRouter({
|
|
12
|
-
|
|
12
|
+
// TanStack Router's RootRoute/Route generic inference rejects bare rootRoute
|
|
13
|
+
// at the type level (works at runtime). `as any` avoids leaking the mismatch
|
|
14
|
+
// into consumer sites that typecheck framework source via npm link.
|
|
15
|
+
routeTree: rootRoute as any,
|
|
13
16
|
history: createMemoryHistory({ initialEntries: ["/"] }),
|
|
14
17
|
});
|
|
15
18
|
|
package/src/sdk/setupApps.ts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
|
|
22
22
|
import { clearInvokeHandlers, registerInvokeHandlers } from "../admin/invoke";
|
|
23
23
|
import { registerSections } from "../cms/registry";
|
|
24
|
+
import {
|
|
25
|
+
registerCommerceLoaders,
|
|
26
|
+
unregisterCommerceLoader,
|
|
27
|
+
} from "../cms/resolve";
|
|
24
28
|
import { RequestContext } from "./requestContext";
|
|
25
29
|
|
|
26
30
|
// ---------------------------------------------------------------------------
|
|
@@ -68,6 +72,17 @@ const appMiddlewares: Array<{
|
|
|
68
72
|
middleware: AppMiddleware;
|
|
69
73
|
}> = [];
|
|
70
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Keys this module wrote into the commerce-loaders map. Tracked so hot-reload
|
|
77
|
+
* can wipe app-owned entries without touching site-local registrations.
|
|
78
|
+
*/
|
|
79
|
+
const appCommerceLoaderKeys = new Set<string>();
|
|
80
|
+
|
|
81
|
+
function clearAppCommerceLoaders() {
|
|
82
|
+
for (const key of appCommerceLoaderKeys) unregisterCommerceLoader(key);
|
|
83
|
+
appCommerceLoaderKeys.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
function registerAppState(name: string, state: unknown) {
|
|
72
87
|
appStates.push({ name, state });
|
|
73
88
|
}
|
|
@@ -143,6 +158,26 @@ function flattenDependencies(apps: AppDefinition[]): AppDefinition[] {
|
|
|
143
158
|
return result;
|
|
144
159
|
}
|
|
145
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Register handlers into the commerce-loaders map used by the CMS resolve path
|
|
163
|
+
* (src/cms/resolve.ts). Tracks the keys so clearAppCommerceLoaders() can revert
|
|
164
|
+
* them on hot-reload without clobbering site-registered loaders.
|
|
165
|
+
*
|
|
166
|
+
* CommerceLoader receives `(props)` only. The admin invoke path passes
|
|
167
|
+
* `(props, req)`; here `req` is undefined. Handlers that need `req` should
|
|
168
|
+
* rely on RequestContext instead of a positional argument.
|
|
169
|
+
*/
|
|
170
|
+
function registerAppCommerceHandlers(
|
|
171
|
+
handlers: Record<string, (props: any, req: Request) => Promise<any>>,
|
|
172
|
+
) {
|
|
173
|
+
const entries: Record<string, (props: any) => Promise<any>> = {};
|
|
174
|
+
for (const [key, handler] of Object.entries(handlers)) {
|
|
175
|
+
entries[key] = (props: any) => handler(props, undefined as unknown as Request);
|
|
176
|
+
appCommerceLoaderKeys.add(key);
|
|
177
|
+
}
|
|
178
|
+
registerCommerceLoaders(entries);
|
|
179
|
+
}
|
|
180
|
+
|
|
146
181
|
// ---------------------------------------------------------------------------
|
|
147
182
|
// Main pipeline
|
|
148
183
|
// ---------------------------------------------------------------------------
|
|
@@ -161,13 +196,17 @@ export async function setupApps(
|
|
|
161
196
|
// Clear previous registrations (safe for hot-reload via onChange)
|
|
162
197
|
clearRegistrations();
|
|
163
198
|
clearInvokeHandlers();
|
|
199
|
+
clearAppCommerceLoaders();
|
|
164
200
|
|
|
165
201
|
for (const app of flattenDependencies(apps as AppDefinition[])) {
|
|
166
202
|
const appWithHandlers = app as AppDefinitionWithHandlers;
|
|
167
203
|
|
|
168
204
|
// 1. Register explicit handlers (pre-unwrapped by the app, e.g. resend)
|
|
205
|
+
// These also go into the commerce-loaders map so the CMS resolve path
|
|
206
|
+
// (src/cms/resolve.ts) can dispatch to them by __resolveType.
|
|
169
207
|
if (appWithHandlers.handlers) {
|
|
170
208
|
registerInvokeHandlers(appWithHandlers.handlers);
|
|
209
|
+
registerAppCommerceHandlers(appWithHandlers.handlers);
|
|
171
210
|
}
|
|
172
211
|
|
|
173
212
|
// 2. Flatten manifest modules → individual invoke handlers
|
|
@@ -189,6 +228,10 @@ export async function setupApps(
|
|
|
189
228
|
[key]: handler,
|
|
190
229
|
[`${key}.ts`]: handler,
|
|
191
230
|
});
|
|
231
|
+
registerAppCommerceHandlers({
|
|
232
|
+
[key]: handler,
|
|
233
|
+
[`${key}.ts`]: handler,
|
|
234
|
+
});
|
|
192
235
|
}
|
|
193
236
|
}
|
|
194
237
|
}
|