@ollie-shop/cli 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/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +6 -0
- package/dist/index.js +201 -6
- package/package.json +1 -1
- package/src/commands/help.tsx +9 -0
- package/src/commands/start.tsx +68 -1
- package/src/utils/esbuild.ts +160 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @ollie-shop/cli@1.
|
|
2
|
+
> @ollie-shop/cli@1.6.0 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
|
|
3
3
|
> tsup
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.tsx
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
[34mCLI[39m Target: node22
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m112.10 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 291ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @ollie-shop/cli
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- df230be: Add a `--browser-logs` flag to `ollieshop start` that streams custom components' browser console logs and uncaught errors to the CLI terminal, attributed per component. Off by default — no console patching or overhead unless the flag is passed, and deploy bundles are unaffected.
|
|
8
|
+
|
|
3
9
|
## 1.5.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -115,6 +115,10 @@ function HelpCommand() {
|
|
|
115
115
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
116
116
|
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "--no-open" }) }),
|
|
117
117
|
/* @__PURE__ */ jsx(Text, { children: "start: don't auto-open Studio (also honored via CI env)" })
|
|
118
|
+
] }),
|
|
119
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
120
|
+
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "--browser-logs" }) }),
|
|
121
|
+
/* @__PURE__ */ jsx(Text, { children: "start: stream custom components' browser console to terminal" })
|
|
118
122
|
] })
|
|
119
123
|
] }),
|
|
120
124
|
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: "Examples:" }) }),
|
|
@@ -122,6 +126,7 @@ function HelpCommand() {
|
|
|
122
126
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop login" }),
|
|
123
127
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --stage dev" }),
|
|
124
128
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --no-open" }),
|
|
129
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --browser-logs" }),
|
|
125
130
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop whoami -o json" }),
|
|
126
131
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop schema store.create" }),
|
|
127
132
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: '$ ollieshop store create --name "My Store" --platform vtex --platform-store-id mystore' }),
|
|
@@ -730,8 +735,106 @@ async function discoverComponents(options = {}) {
|
|
|
730
735
|
}
|
|
731
736
|
return components;
|
|
732
737
|
}
|
|
738
|
+
function browserLogBridge(endpoint) {
|
|
739
|
+
const target = JSON.stringify(endpoint);
|
|
740
|
+
return `(() => {
|
|
741
|
+
if (typeof window === "undefined" || window.__ollieLogBridge) return;
|
|
742
|
+
window.__ollieLogBridge = true;
|
|
743
|
+
|
|
744
|
+
const endpoint = ${target};
|
|
745
|
+
const SLOT_FRAME = /ollie-slot\\/([^.\\s:]+)\\.js/;
|
|
746
|
+
const LEVELS = ["log", "info", "warn", "error", "debug"];
|
|
747
|
+
const MAX_MESSAGE = 2000;
|
|
748
|
+
const MAX_BATCH = 20;
|
|
749
|
+
const FLUSH_MS = 200;
|
|
750
|
+
|
|
751
|
+
let queue = [];
|
|
752
|
+
let flushTimer = null;
|
|
753
|
+
|
|
754
|
+
function format(value) {
|
|
755
|
+
if (typeof value === "string") return value;
|
|
756
|
+
try {
|
|
757
|
+
return JSON.stringify(value);
|
|
758
|
+
} catch (_) {
|
|
759
|
+
return String(value);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function slotName(stack) {
|
|
764
|
+
const match = (stack || "").match(SLOT_FRAME);
|
|
765
|
+
return match ? match[1] : null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function flush() {
|
|
769
|
+
if (flushTimer) {
|
|
770
|
+
clearTimeout(flushTimer);
|
|
771
|
+
flushTimer = null;
|
|
772
|
+
}
|
|
773
|
+
if (queue.length === 0) return;
|
|
774
|
+
const batch = queue;
|
|
775
|
+
queue = [];
|
|
776
|
+
try {
|
|
777
|
+
fetch(endpoint, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
mode: "cors",
|
|
780
|
+
keepalive: true,
|
|
781
|
+
headers: { "Content-Type": "text/plain" },
|
|
782
|
+
body: JSON.stringify({ batch: batch }),
|
|
783
|
+
}).catch(function () {});
|
|
784
|
+
} catch (_) {}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function forward(level, component, args) {
|
|
788
|
+
try {
|
|
789
|
+
let message = args.map(format).join(" ");
|
|
790
|
+
if (message.length > MAX_MESSAGE) {
|
|
791
|
+
message = message.slice(0, MAX_MESSAGE) + " \u2026(truncated)";
|
|
792
|
+
}
|
|
793
|
+
queue.push({ level: level, component: component, message: message });
|
|
794
|
+
if (queue.length >= MAX_BATCH) {
|
|
795
|
+
flush();
|
|
796
|
+
} else if (!flushTimer) {
|
|
797
|
+
flushTimer = setTimeout(flush, FLUSH_MS);
|
|
798
|
+
}
|
|
799
|
+
} catch (_) {}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (const level of LEVELS) {
|
|
803
|
+
const original = console[level]
|
|
804
|
+
? console[level].bind(console)
|
|
805
|
+
: function () {};
|
|
806
|
+
console[level] = function () {
|
|
807
|
+
const args = Array.prototype.slice.call(arguments);
|
|
808
|
+
original.apply(null, args);
|
|
809
|
+
const component = slotName(new Error().stack);
|
|
810
|
+
if (component) forward(level, component, args);
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
window.addEventListener("error", function (event) {
|
|
815
|
+
const component = slotName(event.error && event.error.stack);
|
|
816
|
+
if (component) forward("error", component, [event.message]);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
820
|
+
const reason = event.reason;
|
|
821
|
+
const component = slotName(reason && reason.stack);
|
|
822
|
+
if (component) {
|
|
823
|
+
const message = reason && reason.message ? reason.message : String(reason);
|
|
824
|
+
forward("error", component, [message]);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
window.addEventListener("pagehide", flush);
|
|
829
|
+
})();`;
|
|
830
|
+
}
|
|
733
831
|
async function createBuildContext(components, options = {}) {
|
|
734
|
-
const {
|
|
832
|
+
const {
|
|
833
|
+
cwd = process.cwd(),
|
|
834
|
+
stage,
|
|
835
|
+
onBuildEnd,
|
|
836
|
+
browserLogsEndpoint
|
|
837
|
+
} = options;
|
|
735
838
|
const outdir = path4.join(cwd, "node_modules/.ollie", "build");
|
|
736
839
|
await fs4.mkdir(outdir, { recursive: true });
|
|
737
840
|
const entryPoints = {};
|
|
@@ -789,7 +892,8 @@ async function createBuildContext(components, options = {}) {
|
|
|
789
892
|
logLevel: "silent",
|
|
790
893
|
// We handle logging ourselves
|
|
791
894
|
jsx: "automatic",
|
|
792
|
-
plugins: [manifestPlugin]
|
|
895
|
+
plugins: [manifestPlugin],
|
|
896
|
+
...browserLogsEndpoint ? { banner: { js: browserLogBridge(browserLogsEndpoint) } } : {}
|
|
793
897
|
});
|
|
794
898
|
return ctx;
|
|
795
899
|
}
|
|
@@ -800,11 +904,13 @@ async function startDevServer(options = {}) {
|
|
|
800
904
|
cwd = process.cwd(),
|
|
801
905
|
stage,
|
|
802
906
|
onRequest,
|
|
803
|
-
onBuildEnd
|
|
907
|
+
onBuildEnd,
|
|
908
|
+
onBrowserLogs
|
|
804
909
|
} = options;
|
|
805
910
|
const servedir = path4.join(cwd, "node_modules/.ollie", "build");
|
|
806
911
|
const componentsDir = path4.join(cwd, "components");
|
|
807
912
|
const internalPort = port + 1;
|
|
913
|
+
const browserLogsEndpoint = onBrowserLogs ? `http://${host}:${port}/__log` : void 0;
|
|
808
914
|
let activeStage = stage ?? "prod";
|
|
809
915
|
let ctx = null;
|
|
810
916
|
let entryNames = /* @__PURE__ */ new Set();
|
|
@@ -813,7 +919,8 @@ async function startDevServer(options = {}) {
|
|
|
813
919
|
ctx = await createBuildContext(components, {
|
|
814
920
|
cwd,
|
|
815
921
|
stage: activeStage,
|
|
816
|
-
onBuildEnd
|
|
922
|
+
onBuildEnd,
|
|
923
|
+
browserLogsEndpoint
|
|
817
924
|
});
|
|
818
925
|
await ctx.rebuild();
|
|
819
926
|
await ctx.watch();
|
|
@@ -1118,6 +1225,45 @@ data: ${payload}
|
|
|
1118
1225
|
});
|
|
1119
1226
|
return;
|
|
1120
1227
|
}
|
|
1228
|
+
if (url.pathname === "/__log" && req.method === "POST") {
|
|
1229
|
+
if (!onBrowserLogs) {
|
|
1230
|
+
res.statusCode = 404;
|
|
1231
|
+
res.end();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1235
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1236
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
1237
|
+
const MAX_LOG_BODY = 64 * 1024;
|
|
1238
|
+
let body = "";
|
|
1239
|
+
let aborted = false;
|
|
1240
|
+
req.on("data", (chunk) => {
|
|
1241
|
+
if (aborted) return;
|
|
1242
|
+
body += chunk.toString();
|
|
1243
|
+
if (body.length > MAX_LOG_BODY) {
|
|
1244
|
+
aborted = true;
|
|
1245
|
+
res.statusCode = 413;
|
|
1246
|
+
res.end();
|
|
1247
|
+
req.destroy();
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
req.on("end", () => {
|
|
1251
|
+
if (aborted) return;
|
|
1252
|
+
try {
|
|
1253
|
+
const parsed2 = JSON.parse(body);
|
|
1254
|
+
const batch = parsed2.batch;
|
|
1255
|
+
const candidates = Array.isArray(batch) ? batch : [parsed2];
|
|
1256
|
+
const entries = candidates.filter(
|
|
1257
|
+
(entry) => typeof entry?.message === "string"
|
|
1258
|
+
);
|
|
1259
|
+
if (entries.length > 0) onBrowserLogs(entries);
|
|
1260
|
+
} catch {
|
|
1261
|
+
}
|
|
1262
|
+
res.statusCode = 204;
|
|
1263
|
+
res.end();
|
|
1264
|
+
});
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1121
1267
|
if (req.method === "OPTIONS") {
|
|
1122
1268
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1123
1269
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
@@ -1222,13 +1368,16 @@ function StartCommand({ args }) {
|
|
|
1222
1368
|
const [state, setState] = useState2({ status: "initializing" });
|
|
1223
1369
|
const [components, setComponents] = useState2([]);
|
|
1224
1370
|
const [logs, setLogs] = useState2([]);
|
|
1371
|
+
const [browserLogs, setBrowserLogs] = useState2([]);
|
|
1225
1372
|
const [buildCount, setBuildCount] = useState2(0);
|
|
1226
1373
|
const [lastBuildTime, setLastBuildTime] = useState2(null);
|
|
1227
1374
|
const logIdRef = useRef(0);
|
|
1375
|
+
const browserLogIdRef = useRef(0);
|
|
1228
1376
|
const rebuildRef = useRef(null);
|
|
1229
1377
|
const stopRef = useRef(null);
|
|
1230
1378
|
const stage = resolveStage(parseArg(args, "--stage", "-s"));
|
|
1231
1379
|
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
1380
|
+
const browserLogsEnabled = args.includes("--browser-logs");
|
|
1232
1381
|
const isInteractive = Boolean(process.stdin.isTTY);
|
|
1233
1382
|
const addLog = useCallback((log) => {
|
|
1234
1383
|
setLogs((prev) => {
|
|
@@ -1251,6 +1400,18 @@ function StartCommand({ args }) {
|
|
|
1251
1400
|
},
|
|
1252
1401
|
[addLog]
|
|
1253
1402
|
);
|
|
1403
|
+
const addBrowserLogs = useCallback((entries) => {
|
|
1404
|
+
if (entries.length === 0) return;
|
|
1405
|
+
setBrowserLogs((prev) => {
|
|
1406
|
+
const mapped = entries.map((entry) => ({
|
|
1407
|
+
...entry,
|
|
1408
|
+
message: sanitizeLogMessage(entry.message),
|
|
1409
|
+
id: ++browserLogIdRef.current,
|
|
1410
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1411
|
+
}));
|
|
1412
|
+
return [...prev, ...mapped].slice(-MAX_LOGS);
|
|
1413
|
+
});
|
|
1414
|
+
}, []);
|
|
1254
1415
|
useEffect2(() => {
|
|
1255
1416
|
let mounted = true;
|
|
1256
1417
|
async function init() {
|
|
@@ -1281,6 +1442,7 @@ function StartCommand({ args }) {
|
|
|
1281
1442
|
port: PORT,
|
|
1282
1443
|
stage,
|
|
1283
1444
|
onRequest: handleRequest,
|
|
1445
|
+
onBrowserLogs: browserLogsEnabled ? addBrowserLogs : void 0,
|
|
1284
1446
|
onBuildEnd: (updatedComponents) => {
|
|
1285
1447
|
setComponents(updatedComponents);
|
|
1286
1448
|
setBuildCount((c) => c + 1);
|
|
@@ -1321,7 +1483,7 @@ function StartCommand({ args }) {
|
|
|
1321
1483
|
mounted = false;
|
|
1322
1484
|
stopRef.current?.();
|
|
1323
1485
|
};
|
|
1324
|
-
}, [stage, handleRequest, noOpen]);
|
|
1486
|
+
}, [stage, handleRequest, addBrowserLogs, browserLogsEnabled, noOpen]);
|
|
1325
1487
|
useInput(
|
|
1326
1488
|
(input, key) => {
|
|
1327
1489
|
if (input === "q" || input === "c" && key.ctrl) {
|
|
@@ -1378,6 +1540,7 @@ function StartCommand({ args }) {
|
|
|
1378
1540
|
/* @__PURE__ */ jsx3(ComponentList, { components }),
|
|
1379
1541
|
/* @__PURE__ */ jsx3(BuildInfo, { buildCount, lastBuildTime }),
|
|
1380
1542
|
/* @__PURE__ */ jsx3(RequestLogs, { logs }),
|
|
1543
|
+
browserLogsEnabled && /* @__PURE__ */ jsx3(BrowserLogs, { logs: browserLogs }),
|
|
1381
1544
|
/* @__PURE__ */ jsx3(Footer, { interactive: isInteractive })
|
|
1382
1545
|
] })
|
|
1383
1546
|
] });
|
|
@@ -1506,6 +1669,38 @@ function RequestLogs({ logs }) {
|
|
|
1506
1669
|
] }, log.id)) })
|
|
1507
1670
|
] });
|
|
1508
1671
|
}
|
|
1672
|
+
function sanitizeLogMessage(message) {
|
|
1673
|
+
let out = "";
|
|
1674
|
+
for (const ch of message) {
|
|
1675
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1676
|
+
out += code < 32 || code >= 127 && code <= 159 ? " " : ch;
|
|
1677
|
+
if (out.length >= 2e3) break;
|
|
1678
|
+
}
|
|
1679
|
+
return out;
|
|
1680
|
+
}
|
|
1681
|
+
function browserLogColor(level) {
|
|
1682
|
+
if (level === "error") return "red";
|
|
1683
|
+
if (level === "warn") return "yellow";
|
|
1684
|
+
if (level === "info") return "cyan";
|
|
1685
|
+
return "gray";
|
|
1686
|
+
}
|
|
1687
|
+
function BrowserLogs({ logs }) {
|
|
1688
|
+
return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1689
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Browser Logs:" }),
|
|
1690
|
+
/* @__PURE__ */ jsx3(Box3, { marginLeft: 2, flexDirection: "column", children: logs.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No browser logs yet..." }) : logs.map((log) => /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1691
|
+
/* @__PURE__ */ jsxs3(Text3, { color: browserLogColor(log.level), children: [
|
|
1692
|
+
log.level.toUpperCase(),
|
|
1693
|
+
" "
|
|
1694
|
+
] }),
|
|
1695
|
+
log.component && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1696
|
+
"[",
|
|
1697
|
+
log.component,
|
|
1698
|
+
"] "
|
|
1699
|
+
] }),
|
|
1700
|
+
/* @__PURE__ */ jsx3(Text3, { children: log.message })
|
|
1701
|
+
] }, log.id)) })
|
|
1702
|
+
] });
|
|
1703
|
+
}
|
|
1509
1704
|
function Footer({ interactive = true }) {
|
|
1510
1705
|
if (!interactive) {
|
|
1511
1706
|
return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Headless (no TTY) \u2014 Ctrl+C to stop" }) });
|
|
@@ -1542,7 +1737,7 @@ function App({ command, args }) {
|
|
|
1542
1737
|
}
|
|
1543
1738
|
}
|
|
1544
1739
|
function VersionCommand() {
|
|
1545
|
-
const version = "1.
|
|
1740
|
+
const version = "1.6.0" ? "1.6.0" : "unknown";
|
|
1546
1741
|
return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1547
1742
|
"ollieshop v",
|
|
1548
1743
|
version
|
package/package.json
CHANGED
package/src/commands/help.tsx
CHANGED
|
@@ -151,6 +151,14 @@ export function HelpCommand() {
|
|
|
151
151
|
</Box>
|
|
152
152
|
<Text>start: don't auto-open Studio (also honored via CI env)</Text>
|
|
153
153
|
</Box>
|
|
154
|
+
<Box>
|
|
155
|
+
<Box width={24}>
|
|
156
|
+
<Text color="yellow">--browser-logs</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
<Text>
|
|
159
|
+
start: stream custom components' browser console to terminal
|
|
160
|
+
</Text>
|
|
161
|
+
</Box>
|
|
154
162
|
</Box>
|
|
155
163
|
|
|
156
164
|
<Box marginTop={1}>
|
|
@@ -160,6 +168,7 @@ export function HelpCommand() {
|
|
|
160
168
|
<Text dimColor>$ ollieshop login</Text>
|
|
161
169
|
<Text dimColor>$ ollieshop start --stage dev</Text>
|
|
162
170
|
<Text dimColor>$ ollieshop start --no-open</Text>
|
|
171
|
+
<Text dimColor>$ ollieshop start --browser-logs</Text>
|
|
163
172
|
<Text dimColor>$ ollieshop whoami -o json</Text>
|
|
164
173
|
<Text dimColor>$ ollieshop schema store.create</Text>
|
|
165
174
|
<Text dimColor>
|
package/src/commands/start.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import open from "open";
|
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { loadConfig, resolveStage } from "../utils/config.js";
|
|
6
6
|
import {
|
|
7
|
+
type BrowserLogEntry,
|
|
7
8
|
type ComponentInfo,
|
|
8
9
|
discoverComponents,
|
|
9
10
|
startDevServer,
|
|
@@ -22,6 +23,14 @@ interface RequestLog {
|
|
|
22
23
|
timestamp: Date;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
interface BrowserLog {
|
|
27
|
+
id: number;
|
|
28
|
+
level: string;
|
|
29
|
+
component?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
timestamp: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
type ServerState =
|
|
26
35
|
| { status: "initializing" }
|
|
27
36
|
| { status: "discovering" }
|
|
@@ -51,9 +60,11 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
51
60
|
const [state, setState] = useState<ServerState>({ status: "initializing" });
|
|
52
61
|
const [components, setComponents] = useState<ComponentInfo[]>([]);
|
|
53
62
|
const [logs, setLogs] = useState<RequestLog[]>([]);
|
|
63
|
+
const [browserLogs, setBrowserLogs] = useState<BrowserLog[]>([]);
|
|
54
64
|
const [buildCount, setBuildCount] = useState(0);
|
|
55
65
|
const [lastBuildTime, setLastBuildTime] = useState<Date | null>(null);
|
|
56
66
|
const logIdRef = useRef(0);
|
|
67
|
+
const browserLogIdRef = useRef(0);
|
|
57
68
|
const rebuildRef = useRef<(() => Promise<void>) | null>(null);
|
|
58
69
|
const stopRef = useRef<(() => Promise<void>) | null>(null);
|
|
59
70
|
|
|
@@ -63,6 +74,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
63
74
|
// agent spawns the dev server and drives its own harness instead. Also honored
|
|
64
75
|
// via the conventional CI env var.
|
|
65
76
|
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
77
|
+
const browserLogsEnabled = args.includes("--browser-logs");
|
|
66
78
|
// Keyboard shortcuts need raw mode, which Ink can only enable on a TTY stdin.
|
|
67
79
|
// When spawned headless (agent/CI/backgrounded), stdin isn't a TTY — guard the
|
|
68
80
|
// input handler so the server runs instead of crashing with "Raw mode is not
|
|
@@ -92,6 +104,19 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
92
104
|
[addLog],
|
|
93
105
|
);
|
|
94
106
|
|
|
107
|
+
const addBrowserLogs = useCallback((entries: BrowserLogEntry[]) => {
|
|
108
|
+
if (entries.length === 0) return;
|
|
109
|
+
setBrowserLogs((prev) => {
|
|
110
|
+
const mapped = entries.map((entry) => ({
|
|
111
|
+
...entry,
|
|
112
|
+
message: sanitizeLogMessage(entry.message),
|
|
113
|
+
id: ++browserLogIdRef.current,
|
|
114
|
+
timestamp: new Date(),
|
|
115
|
+
}));
|
|
116
|
+
return [...prev, ...mapped].slice(-MAX_LOGS);
|
|
117
|
+
});
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
95
120
|
// Initialize server
|
|
96
121
|
useEffect(() => {
|
|
97
122
|
let mounted = true;
|
|
@@ -134,6 +159,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
134
159
|
port: PORT,
|
|
135
160
|
stage,
|
|
136
161
|
onRequest: handleRequest,
|
|
162
|
+
onBrowserLogs: browserLogsEnabled ? addBrowserLogs : undefined,
|
|
137
163
|
onBuildEnd: (updatedComponents) => {
|
|
138
164
|
setComponents(updatedComponents);
|
|
139
165
|
setBuildCount((c) => c + 1);
|
|
@@ -181,7 +207,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
181
207
|
mounted = false;
|
|
182
208
|
stopRef.current?.();
|
|
183
209
|
};
|
|
184
|
-
}, [stage, handleRequest, noOpen]);
|
|
210
|
+
}, [stage, handleRequest, addBrowserLogs, browserLogsEnabled, noOpen]);
|
|
185
211
|
|
|
186
212
|
// Handle keyboard input (only when attached to a TTY — see isInteractive above)
|
|
187
213
|
useInput(
|
|
@@ -255,6 +281,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
255
281
|
<ComponentList components={components} />
|
|
256
282
|
<BuildInfo buildCount={buildCount} lastBuildTime={lastBuildTime} />
|
|
257
283
|
<RequestLogs logs={logs} />
|
|
284
|
+
{browserLogsEnabled && <BrowserLogs logs={browserLogs} />}
|
|
258
285
|
<Footer interactive={isInteractive} />
|
|
259
286
|
</>
|
|
260
287
|
)}
|
|
@@ -404,6 +431,46 @@ function RequestLogs({ logs }: { logs: RequestLog[] }) {
|
|
|
404
431
|
);
|
|
405
432
|
}
|
|
406
433
|
|
|
434
|
+
function sanitizeLogMessage(message: string): string {
|
|
435
|
+
let out = "";
|
|
436
|
+
for (const ch of message) {
|
|
437
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
438
|
+
out += code < 0x20 || (code >= 0x7f && code <= 0x9f) ? " " : ch;
|
|
439
|
+
if (out.length >= 2000) break;
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function browserLogColor(level: string): string {
|
|
445
|
+
if (level === "error") return "red";
|
|
446
|
+
if (level === "warn") return "yellow";
|
|
447
|
+
if (level === "info") return "cyan";
|
|
448
|
+
return "gray";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function BrowserLogs({ logs }: { logs: BrowserLog[] }) {
|
|
452
|
+
return (
|
|
453
|
+
<Box marginTop={1} flexDirection="column">
|
|
454
|
+
<Text bold>Browser Logs:</Text>
|
|
455
|
+
<Box marginLeft={2} flexDirection="column">
|
|
456
|
+
{logs.length === 0 ? (
|
|
457
|
+
<Text dimColor>No browser logs yet...</Text>
|
|
458
|
+
) : (
|
|
459
|
+
logs.map((log) => (
|
|
460
|
+
<Box key={log.id}>
|
|
461
|
+
<Text color={browserLogColor(log.level)}>
|
|
462
|
+
{log.level.toUpperCase()}{" "}
|
|
463
|
+
</Text>
|
|
464
|
+
{log.component && <Text dimColor>[{log.component}] </Text>}
|
|
465
|
+
<Text>{log.message}</Text>
|
|
466
|
+
</Box>
|
|
467
|
+
))
|
|
468
|
+
)}
|
|
469
|
+
</Box>
|
|
470
|
+
</Box>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
407
474
|
function Footer({ interactive = true }: { interactive?: boolean }) {
|
|
408
475
|
if (!interactive) {
|
|
409
476
|
return (
|
package/src/utils/esbuild.ts
CHANGED
|
@@ -213,10 +213,111 @@ export interface BuildResult {
|
|
|
213
213
|
buildTime: number;
|
|
214
214
|
}
|
|
215
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
|
+
|
|
216
316
|
export interface CreateBuildContextOptions {
|
|
217
317
|
cwd?: string;
|
|
218
318
|
stage?: string;
|
|
219
319
|
onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
|
|
320
|
+
browserLogsEndpoint?: string;
|
|
220
321
|
}
|
|
221
322
|
|
|
222
323
|
/**
|
|
@@ -227,7 +328,12 @@ export async function createBuildContext(
|
|
|
227
328
|
components: ComponentInfo[],
|
|
228
329
|
options: CreateBuildContextOptions = {},
|
|
229
330
|
): Promise<esbuild.BuildContext> {
|
|
230
|
-
const {
|
|
331
|
+
const {
|
|
332
|
+
cwd = process.cwd(),
|
|
333
|
+
stage,
|
|
334
|
+
onBuildEnd,
|
|
335
|
+
browserLogsEndpoint,
|
|
336
|
+
} = options;
|
|
231
337
|
const outdir = path.join(cwd, "node_modules/.ollie", "build");
|
|
232
338
|
|
|
233
339
|
// Ensure output directory exists
|
|
@@ -303,6 +409,9 @@ export async function createBuildContext(
|
|
|
303
409
|
logLevel: "silent", // We handle logging ourselves
|
|
304
410
|
jsx: "automatic",
|
|
305
411
|
plugins: [manifestPlugin],
|
|
412
|
+
...(browserLogsEndpoint
|
|
413
|
+
? { banner: { js: browserLogBridge(browserLogsEndpoint) } }
|
|
414
|
+
: {}),
|
|
306
415
|
});
|
|
307
416
|
|
|
308
417
|
return ctx;
|
|
@@ -327,6 +436,7 @@ export async function startDevServer(
|
|
|
327
436
|
stage?: string;
|
|
328
437
|
onRequest?: (args: esbuild.ServeOnRequestArgs) => void;
|
|
329
438
|
onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
|
|
439
|
+
onBrowserLogs?: (entries: BrowserLogEntry[]) => void;
|
|
330
440
|
} = {},
|
|
331
441
|
): Promise<
|
|
332
442
|
ServeResult & { rebuild: () => Promise<void>; stop: () => Promise<void> }
|
|
@@ -338,11 +448,15 @@ export async function startDevServer(
|
|
|
338
448
|
stage,
|
|
339
449
|
onRequest,
|
|
340
450
|
onBuildEnd,
|
|
451
|
+
onBrowserLogs,
|
|
341
452
|
} = options;
|
|
342
453
|
|
|
343
454
|
const servedir = path.join(cwd, "node_modules/.ollie", "build");
|
|
344
455
|
const componentsDir = path.join(cwd, "components");
|
|
345
456
|
const internalPort = port + 1;
|
|
457
|
+
const browserLogsEndpoint = onBrowserLogs
|
|
458
|
+
? `http://${host}:${port}/__log`
|
|
459
|
+
: undefined;
|
|
346
460
|
|
|
347
461
|
// The active stage is mutable: the admin can switch it at runtime via POST /stage.
|
|
348
462
|
let activeStage = stage ?? "prod";
|
|
@@ -356,6 +470,7 @@ export async function startDevServer(
|
|
|
356
470
|
cwd,
|
|
357
471
|
stage: activeStage,
|
|
358
472
|
onBuildEnd,
|
|
473
|
+
browserLogsEndpoint,
|
|
359
474
|
});
|
|
360
475
|
await ctx.rebuild();
|
|
361
476
|
await ctx.watch();
|
|
@@ -731,6 +846,50 @@ export async function startDevServer(
|
|
|
731
846
|
return;
|
|
732
847
|
}
|
|
733
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
|
+
|
|
734
893
|
// Handle CORS preflight
|
|
735
894
|
if (req.method === "OPTIONS") {
|
|
736
895
|
res.setHeader("Access-Control-Allow-Origin", "*");
|