@tokenoftrust/storefront-runner 2.0.36 → 2.0.38
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.
|
|
3
|
+
"version": "2.0.38",
|
|
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",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant public/ mirroring that cannot lose the whole tree to one bad file.
|
|
3
|
+
*
|
|
4
|
+
* The failure this exists to prevent (fb-1789008798249-sc4tgy): copy-tenant-assets
|
|
5
|
+
* wiped its destination and then ran a single recursive `cp`. One unreadable entry —
|
|
6
|
+
* a blog image referenced in the tree but missing on disk, a dangling symlink — threw
|
|
7
|
+
* mid-copy, so the destination was left EMPTY. Every tenant asset then 404'd,
|
|
8
|
+
* `pages/*.css` included, while HTML routes still returned 200 because pages render
|
|
9
|
+
* from content/ rather than public/. The result is a silently unstyled site with no
|
|
10
|
+
* signal beyond one advisory line, which is close to the worst shape a dev-loop bug
|
|
11
|
+
* can take: it looks like a CSS problem in the tenant's own content.
|
|
12
|
+
*
|
|
13
|
+
* So: walk the tree entry by entry, skip what cannot be read, and report it. A
|
|
14
|
+
* missing image costs that image, never the stylesheet next to it.
|
|
15
|
+
*
|
|
16
|
+
* Injected fs so the walk is testable without a real tree.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} CopyTreeDeps
|
|
21
|
+
* @property {(dir: string) => {name: string, isDirectory: () => boolean, isFile: () => boolean,
|
|
22
|
+
* isSymbolicLink: () => boolean}[]} readdir entries with types
|
|
23
|
+
* @property {(dir: string) => void} mkdir recursive
|
|
24
|
+
* @property {(from: string, to: string) => void} copyFile
|
|
25
|
+
* @property {(...parts: string[]) => string} join
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Copy `from` into `to`, skipping any entry that throws.
|
|
30
|
+
* @param {string} from @param {string} to @param {CopyTreeDeps} deps
|
|
31
|
+
* @returns {{copied: number, skipped: {path: string, reason: string}[]}}
|
|
32
|
+
*/
|
|
33
|
+
export function copyTreeSkippingUnreadable(from, to, deps) {
|
|
34
|
+
const { readdir, mkdir, copyFile, join } = deps;
|
|
35
|
+
let copied = 0;
|
|
36
|
+
/** @type {{path: string, reason: string}[]} */
|
|
37
|
+
const skipped = [];
|
|
38
|
+
|
|
39
|
+
/** @param {string} src @param {string} dst @param {string} rel */
|
|
40
|
+
const walk = (src, dst, rel) => {
|
|
41
|
+
/** @type {ReturnType<CopyTreeDeps['readdir']>} */
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = readdir(src);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// An unreadable DIRECTORY costs only its own subtree, not its siblings.
|
|
47
|
+
skipped.push({ path: rel || '.', reason: reasonOf(err) });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
mkdir(dst);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
skipped.push({ path: rel || '.', reason: reasonOf(err) });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const childRel = rel ? join(rel, entry.name) : entry.name;
|
|
58
|
+
const childSrc = join(src, entry.name);
|
|
59
|
+
const childDst = join(dst, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
walk(childSrc, childDst, childRel);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// copyFile follows symlinks, so a DANGLING link throws here and is skipped —
|
|
66
|
+
// which is the common shape of "referenced but missing".
|
|
67
|
+
copyFile(childSrc, childDst);
|
|
68
|
+
copied++;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
skipped.push({ path: childRel, reason: reasonOf(err) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
walk(from, to, '');
|
|
76
|
+
return { copied, skipped };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {unknown} err */
|
|
80
|
+
function reasonOf(err) {
|
|
81
|
+
const e = /** @type {{code?: string, message?: string}} */ (err);
|
|
82
|
+
return e?.code || e?.message || String(err);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The operator-facing banner for skipped files. Loud on purpose: the old behaviour
|
|
87
|
+
* degraded to an unstyled site behind one advisory line, so a skip has to be
|
|
88
|
+
* impossible to scroll past — while still leaving exit 0, because a missing blog
|
|
89
|
+
* image must not stop the dev loop.
|
|
90
|
+
* @param {{path: string, reason: string}[]} skipped
|
|
91
|
+
* @param {number} [limit] how many paths to name before summarising
|
|
92
|
+
* @returns {string[]} lines
|
|
93
|
+
*/
|
|
94
|
+
export function formatSkippedAssets(skipped, limit = 10) {
|
|
95
|
+
if (skipped.length === 0) return [];
|
|
96
|
+
const lines = [
|
|
97
|
+
'',
|
|
98
|
+
`⚠ copy-tenant-assets skipped ${skipped.length} unreadable file(s) — those URLs will 404:`,
|
|
99
|
+
];
|
|
100
|
+
for (const { path, reason } of skipped.slice(0, limit)) lines.push(` ${path} (${reason})`);
|
|
101
|
+
if (skipped.length > limit) lines.push(` … and ${skipped.length - limit} more`);
|
|
102
|
+
lines.push(' Everything else copied, so the rest of the tenant\'s assets still serve.');
|
|
103
|
+
lines.push('');
|
|
104
|
+
return lines;
|
|
105
|
+
}
|