@ollie-shop/cli 1.4.0 → 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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @ollie-shop/cli@1.4.0 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
2
+ > @ollie-shop/cli@1.4.1 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.tsx
@@ -9,5 +9,5 @@
9
9
  CLI Target: node22
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
- ESM dist/index.js 93.69 KB
13
- ESM ⚡️ Build success in 295ms
12
+ ESM dist/index.js 95.89 KB
13
+ ESM ⚡️ Build success in 248ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 1.4.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.js CHANGED
@@ -700,6 +700,7 @@ async function startDevServer(options = {}) {
700
700
  ctx = null;
701
701
  if (oldCtx) await oldCtx.dispose();
702
702
  await buildAndServe(components);
703
+ notifyComponentsChanged(components);
703
704
  }
704
705
  await buildAndServe(await discoverComponents({ cwd, stage }));
705
706
  async function currentComponentNames() {
@@ -737,8 +738,80 @@ async function startDevServer(options = {}) {
737
738
  if (watchTimer) clearTimeout(watchTimer);
738
739
  watchTimer = setTimeout(maybeRecreate, 150);
739
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
+ }
740
794
  const proxyServer = http.createServer(async (req, res) => {
741
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
+ }
742
815
  if (url.pathname === "/bundle" && req.method === "GET") {
743
816
  const componentPath = url.searchParams.get("path");
744
817
  if (!componentPath) {
@@ -896,6 +969,9 @@ async function startDevServer(options = {}) {
896
969
  stop: async () => {
897
970
  if (watchTimer) clearTimeout(watchTimer);
898
971
  componentsWatcher.close();
972
+ teardownUpstream();
973
+ for (const client of sseClients) client.end();
974
+ sseClients.clear();
899
975
  proxyServer.close();
900
976
  await ctx?.dispose();
901
977
  }
@@ -1264,7 +1340,7 @@ function App({ command, args }) {
1264
1340
  }
1265
1341
  }
1266
1342
  function VersionCommand() {
1267
- const version = "1.4.0" ? "1.4.0" : "unknown";
1343
+ const version = "1.4.1" ? "1.4.1" : "unknown";
1268
1344
  return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
1269
1345
  "ollieshop v",
1270
1346
  version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ollie-shop/cli",
3
- "version": "1.4.0",
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",
@@ -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
  },