@ollie-shop/cli 1.3.3 → 1.4.1
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/CONTEXT.md +8 -0
- package/README.md +1 -0
- package/dist/index.js +121 -27
- package/package.json +4 -5
- package/src/commands/help.tsx +7 -0
- package/src/commands/start.tsx +53 -27
- package/src/utils/esbuild.ts +86 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @ollie-shop/cli@1.
|
|
2
|
+
> @ollie-shop/cli@1.4.1 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[32m95.89 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 248ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ollie-shop/cli
|
|
2
2
|
|
|
3
|
+
## 1.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 630ae3e: Keep Studio live reload working after a component is added or removed. The dev server proxy now owns the `/esbuild` connection and re-subscribes across context recreates, so the browser's EventSource no longer drops when the esbuild context is disposed.
|
|
8
|
+
|
|
9
|
+
## 1.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- bac681e: Add a `--no-open` flag to `ollieshop start` (also honored via the `CI` env var) so the dev server can run headless without auto-opening Studio in the browser — useful when an agent spawns it and drives its own preview harness. `ollieshop start` now also runs without a TTY: it no longer crashes with "Raw mode is not supported" when backgrounded, and simply disables keyboard shortcuts in that case. Upgrade the CLI to React 19 and Ink 6.
|
|
14
|
+
|
|
3
15
|
## 1.3.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/CONTEXT.md
CHANGED
|
@@ -78,6 +78,14 @@ The `deploy` command bundles a component directory into a zip, uploads it to the
|
|
|
78
78
|
|
|
79
79
|
Terminal build statuses: `SUCCEEDED`, `FAILED`, `STOPPED`, `TIMED_OUT`, `FAULT`
|
|
80
80
|
|
|
81
|
+
## Dev Server (agent / harness mode)
|
|
82
|
+
|
|
83
|
+
`ollieshop start` runs the local build+serve dev server (port 4000: `/manifest.json`, `/<Name>/index.js`, `/<Name>/index.css`, SSE `/esbuild`) and normally **auto-opens the admin Studio in a browser**. When you (an agent) are spawning it, pass `--no-open` so it does **not** open a browser — then drive your own preview harness against the dev server instead. The flag is also honored automatically when the `CI` env var is set.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
ollieshop start --no-open # build+serve only; no browser is opened
|
|
87
|
+
```
|
|
88
|
+
|
|
81
89
|
## Response Format
|
|
82
90
|
|
|
83
91
|
All commands return:
|
package/README.md
CHANGED
|
@@ -172,6 +172,7 @@ ollieshop init --store-id <STORE_UUID> --version-id <VERSION_UUID> -o json
|
|
|
172
172
|
| `--fields a,b,c` | | Limit output fields (comma-separated) |
|
|
173
173
|
| `--data '{...}'` | `-d` | Raw JSON payload for mutations (alternative to individual flags) |
|
|
174
174
|
| `--stage <name>` | `-s` | Config stage — loads `ollie.<stage>.json` instead of `ollie.json` |
|
|
175
|
+
| `--no-open` | | `start` only: don't auto-open Studio in the browser (also honored via the `CI` env var) |
|
|
175
176
|
|
|
176
177
|
## Response Format
|
|
177
178
|
|
package/dist/index.js
CHANGED
|
@@ -107,12 +107,17 @@ function HelpCommand() {
|
|
|
107
107
|
"{stage}",
|
|
108
108
|
".json)"
|
|
109
109
|
] })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
112
|
+
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "--no-open" }) }),
|
|
113
|
+
/* @__PURE__ */ jsx(Text, { children: "start: don't auto-open Studio (also honored via CI env)" })
|
|
110
114
|
] })
|
|
111
115
|
] }),
|
|
112
116
|
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: "Examples:" }) }),
|
|
113
117
|
/* @__PURE__ */ jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [
|
|
114
118
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop login" }),
|
|
115
119
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --stage dev" }),
|
|
120
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --no-open" }),
|
|
116
121
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop whoami -o json" }),
|
|
117
122
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop schema store.create" }),
|
|
118
123
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: '$ ollieshop store create --name "My Store" --platform vtex --platform-store-id mystore' }),
|
|
@@ -695,6 +700,7 @@ async function startDevServer(options = {}) {
|
|
|
695
700
|
ctx = null;
|
|
696
701
|
if (oldCtx) await oldCtx.dispose();
|
|
697
702
|
await buildAndServe(components);
|
|
703
|
+
notifyComponentsChanged(components);
|
|
698
704
|
}
|
|
699
705
|
await buildAndServe(await discoverComponents({ cwd, stage }));
|
|
700
706
|
async function currentComponentNames() {
|
|
@@ -732,8 +738,80 @@ async function startDevServer(options = {}) {
|
|
|
732
738
|
if (watchTimer) clearTimeout(watchTimer);
|
|
733
739
|
watchTimer = setTimeout(maybeRecreate, 150);
|
|
734
740
|
});
|
|
741
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
742
|
+
let upstreamReq = null;
|
|
743
|
+
let upstreamRetry = null;
|
|
744
|
+
function broadcast(chunk) {
|
|
745
|
+
for (const client of sseClients) {
|
|
746
|
+
if (!client.writableEnded) client.write(chunk);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function connectUpstream() {
|
|
750
|
+
if (upstreamReq || sseClients.size === 0) return;
|
|
751
|
+
const req = http.request(
|
|
752
|
+
{
|
|
753
|
+
hostname: host,
|
|
754
|
+
port: internalPort,
|
|
755
|
+
path: "/esbuild",
|
|
756
|
+
method: "GET",
|
|
757
|
+
headers: { accept: "text/event-stream" }
|
|
758
|
+
},
|
|
759
|
+
(upstream) => {
|
|
760
|
+
upstream.on("data", broadcast);
|
|
761
|
+
upstream.on("close", onUpstreamLost);
|
|
762
|
+
upstream.on("error", onUpstreamLost);
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
req.on("error", onUpstreamLost);
|
|
766
|
+
req.end();
|
|
767
|
+
upstreamReq = req;
|
|
768
|
+
}
|
|
769
|
+
function onUpstreamLost() {
|
|
770
|
+
if (!upstreamReq) return;
|
|
771
|
+
upstreamReq = null;
|
|
772
|
+
if (upstreamRetry) clearTimeout(upstreamRetry);
|
|
773
|
+
if (sseClients.size === 0) return;
|
|
774
|
+
upstreamRetry = setTimeout(connectUpstream, 200);
|
|
775
|
+
}
|
|
776
|
+
function teardownUpstream() {
|
|
777
|
+
if (upstreamRetry) {
|
|
778
|
+
clearTimeout(upstreamRetry);
|
|
779
|
+
upstreamRetry = null;
|
|
780
|
+
}
|
|
781
|
+
const req = upstreamReq;
|
|
782
|
+
upstreamReq = null;
|
|
783
|
+
req?.destroy();
|
|
784
|
+
}
|
|
785
|
+
function notifyComponentsChanged(components) {
|
|
786
|
+
if (sseClients.size === 0) return;
|
|
787
|
+
const updated = components.map((c) => `/${c.name}/index.js`);
|
|
788
|
+
const payload = JSON.stringify({ added: [], removed: [], updated });
|
|
789
|
+
broadcast(`event: change
|
|
790
|
+
data: ${payload}
|
|
791
|
+
|
|
792
|
+
`);
|
|
793
|
+
}
|
|
735
794
|
const proxyServer = http.createServer(async (req, res) => {
|
|
736
795
|
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
796
|
+
if (url.pathname === "/esbuild" && req.method === "GET") {
|
|
797
|
+
res.writeHead(200, {
|
|
798
|
+
"Content-Type": "text/event-stream",
|
|
799
|
+
"Cache-Control": "no-cache",
|
|
800
|
+
Connection: "keep-alive",
|
|
801
|
+
"Access-Control-Allow-Origin": "*",
|
|
802
|
+
"Access-Control-Allow-Private-Network": "true"
|
|
803
|
+
});
|
|
804
|
+
res.write("retry: 500\n\n");
|
|
805
|
+
sseClients.add(res);
|
|
806
|
+
connectUpstream();
|
|
807
|
+
const dropClient = () => {
|
|
808
|
+
sseClients.delete(res);
|
|
809
|
+
if (sseClients.size === 0) teardownUpstream();
|
|
810
|
+
};
|
|
811
|
+
req.on("close", dropClient);
|
|
812
|
+
res.on("error", dropClient);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
737
815
|
if (url.pathname === "/bundle" && req.method === "GET") {
|
|
738
816
|
const componentPath = url.searchParams.get("path");
|
|
739
817
|
if (!componentPath) {
|
|
@@ -891,6 +969,9 @@ async function startDevServer(options = {}) {
|
|
|
891
969
|
stop: async () => {
|
|
892
970
|
if (watchTimer) clearTimeout(watchTimer);
|
|
893
971
|
componentsWatcher.close();
|
|
972
|
+
teardownUpstream();
|
|
973
|
+
for (const client of sseClients) client.end();
|
|
974
|
+
sseClients.clear();
|
|
894
975
|
proxyServer.close();
|
|
895
976
|
await ctx?.dispose();
|
|
896
977
|
}
|
|
@@ -945,6 +1026,8 @@ function StartCommand({ args }) {
|
|
|
945
1026
|
const rebuildRef = useRef(null);
|
|
946
1027
|
const stopRef = useRef(null);
|
|
947
1028
|
const stage = resolveStage(parseArg(args, "--stage", "-s"));
|
|
1029
|
+
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
1030
|
+
const isInteractive = Boolean(process.stdin.isTTY);
|
|
948
1031
|
const addLog = useCallback((log) => {
|
|
949
1032
|
setLogs((prev) => {
|
|
950
1033
|
const newLog = {
|
|
@@ -1015,12 +1098,14 @@ function StartCommand({ args }) {
|
|
|
1015
1098
|
storeId: config.storeId,
|
|
1016
1099
|
versionId: config.versionId
|
|
1017
1100
|
});
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1101
|
+
if (!noOpen) {
|
|
1102
|
+
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
1103
|
+
studioUrl.searchParams.set("storeId", config.storeId);
|
|
1104
|
+
if (config.versionId) {
|
|
1105
|
+
studioUrl.searchParams.set("versionId", config.versionId);
|
|
1106
|
+
}
|
|
1107
|
+
open(studioUrl.toString());
|
|
1022
1108
|
}
|
|
1023
|
-
open(studioUrl.toString());
|
|
1024
1109
|
} catch (error) {
|
|
1025
1110
|
if (!mounted) return;
|
|
1026
1111
|
setState({
|
|
@@ -1034,23 +1119,26 @@ function StartCommand({ args }) {
|
|
|
1034
1119
|
mounted = false;
|
|
1035
1120
|
stopRef.current?.();
|
|
1036
1121
|
};
|
|
1037
|
-
}, [stage, handleRequest]);
|
|
1038
|
-
useInput(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (input === "r" && state.status === "running") {
|
|
1043
|
-
rebuildRef.current?.();
|
|
1044
|
-
}
|
|
1045
|
-
if (input === "o" && state.status === "running") {
|
|
1046
|
-
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
1047
|
-
studioUrl.searchParams.set("storeId", state.storeId);
|
|
1048
|
-
if (state.versionId) {
|
|
1049
|
-
studioUrl.searchParams.set("versionId", state.versionId);
|
|
1122
|
+
}, [stage, handleRequest, noOpen]);
|
|
1123
|
+
useInput(
|
|
1124
|
+
(input, key) => {
|
|
1125
|
+
if (input === "q" || input === "c" && key.ctrl) {
|
|
1126
|
+
stopRef.current?.().then(() => exit());
|
|
1050
1127
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1128
|
+
if (input === "r" && state.status === "running") {
|
|
1129
|
+
rebuildRef.current?.();
|
|
1130
|
+
}
|
|
1131
|
+
if (input === "o" && state.status === "running") {
|
|
1132
|
+
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
1133
|
+
studioUrl.searchParams.set("storeId", state.storeId);
|
|
1134
|
+
if (state.versionId) {
|
|
1135
|
+
studioUrl.searchParams.set("versionId", state.versionId);
|
|
1136
|
+
}
|
|
1137
|
+
open(studioUrl.toString());
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
{ isActive: isInteractive }
|
|
1141
|
+
);
|
|
1054
1142
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
1055
1143
|
/* @__PURE__ */ jsx3(Header, {}),
|
|
1056
1144
|
state.status === "initializing" && /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
@@ -1081,13 +1169,14 @@ function StartCommand({ args }) {
|
|
|
1081
1169
|
port: state.port,
|
|
1082
1170
|
stage,
|
|
1083
1171
|
storeId: state.storeId,
|
|
1084
|
-
versionId: state.versionId
|
|
1172
|
+
versionId: state.versionId,
|
|
1173
|
+
noOpen
|
|
1085
1174
|
}
|
|
1086
1175
|
),
|
|
1087
1176
|
/* @__PURE__ */ jsx3(ComponentList, { components }),
|
|
1088
1177
|
/* @__PURE__ */ jsx3(BuildInfo, { buildCount, lastBuildTime }),
|
|
1089
1178
|
/* @__PURE__ */ jsx3(RequestLogs, { logs }),
|
|
1090
|
-
/* @__PURE__ */ jsx3(Footer, {})
|
|
1179
|
+
/* @__PURE__ */ jsx3(Footer, { interactive: isInteractive })
|
|
1091
1180
|
] })
|
|
1092
1181
|
] });
|
|
1093
1182
|
}
|
|
@@ -1102,7 +1191,8 @@ function ServerInfo({
|
|
|
1102
1191
|
port,
|
|
1103
1192
|
stage,
|
|
1104
1193
|
storeId,
|
|
1105
|
-
versionId
|
|
1194
|
+
versionId,
|
|
1195
|
+
noOpen
|
|
1106
1196
|
}) {
|
|
1107
1197
|
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
1108
1198
|
studioUrl.searchParams.set("storeId", storeId);
|
|
@@ -1129,7 +1219,8 @@ function ServerInfo({
|
|
|
1129
1219
|
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
|
|
1130
1220
|
/* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713 " }),
|
|
1131
1221
|
/* @__PURE__ */ jsx3(Text3, { children: "Studio: " }),
|
|
1132
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: studioUrl.toString() })
|
|
1222
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: studioUrl.toString() }),
|
|
1223
|
+
noOpen && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (auto-open off \u2014 press o)" })
|
|
1133
1224
|
] }),
|
|
1134
1225
|
/* @__PURE__ */ jsx3(Box3, { marginLeft: 2, marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1135
1226
|
"Components: http://",
|
|
@@ -1213,7 +1304,10 @@ function RequestLogs({ logs }) {
|
|
|
1213
1304
|
] }, log.id)) })
|
|
1214
1305
|
] });
|
|
1215
1306
|
}
|
|
1216
|
-
function Footer() {
|
|
1307
|
+
function Footer({ interactive = true }) {
|
|
1308
|
+
if (!interactive) {
|
|
1309
|
+
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" }) });
|
|
1310
|
+
}
|
|
1217
1311
|
return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
|
|
1218
1312
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Press " }),
|
|
1219
1313
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "q" }),
|
|
@@ -1246,7 +1340,7 @@ function App({ command, args }) {
|
|
|
1246
1340
|
}
|
|
1247
1341
|
}
|
|
1248
1342
|
function VersionCommand() {
|
|
1249
|
-
const version = "1.
|
|
1343
|
+
const version = "1.4.1" ? "1.4.1" : "unknown";
|
|
1250
1344
|
return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
1251
1345
|
"ollieshop v",
|
|
1252
1346
|
version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ollie-shop/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Ollie Shop CLI - Development tools for custom checkouts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,18 +13,17 @@
|
|
|
13
13
|
"archiver": "^7.0.1",
|
|
14
14
|
"esbuild": "^0.24.0",
|
|
15
15
|
"glob": "^11.0.0",
|
|
16
|
-
"ink": "^
|
|
16
|
+
"ink": "^6.8.0",
|
|
17
17
|
"jwt-decode": "^4.0.0",
|
|
18
18
|
"open": "^10.1.0",
|
|
19
|
-
"react": "^
|
|
19
|
+
"react": "^19.2.0",
|
|
20
20
|
"zod": "^3.24.2",
|
|
21
21
|
"zod-to-json-schema": "^3.24.5"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/archiver": "^6.0.3",
|
|
25
|
-
"@types/ink": "^2.0.3",
|
|
26
25
|
"@types/node": "^22.15.23",
|
|
27
|
-
"@types/react": "^
|
|
26
|
+
"@types/react": "^19.2.0",
|
|
28
27
|
"tsup": "^8.0.1",
|
|
29
28
|
"typescript": "^5.7.3"
|
|
30
29
|
},
|
package/src/commands/help.tsx
CHANGED
|
@@ -139,6 +139,12 @@ export function HelpCommand() {
|
|
|
139
139
|
</Box>
|
|
140
140
|
<Text>Config stage (loads ollie.{"{stage}"}.json)</Text>
|
|
141
141
|
</Box>
|
|
142
|
+
<Box>
|
|
143
|
+
<Box width={24}>
|
|
144
|
+
<Text color="yellow">--no-open</Text>
|
|
145
|
+
</Box>
|
|
146
|
+
<Text>start: don't auto-open Studio (also honored via CI env)</Text>
|
|
147
|
+
</Box>
|
|
142
148
|
</Box>
|
|
143
149
|
|
|
144
150
|
<Box marginTop={1}>
|
|
@@ -147,6 +153,7 @@ export function HelpCommand() {
|
|
|
147
153
|
<Box marginLeft={2} flexDirection="column">
|
|
148
154
|
<Text dimColor>$ ollieshop login</Text>
|
|
149
155
|
<Text dimColor>$ ollieshop start --stage dev</Text>
|
|
156
|
+
<Text dimColor>$ ollieshop start --no-open</Text>
|
|
150
157
|
<Text dimColor>$ ollieshop whoami -o json</Text>
|
|
151
158
|
<Text dimColor>$ ollieshop schema store.create</Text>
|
|
152
159
|
<Text dimColor>
|
package/src/commands/start.tsx
CHANGED
|
@@ -59,6 +59,15 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
59
59
|
|
|
60
60
|
// Parse args
|
|
61
61
|
const stage = resolveStage(parseArg(args, "--stage", "-s"));
|
|
62
|
+
// Suppress auto-opening Studio in the browser (Storybook-style). Useful when an
|
|
63
|
+
// agent spawns the dev server and drives its own harness instead. Also honored
|
|
64
|
+
// via the conventional CI env var.
|
|
65
|
+
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
66
|
+
// Keyboard shortcuts need raw mode, which Ink can only enable on a TTY stdin.
|
|
67
|
+
// When spawned headless (agent/CI/backgrounded), stdin isn't a TTY — guard the
|
|
68
|
+
// input handler so the server runs instead of crashing with "Raw mode is not
|
|
69
|
+
// supported on the current process.stdin".
|
|
70
|
+
const isInteractive = Boolean(process.stdin.isTTY);
|
|
62
71
|
|
|
63
72
|
const addLog = useCallback((log: Omit<RequestLog, "id" | "timestamp">) => {
|
|
64
73
|
setLogs((prev) => {
|
|
@@ -148,13 +157,15 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
148
157
|
versionId: config.versionId,
|
|
149
158
|
});
|
|
150
159
|
|
|
151
|
-
// Open Studio in browser
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
// Open Studio in browser (unless suppressed)
|
|
161
|
+
if (!noOpen) {
|
|
162
|
+
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
163
|
+
studioUrl.searchParams.set("storeId", config.storeId);
|
|
164
|
+
if (config.versionId) {
|
|
165
|
+
studioUrl.searchParams.set("versionId", config.versionId);
|
|
166
|
+
}
|
|
167
|
+
open(studioUrl.toString());
|
|
156
168
|
}
|
|
157
|
-
open(studioUrl.toString());
|
|
158
169
|
} catch (error) {
|
|
159
170
|
if (!mounted) return;
|
|
160
171
|
setState({
|
|
@@ -170,29 +181,32 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
170
181
|
mounted = false;
|
|
171
182
|
stopRef.current?.();
|
|
172
183
|
};
|
|
173
|
-
}, [stage, handleRequest]);
|
|
184
|
+
}, [stage, handleRequest, noOpen]);
|
|
174
185
|
|
|
175
|
-
// Handle keyboard input
|
|
176
|
-
useInput(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
// Handle keyboard input (only when attached to a TTY — see isInteractive above)
|
|
187
|
+
useInput(
|
|
188
|
+
(input, key) => {
|
|
189
|
+
if (input === "q" || (input === "c" && key.ctrl)) {
|
|
190
|
+
stopRef.current?.().then(() => exit());
|
|
191
|
+
}
|
|
180
192
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
// Manual rebuild with 'r' (manifest is updated by the plugin)
|
|
194
|
+
if (input === "r" && state.status === "running") {
|
|
195
|
+
rebuildRef.current?.();
|
|
196
|
+
}
|
|
185
197
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
// Open Studio in browser with 'o'
|
|
199
|
+
if (input === "o" && state.status === "running") {
|
|
200
|
+
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
201
|
+
studioUrl.searchParams.set("storeId", state.storeId);
|
|
202
|
+
if (state.versionId) {
|
|
203
|
+
studioUrl.searchParams.set("versionId", state.versionId);
|
|
204
|
+
}
|
|
205
|
+
open(studioUrl.toString());
|
|
192
206
|
}
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
207
|
+
},
|
|
208
|
+
{ isActive: isInteractive },
|
|
209
|
+
);
|
|
196
210
|
|
|
197
211
|
return (
|
|
198
212
|
<Box flexDirection="column" gap={1}>
|
|
@@ -236,11 +250,12 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
236
250
|
stage={stage}
|
|
237
251
|
storeId={state.storeId}
|
|
238
252
|
versionId={state.versionId}
|
|
253
|
+
noOpen={noOpen}
|
|
239
254
|
/>
|
|
240
255
|
<ComponentList components={components} />
|
|
241
256
|
<BuildInfo buildCount={buildCount} lastBuildTime={lastBuildTime} />
|
|
242
257
|
<RequestLogs logs={logs} />
|
|
243
|
-
<Footer />
|
|
258
|
+
<Footer interactive={isInteractive} />
|
|
244
259
|
</>
|
|
245
260
|
)}
|
|
246
261
|
</Box>
|
|
@@ -264,12 +279,14 @@ function ServerInfo({
|
|
|
264
279
|
stage,
|
|
265
280
|
storeId,
|
|
266
281
|
versionId,
|
|
282
|
+
noOpen,
|
|
267
283
|
}: {
|
|
268
284
|
host: string;
|
|
269
285
|
port: number;
|
|
270
286
|
stage?: string;
|
|
271
287
|
storeId: string;
|
|
272
288
|
versionId?: string;
|
|
289
|
+
noOpen?: boolean;
|
|
273
290
|
}) {
|
|
274
291
|
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
275
292
|
|
|
@@ -301,6 +318,7 @@ function ServerInfo({
|
|
|
301
318
|
<Text bold color="magenta">
|
|
302
319
|
{studioUrl.toString()}
|
|
303
320
|
</Text>
|
|
321
|
+
{noOpen && <Text dimColor> (auto-open off — press o)</Text>}
|
|
304
322
|
</Box>
|
|
305
323
|
<Box marginLeft={2} marginTop={1}>
|
|
306
324
|
<Text dimColor>
|
|
@@ -386,7 +404,15 @@ function RequestLogs({ logs }: { logs: RequestLog[] }) {
|
|
|
386
404
|
);
|
|
387
405
|
}
|
|
388
406
|
|
|
389
|
-
function Footer() {
|
|
407
|
+
function Footer({ interactive = true }: { interactive?: boolean }) {
|
|
408
|
+
if (!interactive) {
|
|
409
|
+
return (
|
|
410
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
411
|
+
<Text dimColor>Headless (no TTY) — Ctrl+C to stop</Text>
|
|
412
|
+
</Box>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
390
416
|
return (
|
|
391
417
|
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
392
418
|
<Text dimColor>Press </Text>
|
package/src/utils/esbuild.ts
CHANGED
|
@@ -267,6 +267,7 @@ export async function startDevServer(
|
|
|
267
267
|
ctx = null;
|
|
268
268
|
if (oldCtx) await oldCtx.dispose();
|
|
269
269
|
await buildAndServe(components);
|
|
270
|
+
notifyComponentsChanged(components);
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
await buildAndServe(await discoverComponents({ cwd, stage }));
|
|
@@ -313,10 +314,92 @@ export async function startDevServer(
|
|
|
313
314
|
watchTimer = setTimeout(maybeRecreate, 150);
|
|
314
315
|
});
|
|
315
316
|
|
|
317
|
+
// esbuild's /esbuild live-reload stream lives on the context, so disposing it
|
|
318
|
+
// during a recreate drops the browser's EventSource — and Studio closes that
|
|
319
|
+
// connection for good on the first error. The proxy owns the downstream
|
|
320
|
+
// connection instead and re-subscribes to esbuild's stream across recreates,
|
|
321
|
+
// so live reload survives a component being added or removed.
|
|
322
|
+
const sseClients = new Set<http.ServerResponse>();
|
|
323
|
+
let upstreamReq: http.ClientRequest | null = null;
|
|
324
|
+
let upstreamRetry: ReturnType<typeof setTimeout> | null = null;
|
|
325
|
+
|
|
326
|
+
function broadcast(chunk: string | Buffer): void {
|
|
327
|
+
for (const client of sseClients) {
|
|
328
|
+
if (!client.writableEnded) client.write(chunk);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function connectUpstream(): void {
|
|
333
|
+
if (upstreamReq || sseClients.size === 0) return;
|
|
334
|
+
const req = http.request(
|
|
335
|
+
{
|
|
336
|
+
hostname: host,
|
|
337
|
+
port: internalPort,
|
|
338
|
+
path: "/esbuild",
|
|
339
|
+
method: "GET",
|
|
340
|
+
headers: { accept: "text/event-stream" },
|
|
341
|
+
},
|
|
342
|
+
(upstream) => {
|
|
343
|
+
upstream.on("data", broadcast);
|
|
344
|
+
upstream.on("close", onUpstreamLost);
|
|
345
|
+
upstream.on("error", onUpstreamLost);
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
req.on("error", onUpstreamLost);
|
|
349
|
+
req.end();
|
|
350
|
+
upstreamReq = req;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function onUpstreamLost(): void {
|
|
354
|
+
if (!upstreamReq) return;
|
|
355
|
+
upstreamReq = null;
|
|
356
|
+
if (upstreamRetry) clearTimeout(upstreamRetry);
|
|
357
|
+
if (sseClients.size === 0) return;
|
|
358
|
+
upstreamRetry = setTimeout(connectUpstream, 200);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function teardownUpstream(): void {
|
|
362
|
+
if (upstreamRetry) {
|
|
363
|
+
clearTimeout(upstreamRetry);
|
|
364
|
+
upstreamRetry = null;
|
|
365
|
+
}
|
|
366
|
+
const req = upstreamReq;
|
|
367
|
+
upstreamReq = null;
|
|
368
|
+
req?.destroy();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function notifyComponentsChanged(components: ComponentInfo[]): void {
|
|
372
|
+
if (sseClients.size === 0) return;
|
|
373
|
+
const updated = components.map((c) => `/${c.name}/index.js`);
|
|
374
|
+
const payload = JSON.stringify({ added: [], removed: [], updated });
|
|
375
|
+
broadcast(`event: change\ndata: ${payload}\n\n`);
|
|
376
|
+
}
|
|
377
|
+
|
|
316
378
|
// Create proxy server that handles /bundle/* and forwards to esbuild
|
|
317
379
|
const proxyServer = http.createServer(async (req, res) => {
|
|
318
380
|
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
319
381
|
|
|
382
|
+
// Hold the Studio live-reload stream open across esbuild recreates
|
|
383
|
+
if (url.pathname === "/esbuild" && req.method === "GET") {
|
|
384
|
+
res.writeHead(200, {
|
|
385
|
+
"Content-Type": "text/event-stream",
|
|
386
|
+
"Cache-Control": "no-cache",
|
|
387
|
+
Connection: "keep-alive",
|
|
388
|
+
"Access-Control-Allow-Origin": "*",
|
|
389
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
390
|
+
});
|
|
391
|
+
res.write("retry: 500\n\n");
|
|
392
|
+
sseClients.add(res);
|
|
393
|
+
connectUpstream();
|
|
394
|
+
const dropClient = () => {
|
|
395
|
+
sseClients.delete(res);
|
|
396
|
+
if (sseClients.size === 0) teardownUpstream();
|
|
397
|
+
};
|
|
398
|
+
req.on("close", dropClient);
|
|
399
|
+
res.on("error", dropClient);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
320
403
|
// Handle /bundle?path=/ComponentName/index.js
|
|
321
404
|
if (url.pathname === "/bundle" && req.method === "GET") {
|
|
322
405
|
const componentPath = url.searchParams.get("path");
|
|
@@ -520,6 +603,9 @@ export async function startDevServer(
|
|
|
520
603
|
stop: async () => {
|
|
521
604
|
if (watchTimer) clearTimeout(watchTimer);
|
|
522
605
|
componentsWatcher.close();
|
|
606
|
+
teardownUpstream();
|
|
607
|
+
for (const client of sseClients) client.end();
|
|
608
|
+
sseClients.clear();
|
|
523
609
|
proxyServer.close();
|
|
524
610
|
await ctx?.dispose();
|
|
525
611
|
},
|