@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.
@@ -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
+ }
@@ -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
  }