@tokenoftrust/storefront-runner 2.2.89 → 2.2.90
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/agency-kit/tools/lib/catalog-model/files.d.mts +48 -0
- package/agency-kit/tools/lib/catalog-model/files.mjs +263 -0
- package/agency-kit/tools/lib/catalog-model/model.mjs +282 -0
- package/agency-kit/tools/lib/tot-repo-mappings.mjs +153 -0
- package/apps/storefront/src/config/devTenantSeed.ts +7 -7
- package/apps/storefront/src/config/publicationSpaceRegistry.ts +1 -0
- package/apps/storefront/src/config/resolver.ts +19 -1
- package/apps/storefront/src/lib/storyblok/provider.ts +25 -1
- package/apps/storefront/src/lib/tot/ToTClient.ts +63 -18
- package/apps/storefront/src/lib/tot/tenantCatalog.ts +126 -0
- package/apps/storefront/src/middleware/controlPlane.ts +5 -5
- package/apps/storefront/src/middleware/renderChain.ts +19 -12
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/catalog-files.ts +51 -0
- package/packages/public-runtime/src/tenant.ts +19 -0
- package/scripts/dev/standalone-graft-plan.mjs +32 -10
- package/scripts/tenant/materialize-tenant-content.mjs +8 -1
- package/scripts/tenant/validate.mjs +69 -31
- package/scripts/tot-dev.mjs +18 -18
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A store's own catalog files — the cold content artifact of ADR 0033 — typed for the platform.
|
|
3
|
+
*
|
|
4
|
+
* The layout, the validator and the parser live once, in the agency kit's pure catalog module
|
|
5
|
+
* (`agency-kit/tools/lib/catalog-model/files.mjs`), which the kit's own tools, the renderer, the
|
|
6
|
+
* reconcile validator and the migration engine all import. This module only narrows its records
|
|
7
|
+
* to the canonical types in `./product.ts`; the kit's model is pinned to those types field for
|
|
8
|
+
* field by `scripts/lib/agency-kit-catalog-drift.test.mjs`.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
readCatalogFile as readKitCatalogFile,
|
|
12
|
+
readCatalogFiles as readKitCatalogFiles,
|
|
13
|
+
type CatalogStore,
|
|
14
|
+
} from "../../../agency-kit/tools/lib/catalog-model/files.mjs";
|
|
15
|
+
import type { CatalogCollection, CatalogProduct } from "./product.js";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
CATALOG_ARTIFACT_PREFIX,
|
|
19
|
+
CATALOG_COLLECTIONS_PREFIX,
|
|
20
|
+
CATALOG_CONTENT_DIRS,
|
|
21
|
+
CATALOG_HANDLE_RE,
|
|
22
|
+
CATALOG_PRODUCTS_PREFIX,
|
|
23
|
+
CatalogFileError,
|
|
24
|
+
catalogFileIssues,
|
|
25
|
+
classifyCatalogPath,
|
|
26
|
+
declaredAvailability,
|
|
27
|
+
planCatalogFiles,
|
|
28
|
+
type CatalogFileKind,
|
|
29
|
+
type CatalogStore,
|
|
30
|
+
type InventoryProvider,
|
|
31
|
+
} from "../../../agency-kit/tools/lib/catalog-model/files.mjs";
|
|
32
|
+
|
|
33
|
+
export type CatalogFileRecord =
|
|
34
|
+
| { kind: "product"; record: CatalogProduct }
|
|
35
|
+
| { kind: "collection"; record: CatalogCollection };
|
|
36
|
+
|
|
37
|
+
/** One validated catalog file as its canonical record. Throws `CatalogFileError`. */
|
|
38
|
+
export function readCatalogFile(relative: string, raw: string, store: CatalogStore): CatalogFileRecord {
|
|
39
|
+
return readKitCatalogFile(relative, raw, store) as unknown as CatalogFileRecord;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A whole validated catalog. Throws `CatalogFileError` naming every failing file. */
|
|
43
|
+
export function readCatalogFiles(
|
|
44
|
+
entries: Iterable<{ path: string; raw: string | null }>,
|
|
45
|
+
store: CatalogStore,
|
|
46
|
+
): { products: CatalogProduct[]; collections: CatalogCollection[] } {
|
|
47
|
+
return readKitCatalogFiles(entries, store) as unknown as {
|
|
48
|
+
products: CatalogProduct[];
|
|
49
|
+
collections: CatalogCollection[];
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -622,6 +622,25 @@ export interface TenantConfig {
|
|
|
622
622
|
*/
|
|
623
623
|
htmlCachePolicy?: HtmlCachePolicy;
|
|
624
624
|
};
|
|
625
|
+
/**
|
|
626
|
+
* Catalog serving (ADR 0033). The store's own `content/catalog/` files are the
|
|
627
|
+
* system of record; this declares which artifact serves reads.
|
|
628
|
+
*
|
|
629
|
+
* `"files"` (the default): the store's catalog files, then the platform's
|
|
630
|
+
* grandfathered in-repo snapshot for the same data scope, then an empty catalog.
|
|
631
|
+
* `"d1"`: the derived D1 serving projection, read on the control plane only.
|
|
632
|
+
* Never inferred from a tenant id.
|
|
633
|
+
*/
|
|
634
|
+
catalog?: {
|
|
635
|
+
serving?: "files" | "d1";
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Availability (hot) provider (ADR 0033). Availability is never read from catalog
|
|
639
|
+
* content files. Absent ⇒ `"mirror"`, the only implemented provider.
|
|
640
|
+
*/
|
|
641
|
+
inventory?: {
|
|
642
|
+
provider: "mirror" | "live-hydrate" | "tot-authoritative";
|
|
643
|
+
};
|
|
625
644
|
/**
|
|
626
645
|
* Storefront product/subscription state — the paywall the platform gates GO-LIVE
|
|
627
646
|
* on. This is the storefront's
|
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* Pure planning for `tot dev --workspace` grafts: which checkout-relative paths
|
|
4
|
-
* get copied
|
|
5
|
-
* each is a file or a directory tree. Decision-only — scripts/tot-dev.mjs
|
|
6
|
-
* the actual fs copy/backup/watch effects per planned entry.
|
|
4
|
+
* get copied where in the in-tree `tenants/<id>/` layout the app globs, and
|
|
5
|
+
* whether each is a file or a directory tree. Decision-only — scripts/tot-dev.mjs
|
|
6
|
+
* does the actual fs copy/backup/watch effects per planned entry.
|
|
7
|
+
*
|
|
8
|
+
* A mapping's `workspace` is where the files live in the checkout; its `repo`
|
|
9
|
+
* names what they are, tenant-relative (`theme.json`, `content/`, …). They land
|
|
10
|
+
* under THIS tenant's directory — the same resolution the hosted reconcile uses
|
|
11
|
+
* (agency-kit/tools/lib/tot-repo-mappings.mjs), whatever tenant segment `repo`
|
|
12
|
+
* spells — so the dev server serves them at the same `/tenants/<tenant>/…` the
|
|
13
|
+
* hosted preview does.
|
|
7
14
|
*/
|
|
15
|
+
import { tenantDirRelative } from "../lib/tenant-dirs.mjs";
|
|
16
|
+
import { parseMappingTarget } from "../../agency-kit/tools/lib/tot-repo-mappings.mjs";
|
|
17
|
+
|
|
18
|
+
const TOT_CONFIG = ".tot/config.json";
|
|
8
19
|
|
|
9
20
|
/**
|
|
10
|
-
* @param {
|
|
11
|
-
*
|
|
12
|
-
*
|
|
21
|
+
* @param {string} tenant canonical tenant id (the checkout config's `tenant`)
|
|
22
|
+
* @param {unknown} mappings the checkout's own `.tot/config.json` `mappings` array
|
|
23
|
+
* (content/, public/, theme.json, …)
|
|
24
|
+
* @returns {{ workspace: string, target: string | null, isDir: boolean }[]} checkout-relative
|
|
25
|
+
* `workspace` and repo-relative `target` (POSIX); `target` is null for a mapping whose `repo` is
|
|
26
|
+
* not a valid `tenants/<id>/<path>` target, which the caller skips
|
|
13
27
|
*/
|
|
14
|
-
export function standaloneGraftPlan(mappings) {
|
|
28
|
+
export function standaloneGraftPlan(tenant, mappings) {
|
|
29
|
+
const root = tenantDirRelative(tenant);
|
|
15
30
|
const plan = [];
|
|
16
31
|
const seen = new Set();
|
|
17
32
|
for (const map of Array.isArray(mappings) ? mappings : []) {
|
|
18
33
|
const workspace = String(map?.workspace ?? "").replace(/\/+$/, "");
|
|
19
34
|
if (!workspace || seen.has(workspace)) continue;
|
|
20
35
|
seen.add(workspace);
|
|
21
|
-
|
|
36
|
+
if (workspace === TOT_CONFIG) {
|
|
37
|
+
plan.push({ workspace, target: `${root}/${TOT_CONFIG}`, isDir: false });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const declared = parseMappingTarget(map?.repo);
|
|
41
|
+
const rel = declared ? declared.rel.replace(/\/+$/, "") : null;
|
|
42
|
+
const target = rel === null ? null : rel ? `${root}/${rel}` : root;
|
|
43
|
+
plan.push({ workspace, target, isDir: map?.kind !== "file" });
|
|
22
44
|
}
|
|
23
45
|
// `.tot/config.json` is never itself one of the declared `mappings` (those map
|
|
24
46
|
// content/public/theme.json) but IS the tenant's own declared platform config
|
|
@@ -28,8 +50,8 @@ export function standaloneGraftPlan(mappings) {
|
|
|
28
50
|
// scripts/tenant/materialize-tenant-content.mjs copies for a Gitea-sourced
|
|
29
51
|
// tenant. Graft it unconditionally so a regulated store's mandated notices
|
|
30
52
|
// resolve from the SAME declared config locally as they would once published.
|
|
31
|
-
if (!seen.has(
|
|
32
|
-
plan.push({ workspace:
|
|
53
|
+
if (!seen.has(TOT_CONFIG)) {
|
|
54
|
+
plan.push({ workspace: TOT_CONFIG, target: `${root}/${TOT_CONFIG}`, isDir: false });
|
|
33
55
|
}
|
|
34
56
|
return plan;
|
|
35
57
|
}
|
|
@@ -71,6 +71,7 @@ import { tmpdir } from "node:os";
|
|
|
71
71
|
import { dirname, join, resolve, sep } from "node:path";
|
|
72
72
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
73
73
|
import { tenantDirRelative } from "../lib/tenant-dirs.mjs";
|
|
74
|
+
import { parseMappingTarget } from "../../agency-kit/tools/lib/tot-repo-mappings.mjs";
|
|
74
75
|
|
|
75
76
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
76
77
|
const repoRoot = resolve(here, "..", "..");
|
|
@@ -153,7 +154,13 @@ function canonicalMappings(tenant) {
|
|
|
153
154
|
*/
|
|
154
155
|
export function copyMapping({ checkoutDir, root, tenant, mapping, log = (/** @type {string} */ _m) => {} }) {
|
|
155
156
|
const src = join(checkoutDir, mapping.workspace.replace(/\/+$/, ""));
|
|
156
|
-
|
|
157
|
+
// The mapping names WHAT the files are (its tenant-relative target); the
|
|
158
|
+
// tenant being materialized names WHOSE — never the target's tenant segment.
|
|
159
|
+
const declared = parseMappingTarget(mapping.repo);
|
|
160
|
+
if (!declared) {
|
|
161
|
+
throw new Error(`mapping target "${mapping.repo}" escapes tenants/${tenant}/ — refusing`);
|
|
162
|
+
}
|
|
163
|
+
const target = resolveTargetPath(root, tenant, `tenants/${tenant}/${declared.rel}`.replace(/\/+$/, ""));
|
|
157
164
|
if (target === resolve(root, tenantDirRelative(tenant))) {
|
|
158
165
|
throw new Error("mapping target cannot replace the tenant root");
|
|
159
166
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Catches, BEFORE a commit reaches the hosted storefront, the classes of tenant
|
|
6
6
|
* defect that otherwise fail silently or as a cryptic runtime/reconcile error:
|
|
7
7
|
* · `.tot/config.json` mapping-shape drift (the #24/#25 regression — a `kind:"dir"`
|
|
8
|
-
* / missing-`repo` mapping makes the flow-back reconcile
|
|
8
|
+
* / missing-`repo` mapping makes the flow-back reconcile refuse it)
|
|
9
9
|
* · malformed `theme.json` / `home.json` / `chrome.json` (an eager Vite glob turns
|
|
10
10
|
* these into a WHOLE-APP BUILD FAILURE)
|
|
11
11
|
* · raw HTML that isn't a servable document, or drops CSS under the CSP nonce
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Design principle — "valid locally ≡ will reconcile in storefront": the checks below
|
|
17
17
|
* mirror the reconcile pipeline's own gate (`@tot/private-controlplane`
|
|
18
|
-
* customization-reconcile.ts `
|
|
18
|
+
* customization-reconcile.ts `validateMappingShape` / `validateRawHtml`, and the shared mapping resolver `tot-repo-mappings.mjs`). Keep them in step.
|
|
19
19
|
*
|
|
20
20
|
* Dependency-free. Two modes (mirrors scripts/tot-dev.mjs):
|
|
21
21
|
* node scripts/tenant/validate.mjs <tenant> # monorepo: tenants/<tenant>/
|
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
isSafeRehostedAssetPathForTenant,
|
|
39
39
|
isSafeStorefrontHref,
|
|
40
40
|
} from "../../packages/public-runtime/src/extension-contract-values.mjs";
|
|
41
|
+
import {
|
|
42
|
+
foreignTenantSegments,
|
|
43
|
+
parseMappingTarget,
|
|
44
|
+
workspacePathForTenantRelative,
|
|
45
|
+
} from "../../agency-kit/tools/lib/tot-repo-mappings.mjs";
|
|
41
46
|
import { validateVisualParityClosure } from "../../packages/cli/src/visual-parity.mjs";
|
|
42
47
|
|
|
43
48
|
/** Catalogued embed slugs a tenant may opt into (mirror of EMBED_PROVIDERS). */
|
|
@@ -97,7 +102,7 @@ const REQUIRED_WORKSPACES = ["content/", "public/", "theme.json"];
|
|
|
97
102
|
|
|
98
103
|
/**
|
|
99
104
|
* Validate a parsed `.tot/config.json` against the shape the reconcile pipeline
|
|
100
|
-
* (`
|
|
105
|
+
* (`validateMappingShape`) requires. Pure — no I/O — so the CI regression test can call it.
|
|
101
106
|
* @param {any} config parsed JSON
|
|
102
107
|
* @param {string} file path for messages
|
|
103
108
|
* @returns {Finding[]}
|
|
@@ -123,18 +128,29 @@ export function validateConfigShape(config, file = ".tot/config.json") {
|
|
|
123
128
|
}
|
|
124
129
|
if (typeof m.workspace !== "string" || !m.workspace) {
|
|
125
130
|
out.push(mk(ERROR, "mapping-workspace", at, "`workspace` must be a non-empty string"));
|
|
126
|
-
} else {
|
|
127
|
-
seen.add(m.workspace.replace(/\/+$/, "") + (m.kind === "tree" ? "/" : ""));
|
|
128
|
-
seen.add(m.workspace);
|
|
129
131
|
}
|
|
130
|
-
// THE regression guard: reconcile
|
|
131
|
-
// knows "file" vs tree — a missing repo or
|
|
132
|
+
// THE regression guard: reconcile reads the tenant-relative target out of
|
|
133
|
+
// m.repo and only knows "file" vs tree — a missing/unparseable repo or
|
|
134
|
+
// kind:"dir" is refused there, so refuse it here first.
|
|
132
135
|
if (typeof m.repo !== "string" || !m.repo) {
|
|
133
136
|
out.push(
|
|
134
137
|
mk(ERROR, "mapping-repo", at,
|
|
135
|
-
`mapping for "${m.workspace ?? "?"}" is missing a string \`repo\` (reconcile
|
|
138
|
+
`mapping for "${m.workspace ?? "?"}" is missing a string \`repo\` (the reconcile refuses it)`,
|
|
136
139
|
`add "repo": "tenants/${config.tenant ?? "<id>"}/${(m.workspace ?? "").replace(/\/+$/, "")}${m.kind === "file" ? "" : "/"}"`),
|
|
137
140
|
);
|
|
141
|
+
} else {
|
|
142
|
+
const target = parseMappingTarget(m.repo);
|
|
143
|
+
if (!target) {
|
|
144
|
+
out.push(
|
|
145
|
+
mk(ERROR, "mapping-repo", at,
|
|
146
|
+
`mapping for "${m.workspace ?? "?"}" targets "${m.repo}" — \`repo\` must be a clean path under tenants/<id>/ (the reconcile refuses it)`,
|
|
147
|
+
`e.g. "repo": "tenants/${config.tenant ?? "<id>"}/content/"`),
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
// Coverage is about WHAT is mapped (the target), not where it lives.
|
|
151
|
+
seen.add(target.rel.replace(/\/+$/, "") + (m.kind === "tree" ? "/" : ""));
|
|
152
|
+
seen.add(target.rel);
|
|
153
|
+
}
|
|
138
154
|
}
|
|
139
155
|
if (!KNOWN_KINDS.has(m.kind)) {
|
|
140
156
|
out.push(
|
|
@@ -998,16 +1014,9 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
998
1014
|
/** @type {Finding[]} */
|
|
999
1015
|
const findings = [];
|
|
1000
1016
|
const rel = (p) => relative(tenantDir, p) || p;
|
|
1001
|
-
const contentDir = join(tenantDir, "content");
|
|
1002
|
-
const publicDir = join(tenantDir, "public");
|
|
1003
|
-
const pagesDir = join(contentDir, "pages-html");
|
|
1004
|
-
|
|
1005
|
-
// 0. basic layout
|
|
1006
|
-
if (!existsSync(contentDir)) {
|
|
1007
|
-
findings.push(mk(ERROR, "layout", "content/", "no content/ directory"));
|
|
1008
|
-
}
|
|
1009
1017
|
|
|
1010
|
-
// 1. .tot/config.json — shape (flow-back readiness)
|
|
1018
|
+
// 1. .tot/config.json — shape (flow-back readiness). Read first: its mappings
|
|
1019
|
+
// say where theme.json / content/ / public/ live in this tree.
|
|
1011
1020
|
const configPath = join(tenantDir, ".tot", "config.json");
|
|
1012
1021
|
let config = null;
|
|
1013
1022
|
if (existsSync(configPath)) {
|
|
@@ -1018,7 +1027,18 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1018
1027
|
config = value;
|
|
1019
1028
|
findings.push(...validateConfigShape(value, ".tot/config.json"));
|
|
1020
1029
|
}
|
|
1021
|
-
}
|
|
1030
|
+
}
|
|
1031
|
+
const located = (tenantRel) => join(tenantDir, workspacePathForTenantRelative(config, tenantRel));
|
|
1032
|
+
const contentDir = located("content");
|
|
1033
|
+
const publicDir = located("public");
|
|
1034
|
+
const pagesDir = join(contentDir, "pages-html");
|
|
1035
|
+
|
|
1036
|
+
// 0. basic layout
|
|
1037
|
+
if (!existsSync(contentDir)) {
|
|
1038
|
+
findings.push(mk(ERROR, "layout", rel(contentDir) + "/", `no ${rel(contentDir)}/ directory`));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (!existsSync(configPath)) {
|
|
1022
1042
|
findings.push(
|
|
1023
1043
|
mk(WARN, "config-absent", ".tot/config.json",
|
|
1024
1044
|
"no .tot/config.json — tenant can serve but is NOT flow-back-ready (reconcile has nothing to map)",
|
|
@@ -1031,7 +1051,28 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1031
1051
|
);
|
|
1032
1052
|
}
|
|
1033
1053
|
const scope = opts.scope || config?.scope;
|
|
1034
|
-
|
|
1054
|
+
// The canonical tenant id: the one the asset rule binds to and the one the
|
|
1055
|
+
// mappings' targets resolve under — never a directory name.
|
|
1056
|
+
const activeTenantId = opts.tenantId || config?.tenant || scope;
|
|
1057
|
+
if (config && activeTenantId) {
|
|
1058
|
+
for (const f of foreignTenantSegments(config, activeTenantId)) {
|
|
1059
|
+
findings.push(
|
|
1060
|
+
mk(WARN, "mapping-tenant-segment", ".tot/config.json",
|
|
1061
|
+
`mapping for "${f.workspace}" targets "${f.repo}", naming tenant "${f.segment}" — this store is "${activeTenantId}". ` +
|
|
1062
|
+
`It still resolves under tenants/${activeTenantId}/ (the segment is not read), and its assets are served at /tenants/${activeTenantId}/...`,
|
|
1063
|
+
`write "repo": "tenants/${activeTenantId}/${parseMappingTarget(f.repo)?.rel ?? ""}" so the config agrees with itself ` +
|
|
1064
|
+
`(the agency kit's tools/repair-tenant-config.mjs rewrites the whole config)`),
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
// Publication refuses a config whose `scope` is not the store's id.
|
|
1069
|
+
if (config && typeof config.tenant === "string" && config.scope !== config.tenant) {
|
|
1070
|
+
findings.push(
|
|
1071
|
+
mk(WARN, "config-scope-mismatch", ".tot/config.json",
|
|
1072
|
+
`"scope" is ${JSON.stringify(config.scope ?? null)} but "tenant" is "${config.tenant}" — publication refuses a config whose scope is not the store's id`,
|
|
1073
|
+
`set "scope" to "${config.tenant}" (the agency kit's tools/repair-tenant-config.mjs rewrites the whole config)`),
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1035
1076
|
// platform-route awareness: which framework routes the host platform owns for this tenant
|
|
1036
1077
|
const mode = opts.mode === "workspace" ? "workspace" : "monorepo";
|
|
1037
1078
|
const hostIsPlatform = mode === "monorepo" || config?.hostPlatform === "tot-storefront";
|
|
@@ -1041,18 +1082,18 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1041
1082
|
}
|
|
1042
1083
|
|
|
1043
1084
|
// 2. theme.json — parse (a malformed one is a WHOLE-APP BUILD FAILURE)
|
|
1044
|
-
const themePath =
|
|
1085
|
+
const themePath = located("theme.json");
|
|
1045
1086
|
if (existsSync(themePath)) {
|
|
1046
1087
|
const { error } = readJsonSafe(themePath);
|
|
1047
1088
|
if (error) {
|
|
1048
1089
|
findings.push(
|
|
1049
|
-
mk(ERROR, "theme-parse",
|
|
1090
|
+
mk(ERROR, "theme-parse", rel(themePath),
|
|
1050
1091
|
`invalid JSON: ${error}`, "theme.json is loaded by an eager Vite glob — a parse error fails the entire storefront build"),
|
|
1051
1092
|
);
|
|
1052
1093
|
}
|
|
1053
1094
|
}
|
|
1054
1095
|
|
|
1055
|
-
const capabilitiesPath =
|
|
1096
|
+
const capabilitiesPath = located("capabilities.json");
|
|
1056
1097
|
if (existsSync(capabilitiesPath)) {
|
|
1057
1098
|
const { value, error } = readJsonSafe(capabilitiesPath);
|
|
1058
1099
|
if (error) {
|
|
@@ -1062,7 +1103,7 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1062
1103
|
}
|
|
1063
1104
|
}
|
|
1064
1105
|
|
|
1065
|
-
const scriptsPath =
|
|
1106
|
+
const scriptsPath = located("scripts.json");
|
|
1066
1107
|
if (existsSync(scriptsPath)) {
|
|
1067
1108
|
const { value, error } = readJsonSafe(scriptsPath);
|
|
1068
1109
|
if (error) {
|
|
@@ -1072,7 +1113,7 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1072
1113
|
}
|
|
1073
1114
|
}
|
|
1074
1115
|
|
|
1075
|
-
const embedsPath =
|
|
1116
|
+
const embedsPath = located("embeds.json");
|
|
1076
1117
|
if (existsSync(embedsPath)) {
|
|
1077
1118
|
const { value, error } = readJsonSafe(embedsPath);
|
|
1078
1119
|
if (error) {
|
|
@@ -1177,9 +1218,7 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1177
1218
|
// content/ plus the root config files.
|
|
1178
1219
|
const conflictScanFiles = [
|
|
1179
1220
|
...walk(contentDir, hasScanExt),
|
|
1180
|
-
...[
|
|
1181
|
-
.map((f) => join(tenantDir, f))
|
|
1182
|
-
.filter((p) => existsSync(p)),
|
|
1221
|
+
...[themePath, capabilitiesPath, scriptsPath, configPath].filter((p) => existsSync(p)),
|
|
1183
1222
|
];
|
|
1184
1223
|
for (const p of conflictScanFiles) {
|
|
1185
1224
|
const lines = detectConflictMarkers(readFileSync(p, "utf8"));
|
|
@@ -1200,9 +1239,8 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
1200
1239
|
// compare against, since the opt-in may be the platform build-time default.
|
|
1201
1240
|
if (existsSync(contentDir)) {
|
|
1202
1241
|
let declaredEmbeds = null;
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const { value } = readJsonSafe(embedsFile);
|
|
1242
|
+
if (existsSync(embedsPath)) {
|
|
1243
|
+
const { value } = readJsonSafe(embedsPath);
|
|
1206
1244
|
if (Array.isArray(value)) declaredEmbeds = value.filter((s) => typeof s === "string");
|
|
1207
1245
|
}
|
|
1208
1246
|
for (const f of lintTenantEmbeds({ dir: contentDir, declared: declaredEmbeds }).findings) {
|
package/scripts/tot-dev.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
import { dirname, resolve, join } from "node:path";
|
|
63
63
|
import { fileURLToPath } from "node:url";
|
|
64
64
|
import { resolveDevSourceRoot } from "./dev/tenant-source-root.mjs";
|
|
65
|
-
import {
|
|
65
|
+
import { tenantDirSegments } from "./lib/tenant-dirs.mjs";
|
|
66
66
|
import { firstFreePort, isPortFree } from "./dev/port-check.mjs";
|
|
67
67
|
import { busyPortMessage, classifyBusyPort, lockPort, unlockPort } from "./dev/port-lock.mjs";
|
|
68
68
|
import { resync, startWatch } from "./dev/checkout-watch.mjs";
|
|
@@ -276,12 +276,12 @@ if (workspacePath) {
|
|
|
276
276
|
console.log(` checkout: ${wsRoot}`);
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
* Graft one checkout path (`wsRel`, checkout-relative)
|
|
280
|
-
* `tenants/<tenant>/` layout the app globs, with the same
|
|
281
|
-
* watch-and-resync machinery every mapping gets. Returns
|
|
279
|
+
* Graft one checkout path (`wsRel`, checkout-relative) to `repoRel` in the
|
|
280
|
+
* in-tree `tenants/<tenant>/` layout the app globs, with the same
|
|
281
|
+
* move-aside/restore + watch-and-resync machinery every mapping gets. Returns
|
|
282
|
+
* true if grafted.
|
|
282
283
|
*/
|
|
283
|
-
function graftOne(wsRel, isDir) {
|
|
284
|
-
const repoRel = join(tenantDirRelative(tenant), wsRel);
|
|
284
|
+
function graftOne(wsRel, repoRel, isDir) {
|
|
285
285
|
|
|
286
286
|
const source = resolve(wsRoot, wsRel); // real files in the checkout
|
|
287
287
|
const target = resolve(repoRoot, repoRel); // where the app globs them
|
|
@@ -320,19 +320,19 @@ if (workspacePath) {
|
|
|
320
320
|
return true;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// standaloneGraftPlan ALSO appends `.tot/config.json` unconditionally — see its
|
|
330
|
-
// own doc comment. Its success does NOT count toward "did we graft a usable
|
|
331
|
-
// site" below: a checkout with config but no content/public/theme is still
|
|
332
|
-
// nothing to serve.
|
|
323
|
+
// Each mapping's checkout files land where its `repo` says, under THIS tenant's
|
|
324
|
+
// directory (see scripts/dev/standalone-graft-plan.mjs) — resolved exactly as
|
|
325
|
+
// the hosted reconcile resolves them. standaloneGraftPlan ALSO appends
|
|
326
|
+
// `.tot/config.json` unconditionally — see its own doc comment. Its success does
|
|
327
|
+
// NOT count toward "did we graft a usable site" below: a checkout with config but
|
|
328
|
+
// no content/public/theme is still nothing to serve.
|
|
333
329
|
let mappedCount = 0;
|
|
334
|
-
for (const { workspace, isDir } of standaloneGraftPlan(mappings)) {
|
|
335
|
-
|
|
330
|
+
for (const { workspace, target, isDir } of standaloneGraftPlan(tenant, mappings)) {
|
|
331
|
+
if (!target) {
|
|
332
|
+
console.warn(` ⚠ mapping for "${workspace}" has no valid "repo" under tenants/<id>/ — skipping (run tot validate)`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const grafted = graftOne(workspace, target, isDir);
|
|
336
336
|
if (workspace === ".tot/config.json") {
|
|
337
337
|
if (!grafted) console.warn(` ⚠ checkout has no .tot/config.json — compliance/capabilities/siteType resolve undeclared.`);
|
|
338
338
|
continue;
|