@decocms/start 0.37.3 → 0.39.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/scripts/analyze-traces.mjs +1117 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/invoke.ts +53 -5
- package/src/admin/setup.ts +7 -1
- package/src/apps/autoconfig.ts +50 -72
- package/src/sdk/invoke.ts +123 -12
- package/src/sdk/requestContext.ts +42 -0
- package/src/sdk/setupApps.ts +211 -0
- package/src/sdk/workerEntry.ts +6 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App system integration pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Consumes AppDefinition objects from @decocms/apps and automates:
|
|
5
|
+
* 1. Invoke handler registration (from manifest + explicit handlers)
|
|
6
|
+
* 2. Section registration (when manifest.sections is available)
|
|
7
|
+
* 3. App middleware registration (with state injection into RequestContext)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { setupApps } from "@decocms/start/sdk/setupApps";
|
|
12
|
+
* import * as vtexApp from "@decocms/apps/vtex/mod";
|
|
13
|
+
* import * as resendApp from "@decocms/apps/resend/mod";
|
|
14
|
+
*
|
|
15
|
+
* const vtex = await vtexApp.configure(blocks["deco-vtex"], resolveSecret);
|
|
16
|
+
* const resend = await resendApp.configure(blocks["deco-resend"], resolveSecret);
|
|
17
|
+
*
|
|
18
|
+
* await setupApps([vtex, resend].filter(Boolean));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { clearInvokeHandlers, registerInvokeHandlers } from "../admin/invoke";
|
|
23
|
+
import { registerSections } from "../cms/registry";
|
|
24
|
+
import { RequestContext } from "./requestContext";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types — mirrors @decocms/apps/commerce/app-types without importing it
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface AppManifest {
|
|
31
|
+
name: string;
|
|
32
|
+
loaders: Record<string, Record<string, unknown>>;
|
|
33
|
+
actions: Record<string, Record<string, unknown>>;
|
|
34
|
+
sections?: Record<string, () => Promise<any>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AppMiddleware {
|
|
38
|
+
(request: Request, next: () => Promise<Response>): Promise<Response>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AppDefinition<TState = unknown> {
|
|
42
|
+
name: string;
|
|
43
|
+
manifest: AppManifest;
|
|
44
|
+
state: TState;
|
|
45
|
+
middleware?: AppMiddleware;
|
|
46
|
+
dependencies?: AppDefinition[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extended definition with optional explicit handlers.
|
|
51
|
+
* autoconfigApps() attaches mod.handlers here before calling setupApps().
|
|
52
|
+
*/
|
|
53
|
+
export interface AppDefinitionWithHandlers<TState = unknown>
|
|
54
|
+
extends AppDefinition<TState> {
|
|
55
|
+
/** Pre-wrapped handlers from the app's mod.ts (e.g. unwrapped VTEX actions). */
|
|
56
|
+
handlers?: Record<string, (props: any, request: Request) => Promise<any>>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// App middleware registry
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/** Per-app state entries — injected into RequestContext.bag on every request. */
|
|
64
|
+
const appStates: Array<{ name: string; state: unknown }> = [];
|
|
65
|
+
|
|
66
|
+
const appMiddlewares: Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
middleware: AppMiddleware;
|
|
69
|
+
}> = [];
|
|
70
|
+
|
|
71
|
+
function registerAppState(name: string, state: unknown) {
|
|
72
|
+
appStates.push({ name, state });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function registerAppMiddleware(
|
|
76
|
+
name: string,
|
|
77
|
+
mw: AppMiddleware,
|
|
78
|
+
) {
|
|
79
|
+
appMiddlewares.push({ name, middleware: mw });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear all registrations. Called before re-running setupApps()
|
|
84
|
+
* on admin hot-reload to prevent duplicate middleware/state entries.
|
|
85
|
+
*/
|
|
86
|
+
function clearRegistrations() {
|
|
87
|
+
appStates.length = 0;
|
|
88
|
+
appMiddlewares.length = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns a chained middleware that runs all registered app middlewares.
|
|
93
|
+
* The site wires this into its own createMiddleware() chain.
|
|
94
|
+
*
|
|
95
|
+
* Before running app middlewares, all app states are injected into
|
|
96
|
+
* RequestContext.bag so loaders can access them via getAppState().
|
|
97
|
+
*
|
|
98
|
+
* Returns undefined if no app states or middlewares were registered.
|
|
99
|
+
*/
|
|
100
|
+
export function getAppMiddleware(): AppMiddleware | undefined {
|
|
101
|
+
if (appStates.length === 0 && appMiddlewares.length === 0) return undefined;
|
|
102
|
+
|
|
103
|
+
return async (request, next) => {
|
|
104
|
+
// Inject all app states into RequestContext bag
|
|
105
|
+
for (const { name, state } of appStates) {
|
|
106
|
+
RequestContext.setBag(`app:${name}:state`, state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Chain app middlewares (first registered runs outermost)
|
|
110
|
+
if (appMiddlewares.length === 0) return next();
|
|
111
|
+
const run = async (i: number): Promise<Response> => {
|
|
112
|
+
if (i >= appMiddlewares.length) return next();
|
|
113
|
+
return appMiddlewares[i].middleware(request, () => run(i + 1));
|
|
114
|
+
};
|
|
115
|
+
return run(0);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Dependency flattening
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Topological sort: dependencies before parents.
|
|
125
|
+
* Combined with first-wins registration in registerInvokeHandlers,
|
|
126
|
+
* this means parent apps can override handlers from their dependencies
|
|
127
|
+
* by providing explicit `handlers` (registered before manifest flatten).
|
|
128
|
+
*/
|
|
129
|
+
function flattenDependencies(apps: AppDefinition[]): AppDefinition[] {
|
|
130
|
+
const seen = new Set<string>();
|
|
131
|
+
const result: AppDefinition[] = [];
|
|
132
|
+
|
|
133
|
+
function visit(app: AppDefinition) {
|
|
134
|
+
if (seen.has(app.name)) return;
|
|
135
|
+
seen.add(app.name);
|
|
136
|
+
if (app.dependencies) {
|
|
137
|
+
for (const dep of app.dependencies) visit(dep);
|
|
138
|
+
}
|
|
139
|
+
result.push(app);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const app of apps) visit(app);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Main pipeline
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Initialize apps from their AppDefinitions.
|
|
152
|
+
*
|
|
153
|
+
* Call once in setup.ts after configuring apps via their mod.configure().
|
|
154
|
+
* Handles: invoke handler registration, section registration, middleware setup.
|
|
155
|
+
*/
|
|
156
|
+
export async function setupApps(
|
|
157
|
+
apps: Array<AppDefinitionWithHandlers | AppDefinition>,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
if (typeof document !== "undefined") return; // server-only
|
|
160
|
+
|
|
161
|
+
// Clear previous registrations (safe for hot-reload via onChange)
|
|
162
|
+
clearRegistrations();
|
|
163
|
+
clearInvokeHandlers();
|
|
164
|
+
|
|
165
|
+
for (const app of flattenDependencies(apps as AppDefinition[])) {
|
|
166
|
+
const appWithHandlers = app as AppDefinitionWithHandlers;
|
|
167
|
+
|
|
168
|
+
// 1. Register explicit handlers (pre-unwrapped by the app, e.g. resend)
|
|
169
|
+
if (appWithHandlers.handlers) {
|
|
170
|
+
registerInvokeHandlers(appWithHandlers.handlers);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Flatten manifest modules → individual invoke handlers
|
|
174
|
+
// manifest.actions["vtex/actions/checkout"] = { getOrCreateCart, addItemsToCart, ... }
|
|
175
|
+
// → register "vtex/actions/checkout/getOrCreateCart" as handler
|
|
176
|
+
for (const category of ["loaders", "actions"] as const) {
|
|
177
|
+
const modules = app.manifest[category];
|
|
178
|
+
if (!modules) continue;
|
|
179
|
+
|
|
180
|
+
for (const [moduleKey, moduleExports] of Object.entries(modules)) {
|
|
181
|
+
for (const [fnName, fn] of Object.entries(
|
|
182
|
+
moduleExports as Record<string, unknown>,
|
|
183
|
+
)) {
|
|
184
|
+
if (typeof fn !== "function") continue;
|
|
185
|
+
const key = `${moduleKey}/${fnName}`;
|
|
186
|
+
const handler = (props: any, req: Request) =>
|
|
187
|
+
(fn as Function)(props, req);
|
|
188
|
+
registerInvokeHandlers({
|
|
189
|
+
[key]: handler,
|
|
190
|
+
[`${key}.ts`]: handler,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 3. Register sections from manifest (future — when apps export sections)
|
|
197
|
+
if (app.manifest.sections) {
|
|
198
|
+
registerSections(
|
|
199
|
+
app.manifest.sections as Record<string, () => Promise<any>>,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 4. Always register app state (so getAppState() works for all apps)
|
|
204
|
+
registerAppState(app.name, app.state);
|
|
205
|
+
|
|
206
|
+
// 5. Register middleware (optional — not all apps have middleware)
|
|
207
|
+
if (app.middleware) {
|
|
208
|
+
registerAppMiddleware(app.name, app.middleware);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { buildHtmlShell } from "./htmlShell";
|
|
|
36
36
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
37
37
|
import { isMobileUA } from "./useDevice";
|
|
38
38
|
import { getRenderShellConfig } from "../admin/setup";
|
|
39
|
+
import { RequestContext } from "./requestContext";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Append Link preload headers for CSS and fonts so the browser starts
|
|
@@ -653,6 +654,10 @@ export function createDecoWorkerEntry(
|
|
|
653
654
|
env: Record<string, unknown>,
|
|
654
655
|
ctx: WorkerExecutionContext,
|
|
655
656
|
): Promise<Response> {
|
|
657
|
+
// Wrap the entire request in a RequestContext so that all code
|
|
658
|
+
// in the call stack (loaders, invoke handlers, vtexFetchWithCookies)
|
|
659
|
+
// can access the request and write response headers.
|
|
660
|
+
return RequestContext.run(request, async () => {
|
|
656
661
|
const url = new URL(request.url);
|
|
657
662
|
|
|
658
663
|
// Admin routes (/_meta, /.decofile, /live/previews) — always handled first
|
|
@@ -879,6 +884,7 @@ export function createDecoWorkerEntry(
|
|
|
879
884
|
// the stream in Workers runtime, causing Error 1101.
|
|
880
885
|
storeInCache(origin);
|
|
881
886
|
return dressResponse(origin, "MISS");
|
|
887
|
+
}); // end RequestContext.run()
|
|
882
888
|
},
|
|
883
889
|
};
|
|
884
890
|
}
|