@tokenoftrust/storefront-runner 2.0.35 → 2.0.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.35",
3
+ "version": "2.0.36",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -31,11 +31,12 @@
31
31
  * reference files that don't exist. The Worker-plane build never sets this, so
32
32
  * R2 shadowing semantics are unchanged there.
33
33
  */
34
- import { cp, mkdir, rm } from "node:fs/promises";
35
- import { existsSync, readdirSync, statSync } from "node:fs";
34
+ import { rm, rename } from "node:fs/promises";
35
+ import { existsSync, readdirSync, statSync, mkdirSync, copyFileSync } from "node:fs";
36
36
  import { join, resolve, dirname } from "node:path";
37
37
  import { fileURLToPath } from "node:url";
38
38
  import { listTenantDirIds, tenantDirSegments } from "../lib/tenant-dirs.mjs";
39
+ import { copyTreeSkippingUnreadable, formatSkippedAssets } from "../lib/tenant-asset-copy.mjs";
39
40
 
40
41
  const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
41
42
  const src = resolve(repoRoot, "tenants");
@@ -45,7 +46,14 @@ const dest = process.env.TENANT_ASSETS_DEST
45
46
 
46
47
  if (!existsSync(src)) process.exit(0);
47
48
 
48
- await rm(dest, { recursive: true, force: true });
49
+ // Build into a staging dir and swap it in at the end, rather than wiping `dest` up
50
+ // front. The old order meant any throw mid-copy left the destination EMPTY: every
51
+ // tenant asset 404'd, pages/*.css included, while HTML kept returning 200 because
52
+ // pages render from content/ — an unstyled site with no signal
53
+ // (fb-1789008798249-sc4tgy). Staging means the previous good mirror survives a
54
+ // failure instead of being replaced by nothing.
55
+ const staging = `${dest}.staging-${process.pid}`;
56
+ await rm(staging, { recursive: true, force: true });
49
57
 
50
58
  const versionedEnv = new Set(
51
59
  (process.env.TENANT_ASSETS_VERSIONED ?? "")
@@ -65,8 +73,17 @@ const isVersioned = (id) =>
65
73
  // is `/tenants/<id>/…` for every tenant, whichever tier its source sits in.
66
74
  const ids = listTenantDirIds(src, readdirSync, (p) => statSync(p).isDirectory(), join);
67
75
 
76
+ const copyDeps = {
77
+ readdir: (dir) => readdirSync(dir, { withFileTypes: true }),
78
+ mkdir: (dir) => mkdirSync(dir, { recursive: true }),
79
+ copyFile: (from, to) => copyFileSync(from, to),
80
+ join,
81
+ };
82
+
68
83
  let copied = 0;
69
84
  let skipped = 0;
85
+ /** @type {{path: string, reason: string}[]} */
86
+ const unreadable = [];
70
87
  for (const id of ids) {
71
88
  const pub = join(tenantDir(id), "public");
72
89
  if (!existsSync(pub)) continue;
@@ -75,12 +92,23 @@ for (const id of ids) {
75
92
  skipped++;
76
93
  continue;
77
94
  }
78
- await mkdir(join(dest, id), { recursive: true });
79
- await cp(pub, join(dest, id), { recursive: true });
95
+ // Per tenant AND per file: one tenant's unreadable entry must not cost another
96
+ // tenant its assets, and one missing image must not cost its own tenant the
97
+ // stylesheet beside it.
98
+ const result = copyTreeSkippingUnreadable(pub, join(staging, id), copyDeps);
99
+ for (const s of result.skipped) unreadable.push({ path: `${id}/${s.path}`, reason: s.reason });
80
100
  copied++;
81
101
  }
82
102
 
103
+ // Swap staging in only once the copy is done.
104
+ await rm(dest, { recursive: true, force: true });
105
+ await rename(staging, dest);
106
+
83
107
  const skipNote = skipped ? ` (skipped ${skipped} versioned → R2)` : "";
84
108
  console.log(
85
109
  `copy-tenant-assets: mirrored ${copied} tenant public/ tree(s) → ${dest}${skipNote}`,
86
110
  );
111
+ // Loud, and AFTER the summary so it is the last thing on screen. Exit stays 0: a
112
+ // missing blog image is not a reason to stop the dev loop, but it is a reason the
113
+ // operator must not have to infer from an unstyled page.
114
+ for (const line of formatSkippedAssets(unreadable)) console.warn(line);