@ollie-shop/cli 1.4.1 → 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/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +12 -0
- package/dist/index.js +523 -16
- package/package.json +1 -1
- package/src/commands/help.tsx +15 -0
- package/src/commands/setup-cmd.ts +142 -0
- package/src/commands/start.tsx +68 -1
- package/src/index.tsx +2 -0
- package/src/utils/config.ts +76 -0
- package/src/utils/esbuild.ts +376 -20
package/src/utils/esbuild.ts
CHANGED
|
@@ -5,14 +5,24 @@ import path from "node:path";
|
|
|
5
5
|
import * as esbuild from "esbuild";
|
|
6
6
|
import { glob } from "glob";
|
|
7
7
|
import { createComponentBundle } from "./bundle.js";
|
|
8
|
+
import {
|
|
9
|
+
type OllieConfig,
|
|
10
|
+
listStages,
|
|
11
|
+
loadConfig,
|
|
12
|
+
upsertComponentEntry,
|
|
13
|
+
} from "./config.js";
|
|
8
14
|
|
|
9
15
|
export interface ComponentInfo {
|
|
10
|
-
/** Component ID from
|
|
16
|
+
/** Component ID from the ollie config (UUID) or generated temporary ID (studio-*) */
|
|
11
17
|
id: string;
|
|
12
18
|
/** Folder name */
|
|
13
19
|
name: string;
|
|
14
|
-
/** Target slot
|
|
20
|
+
/** Target slot (from the ollie config) */
|
|
15
21
|
slot?: string;
|
|
22
|
+
/** True when the folder belongs to another stage's config, not the active one */
|
|
23
|
+
otherStage?: boolean;
|
|
24
|
+
/** True when the folder isn't linked to a component id (synthetic studio-* id) */
|
|
25
|
+
unlinked?: boolean;
|
|
16
26
|
entryPoint: string;
|
|
17
27
|
outfile: string;
|
|
18
28
|
}
|
|
@@ -33,7 +43,7 @@ interface ComponentMeta {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
/**
|
|
36
|
-
* Reads the meta.json file for a component.
|
|
46
|
+
* Reads the meta.json file for a component (legacy, per-folder source).
|
|
37
47
|
* Supports stage-specific meta files: meta.{stage}.json
|
|
38
48
|
*/
|
|
39
49
|
async function readComponentMeta(
|
|
@@ -66,35 +76,118 @@ export interface DiscoverComponentsOptions {
|
|
|
66
76
|
stage?: string;
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
/** Maps resolved component dirs -> {id, slot} for a stage's config map. */
|
|
80
|
+
async function componentsByDir(
|
|
81
|
+
cwd: string,
|
|
82
|
+
stageLabel: string,
|
|
83
|
+
): Promise<Map<string, { id: string; slot?: string }>> {
|
|
84
|
+
const byDir = new Map<string, { id: string; slot?: string }>();
|
|
85
|
+
let config: OllieConfig | null = null;
|
|
86
|
+
try {
|
|
87
|
+
config = await loadConfig({ cwd, stage: stageLabel });
|
|
88
|
+
} catch {
|
|
89
|
+
return byDir;
|
|
90
|
+
}
|
|
91
|
+
for (const [id, entry] of Object.entries(config?.components ?? {})) {
|
|
92
|
+
const dir = path.resolve(cwd, entry.path);
|
|
93
|
+
if (!byDir.has(dir)) byDir.set(dir, { id, slot: entry.slot });
|
|
94
|
+
}
|
|
95
|
+
return byDir;
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
/**
|
|
70
|
-
* Discovers components
|
|
71
|
-
*
|
|
99
|
+
* Discovers components, config-first:
|
|
100
|
+
* 1. The active stage's `components` map — the source of truth; entries may
|
|
101
|
+
* point inside or outside components/.
|
|
102
|
+
* 2. Folders under components/ that belong to ANOTHER stage's config are
|
|
103
|
+
* surfaced with their real id and flagged `otherStage` (hidden by default
|
|
104
|
+
* in Studio, shown via "show all").
|
|
105
|
+
* 3. Remaining folders fall back to legacy per-folder meta.json; folders with
|
|
106
|
+
* neither get a temporary studio-* id (unlinked).
|
|
72
107
|
*/
|
|
73
108
|
export async function discoverComponents(
|
|
74
109
|
options: DiscoverComponentsOptions = {},
|
|
75
110
|
): Promise<ComponentInfo[]> {
|
|
76
111
|
const { cwd = process.cwd(), stage } = options;
|
|
77
|
-
const
|
|
112
|
+
const activeStage = stage ?? "prod";
|
|
113
|
+
const buildDir = path.join(cwd, "node_modules/.ollie", "build");
|
|
114
|
+
const components: ComponentInfo[] = [];
|
|
115
|
+
const mappedDirs = new Set<string>();
|
|
116
|
+
|
|
117
|
+
const activeByDir = await componentsByDir(cwd, activeStage);
|
|
78
118
|
|
|
119
|
+
// Component dirs claimed by other stages (so we don't mistake them for new)
|
|
120
|
+
const otherStageByDir = new Map<string, { id: string; slot?: string }>();
|
|
121
|
+
try {
|
|
122
|
+
for (const { stage: label } of await listStages(cwd)) {
|
|
123
|
+
if (label === activeStage) continue;
|
|
124
|
+
for (const [dir, entry] of await componentsByDir(cwd, label)) {
|
|
125
|
+
if (!activeByDir.has(dir) && !otherStageByDir.has(dir)) {
|
|
126
|
+
otherStageByDir.set(dir, entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// No other stages
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. Active stage's config map (source of truth)
|
|
135
|
+
for (const [componentDir, entry] of activeByDir) {
|
|
136
|
+
const entryPoint = path.join(componentDir, "index.tsx");
|
|
137
|
+
try {
|
|
138
|
+
await fs.access(entryPoint);
|
|
139
|
+
} catch {
|
|
140
|
+
console.warn(
|
|
141
|
+
`${entry.id}: ${componentDir}/index.tsx not found - skipping`,
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const name = path.basename(componentDir);
|
|
146
|
+
mappedDirs.add(componentDir);
|
|
147
|
+
components.push({
|
|
148
|
+
id: entry.id,
|
|
149
|
+
name,
|
|
150
|
+
slot: entry.slot,
|
|
151
|
+
entryPoint,
|
|
152
|
+
outfile: path.join(buildDir, name, "index.js"),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Folders under components/ not in the active map
|
|
157
|
+
const componentsDir = path.join(cwd, "components");
|
|
79
158
|
try {
|
|
80
159
|
await fs.access(componentsDir);
|
|
81
160
|
} catch {
|
|
82
|
-
return
|
|
161
|
+
return components;
|
|
83
162
|
}
|
|
84
163
|
|
|
85
164
|
const entries = await glob("*/index.tsx", { cwd: componentsDir });
|
|
86
|
-
const components: ComponentInfo[] = [];
|
|
87
|
-
|
|
88
165
|
for (const entry of entries) {
|
|
89
166
|
const name = path.dirname(entry);
|
|
90
|
-
const componentDir = path.
|
|
91
|
-
|
|
167
|
+
const componentDir = path.resolve(componentsDir, name);
|
|
168
|
+
if (mappedDirs.has(componentDir)) continue;
|
|
169
|
+
|
|
170
|
+
const entryPoint = path.join(componentsDir, entry);
|
|
171
|
+
const outfile = path.join(buildDir, name, "index.js");
|
|
172
|
+
|
|
173
|
+
// Belongs to another stage's config: surface with its real id, flagged
|
|
174
|
+
const other = otherStageByDir.get(componentDir);
|
|
175
|
+
if (other) {
|
|
176
|
+
components.push({
|
|
177
|
+
id: other.id,
|
|
178
|
+
name,
|
|
179
|
+
slot: other.slot,
|
|
180
|
+
otherStage: true,
|
|
181
|
+
entryPoint,
|
|
182
|
+
outfile,
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
92
186
|
|
|
93
|
-
//
|
|
187
|
+
// Legacy per-folder meta.json fallback
|
|
188
|
+
const meta = await readComponentMeta(componentDir, stage);
|
|
94
189
|
const id = meta?.id ?? `studio-${name}`;
|
|
95
190
|
const isUnlinked = !meta?.id;
|
|
96
|
-
|
|
97
|
-
// Unlinked components without a slot can still be built, just not placed in checkout
|
|
98
191
|
if (isUnlinked && !meta?.slot) {
|
|
99
192
|
console.warn(
|
|
100
193
|
`${name}: no slot defined - component will be built but not placed in checkout`,
|
|
@@ -105,8 +198,9 @@ export async function discoverComponents(
|
|
|
105
198
|
id,
|
|
106
199
|
name,
|
|
107
200
|
slot: meta?.slot,
|
|
108
|
-
|
|
109
|
-
|
|
201
|
+
...(isUnlinked ? { unlinked: true } : {}),
|
|
202
|
+
entryPoint,
|
|
203
|
+
outfile,
|
|
110
204
|
});
|
|
111
205
|
}
|
|
112
206
|
|
|
@@ -119,10 +213,111 @@ export interface BuildResult {
|
|
|
119
213
|
buildTime: number;
|
|
120
214
|
}
|
|
121
215
|
|
|
216
|
+
export interface BrowserLogEntry {
|
|
217
|
+
level: string;
|
|
218
|
+
component?: string;
|
|
219
|
+
message: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function browserLogBridge(endpoint: string): string {
|
|
223
|
+
const target = JSON.stringify(endpoint);
|
|
224
|
+
return `(() => {
|
|
225
|
+
if (typeof window === "undefined" || window.__ollieLogBridge) return;
|
|
226
|
+
window.__ollieLogBridge = true;
|
|
227
|
+
|
|
228
|
+
const endpoint = ${target};
|
|
229
|
+
const SLOT_FRAME = /ollie-slot\\/([^.\\s:]+)\\.js/;
|
|
230
|
+
const LEVELS = ["log", "info", "warn", "error", "debug"];
|
|
231
|
+
const MAX_MESSAGE = 2000;
|
|
232
|
+
const MAX_BATCH = 20;
|
|
233
|
+
const FLUSH_MS = 200;
|
|
234
|
+
|
|
235
|
+
let queue = [];
|
|
236
|
+
let flushTimer = null;
|
|
237
|
+
|
|
238
|
+
function format(value) {
|
|
239
|
+
if (typeof value === "string") return value;
|
|
240
|
+
try {
|
|
241
|
+
return JSON.stringify(value);
|
|
242
|
+
} catch (_) {
|
|
243
|
+
return String(value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function slotName(stack) {
|
|
248
|
+
const match = (stack || "").match(SLOT_FRAME);
|
|
249
|
+
return match ? match[1] : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function flush() {
|
|
253
|
+
if (flushTimer) {
|
|
254
|
+
clearTimeout(flushTimer);
|
|
255
|
+
flushTimer = null;
|
|
256
|
+
}
|
|
257
|
+
if (queue.length === 0) return;
|
|
258
|
+
const batch = queue;
|
|
259
|
+
queue = [];
|
|
260
|
+
try {
|
|
261
|
+
fetch(endpoint, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
mode: "cors",
|
|
264
|
+
keepalive: true,
|
|
265
|
+
headers: { "Content-Type": "text/plain" },
|
|
266
|
+
body: JSON.stringify({ batch: batch }),
|
|
267
|
+
}).catch(function () {});
|
|
268
|
+
} catch (_) {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function forward(level, component, args) {
|
|
272
|
+
try {
|
|
273
|
+
let message = args.map(format).join(" ");
|
|
274
|
+
if (message.length > MAX_MESSAGE) {
|
|
275
|
+
message = message.slice(0, MAX_MESSAGE) + " …(truncated)";
|
|
276
|
+
}
|
|
277
|
+
queue.push({ level: level, component: component, message: message });
|
|
278
|
+
if (queue.length >= MAX_BATCH) {
|
|
279
|
+
flush();
|
|
280
|
+
} else if (!flushTimer) {
|
|
281
|
+
flushTimer = setTimeout(flush, FLUSH_MS);
|
|
282
|
+
}
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const level of LEVELS) {
|
|
287
|
+
const original = console[level]
|
|
288
|
+
? console[level].bind(console)
|
|
289
|
+
: function () {};
|
|
290
|
+
console[level] = function () {
|
|
291
|
+
const args = Array.prototype.slice.call(arguments);
|
|
292
|
+
original.apply(null, args);
|
|
293
|
+
const component = slotName(new Error().stack);
|
|
294
|
+
if (component) forward(level, component, args);
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
window.addEventListener("error", function (event) {
|
|
299
|
+
const component = slotName(event.error && event.error.stack);
|
|
300
|
+
if (component) forward("error", component, [event.message]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
304
|
+
const reason = event.reason;
|
|
305
|
+
const component = slotName(reason && reason.stack);
|
|
306
|
+
if (component) {
|
|
307
|
+
const message = reason && reason.message ? reason.message : String(reason);
|
|
308
|
+
forward("error", component, [message]);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
window.addEventListener("pagehide", flush);
|
|
313
|
+
})();`;
|
|
314
|
+
}
|
|
315
|
+
|
|
122
316
|
export interface CreateBuildContextOptions {
|
|
123
317
|
cwd?: string;
|
|
124
318
|
stage?: string;
|
|
125
319
|
onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
|
|
320
|
+
browserLogsEndpoint?: string;
|
|
126
321
|
}
|
|
127
322
|
|
|
128
323
|
/**
|
|
@@ -133,7 +328,12 @@ export async function createBuildContext(
|
|
|
133
328
|
components: ComponentInfo[],
|
|
134
329
|
options: CreateBuildContextOptions = {},
|
|
135
330
|
): Promise<esbuild.BuildContext> {
|
|
136
|
-
const {
|
|
331
|
+
const {
|
|
332
|
+
cwd = process.cwd(),
|
|
333
|
+
stage,
|
|
334
|
+
onBuildEnd,
|
|
335
|
+
browserLogsEndpoint,
|
|
336
|
+
} = options;
|
|
137
337
|
const outdir = path.join(cwd, "node_modules/.ollie", "build");
|
|
138
338
|
|
|
139
339
|
// Ensure output directory exists
|
|
@@ -209,6 +409,9 @@ export async function createBuildContext(
|
|
|
209
409
|
logLevel: "silent", // We handle logging ourselves
|
|
210
410
|
jsx: "automatic",
|
|
211
411
|
plugins: [manifestPlugin],
|
|
412
|
+
...(browserLogsEndpoint
|
|
413
|
+
? { banner: { js: browserLogBridge(browserLogsEndpoint) } }
|
|
414
|
+
: {}),
|
|
212
415
|
});
|
|
213
416
|
|
|
214
417
|
return ctx;
|
|
@@ -233,6 +436,7 @@ export async function startDevServer(
|
|
|
233
436
|
stage?: string;
|
|
234
437
|
onRequest?: (args: esbuild.ServeOnRequestArgs) => void;
|
|
235
438
|
onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
|
|
439
|
+
onBrowserLogs?: (entries: BrowserLogEntry[]) => void;
|
|
236
440
|
} = {},
|
|
237
441
|
): Promise<
|
|
238
442
|
ServeResult & { rebuild: () => Promise<void>; stop: () => Promise<void> }
|
|
@@ -244,25 +448,37 @@ export async function startDevServer(
|
|
|
244
448
|
stage,
|
|
245
449
|
onRequest,
|
|
246
450
|
onBuildEnd,
|
|
451
|
+
onBrowserLogs,
|
|
247
452
|
} = options;
|
|
248
453
|
|
|
249
454
|
const servedir = path.join(cwd, "node_modules/.ollie", "build");
|
|
250
455
|
const componentsDir = path.join(cwd, "components");
|
|
251
456
|
const internalPort = port + 1;
|
|
457
|
+
const browserLogsEndpoint = onBrowserLogs
|
|
458
|
+
? `http://${host}:${port}/__log`
|
|
459
|
+
: undefined;
|
|
460
|
+
|
|
461
|
+
// The active stage is mutable: the admin can switch it at runtime via POST /stage.
|
|
462
|
+
let activeStage = stage ?? "prod";
|
|
252
463
|
|
|
253
464
|
let ctx: esbuild.BuildContext | null = null;
|
|
254
465
|
let entryNames = new Set<string>();
|
|
255
466
|
|
|
256
467
|
async function buildAndServe(components: ComponentInfo[]): Promise<void> {
|
|
257
468
|
entryNames = new Set(components.map((c) => c.name));
|
|
258
|
-
ctx = await createBuildContext(components, {
|
|
469
|
+
ctx = await createBuildContext(components, {
|
|
470
|
+
cwd,
|
|
471
|
+
stage: activeStage,
|
|
472
|
+
onBuildEnd,
|
|
473
|
+
browserLogsEndpoint,
|
|
474
|
+
});
|
|
259
475
|
await ctx.rebuild();
|
|
260
476
|
await ctx.watch();
|
|
261
477
|
await ctx.serve({ port: internalPort, host, servedir, onRequest });
|
|
262
478
|
}
|
|
263
479
|
|
|
264
480
|
async function recreate(): Promise<void> {
|
|
265
|
-
const components = await discoverComponents({ cwd, stage });
|
|
481
|
+
const components = await discoverComponents({ cwd, stage: activeStage });
|
|
266
482
|
const oldCtx = ctx;
|
|
267
483
|
ctx = null;
|
|
268
484
|
if (oldCtx) await oldCtx.dispose();
|
|
@@ -270,7 +486,7 @@ export async function startDevServer(
|
|
|
270
486
|
notifyComponentsChanged(components);
|
|
271
487
|
}
|
|
272
488
|
|
|
273
|
-
await buildAndServe(await discoverComponents({ cwd, stage }));
|
|
489
|
+
await buildAndServe(await discoverComponents({ cwd, stage: activeStage }));
|
|
274
490
|
|
|
275
491
|
async function currentComponentNames(): Promise<Set<string>> {
|
|
276
492
|
try {
|
|
@@ -459,6 +675,62 @@ export async function startDevServer(
|
|
|
459
675
|
return;
|
|
460
676
|
}
|
|
461
677
|
|
|
678
|
+
// List available stages (from local ollie*.json) and the active one
|
|
679
|
+
if (url.pathname === "/stages" && req.method === "GET") {
|
|
680
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
681
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
682
|
+
res.setHeader("Content-Type", "application/json");
|
|
683
|
+
const stages = await listStages(cwd);
|
|
684
|
+
res.end(JSON.stringify({ active: activeStage, stages }));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Switch the active stage at runtime: re-discover + rebuild for it
|
|
689
|
+
if (url.pathname === "/stage" && req.method === "POST") {
|
|
690
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
691
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
692
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
693
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
694
|
+
|
|
695
|
+
let body = "";
|
|
696
|
+
req.on("data", (chunk) => {
|
|
697
|
+
body += chunk.toString();
|
|
698
|
+
});
|
|
699
|
+
req.on("end", async () => {
|
|
700
|
+
try {
|
|
701
|
+
const { stage: next } = JSON.parse(body) as { stage?: unknown };
|
|
702
|
+
if (typeof next !== "string" || !next) {
|
|
703
|
+
res.statusCode = 400;
|
|
704
|
+
res.setHeader("Content-Type", "application/json");
|
|
705
|
+
res.end(JSON.stringify({ error: "Missing 'stage' in body" }));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
activeStage = next;
|
|
709
|
+
await recreate();
|
|
710
|
+
let versionId: string | undefined;
|
|
711
|
+
try {
|
|
712
|
+
versionId = (await loadConfig({ cwd, stage: activeStage }))
|
|
713
|
+
?.versionId;
|
|
714
|
+
} catch {
|
|
715
|
+
// Stage config may be missing/invalid
|
|
716
|
+
}
|
|
717
|
+
res.setHeader("Content-Type", "application/json");
|
|
718
|
+
res.end(
|
|
719
|
+
JSON.stringify({ success: true, stage: activeStage, versionId }),
|
|
720
|
+
);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
res.statusCode = 400;
|
|
723
|
+
res.setHeader("Content-Type", "application/json");
|
|
724
|
+
res.end(
|
|
725
|
+
JSON.stringify({
|
|
726
|
+
error: err instanceof Error ? err.message : "Invalid JSON body",
|
|
727
|
+
}),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
462
734
|
// Handle POST /meta?component=ComponentName to update meta.json
|
|
463
735
|
if (url.pathname === "/meta" && req.method === "POST") {
|
|
464
736
|
const componentName = url.searchParams.get("component");
|
|
@@ -487,6 +759,40 @@ export async function startDevServer(
|
|
|
487
759
|
req.on("end", async () => {
|
|
488
760
|
try {
|
|
489
761
|
const updates = JSON.parse(body) as Record<string, unknown>;
|
|
762
|
+
|
|
763
|
+
// A project opts into the config model by having a `components` map.
|
|
764
|
+
// Otherwise we keep writing the legacy per-folder meta.json.
|
|
765
|
+
let cfg: OllieConfig | null = null;
|
|
766
|
+
try {
|
|
767
|
+
cfg = await loadConfig({ cwd, stage: activeStage });
|
|
768
|
+
} catch {
|
|
769
|
+
// Treat as legacy if the config can't be loaded
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (cfg?.components !== undefined) {
|
|
773
|
+
const id = typeof updates.id === "string" ? updates.id : undefined;
|
|
774
|
+
if (!id) {
|
|
775
|
+
res.statusCode = 400;
|
|
776
|
+
res.setHeader("Content-Type", "application/json");
|
|
777
|
+
res.end(JSON.stringify({ error: "Missing 'id' in body" }));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const slot =
|
|
781
|
+
typeof updates.slot === "string" ? updates.slot : undefined;
|
|
782
|
+
const entry = await upsertComponentEntry(
|
|
783
|
+
id,
|
|
784
|
+
{ defaultPath: `components/${componentName}`, slot },
|
|
785
|
+
{ cwd, stage: activeStage },
|
|
786
|
+
);
|
|
787
|
+
// Rebuild so the manifest reflects the new mapping/slot
|
|
788
|
+
await ctx?.rebuild();
|
|
789
|
+
res.setHeader("Content-Type", "application/json");
|
|
790
|
+
res.end(
|
|
791
|
+
JSON.stringify({ success: true, component: { id, ...entry } }),
|
|
792
|
+
);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
490
796
|
const metaPath = path.join(
|
|
491
797
|
cwd,
|
|
492
798
|
"components",
|
|
@@ -540,6 +846,50 @@ export async function startDevServer(
|
|
|
540
846
|
return;
|
|
541
847
|
}
|
|
542
848
|
|
|
849
|
+
if (url.pathname === "/__log" && req.method === "POST") {
|
|
850
|
+
if (!onBrowserLogs) {
|
|
851
|
+
res.statusCode = 404;
|
|
852
|
+
res.end();
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
857
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
858
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
859
|
+
|
|
860
|
+
const MAX_LOG_BODY = 64 * 1024;
|
|
861
|
+
let body = "";
|
|
862
|
+
let aborted = false;
|
|
863
|
+
req.on("data", (chunk) => {
|
|
864
|
+
if (aborted) return;
|
|
865
|
+
body += chunk.toString();
|
|
866
|
+
if (body.length > MAX_LOG_BODY) {
|
|
867
|
+
aborted = true;
|
|
868
|
+
res.statusCode = 413;
|
|
869
|
+
res.end();
|
|
870
|
+
req.destroy();
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
req.on("end", () => {
|
|
874
|
+
if (aborted) return;
|
|
875
|
+
try {
|
|
876
|
+
const parsed: unknown = JSON.parse(body);
|
|
877
|
+
const batch = (parsed as { batch?: unknown }).batch;
|
|
878
|
+
const candidates: unknown[] = Array.isArray(batch) ? batch : [parsed];
|
|
879
|
+
const entries = candidates.filter(
|
|
880
|
+
(entry): entry is BrowserLogEntry =>
|
|
881
|
+
typeof (entry as BrowserLogEntry)?.message === "string",
|
|
882
|
+
);
|
|
883
|
+
if (entries.length > 0) onBrowserLogs(entries);
|
|
884
|
+
} catch {
|
|
885
|
+
// Ignore malformed log payloads
|
|
886
|
+
}
|
|
887
|
+
res.statusCode = 204;
|
|
888
|
+
res.end();
|
|
889
|
+
});
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
543
893
|
// Handle CORS preflight
|
|
544
894
|
if (req.method === "OPTIONS") {
|
|
545
895
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -641,6 +991,10 @@ export interface ManifestEntry {
|
|
|
641
991
|
css?: string;
|
|
642
992
|
/** Target slot for components */
|
|
643
993
|
slot?: string;
|
|
994
|
+
/** True when the component belongs to another stage's config */
|
|
995
|
+
otherStage?: boolean;
|
|
996
|
+
/** True when the component isn't linked to a component id (studio-*) */
|
|
997
|
+
unlinked?: boolean;
|
|
644
998
|
}
|
|
645
999
|
|
|
646
1000
|
/**
|
|
@@ -690,6 +1044,8 @@ export async function writeManifest(
|
|
|
690
1044
|
name: c.name,
|
|
691
1045
|
js: `/${c.name}/index.js`,
|
|
692
1046
|
slot: c.slot,
|
|
1047
|
+
...(c.otherStage ? { otherStage: true } : {}),
|
|
1048
|
+
...(c.unlinked ? { unlinked: true } : {}),
|
|
693
1049
|
};
|
|
694
1050
|
|
|
695
1051
|
// Check if CSS exists
|