@tokenoftrust/storefront-runner 2.4.27 → 2.4.29
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.4.
|
|
3
|
+
"version": "2.4.29",
|
|
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",
|
|
@@ -24,6 +24,18 @@ export const SPACES_DIR = "spaces";
|
|
|
24
24
|
export const TOPOLOGY_FILE = ".tot/publication-spaces.json";
|
|
25
25
|
export const TOPOLOGY_VERSION = 1;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Platform-owned paths no space serves (packages/public-runtime PUBLICATION_SPACE_RESERVED_MOUNTS;
|
|
29
|
+
* the ownership parity test holds the two equal).
|
|
30
|
+
*/
|
|
31
|
+
export const PLATFORM_PATHS = Object.freeze([
|
|
32
|
+
"/.well-known", "/__evidence", "/__preview", "/_a", "/_astro", "/_style-guide", "/admin", "/api",
|
|
33
|
+
"/apps", "/assets", "/auth", "/brand", "/capabilities", "/cockpit", "/dashboard", "/dev",
|
|
34
|
+
"/favicon.svg", "/health", "/js", "/feed.json", "/llms.txt", "/publication-discovery",
|
|
35
|
+
"/search-index.json", "/sitemap.xml", "/platform", "/preview", "/products", "/collections",
|
|
36
|
+
"/search", "/saved", "/robots.txt", "/shared", "/style-guide", "/tenants",
|
|
37
|
+
]);
|
|
38
|
+
|
|
27
39
|
const SPACE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/;
|
|
28
40
|
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
29
41
|
|
|
@@ -145,15 +157,17 @@ export function mountedSpaces(topology) {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
/**
|
|
148
|
-
* Which space owns a URL path:
|
|
149
|
-
*
|
|
160
|
+
* Which space owns a URL path: none for a platform path (`/api`, `/admin`, ...), else the longest
|
|
161
|
+
* canonical mount (or alias) that owns it, else the root. Null for a path that cannot be read as one.
|
|
150
162
|
* @param {Topology} topology
|
|
151
163
|
* @param {string} urlPath
|
|
152
|
-
* @returns {{ space: TopologySpace, via: "mount"|"alias"|"root" } | null}
|
|
164
|
+
* @returns {{ space: TopologySpace, via: "mount"|"alias"|"root" } | { space: null, via: "platform" } | null}
|
|
153
165
|
*/
|
|
154
166
|
export function owningSpace(topology, urlPath) {
|
|
155
167
|
const path = normalizeRequestPath(urlPath);
|
|
156
168
|
if (path === null) return null;
|
|
169
|
+
const folded = path.toLowerCase();
|
|
170
|
+
if (PLATFORM_PATHS.some((reserved) => mountMatchesPath(reserved, folded))) return { space: null, via: "platform" };
|
|
157
171
|
/** @type {{ space: TopologySpace, via: "mount"|"alias", mount: string } | null} */
|
|
158
172
|
let best = null;
|
|
159
173
|
for (const space of topology.spaces) {
|
|
@@ -221,3 +235,145 @@ export function excludeFromRootRepo(workspace, patterns) {
|
|
|
221
235
|
if (!missing.length) return;
|
|
222
236
|
appendFileSync(file, `${current && !current.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`);
|
|
223
237
|
}
|
|
238
|
+
|
|
239
|
+
/** The post collections a space can carry; each is served under its base path (default: its name). */
|
|
240
|
+
export const COLLECTIONS = ["blog", "the-build"];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The URL path a ROOT checkout file renders at, for the files whose URL the layout fixes:
|
|
244
|
+
* `content/pages-html/<slug>.html` and `content/pages/<slug>.json` at `/<slug>`, a collection's
|
|
245
|
+
* files under its base path. Null for files with no URL of their own (theme, chrome, assets).
|
|
246
|
+
* @param {TopologySpace} root
|
|
247
|
+
* @param {string} relPath path relative to the root checkout
|
|
248
|
+
*/
|
|
249
|
+
export function rootFileUrlPath(root, relPath) {
|
|
250
|
+
const rel = String(relPath).replace(/\\/g, "/");
|
|
251
|
+
let m = /^content\/pages-html\/(.+)\.html$/.exec(rel) ?? /^content\/pages\/(.+)\.json$/.exec(rel);
|
|
252
|
+
if (m) return `/${m[1]}`;
|
|
253
|
+
m = /^content\/([^/]+)\//.exec(rel);
|
|
254
|
+
if (m && COLLECTIONS.includes(m[1])) {
|
|
255
|
+
const base = root.collections?.[m[1]] ?? m[1];
|
|
256
|
+
return base ? `/${base.replace(/^\/+|\/+$/g, "")}` : null;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Root files whose URL a mounted space owns. Visitors get the space's page at that URL, never the
|
|
263
|
+
* root's, so the file is dead weight in the root and a change to it ships nowhere.
|
|
264
|
+
* @param {Topology} topology
|
|
265
|
+
* @param {string[]} rootFiles paths relative to the root checkout
|
|
266
|
+
* @returns {Array<{ level: "warn", rule: string, file: string, message: string, fix: string }>}
|
|
267
|
+
*/
|
|
268
|
+
export function rootFilesOwnedByMounts(topology, rootFiles) {
|
|
269
|
+
const root = rootSpace(topology);
|
|
270
|
+
const findings = [];
|
|
271
|
+
for (const file of rootFiles) {
|
|
272
|
+
const urlPath = rootFileUrlPath(root, file);
|
|
273
|
+
if (!urlPath) continue;
|
|
274
|
+
const owner = owningSpace(topology, urlPath);
|
|
275
|
+
if (!owner || !owner.space || owner.space.mount === "/") continue;
|
|
276
|
+
findings.push({
|
|
277
|
+
level: /** @type {"warn"} */ ("warn"),
|
|
278
|
+
rule: "publication-space-owns-url",
|
|
279
|
+
file,
|
|
280
|
+
message: `${urlPath} is served by the mounted space "${owner.space.id}" (${owner.space.source.forge}:${owner.space.source.repo}, mount ${owner.space.mount}), so this root file never reaches a visitor.`,
|
|
281
|
+
fix: `move it into ${spaceDir(owner.space)}/ (the "${owner.space.id}" repository), or delete it from the root`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return findings;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Who owns a URL path. `spaceId` is null for a platform path, which no repository serves.
|
|
289
|
+
* @param {Topology} topology @param {string} urlPath
|
|
290
|
+
* @returns {{ path: string, spaceId: string|null, mount: string|null, via: string, repo: string|null, dir: string|null } | null}
|
|
291
|
+
*/
|
|
292
|
+
export function describeOwner(topology, urlPath) {
|
|
293
|
+
const owner = owningSpace(topology, urlPath);
|
|
294
|
+
if (!owner) return null;
|
|
295
|
+
const { space, via } = owner;
|
|
296
|
+
if (!space) {
|
|
297
|
+
return { path: /** @type {string} */ (normalizeRequestPath(urlPath)), spaceId: null, mount: null, via, repo: null, dir: null };
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
path: /** @type {string} */ (normalizeRequestPath(urlPath)),
|
|
301
|
+
spaceId: space.id,
|
|
302
|
+
mount: space.mount,
|
|
303
|
+
via,
|
|
304
|
+
repo: `${space.source.forge}:${space.source.repo}`,
|
|
305
|
+
dir: spaceDir(space) || ".",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Route changed workspace files to the repository that ships them. Paths are relative to the
|
|
311
|
+
* root checkout (a space's files as `spaces/<id>/...`). Each file lands in its owning space's
|
|
312
|
+
* group; a root file whose URL a mounted space owns is ALSO named as misrouted (it ships with the
|
|
313
|
+
* root and still never reaches a visitor); a file under `spaces/` naming no declared space is
|
|
314
|
+
* unowned. Pure.
|
|
315
|
+
* @param {Topology} topology
|
|
316
|
+
* @param {string[]} files
|
|
317
|
+
* @returns {{ groups: Array<{ space: TopologySpace, files: string[] }>,
|
|
318
|
+
* misrouted: Array<{ file: string, owner: TopologySpace }>, unowned: string[] }}
|
|
319
|
+
*/
|
|
320
|
+
export function routeWorkspaceChanges(topology, files) {
|
|
321
|
+
const root = rootSpace(topology);
|
|
322
|
+
/** @type {Map<string, string[]>} */
|
|
323
|
+
const byId = new Map(topology.spaces.map((space) => [space.id, []]));
|
|
324
|
+
const misrouted = [];
|
|
325
|
+
const unowned = [];
|
|
326
|
+
for (const file of files) {
|
|
327
|
+
const owner = spaceForWorkspaceFile(topology, file);
|
|
328
|
+
if (!owner) { unowned.push(file); continue; }
|
|
329
|
+
/** @type {string[]} */ (byId.get(owner.space.id)).push(file);
|
|
330
|
+
if (owner.space.mount === "/") {
|
|
331
|
+
const urlPath = rootFileUrlPath(root, file);
|
|
332
|
+
const served = urlPath ? owningSpace(topology, urlPath) : null;
|
|
333
|
+
if (served?.space && served.space.mount !== "/") misrouted.push({ file, owner: served.space });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
groups: topology.spaces.map((space) => ({ space, files: /** @type {string[]} */ (byId.get(space.id)) })),
|
|
338
|
+
misrouted,
|
|
339
|
+
unowned,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Parse `git status --porcelain=v1 -z` into changed paths (a rename yields its new path). Pure.
|
|
345
|
+
* @param {string} out
|
|
346
|
+
* @returns {string[]}
|
|
347
|
+
*/
|
|
348
|
+
export function parsePorcelainZ(out) {
|
|
349
|
+
const parts = String(out).split("\0");
|
|
350
|
+
const paths = [];
|
|
351
|
+
for (let i = 0; i < parts.length; i++) {
|
|
352
|
+
const entry = parts[i];
|
|
353
|
+
if (!entry || entry.length < 4) continue;
|
|
354
|
+
const status = entry.slice(0, 2);
|
|
355
|
+
paths.push(entry.slice(3));
|
|
356
|
+
if (status[0] === "R" || status[0] === "C") i++; // the next field is the rename's source
|
|
357
|
+
}
|
|
358
|
+
return paths;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Every changed file in a workspace, relative to the root checkout: the root repository's own
|
|
363
|
+
* changes plus each cloned space's (`spaces/<id>/...`). `git(dir, args)` runs git in `dir`.
|
|
364
|
+
* @param {string} workspace
|
|
365
|
+
* @param {Topology} topology
|
|
366
|
+
* @param {(dir: string, args: string[]) => string} git
|
|
367
|
+
* @returns {string[]}
|
|
368
|
+
*/
|
|
369
|
+
export function workspaceChangedFiles(workspace, topology, git) {
|
|
370
|
+
const files = parsePorcelainZ(git(workspace, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]));
|
|
371
|
+
for (const space of mountedSpaces(topology)) {
|
|
372
|
+
const dir = join(workspace, spaceDir(space));
|
|
373
|
+
if (!existsSync(join(dir, ".git"))) continue;
|
|
374
|
+
for (const file of parsePorcelainZ(git(dir, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]))) {
|
|
375
|
+
files.push(`${spaceDir(space)}/${file}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return files;
|
|
379
|
+
}
|