@ollie-shop/cli 1.5.0 → 1.7.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 +16 -0
- package/dist/index.js +320 -15
- package/package.json +1 -1
- package/src/commands/component-cmd.ts +88 -2
- package/src/commands/deploy-cmd.ts +2 -1
- package/src/commands/help.tsx +11 -2
- package/src/commands/start.tsx +68 -1
- package/src/core/component.ts +51 -1
- package/src/core/schema.ts +18 -0
- package/src/utils/bundle.ts +22 -6
- 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.7.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[32m115.94 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 278ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @ollie-shop/cli
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3d5d259: Add a `component update` command so custom component props (and name, slot, active, version) can be changed after creation directly from the CLI. Previously the CLI could only `create` and `list` components, forcing prop edits through the Studio UI. Supports partial updates via flags (`--id`, `--name`, `--slot`, `--active`, `--props`, `--version-id`) or a full `--data` JSON payload, mirroring the existing `business-rule update` command.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 616e0ea: Fix `deploy --function-id` looking for the function source under `components/`. The deploy bundler was hardcoded to `components/<name>` and always emitted a component-style `index.tsx` (`export { default }`), so function deploys failed with `Component "<name>" not found in components/` — or bundled the wrong entry point. It now resolves function sources from `functions/<name>` and emits a function-style `index.ts` (`export { handler }`) that the builder's `build_function` step expects.
|
|
12
|
+
|
|
13
|
+
## 1.6.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
3
19
|
## 1.5.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -47,8 +47,8 @@ function HelpCommand() {
|
|
|
47
47
|
/* @__PURE__ */ jsx(Text, { children: "Create or list versions" })
|
|
48
48
|
] }),
|
|
49
49
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
50
|
-
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "component create|list" }) }),
|
|
51
|
-
/* @__PURE__ */ jsx(Text, { children: "Create or list components" })
|
|
50
|
+
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "component create|update|list" }) }),
|
|
51
|
+
/* @__PURE__ */ jsx(Text, { children: "Create, update or list components" })
|
|
52
52
|
] }),
|
|
53
53
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
54
54
|
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "function create|list" }) }),
|
|
@@ -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' }),
|
|
@@ -515,12 +520,19 @@ import path3 from "path";
|
|
|
515
520
|
import { PassThrough } from "stream";
|
|
516
521
|
import archiver from "archiver";
|
|
517
522
|
async function createComponentBundle(options) {
|
|
518
|
-
const {
|
|
519
|
-
|
|
523
|
+
const {
|
|
524
|
+
cwd = process.cwd(),
|
|
525
|
+
componentName,
|
|
526
|
+
resourceType = "component"
|
|
527
|
+
} = options;
|
|
528
|
+
const sourceDir = resourceType === "function" ? "functions" : "components";
|
|
529
|
+
const componentDir = path3.join(cwd, sourceDir, componentName);
|
|
520
530
|
try {
|
|
521
531
|
await fs3.access(componentDir);
|
|
522
532
|
} catch {
|
|
523
|
-
throw new Error(
|
|
533
|
+
throw new Error(
|
|
534
|
+
`${resourceType === "function" ? "Function" : "Component"} "${componentName}" not found in ${sourceDir}/`
|
|
535
|
+
);
|
|
524
536
|
}
|
|
525
537
|
const packageJsonPath = path3.join(cwd, "package.json");
|
|
526
538
|
try {
|
|
@@ -536,9 +548,15 @@ async function createComponentBundle(options) {
|
|
|
536
548
|
archive.on("error", (err) => {
|
|
537
549
|
output.destroy(err);
|
|
538
550
|
});
|
|
539
|
-
|
|
551
|
+
if (resourceType === "function") {
|
|
552
|
+
const entryPoint = `export { handler } from './functions/${componentName}';
|
|
553
|
+
`;
|
|
554
|
+
archive.append(entryPoint, { name: "index.ts" });
|
|
555
|
+
} else {
|
|
556
|
+
const entryPoint = `export { default } from './components/${componentName}';
|
|
540
557
|
`;
|
|
541
|
-
|
|
558
|
+
archive.append(entryPoint, { name: "index.tsx" });
|
|
559
|
+
}
|
|
542
560
|
archive.file(packageJsonPath, { name: "package.json" });
|
|
543
561
|
const entries = await fs3.readdir(cwd, { withFileTypes: true });
|
|
544
562
|
for (const entry of entries) {
|
|
@@ -730,8 +748,106 @@ async function discoverComponents(options = {}) {
|
|
|
730
748
|
}
|
|
731
749
|
return components;
|
|
732
750
|
}
|
|
751
|
+
function browserLogBridge(endpoint) {
|
|
752
|
+
const target = JSON.stringify(endpoint);
|
|
753
|
+
return `(() => {
|
|
754
|
+
if (typeof window === "undefined" || window.__ollieLogBridge) return;
|
|
755
|
+
window.__ollieLogBridge = true;
|
|
756
|
+
|
|
757
|
+
const endpoint = ${target};
|
|
758
|
+
const SLOT_FRAME = /ollie-slot\\/([^.\\s:]+)\\.js/;
|
|
759
|
+
const LEVELS = ["log", "info", "warn", "error", "debug"];
|
|
760
|
+
const MAX_MESSAGE = 2000;
|
|
761
|
+
const MAX_BATCH = 20;
|
|
762
|
+
const FLUSH_MS = 200;
|
|
763
|
+
|
|
764
|
+
let queue = [];
|
|
765
|
+
let flushTimer = null;
|
|
766
|
+
|
|
767
|
+
function format(value) {
|
|
768
|
+
if (typeof value === "string") return value;
|
|
769
|
+
try {
|
|
770
|
+
return JSON.stringify(value);
|
|
771
|
+
} catch (_) {
|
|
772
|
+
return String(value);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function slotName(stack) {
|
|
777
|
+
const match = (stack || "").match(SLOT_FRAME);
|
|
778
|
+
return match ? match[1] : null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function flush() {
|
|
782
|
+
if (flushTimer) {
|
|
783
|
+
clearTimeout(flushTimer);
|
|
784
|
+
flushTimer = null;
|
|
785
|
+
}
|
|
786
|
+
if (queue.length === 0) return;
|
|
787
|
+
const batch = queue;
|
|
788
|
+
queue = [];
|
|
789
|
+
try {
|
|
790
|
+
fetch(endpoint, {
|
|
791
|
+
method: "POST",
|
|
792
|
+
mode: "cors",
|
|
793
|
+
keepalive: true,
|
|
794
|
+
headers: { "Content-Type": "text/plain" },
|
|
795
|
+
body: JSON.stringify({ batch: batch }),
|
|
796
|
+
}).catch(function () {});
|
|
797
|
+
} catch (_) {}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function forward(level, component, args) {
|
|
801
|
+
try {
|
|
802
|
+
let message = args.map(format).join(" ");
|
|
803
|
+
if (message.length > MAX_MESSAGE) {
|
|
804
|
+
message = message.slice(0, MAX_MESSAGE) + " \u2026(truncated)";
|
|
805
|
+
}
|
|
806
|
+
queue.push({ level: level, component: component, message: message });
|
|
807
|
+
if (queue.length >= MAX_BATCH) {
|
|
808
|
+
flush();
|
|
809
|
+
} else if (!flushTimer) {
|
|
810
|
+
flushTimer = setTimeout(flush, FLUSH_MS);
|
|
811
|
+
}
|
|
812
|
+
} catch (_) {}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
for (const level of LEVELS) {
|
|
816
|
+
const original = console[level]
|
|
817
|
+
? console[level].bind(console)
|
|
818
|
+
: function () {};
|
|
819
|
+
console[level] = function () {
|
|
820
|
+
const args = Array.prototype.slice.call(arguments);
|
|
821
|
+
original.apply(null, args);
|
|
822
|
+
const component = slotName(new Error().stack);
|
|
823
|
+
if (component) forward(level, component, args);
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
window.addEventListener("error", function (event) {
|
|
828
|
+
const component = slotName(event.error && event.error.stack);
|
|
829
|
+
if (component) forward("error", component, [event.message]);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
833
|
+
const reason = event.reason;
|
|
834
|
+
const component = slotName(reason && reason.stack);
|
|
835
|
+
if (component) {
|
|
836
|
+
const message = reason && reason.message ? reason.message : String(reason);
|
|
837
|
+
forward("error", component, [message]);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
window.addEventListener("pagehide", flush);
|
|
842
|
+
})();`;
|
|
843
|
+
}
|
|
733
844
|
async function createBuildContext(components, options = {}) {
|
|
734
|
-
const {
|
|
845
|
+
const {
|
|
846
|
+
cwd = process.cwd(),
|
|
847
|
+
stage,
|
|
848
|
+
onBuildEnd,
|
|
849
|
+
browserLogsEndpoint
|
|
850
|
+
} = options;
|
|
735
851
|
const outdir = path4.join(cwd, "node_modules/.ollie", "build");
|
|
736
852
|
await fs4.mkdir(outdir, { recursive: true });
|
|
737
853
|
const entryPoints = {};
|
|
@@ -789,7 +905,8 @@ async function createBuildContext(components, options = {}) {
|
|
|
789
905
|
logLevel: "silent",
|
|
790
906
|
// We handle logging ourselves
|
|
791
907
|
jsx: "automatic",
|
|
792
|
-
plugins: [manifestPlugin]
|
|
908
|
+
plugins: [manifestPlugin],
|
|
909
|
+
...browserLogsEndpoint ? { banner: { js: browserLogBridge(browserLogsEndpoint) } } : {}
|
|
793
910
|
});
|
|
794
911
|
return ctx;
|
|
795
912
|
}
|
|
@@ -800,11 +917,13 @@ async function startDevServer(options = {}) {
|
|
|
800
917
|
cwd = process.cwd(),
|
|
801
918
|
stage,
|
|
802
919
|
onRequest,
|
|
803
|
-
onBuildEnd
|
|
920
|
+
onBuildEnd,
|
|
921
|
+
onBrowserLogs
|
|
804
922
|
} = options;
|
|
805
923
|
const servedir = path4.join(cwd, "node_modules/.ollie", "build");
|
|
806
924
|
const componentsDir = path4.join(cwd, "components");
|
|
807
925
|
const internalPort = port + 1;
|
|
926
|
+
const browserLogsEndpoint = onBrowserLogs ? `http://${host}:${port}/__log` : void 0;
|
|
808
927
|
let activeStage = stage ?? "prod";
|
|
809
928
|
let ctx = null;
|
|
810
929
|
let entryNames = /* @__PURE__ */ new Set();
|
|
@@ -813,7 +932,8 @@ async function startDevServer(options = {}) {
|
|
|
813
932
|
ctx = await createBuildContext(components, {
|
|
814
933
|
cwd,
|
|
815
934
|
stage: activeStage,
|
|
816
|
-
onBuildEnd
|
|
935
|
+
onBuildEnd,
|
|
936
|
+
browserLogsEndpoint
|
|
817
937
|
});
|
|
818
938
|
await ctx.rebuild();
|
|
819
939
|
await ctx.watch();
|
|
@@ -1118,6 +1238,45 @@ data: ${payload}
|
|
|
1118
1238
|
});
|
|
1119
1239
|
return;
|
|
1120
1240
|
}
|
|
1241
|
+
if (url.pathname === "/__log" && req.method === "POST") {
|
|
1242
|
+
if (!onBrowserLogs) {
|
|
1243
|
+
res.statusCode = 404;
|
|
1244
|
+
res.end();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1248
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1249
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
1250
|
+
const MAX_LOG_BODY = 64 * 1024;
|
|
1251
|
+
let body = "";
|
|
1252
|
+
let aborted = false;
|
|
1253
|
+
req.on("data", (chunk) => {
|
|
1254
|
+
if (aborted) return;
|
|
1255
|
+
body += chunk.toString();
|
|
1256
|
+
if (body.length > MAX_LOG_BODY) {
|
|
1257
|
+
aborted = true;
|
|
1258
|
+
res.statusCode = 413;
|
|
1259
|
+
res.end();
|
|
1260
|
+
req.destroy();
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
req.on("end", () => {
|
|
1264
|
+
if (aborted) return;
|
|
1265
|
+
try {
|
|
1266
|
+
const parsed2 = JSON.parse(body);
|
|
1267
|
+
const batch = parsed2.batch;
|
|
1268
|
+
const candidates = Array.isArray(batch) ? batch : [parsed2];
|
|
1269
|
+
const entries = candidates.filter(
|
|
1270
|
+
(entry) => typeof entry?.message === "string"
|
|
1271
|
+
);
|
|
1272
|
+
if (entries.length > 0) onBrowserLogs(entries);
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
res.statusCode = 204;
|
|
1276
|
+
res.end();
|
|
1277
|
+
});
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1121
1280
|
if (req.method === "OPTIONS") {
|
|
1122
1281
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1123
1282
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
@@ -1222,13 +1381,16 @@ function StartCommand({ args }) {
|
|
|
1222
1381
|
const [state, setState] = useState2({ status: "initializing" });
|
|
1223
1382
|
const [components, setComponents] = useState2([]);
|
|
1224
1383
|
const [logs, setLogs] = useState2([]);
|
|
1384
|
+
const [browserLogs, setBrowserLogs] = useState2([]);
|
|
1225
1385
|
const [buildCount, setBuildCount] = useState2(0);
|
|
1226
1386
|
const [lastBuildTime, setLastBuildTime] = useState2(null);
|
|
1227
1387
|
const logIdRef = useRef(0);
|
|
1388
|
+
const browserLogIdRef = useRef(0);
|
|
1228
1389
|
const rebuildRef = useRef(null);
|
|
1229
1390
|
const stopRef = useRef(null);
|
|
1230
1391
|
const stage = resolveStage(parseArg(args, "--stage", "-s"));
|
|
1231
1392
|
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
1393
|
+
const browserLogsEnabled = args.includes("--browser-logs");
|
|
1232
1394
|
const isInteractive = Boolean(process.stdin.isTTY);
|
|
1233
1395
|
const addLog = useCallback((log) => {
|
|
1234
1396
|
setLogs((prev) => {
|
|
@@ -1251,6 +1413,18 @@ function StartCommand({ args }) {
|
|
|
1251
1413
|
},
|
|
1252
1414
|
[addLog]
|
|
1253
1415
|
);
|
|
1416
|
+
const addBrowserLogs = useCallback((entries) => {
|
|
1417
|
+
if (entries.length === 0) return;
|
|
1418
|
+
setBrowserLogs((prev) => {
|
|
1419
|
+
const mapped = entries.map((entry) => ({
|
|
1420
|
+
...entry,
|
|
1421
|
+
message: sanitizeLogMessage(entry.message),
|
|
1422
|
+
id: ++browserLogIdRef.current,
|
|
1423
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1424
|
+
}));
|
|
1425
|
+
return [...prev, ...mapped].slice(-MAX_LOGS);
|
|
1426
|
+
});
|
|
1427
|
+
}, []);
|
|
1254
1428
|
useEffect2(() => {
|
|
1255
1429
|
let mounted = true;
|
|
1256
1430
|
async function init() {
|
|
@@ -1281,6 +1455,7 @@ function StartCommand({ args }) {
|
|
|
1281
1455
|
port: PORT,
|
|
1282
1456
|
stage,
|
|
1283
1457
|
onRequest: handleRequest,
|
|
1458
|
+
onBrowserLogs: browserLogsEnabled ? addBrowserLogs : void 0,
|
|
1284
1459
|
onBuildEnd: (updatedComponents) => {
|
|
1285
1460
|
setComponents(updatedComponents);
|
|
1286
1461
|
setBuildCount((c) => c + 1);
|
|
@@ -1321,7 +1496,7 @@ function StartCommand({ args }) {
|
|
|
1321
1496
|
mounted = false;
|
|
1322
1497
|
stopRef.current?.();
|
|
1323
1498
|
};
|
|
1324
|
-
}, [stage, handleRequest, noOpen]);
|
|
1499
|
+
}, [stage, handleRequest, addBrowserLogs, browserLogsEnabled, noOpen]);
|
|
1325
1500
|
useInput(
|
|
1326
1501
|
(input, key) => {
|
|
1327
1502
|
if (input === "q" || input === "c" && key.ctrl) {
|
|
@@ -1378,6 +1553,7 @@ function StartCommand({ args }) {
|
|
|
1378
1553
|
/* @__PURE__ */ jsx3(ComponentList, { components }),
|
|
1379
1554
|
/* @__PURE__ */ jsx3(BuildInfo, { buildCount, lastBuildTime }),
|
|
1380
1555
|
/* @__PURE__ */ jsx3(RequestLogs, { logs }),
|
|
1556
|
+
browserLogsEnabled && /* @__PURE__ */ jsx3(BrowserLogs, { logs: browserLogs }),
|
|
1381
1557
|
/* @__PURE__ */ jsx3(Footer, { interactive: isInteractive })
|
|
1382
1558
|
] })
|
|
1383
1559
|
] });
|
|
@@ -1506,6 +1682,38 @@ function RequestLogs({ logs }) {
|
|
|
1506
1682
|
] }, log.id)) })
|
|
1507
1683
|
] });
|
|
1508
1684
|
}
|
|
1685
|
+
function sanitizeLogMessage(message) {
|
|
1686
|
+
let out = "";
|
|
1687
|
+
for (const ch of message) {
|
|
1688
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1689
|
+
out += code < 32 || code >= 127 && code <= 159 ? " " : ch;
|
|
1690
|
+
if (out.length >= 2e3) break;
|
|
1691
|
+
}
|
|
1692
|
+
return out;
|
|
1693
|
+
}
|
|
1694
|
+
function browserLogColor(level) {
|
|
1695
|
+
if (level === "error") return "red";
|
|
1696
|
+
if (level === "warn") return "yellow";
|
|
1697
|
+
if (level === "info") return "cyan";
|
|
1698
|
+
return "gray";
|
|
1699
|
+
}
|
|
1700
|
+
function BrowserLogs({ logs }) {
|
|
1701
|
+
return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1702
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Browser Logs:" }),
|
|
1703
|
+
/* @__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: [
|
|
1704
|
+
/* @__PURE__ */ jsxs3(Text3, { color: browserLogColor(log.level), children: [
|
|
1705
|
+
log.level.toUpperCase(),
|
|
1706
|
+
" "
|
|
1707
|
+
] }),
|
|
1708
|
+
log.component && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1709
|
+
"[",
|
|
1710
|
+
log.component,
|
|
1711
|
+
"] "
|
|
1712
|
+
] }),
|
|
1713
|
+
/* @__PURE__ */ jsx3(Text3, { children: log.message })
|
|
1714
|
+
] }, log.id)) })
|
|
1715
|
+
] });
|
|
1716
|
+
}
|
|
1509
1717
|
function Footer({ interactive = true }) {
|
|
1510
1718
|
if (!interactive) {
|
|
1511
1719
|
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 +1750,7 @@ function App({ command, args }) {
|
|
|
1542
1750
|
}
|
|
1543
1751
|
}
|
|
1544
1752
|
function VersionCommand() {
|
|
1545
|
-
const version = "1.
|
|
1753
|
+
const version = "1.7.0" ? "1.7.0" : "unknown";
|
|
1546
1754
|
return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1547
1755
|
"ollieshop v",
|
|
1548
1756
|
version
|
|
@@ -2068,6 +2276,14 @@ var componentCreateSchema = z2.object({
|
|
|
2068
2276
|
active: z2.boolean().default(true).describe("Whether component is active"),
|
|
2069
2277
|
props: z2.record(z2.unknown()).nullable().default(null).describe("Default component props as JSON object")
|
|
2070
2278
|
});
|
|
2279
|
+
var componentUpdateSchema = z2.object({
|
|
2280
|
+
id: z2.string().uuid().describe("Component UUID to update"),
|
|
2281
|
+
versionId: z2.string().uuid().optional().describe("Move component to a different version UUID"),
|
|
2282
|
+
name: z2.string().min(1).optional().describe("Component name"),
|
|
2283
|
+
slot: z2.string().min(1).optional().describe("Target slot"),
|
|
2284
|
+
active: z2.boolean().optional().describe("Whether component is active"),
|
|
2285
|
+
props: z2.record(z2.unknown()).nullable().optional().describe("Component props as JSON object (null clears them)")
|
|
2286
|
+
});
|
|
2071
2287
|
var componentListSchema = z2.object({
|
|
2072
2288
|
storeId: z2.string().uuid().describe("Store UUID"),
|
|
2073
2289
|
versionId: z2.string().uuid().optional().describe("Optional version UUID to filter by")
|
|
@@ -2099,6 +2315,7 @@ var schemas = {
|
|
|
2099
2315
|
},
|
|
2100
2316
|
component: {
|
|
2101
2317
|
create: componentCreateSchema,
|
|
2318
|
+
update: componentUpdateSchema,
|
|
2102
2319
|
list: componentListSchema
|
|
2103
2320
|
},
|
|
2104
2321
|
function: {
|
|
@@ -2155,6 +2372,32 @@ async function createComponent(client, input) {
|
|
|
2155
2372
|
}
|
|
2156
2373
|
return { data: { id: data.id } };
|
|
2157
2374
|
}
|
|
2375
|
+
async function updateComponent(client, id, input) {
|
|
2376
|
+
const parsed2 = componentUpdateSchema.safeParse({ id, ...input });
|
|
2377
|
+
if (!parsed2.success) {
|
|
2378
|
+
return {
|
|
2379
|
+
error: { message: parsed2.error.issues.map((i) => i.message).join("; ") }
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
const updatePayload = {};
|
|
2383
|
+
if (input.name !== void 0) updatePayload.name = input.name;
|
|
2384
|
+
if (input.slot !== void 0) updatePayload.slot = input.slot;
|
|
2385
|
+
if (input.active !== void 0) updatePayload.active = input.active;
|
|
2386
|
+
if (input.versionId !== void 0) updatePayload.version_id = input.versionId;
|
|
2387
|
+
if (input.props !== void 0) updatePayload.props = input.props;
|
|
2388
|
+
if (Object.keys(updatePayload).length === 0) {
|
|
2389
|
+
return {
|
|
2390
|
+
error: {
|
|
2391
|
+
message: "No fields to update. Provide at least one of: --name, --slot, --active, --props, --version-id."
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
const { data, error } = await client.from("components").update(updatePayload).eq("id", id).select("id").single();
|
|
2396
|
+
if (error) {
|
|
2397
|
+
return { error: { message: error.message } };
|
|
2398
|
+
}
|
|
2399
|
+
return { data: { id: data.id } };
|
|
2400
|
+
}
|
|
2158
2401
|
async function listComponents(client, storeId, versionId) {
|
|
2159
2402
|
let query = client.from("components").select(
|
|
2160
2403
|
"id, name, slot, active, version_id, props, created_at, versions!inner(id, name)"
|
|
@@ -2173,9 +2416,10 @@ async function listComponents(client, storeId, versionId) {
|
|
|
2173
2416
|
async function componentCommand(parsed2) {
|
|
2174
2417
|
const sub = parsed2.subcommand;
|
|
2175
2418
|
if (sub === "create") return componentCreateCommand(parsed2);
|
|
2419
|
+
if (sub === "update") return componentUpdateCommand(parsed2);
|
|
2176
2420
|
if (sub === "list" || sub === "ls") return componentListCommand(parsed2);
|
|
2177
2421
|
console.error(
|
|
2178
|
-
`Unknown component subcommand: ${sub}. Use: component create | component list`
|
|
2422
|
+
`Unknown component subcommand: ${sub}. Use: component create | component update | component list`
|
|
2179
2423
|
);
|
|
2180
2424
|
process.exit(1);
|
|
2181
2425
|
}
|
|
@@ -2226,6 +2470,66 @@ async function componentCreateCommand(parsed2) {
|
|
|
2226
2470
|
process.exit(1);
|
|
2227
2471
|
}
|
|
2228
2472
|
}
|
|
2473
|
+
async function componentUpdateCommand(parsed2) {
|
|
2474
|
+
const format = detectOutputFormat(parsed2.global.output);
|
|
2475
|
+
try {
|
|
2476
|
+
let id;
|
|
2477
|
+
let input;
|
|
2478
|
+
if (parsed2.global.data) {
|
|
2479
|
+
const raw = JSON.parse(parsed2.global.data);
|
|
2480
|
+
id = validateUuid(validateRequired(raw.id, "id"), "id");
|
|
2481
|
+
input = {
|
|
2482
|
+
versionId: raw.versionId !== void 0 ? validateUuid(raw.versionId, "versionId") : void 0,
|
|
2483
|
+
name: raw.name,
|
|
2484
|
+
slot: raw.slot,
|
|
2485
|
+
active: typeof raw.active === "boolean" ? raw.active : void 0,
|
|
2486
|
+
props: raw.props
|
|
2487
|
+
};
|
|
2488
|
+
} else {
|
|
2489
|
+
id = validateUuid(
|
|
2490
|
+
validateRequired(getFlag(parsed2.flags, "id"), "id"),
|
|
2491
|
+
"id"
|
|
2492
|
+
);
|
|
2493
|
+
const versionId = getFlag(parsed2.flags, "version-id");
|
|
2494
|
+
if (versionId) validateUuid(versionId, "version-id");
|
|
2495
|
+
const activeRaw = parsed2.flags.active;
|
|
2496
|
+
let active;
|
|
2497
|
+
if (activeRaw === "true" || activeRaw === true) {
|
|
2498
|
+
active = true;
|
|
2499
|
+
} else if (activeRaw === "false") {
|
|
2500
|
+
active = false;
|
|
2501
|
+
}
|
|
2502
|
+
const propsRaw = getFlag(parsed2.flags, "props");
|
|
2503
|
+
input = {
|
|
2504
|
+
versionId,
|
|
2505
|
+
name: getFlag(parsed2.flags, "name", "n"),
|
|
2506
|
+
slot: getFlag(parsed2.flags, "slot", "s"),
|
|
2507
|
+
active,
|
|
2508
|
+
props: propsRaw ? JSON.parse(propsRaw) : void 0
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
if (parsed2.global.dryRun) {
|
|
2512
|
+
outputDryRun(
|
|
2513
|
+
"component.update",
|
|
2514
|
+
{ id, ...input },
|
|
2515
|
+
format
|
|
2516
|
+
);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const client = await getAuthenticatedClient();
|
|
2520
|
+
const result = await updateComponent(client, id, input);
|
|
2521
|
+
outputResult(result, format, parsed2.global.fields);
|
|
2522
|
+
if (result.error) process.exit(1);
|
|
2523
|
+
} catch (err) {
|
|
2524
|
+
outputResult(
|
|
2525
|
+
{
|
|
2526
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
2527
|
+
},
|
|
2528
|
+
format
|
|
2529
|
+
);
|
|
2530
|
+
process.exit(1);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2229
2533
|
async function componentListCommand(parsed2) {
|
|
2230
2534
|
const format = detectOutputFormat(parsed2.global.output);
|
|
2231
2535
|
try {
|
|
@@ -2393,7 +2697,8 @@ async function deployCommand(parsed2) {
|
|
|
2393
2697
|
}
|
|
2394
2698
|
const stream = await createComponentBundle({
|
|
2395
2699
|
componentName,
|
|
2396
|
-
cwd: process.cwd()
|
|
2700
|
+
cwd: process.cwd(),
|
|
2701
|
+
resourceType
|
|
2397
2702
|
});
|
|
2398
2703
|
const chunks = [];
|
|
2399
2704
|
for await (const chunk of stream) {
|
package/package.json
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createComponent,
|
|
3
|
+
listComponents,
|
|
4
|
+
updateComponent,
|
|
5
|
+
} from "../core/component.js";
|
|
2
6
|
import {
|
|
3
7
|
detectOutputFormat,
|
|
4
8
|
outputDryRun,
|
|
@@ -11,10 +15,11 @@ import { validateRequired, validateUuid } from "../utils/validate.js";
|
|
|
11
15
|
export async function componentCommand(parsed: ParsedArgs): Promise<void> {
|
|
12
16
|
const sub = parsed.subcommand;
|
|
13
17
|
if (sub === "create") return componentCreateCommand(parsed);
|
|
18
|
+
if (sub === "update") return componentUpdateCommand(parsed);
|
|
14
19
|
if (sub === "list" || sub === "ls") return componentListCommand(parsed);
|
|
15
20
|
|
|
16
21
|
console.error(
|
|
17
|
-
`Unknown component subcommand: ${sub}. Use: component create | component list`,
|
|
22
|
+
`Unknown component subcommand: ${sub}. Use: component create | component update | component list`,
|
|
18
23
|
);
|
|
19
24
|
process.exit(1);
|
|
20
25
|
}
|
|
@@ -79,6 +84,87 @@ async function componentCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
async function componentUpdateCommand(parsed: ParsedArgs): Promise<void> {
|
|
88
|
+
const format = detectOutputFormat(parsed.global.output);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
let id: string;
|
|
92
|
+
let input: {
|
|
93
|
+
versionId?: string;
|
|
94
|
+
name?: string;
|
|
95
|
+
slot?: string;
|
|
96
|
+
active?: boolean;
|
|
97
|
+
props?: Record<string, unknown> | null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (parsed.global.data) {
|
|
101
|
+
const raw = JSON.parse(parsed.global.data);
|
|
102
|
+
id = validateUuid(validateRequired(raw.id, "id"), "id");
|
|
103
|
+
input = {
|
|
104
|
+
versionId:
|
|
105
|
+
raw.versionId !== undefined
|
|
106
|
+
? validateUuid(raw.versionId, "versionId")
|
|
107
|
+
: undefined,
|
|
108
|
+
name: raw.name,
|
|
109
|
+
slot: raw.slot,
|
|
110
|
+
active: typeof raw.active === "boolean" ? raw.active : undefined,
|
|
111
|
+
props: raw.props,
|
|
112
|
+
};
|
|
113
|
+
} else {
|
|
114
|
+
id = validateUuid(
|
|
115
|
+
validateRequired(getFlag(parsed.flags, "id"), "id"),
|
|
116
|
+
"id",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const versionId = getFlag(parsed.flags, "version-id");
|
|
120
|
+
if (versionId) validateUuid(versionId, "version-id");
|
|
121
|
+
|
|
122
|
+
const activeRaw = parsed.flags.active;
|
|
123
|
+
let active: boolean | undefined;
|
|
124
|
+
if (activeRaw === "true" || activeRaw === true) {
|
|
125
|
+
active = true;
|
|
126
|
+
} else if (activeRaw === "false") {
|
|
127
|
+
active = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const propsRaw = getFlag(parsed.flags, "props");
|
|
131
|
+
|
|
132
|
+
input = {
|
|
133
|
+
versionId,
|
|
134
|
+
name: getFlag(parsed.flags, "name", "n"),
|
|
135
|
+
slot: getFlag(parsed.flags, "slot", "s"),
|
|
136
|
+
active,
|
|
137
|
+
props: propsRaw
|
|
138
|
+
? (JSON.parse(propsRaw) as Record<string, unknown> | null)
|
|
139
|
+
: undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parsed.global.dryRun) {
|
|
144
|
+
outputDryRun(
|
|
145
|
+
"component.update",
|
|
146
|
+
{ id, ...input } as Record<string, unknown>,
|
|
147
|
+
format,
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const client = await getAuthenticatedClient();
|
|
153
|
+
const result = await updateComponent(client, id, input);
|
|
154
|
+
|
|
155
|
+
outputResult(result, format, parsed.global.fields);
|
|
156
|
+
if (result.error) process.exit(1);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
outputResult(
|
|
159
|
+
{
|
|
160
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
161
|
+
},
|
|
162
|
+
format,
|
|
163
|
+
);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
82
168
|
async function componentListCommand(parsed: ParsedArgs): Promise<void> {
|
|
83
169
|
const format = detectOutputFormat(parsed.global.output);
|
|
84
170
|
|
|
@@ -61,10 +61,11 @@ export async function deployCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
61
61
|
timeout = Number(getFlag(parsed.flags, "timeout") ?? "300");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Bundle the
|
|
64
|
+
// Bundle the resource
|
|
65
65
|
const stream = await createComponentBundle({
|
|
66
66
|
componentName,
|
|
67
67
|
cwd: process.cwd(),
|
|
68
|
+
resourceType,
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
// Collect the zip buffer
|
package/src/commands/help.tsx
CHANGED
|
@@ -57,9 +57,9 @@ export function HelpCommand() {
|
|
|
57
57
|
</Box>
|
|
58
58
|
<Box>
|
|
59
59
|
<Box width={24}>
|
|
60
|
-
<Text color="green">component create|list</Text>
|
|
60
|
+
<Text color="green">component create|update|list</Text>
|
|
61
61
|
</Box>
|
|
62
|
-
<Text>Create or list components</Text>
|
|
62
|
+
<Text>Create, update or list components</Text>
|
|
63
63
|
</Box>
|
|
64
64
|
<Box>
|
|
65
65
|
<Box width={24}>
|
|
@@ -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/core/component.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { componentCreateSchema } from "./schema.js";
|
|
2
|
+
import { componentCreateSchema, componentUpdateSchema } from "./schema.js";
|
|
3
3
|
|
|
4
4
|
export interface CreateComponentInput {
|
|
5
5
|
versionId: string;
|
|
@@ -9,6 +9,14 @@ export interface CreateComponentInput {
|
|
|
9
9
|
props?: Record<string, unknown> | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface UpdateComponentInput {
|
|
13
|
+
versionId?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
slot?: string;
|
|
16
|
+
active?: boolean;
|
|
17
|
+
props?: Record<string, unknown> | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export interface ComponentRecord {
|
|
13
21
|
id: string;
|
|
14
22
|
name: string;
|
|
@@ -50,6 +58,48 @@ export async function createComponent(
|
|
|
50
58
|
return { data: { id: data.id } };
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
export async function updateComponent(
|
|
62
|
+
client: SupabaseClient,
|
|
63
|
+
id: string,
|
|
64
|
+
input: UpdateComponentInput,
|
|
65
|
+
): Promise<{ data?: { id: string }; error?: { message: string } }> {
|
|
66
|
+
const parsed = componentUpdateSchema.safeParse({ id, ...input });
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
return {
|
|
69
|
+
error: { message: parsed.error.issues.map((i) => i.message).join("; ") },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const updatePayload: Record<string, unknown> = {};
|
|
74
|
+
if (input.name !== undefined) updatePayload.name = input.name;
|
|
75
|
+
if (input.slot !== undefined) updatePayload.slot = input.slot;
|
|
76
|
+
if (input.active !== undefined) updatePayload.active = input.active;
|
|
77
|
+
if (input.versionId !== undefined) updatePayload.version_id = input.versionId;
|
|
78
|
+
if (input.props !== undefined) updatePayload.props = input.props;
|
|
79
|
+
|
|
80
|
+
if (Object.keys(updatePayload).length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
error: {
|
|
83
|
+
message:
|
|
84
|
+
"No fields to update. Provide at least one of: --name, --slot, --active, --props, --version-id.",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { data, error } = await client
|
|
90
|
+
.from("components")
|
|
91
|
+
.update(updatePayload)
|
|
92
|
+
.eq("id", id)
|
|
93
|
+
.select("id")
|
|
94
|
+
.single();
|
|
95
|
+
|
|
96
|
+
if (error) {
|
|
97
|
+
return { error: { message: error.message } };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { data: { id: data.id } };
|
|
101
|
+
}
|
|
102
|
+
|
|
53
103
|
export async function listComponents(
|
|
54
104
|
client: SupabaseClient,
|
|
55
105
|
storeId: string,
|
package/src/core/schema.ts
CHANGED
|
@@ -52,6 +52,23 @@ export const componentCreateSchema = z.object({
|
|
|
52
52
|
.describe("Default component props as JSON object"),
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
export const componentUpdateSchema = z.object({
|
|
56
|
+
id: z.string().uuid().describe("Component UUID to update"),
|
|
57
|
+
versionId: z
|
|
58
|
+
.string()
|
|
59
|
+
.uuid()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Move component to a different version UUID"),
|
|
62
|
+
name: z.string().min(1).optional().describe("Component name"),
|
|
63
|
+
slot: z.string().min(1).optional().describe("Target slot"),
|
|
64
|
+
active: z.boolean().optional().describe("Whether component is active"),
|
|
65
|
+
props: z
|
|
66
|
+
.record(z.unknown())
|
|
67
|
+
.nullable()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Component props as JSON object (null clears them)"),
|
|
70
|
+
});
|
|
71
|
+
|
|
55
72
|
export const componentListSchema = z.object({
|
|
56
73
|
storeId: z.string().uuid().describe("Store UUID"),
|
|
57
74
|
versionId: z
|
|
@@ -109,6 +126,7 @@ const schemas: Record<string, SchemaMap> = {
|
|
|
109
126
|
},
|
|
110
127
|
component: {
|
|
111
128
|
create: componentCreateSchema,
|
|
129
|
+
update: componentUpdateSchema,
|
|
112
130
|
list: componentListSchema,
|
|
113
131
|
},
|
|
114
132
|
function: {
|
package/src/utils/bundle.ts
CHANGED
|
@@ -5,11 +5,15 @@ import archiver from "archiver";
|
|
|
5
5
|
|
|
6
6
|
// TODO: Implement .ollieignore file support for custom exclusions
|
|
7
7
|
|
|
8
|
+
export type BundleResourceType = "component" | "function";
|
|
9
|
+
|
|
8
10
|
export interface BundleOptions {
|
|
9
11
|
/** Project root directory */
|
|
10
12
|
cwd?: string;
|
|
11
13
|
/** Component name (folder name in components/) */
|
|
12
14
|
componentName: string;
|
|
15
|
+
/** Resource type — decides the source folder and entry point shape */
|
|
16
|
+
resourceType?: BundleResourceType;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export interface BundleResult {
|
|
@@ -33,14 +37,21 @@ export interface BundleResult {
|
|
|
33
37
|
export async function createComponentBundle(
|
|
34
38
|
options: BundleOptions,
|
|
35
39
|
): Promise<PassThrough> {
|
|
36
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
cwd = process.cwd(),
|
|
42
|
+
componentName,
|
|
43
|
+
resourceType = "component",
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
const sourceDir = resourceType === "function" ? "functions" : "components";
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
const componentDir = path.join(cwd, "components", componentName);
|
|
48
|
+
const componentDir = path.join(cwd, sourceDir, componentName);
|
|
40
49
|
try {
|
|
41
50
|
await fs.access(componentDir);
|
|
42
51
|
} catch {
|
|
43
|
-
throw new Error(
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${resourceType === "function" ? "Function" : "Component"} "${componentName}" not found in ${sourceDir}/`,
|
|
54
|
+
);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
// Verify package.json exists
|
|
@@ -67,8 +78,13 @@ export async function createComponentBundle(
|
|
|
67
78
|
});
|
|
68
79
|
|
|
69
80
|
// Add synthetic entry point
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
if (resourceType === "function") {
|
|
82
|
+
const entryPoint = `export { handler } from './functions/${componentName}';\n`;
|
|
83
|
+
archive.append(entryPoint, { name: "index.ts" });
|
|
84
|
+
} else {
|
|
85
|
+
const entryPoint = `export { default } from './components/${componentName}';\n`;
|
|
86
|
+
archive.append(entryPoint, { name: "index.tsx" });
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
// Add package.json
|
|
74
90
|
archive.file(packageJsonPath, { name: "package.json" });
|
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", "*");
|