@tokenoftrust/storefront-runner 2.2.85 → 2.2.87

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.
@@ -63,8 +63,10 @@ import { dirname, resolve, join } from "node:path";
63
63
  import { fileURLToPath } from "node:url";
64
64
  import { resolveDevSourceRoot } from "./dev/tenant-source-root.mjs";
65
65
  import { tenantDirRelative, tenantDirSegments } from "./lib/tenant-dirs.mjs";
66
- import { firstFreePort } from "./dev/port-check.mjs";
66
+ import { firstFreePort, isPortFree } from "./dev/port-check.mjs";
67
+ import { busyPortMessage, classifyBusyPort, lockPort, unlockPort } from "./dev/port-lock.mjs";
67
68
  import { resync, startWatch } from "./dev/checkout-watch.mjs";
69
+ import { standaloneGraftPlan } from "./dev/standalone-graft-plan.mjs";
68
70
  import { findMissingCtaIds } from "./tenant/validate.mjs";
69
71
 
70
72
  const here = dirname(fileURLToPath(import.meta.url));
@@ -273,25 +275,18 @@ if (workspacePath) {
273
275
  console.log(`\n tot dev — standalone (${tenant})`);
274
276
  console.log(` checkout: ${wsRoot}`);
275
277
 
276
- for (const map of mappings) {
277
- // `workspace` is the checkout-relative prefix (e.g. "content/", "theme.json").
278
- // COLOCATION: a tenant IS the repo-root dir `tenants/<id>/`, and the checkout
279
- // (a `git subtree split --prefix=tenants/<id>` of that dir) has the SAME layout
280
- // at its root — so the graft is an identity map: content/ -> tenants/<id>/content/,
281
- // public/ -> tenants/<id>/public/, theme.json -> tenants/<id>/theme.json. We map
282
- // structurally (ignoring any legacy `repo` field in the checkout's .tot/config.json).
283
- const wsRel = String(map.workspace ?? "").replace(/\/+$/, "");
284
- if (!wsRel) continue;
278
+ /**
279
+ * Graft one checkout path (`wsRel`, checkout-relative) into the in-tree
280
+ * `tenants/<tenant>/` layout the app globs, with the same move-aside/restore +
281
+ * watch-and-resync machinery every mapping gets. Returns true if grafted.
282
+ */
283
+ function graftOne(wsRel, isDir) {
285
284
  const repoRel = join(tenantDirRelative(tenant), wsRel);
286
285
 
287
286
  const source = resolve(wsRoot, wsRel); // real files in the checkout
288
287
  const target = resolve(repoRoot, repoRel); // where the app globs them
289
- const isDir = map.kind !== "file";
290
288
 
291
- if (!existsSync(source)) {
292
- console.warn(` ⚠ checkout has no ${wsRel} — skipping (${map.kind ?? "?"})`);
293
- continue;
294
- }
289
+ if (!existsSync(source)) return false;
295
290
 
296
291
  // Ensure the destination's parent exists (e.g. content/ or themes/tenants/).
297
292
  mkdirSync(dirname(target), { recursive: true });
@@ -322,9 +317,31 @@ if (workspacePath) {
322
317
  }
323
318
 
324
319
  console.log(` ↪ ${repoRel} ⇐ ${wsRel}${backup ? " (in-tree content backed up)" : ""}`);
320
+ return true;
325
321
  }
326
322
 
327
- if (grafts.length === 0) {
323
+ // `workspace` is the checkout-relative prefix (e.g. "content/", "theme.json").
324
+ // COLOCATION: a tenant IS the repo-root dir `tenants/<id>/`, and the checkout
325
+ // (a `git subtree split --prefix=tenants/<id>` of that dir) has the SAME layout
326
+ // at its root — so the graft is an identity map: content/ -> tenants/<id>/content/,
327
+ // public/ -> tenants/<id>/public/, theme.json -> tenants/<id>/theme.json. We map
328
+ // structurally (ignoring any legacy `repo` field in the checkout's .tot/config.json).
329
+ // standaloneGraftPlan ALSO appends `.tot/config.json` unconditionally — see its
330
+ // own doc comment. Its success does NOT count toward "did we graft a usable
331
+ // site" below: a checkout with config but no content/public/theme is still
332
+ // nothing to serve.
333
+ let mappedCount = 0;
334
+ for (const { workspace, isDir } of standaloneGraftPlan(mappings)) {
335
+ const grafted = graftOne(workspace, isDir);
336
+ if (workspace === ".tot/config.json") {
337
+ if (!grafted) console.warn(` ⚠ checkout has no .tot/config.json — compliance/capabilities/siteType resolve undeclared.`);
338
+ continue;
339
+ }
340
+ if (grafted) mappedCount++;
341
+ else console.warn(` ⚠ checkout has no ${workspace} — skipping`);
342
+ }
343
+
344
+ if (mappedCount === 0) {
328
345
  console.error(`✗ nothing to graft — the checkout had none of the mapped prefixes.`);
329
346
  cleanupGrafts();
330
347
  process.exit(1);
@@ -354,12 +371,19 @@ if (workspacePath) {
354
371
  // which then mismatches the URL we (and the CLI's liveness poll) advertise.
355
372
  // Instead we pick the first free port ourselves and PRINT it, so a busy default
356
373
  // port "just works" (better UX than failing). See port-check.mjs.
374
+ //
375
+ // BEFORE drifting, classify WHY the requested port is busy (port-lock.mjs). When
376
+ // an earlier `tot dev` session holds it — still running but possibly wedged after
377
+ // a renderer crash, or exited with the socket lingering — the developer's browser
378
+ // tab on that port is NOT this session, so say so and name the process to stop.
357
379
  const requestedPort = Number(flag("port", "4321"));
380
+ const busy = await classifyBusyPort(requestedPort, { isFree: isPortFree });
358
381
  const resolvedPort = await firstFreePort(requestedPort);
359
382
  const port = String(resolvedPort);
360
383
  if (resolvedPort !== requestedPort) {
361
- console.log(` ⚠ port ${requestedPort} was busy — starting on ${resolvedPort} instead.`);
384
+ console.log(busyPortMessage(requestedPort, resolvedPort, busy));
362
385
  }
386
+ lockPort(resolvedPort);
363
387
 
364
388
  // Land the developer on their store ROOT (plain save→reload HMR against their own
365
389
  // site) — not the /dev cockpit, which is a hosted-only surface excluded from the
@@ -429,15 +453,20 @@ const child = spawn(
429
453
  { cwd: appDir, stdio: "inherit", env: spawnEnv },
430
454
  );
431
455
 
432
- // Clean up grafts on every exit path so we never leave symlinks in the tree.
456
+ // Clean up grafts + the port lock on every exit path so we never leave
457
+ // symlinks in the tree, or a lock a LATER run would misread as still live.
433
458
  function shutdown(signal) {
434
459
  child.kill(signal);
435
460
  }
436
- child.on("exit", (code) => {
461
+ function cleanupAll() {
437
462
  cleanupGrafts();
463
+ unlockPort(resolvedPort);
464
+ }
465
+ child.on("exit", (code) => {
466
+ cleanupAll();
438
467
  process.exit(code ?? 0);
439
468
  });
440
469
  process.on("SIGINT", () => shutdown("SIGINT"));
441
470
  process.on("SIGTERM", () => shutdown("SIGTERM"));
442
471
  // Belt-and-suspenders: if the parent dies without the child exit firing.
443
- process.on("exit", cleanupGrafts);
472
+ process.on("exit", cleanupAll);