@tokenoftrust/storefront-runner 3.1.1 → 3.2.0

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.
@@ -9,11 +9,11 @@
9
9
  *
10
10
  * - one per entry of {@link FORBIDDEN_PUBLIC_DIRS}: any file under a directory a media download
11
11
  * writes (`public/catalog-images/` is `catalog-images-in-repo`, `public/blog/wp-content/uploads/`
12
- * is `blog-media-in-repo`). Decided from paths alone, so a caller without sizes still refuses
13
- * it. This list is the one source the validators and the reconcile read; the Gitea
14
- * pre-receive size guard names the same directories.
12
+ * is `blog-media-in-repo`), each with its own remedy. Decided from paths alone, so a caller
13
+ * without sizes still refuses it. This list is the one source the validators and the reconcile
14
+ * read; the Gitea pre-receive size guard names the same directories.
15
15
  * - `public-binary-oversized` — one raster or media file over {@link PUBLIC_BINARY_LIMITS}
16
- * `maxFileBytes`.
16
+ * `maxFileBytes`. An image's remedy is {@link IMAGE_ADD_COMMAND}.
17
17
  * - `public-tree-over-budget` — the whole `public/` tree over `maxTotalBytes`.
18
18
  *
19
19
  * The size findings need `bytes`; an entry without it is judged by path only.
@@ -31,14 +31,24 @@ export const PUBLIC_BINARY_LIMITS = Object.freeze({
31
31
  maxTotalBytes: 32 * 1024 * 1024,
32
32
  });
33
33
 
34
+ /** The kit command that moves downloaded catalog images and blog media back to their source URLs. */
35
+ export const UNHOST_IMAGES_COMMAND = "node agency-kit/tools/import-catalog.mjs --store <checkout> --unhost-images --write";
36
+
37
+ /** The command that puts an image the author holds into the platform pool and prints its `/_a/` URL. */
38
+ export const IMAGE_ADD_COMMAND = "tot image add <file>";
39
+
34
40
  /**
35
41
  * The directories under `public/` a media download writes. No file under one may be committed.
42
+ * `remedy` is the fix for files of that kind.
36
43
  *
37
- * @type {ReadonlyArray<Readonly<{ dir: string, code: string, noun: string }>>}
44
+ * @type {ReadonlyArray<Readonly<{ dir: string, code: string, noun: string, remedy: string }>>}
38
45
  */
39
46
  export const FORBIDDEN_PUBLIC_DIRS = Object.freeze([
40
- Object.freeze({ dir: "catalog-images/", code: "catalog-images-in-repo", noun: "catalog image" }),
41
- Object.freeze({ dir: "blog/wp-content/uploads/", code: "blog-media-in-repo", noun: "blog media" }),
47
+ Object.freeze({ dir: "catalog-images/", code: "catalog-images-in-repo", noun: "catalog image", remedy: UNHOST_IMAGES_COMMAND }),
48
+ Object.freeze({
49
+ dir: "blog/wp-content/uploads/", code: "blog-media-in-repo", noun: "blog media",
50
+ remedy: `a new post image: ${IMAGE_ADD_COMMAND}, then reference the /_a/ URL it prints; media a migration downloaded: ${UNHOST_IMAGES_COMMAND}`,
51
+ }),
42
52
  ]);
43
53
 
44
54
  /** The finding codes decided from paths alone. */
@@ -49,11 +59,11 @@ export function isForbiddenPublicPath(rel) {
49
59
  return FORBIDDEN_PUBLIC_DIRS.some((d) => rel.startsWith(d.dir));
50
60
  }
51
61
 
52
- /** The kit command that moves downloaded catalog images and blog media back to their source URLs. */
53
- export const UNHOST_IMAGES_COMMAND = "node agency-kit/tools/import-catalog.mjs --store <checkout> --unhost-images --write";
54
-
55
62
  const RASTER_OR_MEDIA = /\.(?:jpe?g|png|gif|webp|avif|bmp|tiff?|heic|heif|ico|jxl|mp4|m4v|mov|webm|mkv|avi|mpe?g|mp3|wav|ogg|oga|m4a|aac|flac|opus)$/i;
56
63
 
64
+ /** The formats {@link IMAGE_ADD_COMMAND} accepts. */
65
+ const UPLOADABLE_IMAGE = /\.(?:jpe?g|png|gif|webp|avif)$/i;
66
+
57
67
  const mb = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
58
68
 
59
69
  /**
@@ -66,7 +76,7 @@ export function publicBinaryFindings(entries, limits = {}) {
66
76
  const maxFileBytes = limits.maxFileBytes ?? PUBLIC_BINARY_LIMITS.maxFileBytes;
67
77
  const maxTotalBytes = limits.maxTotalBytes ?? PUBLIC_BINARY_LIMITS.maxTotalBytes;
68
78
  const findings = [];
69
- for (const { dir, code, noun } of FORBIDDEN_PUBLIC_DIRS) {
79
+ for (const { dir, code, noun, remedy } of FORBIDDEN_PUBLIC_DIRS) {
70
80
  const files = entries.filter((e) => e.rel.startsWith(dir));
71
81
  if (files.length === 0) continue;
72
82
  const bytes = files.reduce((n, e) => n + (e.bytes ?? 0), 0);
@@ -74,8 +84,8 @@ export function publicBinaryFindings(entries, limits = {}) {
74
84
  code,
75
85
  path: `public/${dir}`,
76
86
  message: `${files.length} ${noun} file(s)${bytes ? ` (${mb(bytes)})` : ""} are committed to the store repo; ` +
77
- "images belong in the platform pool, not the repo; until platform capture owns them, keep each image at its source URL",
78
- remedy: UNHOST_IMAGES_COMMAND,
87
+ "images belong in the platform pool, not the repo",
88
+ remedy,
79
89
  });
80
90
  }
81
91
  for (const e of entries) {
@@ -86,7 +96,9 @@ export function publicBinaryFindings(entries, limits = {}) {
86
96
  path: `public/${e.rel}`,
87
97
  message: `${mb(e.bytes)} raster/media file exceeds the ${mb(maxFileBytes)} per-file limit for the store repo; ` +
88
98
  "images and media belong in the platform pool",
89
- remedy: "reference the file by its source URL, or compress it under the limit",
99
+ remedy: UPLOADABLE_IMAGE.test(e.rel)
100
+ ? `${IMAGE_ADD_COMMAND}, then reference the /_a/ URL it prints instead of the file`
101
+ : "reference the file by its source URL, or compress it under the limit",
90
102
  });
91
103
  }
92
104
  }
@@ -98,7 +110,8 @@ export function publicBinaryFindings(entries, limits = {}) {
98
110
  path: "public/",
99
111
  message: `public/ holds ${mb(total)} across ${entries.length} file(s), over the ${mb(maxTotalBytes)} store-repo budget; ` +
100
112
  "images and media belong in the platform pool",
101
- remedy: "reference large or bulk assets by their source URL instead of committing them",
113
+ remedy: `upload images with ${IMAGE_ADD_COMMAND} and reference the /_a/ URLs it prints; ` +
114
+ "reference other large assets by their source URL",
102
115
  });
103
116
  }
104
117
  }
@@ -44,7 +44,7 @@ export type ManifestEntry = {
44
44
  address: string;
45
45
  byteLength: number;
46
46
  mediaType: string;
47
- provenance?: { kind: "local" | "remote"; locator: string };
47
+ provenance?: { kind: "local" | "remote" | "upload"; locator: string };
48
48
  };
49
49
 
50
50
  function objectKey(tenantId: string, address: string): string {
@@ -11,6 +11,10 @@
11
11
  const { defineWorkersConfig } = require("@cloudflare/vitest-pool-workers/config");
12
12
  const esbuild = require("esbuild");
13
13
  const path = require("node:path");
14
+ // The check step's CPU budget sizes the worker pool (scripts/lib/step-cpus.cjs). Concurrent cells
15
+ // are not held to it: they interleave on the one workerd isolate thread and wait on alarms, so 24
16
+ // at once keep under one CPU busy (measured 2026-10-22: 11.5 CPU-s over 16.8s at 24; 87s at 3).
17
+ const { stepWorkers } = require("../../../../scripts/lib/step-cpus.cjs");
14
18
 
15
19
  const compatibilityDate = "2024-11-01";
16
20
  const compatibilityFlags = ["nodejs_compat"];
@@ -20,6 +24,7 @@ const HOST = "pipeline-host";
20
24
  * @param {{ root: string; name: string; include: string[]; maxConcurrency?: number; testTimeout?: number }} options
21
25
  */
22
26
  function pipelinePoolConfig({ root, name, include, maxConcurrency, testTimeout = 60_000 }) {
27
+ const workers = stepWorkers();
23
28
  const hostModule = esbuild.buildSync({
24
29
  entryPoints: [path.join(root, "test/pipeline/pipeline-host.ts")],
25
30
  bundle: true,
@@ -42,6 +47,8 @@ function pipelinePoolConfig({ root, name, include, maxConcurrency, testTimeout =
42
47
  testTimeout,
43
48
  hookTimeout: 60_000,
44
49
  ...(maxConcurrency ? { maxConcurrency } : {}),
50
+ maxWorkers: workers.maxWorkers,
51
+ minWorkers: workers.minWorkers,
45
52
  poolOptions: {
46
53
  workers: {
47
54
  isolatedStorage: false,
@@ -3,8 +3,12 @@
3
3
  // node_modules/.vite/vitest.
4
4
  process.env.VITE_CJS_IGNORE_WARNING = "1";
5
5
  const { defineConfig } = require("vitest/config");
6
+ // The check step's CPU budget sizes the worker pool (scripts/lib/step-cpus.cjs).
7
+ const { stepWorkers } = require("../../scripts/lib/step-cpus.cjs");
6
8
  const path = require("node:path");
7
9
 
10
+ const workers = stepWorkers();
11
+
8
12
  // Storefront unit tests — pure TS logic (tenant routing, search, query engine,
9
13
  // image-attr resolution, edge cache, etc.). We deliberately do NOT render .astro
10
14
  // components here: the Astro Container API needs Astro's Vite pipeline, whose
@@ -34,5 +38,7 @@ module.exports = defineConfig({
34
38
  // Folds and CI run many suites at once; a fixture-heavy test that takes 2s alone can pass 5s
35
39
  // under that load. A genuinely hung test still fails, only later.
36
40
  testTimeout: 30_000,
41
+ maxWorkers: workers.maxWorkers,
42
+ minWorkers: workers.minWorkers,
37
43
  },
38
44
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
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",
@@ -28,7 +28,7 @@ export type CaptureRewrite = {
28
28
  /** A sealed capture-manifest entry, as `original-image-capture.ts` writes it. */
29
29
  export interface CapturedManifestEntry {
30
30
  /** Remote `locator` is query-stripped for display; remote entries are matched by `locatorDigest`. */
31
- provenance: { kind: "local" | "remote"; locator: string };
31
+ provenance: { kind: "local" | "remote" | "upload"; locator: string };
32
32
  address: string;
33
33
  mediaType: string;
34
34
  /** Remote entries: digest of the exact fetched locator, query included (`captureLocatorDigest`). */
@@ -141,7 +141,7 @@ export async function digestCaptureManifest(manifest: {
141
141
  version: 1;
142
142
  revisionId: string;
143
143
  entries: ReadonlyArray<{
144
- provenance: { kind: "local" | "remote"; locator: string };
144
+ provenance: { kind: "local" | "remote" | "upload"; locator: string };
145
145
  address: string;
146
146
  byteLength: number;
147
147
  mediaType: string;
@@ -3,6 +3,10 @@
3
3
  // node_modules/.vite/vitest.
4
4
  process.env.VITE_CJS_IGNORE_WARNING = "1";
5
5
  const { defineConfig } = require("vitest/config");
6
+ // The check step's CPU budget sizes the worker pool (scripts/lib/step-cpus.cjs).
7
+ const { stepWorkers } = require("../../scripts/lib/step-cpus.cjs");
8
+
9
+ const workers = stepWorkers();
6
10
 
7
11
  module.exports = defineConfig({
8
12
  test: {
@@ -12,5 +16,7 @@ module.exports = defineConfig({
12
16
  // Folds and CI run many suites at once; a fixture-heavy test that takes 0.5s alone can pass 5s
13
17
  // under that load. A genuinely hung test still fails, only later.
14
18
  testTimeout: 30_000,
19
+ maxWorkers: workers.maxWorkers,
20
+ minWorkers: workers.minWorkers,
15
21
  },
16
22
  });
@@ -0,0 +1,48 @@
1
+ // The CPU budget of the check step this process runs in. The check runner
2
+ // (scripts/lib/check-runner.mjs) reserves slots for each step and exports the count as
3
+ // TOT_STEP_CPUS; every pool a step starts (vitest workers, `node --test`
4
+ // files, `pnpm -r` packages) sizes itself from it, so a step never uses more CPUs than it reserved.
5
+ // With TOT_STEP_CPUS unset (a suite run by hand) every pool keeps its own default.
6
+ //
7
+ // CommonJS so the in-memory `.cts` vitest configs can require it as well as ESM can import it.
8
+ "use strict";
9
+
10
+ const STEP_CPUS_ENV = "TOT_STEP_CPUS";
11
+
12
+ /**
13
+ * The step's CPU budget, or null outside a budgeted step.
14
+ * @param {NodeJS.ProcessEnv} [env]
15
+ * @returns {number | null}
16
+ */
17
+ function stepCpus(env = process.env) {
18
+ const raw = env[STEP_CPUS_ENV];
19
+ if (raw === undefined || raw === "") return null;
20
+ const cpus = Number(raw);
21
+ if (!Number.isInteger(cpus) || cpus < 1) throw new Error(`${STEP_CPUS_ENV} must be a positive integer, got "${raw}"`);
22
+ return cpus;
23
+ }
24
+
25
+ /**
26
+ * vitest's worker pool for this step: at most the budget. Both are undefined outside a budgeted
27
+ * step, which leaves vitest's defaults.
28
+ * @param {NodeJS.ProcessEnv} [env]
29
+ * @returns {{ maxWorkers: number | undefined, minWorkers: number | undefined }}
30
+ */
31
+ function stepWorkers(env = process.env) {
32
+ const cpus = stepCpus(env);
33
+ return { maxWorkers: cpus ?? undefined, minWorkers: cpus === null ? undefined : 1 };
34
+ }
35
+
36
+ /**
37
+ * `node --test` flags with the run's file concurrency held to the budget. A flag list that already
38
+ * sets --test-concurrency keeps it.
39
+ * @param {string[]} flags
40
+ * @param {NodeJS.ProcessEnv} [env]
41
+ */
42
+ function nodeTestFlags(flags, env = process.env) {
43
+ const cpus = stepCpus(env);
44
+ if (cpus === null || flags.some((flag) => flag.startsWith("--test-concurrency"))) return flags;
45
+ return [...flags, `--test-concurrency=${cpus}`];
46
+ }
47
+
48
+ module.exports = { STEP_CPUS_ENV, stepCpus, stepWorkers, nodeTestFlags };