@tokenoftrust/storefront-runner 2.4.27 → 2.4.28

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.27",
3
+ "version": "2.4.28",
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: the longest canonical mount (or alias) that owns it, else the root.
149
- * Returns null for a path that cannot be read as a URL path.
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,73 @@ 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
+ }