@homepages/template-kit 0.9.0-dev-20260719205406 → 0.9.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.
- package/CHANGELOG.md +60 -59
- package/README.md +14 -4
- package/dist/contracts/fill-treatments.d.ts +23 -0
- package/dist/fixtures/sample-property.d.ts +4 -7
- package/dist/fixtures/sample-property.js +41 -97
- package/dist/fixtures/scenario.d.ts +1 -1
- package/dist/fixtures/scenarios/for-rent-condo.js +24 -44
- package/dist/fixtures/scenarios/luxury-multi-unit.js +1 -1
- package/dist/fixtures/scenarios/sparse-single-family.js +14 -26
- package/dist/fixtures.d.ts +2 -2
- package/dist/fixtures.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/islands/discover.js +1 -1
- package/dist/islands/esbuild-plugin.js +1 -1
- package/dist/islands/wrap.d.ts +9 -1
- package/dist/manifest.json +940 -0
- package/dist/media/pack.d.ts +49 -0
- package/dist/media/pack.js +69 -0
- package/dist/media/resolve-fixture-media.d.ts +22 -0
- package/dist/media/resolve-fixture-media.js +73 -0
- package/dist/media/resolve-media.js +195 -0
- package/dist/package.js +1 -1
- package/dist/schema/property-facts.d.ts +2 -2
- package/dist/schema/resolve-section-ref.d.ts +4 -0
- package/dist/schema/rows.d.ts +9 -0
- package/dist/schema/section-nav.d.ts +5 -1
- package/dist/schema/slot-types.d.ts +1 -1
- package/dist/ssr.d.ts +19 -2
- package/dist/ssr.js +32 -13
- package/guide/INDEX.md +5 -0
- package/guide/check.md +1 -0
- package/guide/dev.md +1 -0
- package/guide/llms.txt +11 -3
- package/guide/recipes/image-slot-crop.md +6 -6
- package/guide/recipes/pick-a-scenario.md +13 -5
- package/guide/schema-system.md +36 -12
- package/guide/upgrading.md +12 -8
- package/package.json +4 -16
- package/dist/canvas-core/chrome-css.d.ts +0 -24
- package/dist/canvas-core/chrome-css.js +0 -403
- package/dist/canvas-core/chrome.d.ts +0 -49
- package/dist/canvas-core/chrome.js +0 -147
- package/dist/canvas-core/frame.d.ts +0 -20
- package/dist/canvas-core/frame.js +0 -23
- package/dist/canvas-core/hidden-tabs.d.ts +0 -16
- package/dist/canvas-core/hidden-tabs.js +0 -84
- package/dist/canvas-core/resolve-target.js +0 -55
- package/dist/canvas-core/scroll.d.ts +0 -10
- package/dist/canvas-core/scroll.js +0 -15
- package/dist/canvas-core/section-dom.d.ts +0 -6
- package/dist/canvas-core/section-dom.js +0 -43
- package/dist/canvas-core/srcdoc.d.ts +0 -8
- package/dist/canvas-core/srcdoc.js +0 -77
- package/dist/canvas-core/types.d.ts +0 -76
- package/dist/canvas-core.d.ts +0 -9
- package/dist/canvas-core.js +0 -9
- package/dist/cli/check/config.js +0 -41
- package/dist/cli/check/css.js +0 -131
- package/dist/cli/check/diagnostics.js +0 -37
- package/dist/cli/check/index.js +0 -94
- package/dist/cli/check/loader.js +0 -149
- package/dist/cli/check/paths.js +0 -15
- package/dist/cli/check/relativize.js +0 -18
- package/dist/cli/check/render-invariants.js +0 -24
- package/dist/cli/check/resolve-tool.js +0 -38
- package/dist/cli/check/section-assets.js +0 -58
- package/dist/cli/check/stages/deps.js +0 -337
- package/dist/cli/check/stages/lint.js +0 -46
- package/dist/cli/check/stages/render.js +0 -97
- package/dist/cli/check/stages/size.js +0 -274
- package/dist/cli/check/stages/tree.js +0 -206
- package/dist/cli/check/stages/typecheck.js +0 -46
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +0 -18
- package/dist/cli/check/stages/validate/facts.js +0 -19
- package/dist/cli/check/stages/validate/formatter-contract.js +0 -14
- package/dist/cli/check/stages/validate/group-contract.js +0 -21
- package/dist/cli/check/stages/validate/index.js +0 -66
- package/dist/cli/check/stages/validate/list-scalar-contract.js +0 -22
- package/dist/cli/check/stages/validate/nav-contract.js +0 -92
- package/dist/cli/check/stages/validate/nested-slot-contract.js +0 -12
- package/dist/cli/check/stages/validate/object-list-contract.js +0 -31
- package/dist/cli/check/stages/validate/orchestrator.js +0 -241
- package/dist/cli/check/stages/validate/produced-by-contract.js +0 -18
- package/dist/cli/check/stages/validate/row-contract.js +0 -31
- package/dist/cli/check/stages/validate/select-contract.js +0 -13
- package/dist/cli/check/stages/validate/source-contract.js +0 -21
- package/dist/cli/check/stages/validate/variant-contract.js +0 -15
- package/dist/cli/check/workspace.js +0 -99
- package/dist/cli/dev/build-css.js +0 -97
- package/dist/cli/dev/catalog.js +0 -35
- package/dist/cli/dev/compose-template.js +0 -39
- package/dist/cli/dev/content-override.js +0 -58
- package/dist/cli/dev/diagnostics.js +0 -68
- package/dist/cli/dev/discover.js +0 -35
- package/dist/cli/dev/fill-state.js +0 -77
- package/dist/cli/dev/index.js +0 -42
- package/dist/cli/dev/inspect.js +0 -38
- package/dist/cli/dev/island-bootstrap.js +0 -38
- package/dist/cli/dev/island-canvas-bootstrap.js +0 -204
- package/dist/cli/dev/island-map.js +0 -35
- package/dist/cli/dev/island-transform.js +0 -31
- package/dist/cli/dev/manifest-instances.js +0 -31
- package/dist/cli/dev/render-section.js +0 -24
- package/dist/cli/dev/screenshot-target.js +0 -24
- package/dist/cli/dev/section-page.js +0 -91
- package/dist/cli/dev/server.js +0 -510
- package/dist/cli/dev/slot-schema.js +0 -12
- package/dist/cli/dev/structure-summary.js +0 -119
- package/dist/cli/dev/tailwind.js +0 -32
- package/dist/cli/dev/tsconfig-paths-plugin.js +0 -90
- package/dist/cli/dev/vite-server.js +0 -39
- package/dist/cli/dev/workspace.js +0 -63
- package/dist/cli/link/index.js +0 -74
- package/dist/cli/link/overlay.js +0 -59
- package/dist/cli/link/watch.js +0 -25
- package/dist/cli/new/emit.js +0 -48
- package/dist/cli/new/index.js +0 -69
- package/dist/cli/new/scaffold/section/_Renderer.tsx +0 -20
- package/dist/cli/new/scaffold/section/_fill-spec.ts +0 -18
- package/dist/cli/new/scaffold/section/_fixtures.ts +0 -13
- package/dist/cli/new/scaffold/section/_schema.ts +0 -24
- package/dist/cli/new/scaffold/template/_manifest.json +0 -9
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +0 -45
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +0 -40
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +0 -14
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +0 -29
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +0 -41
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +0 -60
- package/dist/cli/new/scaffold/template/theme.css +0 -24
- package/dist/cli/new/scaffold/template/theme.ts +0 -27
- package/dist/cli/new/scaffold-assets.js +0 -20
- package/dist/cli/pack/collect.js +0 -44
- package/dist/cli/pack/guards.js +0 -57
- package/dist/cli/pack/index.js +0 -66
- package/dist/cli/pack/manifest.js +0 -15
- package/dist/cli/pack/zip.js +0 -86
- package/dist/cli/theme/generate.js +0 -43
- package/dist/cli/theme/index.js +0 -33
- package/dist/cli/theme/load-theme.js +0 -69
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -138
- package/dist/dev-client/assets/index-CJa9G1Na.css +0 -1
- package/dist/dev-client/assets/index-C_1atZs4.js +0 -450
- package/dist/dev-client/index.html +0 -13
- package/dist/tokens.css +0 -21
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//#region src/cli/check/stages/validate/list-scalar-contract.ts
|
|
2
|
-
const SCALAR_ELEMENT_TYPES = /* @__PURE__ */ new Set(["text", "number"]);
|
|
3
|
-
function checkListSlot(slot, label, sectionKey) {
|
|
4
|
-
if (slot.element == null) throw new Error(`Section "${sectionKey}": ${label} must declare \`element\` (a scalar Slot, text|number).`);
|
|
5
|
-
if (!SCALAR_ELEMENT_TYPES.has(slot.element.type ?? "")) throw new Error(`Section "${sectionKey}": ${label} element must be a scalar Slot (text|number), got "${slot.element.type}".`);
|
|
6
|
-
}
|
|
7
|
-
function assertListScalarContract(schema, sectionKey) {
|
|
8
|
-
for (const [slotId, slotUnknown] of Object.entries(schema.slots)) {
|
|
9
|
-
const slot = slotUnknown;
|
|
10
|
-
if (slot.type === "list") {
|
|
11
|
-
checkListSlot(slot, `list "${slotId}"`, sectionKey);
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
if (slot.type === "object_list" && slot.item != null) for (const [childId, child] of Object.entries(slot.item)) {
|
|
15
|
-
if (child.type !== "list") continue;
|
|
16
|
-
checkListSlot(child, `object_list "${slotId}" field "${childId}"`, sectionKey);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
//#endregion
|
|
22
|
-
export { assertListScalarContract };
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { FixtureModuleShape, buildFixtureSet } from "../../../../schema/fixture-schema.js";
|
|
2
|
-
import { TemplateManifest } from "../../../../schema/manifest.js";
|
|
3
|
-
import { loadSection } from "../../loader.js";
|
|
4
|
-
import { renderSectionHtml } from "../../../../ssr.js";
|
|
5
|
-
import { resolveSectionRef } from "../../../../schema/resolve-section-ref.js";
|
|
6
|
-
import { materializeSection } from "./orchestrator.js";
|
|
7
|
-
import { resolveAnchorMap } from "../../../../schema/section-nav.js";
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
|
|
11
|
-
//#region src/cli/check/stages/validate/nav-contract.ts
|
|
12
|
-
const ALLOWED_EXTERNAL_ANCHORS = ["#privacy-policy"];
|
|
13
|
-
function guardedAnchors(anchors, templateKey) {
|
|
14
|
-
return new Proxy(anchors, { get(target, prop, receiver) {
|
|
15
|
-
if (typeof prop === "string" && !(prop in target)) throw new Error(`Template "${templateKey}": section renderer referenced nav.anchors["${prop}"], but no composed section resolves to that key (dangling cross-section link).`);
|
|
16
|
-
return Reflect.get(target, prop, receiver);
|
|
17
|
-
} });
|
|
18
|
-
}
|
|
19
|
-
function collectNavViolations(html, allow) {
|
|
20
|
-
const ids = /* @__PURE__ */ new Set();
|
|
21
|
-
for (const m of html.matchAll(/\sid="([^"]*)"/g)) ids.add(m[1]);
|
|
22
|
-
const allowSet = new Set(allow);
|
|
23
|
-
const violations = /* @__PURE__ */ new Set();
|
|
24
|
-
for (const m of html.matchAll(/\shref="(#[^"]*)"/g)) {
|
|
25
|
-
const href = m[1];
|
|
26
|
-
if (allowSet.has(href)) continue;
|
|
27
|
-
if (!ids.has(href.slice(1))) violations.add(href);
|
|
28
|
-
}
|
|
29
|
-
return [...violations];
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Loads the template's manifest.json, resolves every composed section
|
|
33
|
-
* (via resolveSectionRef — bare refs qualify to `<templateKey>/<name>`; the
|
|
34
|
-
* kit does not yet model a `shared/` library, so those never match a local
|
|
35
|
-
* section), builds the ONE composed anchor map keyed by bare section name
|
|
36
|
-
* (the namespace renderers actually read via `nav.anchors[bareName]`),
|
|
37
|
-
* renders each instance's "typical" fixture with that composed nav, and
|
|
38
|
-
* throws naming the template + every dangling in-page link.
|
|
39
|
-
*/
|
|
40
|
-
async function assertNavContract(workspaceRoot, template) {
|
|
41
|
-
const raw = JSON.parse(await readFile(join(template.dir, "manifest.json"), "utf8"));
|
|
42
|
-
const manifest = TemplateManifest.parse(raw);
|
|
43
|
-
const sectionByName = new Map(template.sections.map((s) => [s.name, s]));
|
|
44
|
-
const composed = manifest.sections.map((inst) => {
|
|
45
|
-
const resolvedKey = resolveSectionRef(manifest.key, inst.section);
|
|
46
|
-
const bareName = resolvedKey.split("/").pop();
|
|
47
|
-
const target = resolvedKey === `${manifest.key}/${bareName}` ? sectionByName.get(bareName) : void 0;
|
|
48
|
-
if (!target) throw new Error(`Template "${template.key}": section instance "${inst.instance_id}" references unknown section "${inst.section}" (resolved "${resolvedKey}").`);
|
|
49
|
-
return target;
|
|
50
|
-
});
|
|
51
|
-
const materializedByName = /* @__PURE__ */ new Map();
|
|
52
|
-
const rendererByName = /* @__PURE__ */ new Map();
|
|
53
|
-
const fixturesByName = /* @__PURE__ */ new Map();
|
|
54
|
-
for (const section of composed) {
|
|
55
|
-
const loaded = await loadSection(workspaceRoot, section);
|
|
56
|
-
const materialized = materializeSection(section.name, loaded);
|
|
57
|
-
materializedByName.set(section.name, {
|
|
58
|
-
anchor: materialized.schema.anchor,
|
|
59
|
-
schema: materialized.schema
|
|
60
|
-
});
|
|
61
|
-
rendererByName.set(section.name, loaded.Renderer);
|
|
62
|
-
fixturesByName.set(section.name, loaded.fixtures);
|
|
63
|
-
}
|
|
64
|
-
const anchors = guardedAnchors(resolveAnchorMap(composed.map((s) => ({
|
|
65
|
-
key: s.name,
|
|
66
|
-
anchor: materializedByName.get(s.name).anchor
|
|
67
|
-
}))), template.key);
|
|
68
|
-
let html = "";
|
|
69
|
-
for (const section of composed) {
|
|
70
|
-
const { anchor, schema } = materializedByName.get(section.name);
|
|
71
|
-
const base = buildFixtureSet({
|
|
72
|
-
schema,
|
|
73
|
-
module: FixtureModuleShape.parse(fixturesByName.get(section.name)),
|
|
74
|
-
selfAnchor: anchor
|
|
75
|
-
}).find((f) => f.id === "typical");
|
|
76
|
-
if (!base) throw new Error(`Template "${template.key}": section "${section.name}" has no "typical" base fixture.`);
|
|
77
|
-
html += renderSectionHtml(rendererByName.get(section.name), {
|
|
78
|
-
slots: base.props.slots,
|
|
79
|
-
options: base.props.options,
|
|
80
|
-
nav: {
|
|
81
|
-
selfAnchor: anchor,
|
|
82
|
-
anchors
|
|
83
|
-
},
|
|
84
|
-
variants: base.props.variants
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
const violations = collectNavViolations(html, ALLOWED_EXTERNAL_ANCHORS);
|
|
88
|
-
if (violations.length > 0) throw new Error(`Template "${template.key}": nav link(s) resolve to no composed section id: ${violations.join(", ")}.`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
92
|
-
export { assertNavContract, collectNavViolations, guardedAnchors };
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { SLOT_TYPES } from "../../../../contracts/slot-catalog.js";
|
|
2
|
-
|
|
3
|
-
//#region src/cli/check/stages/validate/nested-slot-contract.ts
|
|
4
|
-
function assertNestedSlotTypes(schema, sectionKey) {
|
|
5
|
-
for (const [slotId, slot] of Object.entries(schema.slots)) {
|
|
6
|
-
if (slot.type !== "object_list" || !slot.item) continue;
|
|
7
|
-
for (const [childId, child] of Object.entries(slot.item)) if (child.type != null && !SLOT_TYPES.includes(child.type)) throw new Error(`Section "${sectionKey}": object_list "${slotId}" item "${childId}" has unknown nested slot type "${child.type}".`);
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
//#endregion
|
|
12
|
-
export { assertNestedSlotTypes };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { COLLECTIONS, collectionForSlot } from "../../../../contracts/slot-catalog.js";
|
|
2
|
-
|
|
3
|
-
//#region src/cli/check/stages/validate/object-list-contract.ts
|
|
4
|
-
const SCALAR_SLOT_TYPES = /* @__PURE__ */ new Set([
|
|
5
|
-
"text",
|
|
6
|
-
"number",
|
|
7
|
-
"select"
|
|
8
|
-
]);
|
|
9
|
-
function assertObjectListContract(schema, sectionKey) {
|
|
10
|
-
for (const [slotId, slotUnknown] of Object.entries(schema.slots)) {
|
|
11
|
-
const slot = slotUnknown;
|
|
12
|
-
if (slot.type !== "object_list") continue;
|
|
13
|
-
const source = slot.source;
|
|
14
|
-
if (source?.kind === "direct") {
|
|
15
|
-
const collection = collectionForSlot({
|
|
16
|
-
type: "object_list",
|
|
17
|
-
source
|
|
18
|
-
});
|
|
19
|
-
if (collection === null) throw new Error(`Section "${sectionKey}": object_list "${slotId}" source.field "${source.field ?? "?"}" is not a known collection (COLLECTIONS in platform/contracts/slot-catalog.ts).`);
|
|
20
|
-
if (!COLLECTIONS[collection].ownership) throw new Error(`Section "${sectionKey}": object_list "${slotId}" collection "${collection}" declares no ownership in COLLECTIONS (platform/contracts/slot-catalog.ts).`);
|
|
21
|
-
}
|
|
22
|
-
const itemSlots = slot.item;
|
|
23
|
-
if (itemSlots == null) throw new Error(`Section "${sectionKey}": object_list "${slotId}" must declare \`item\` (a record of nested slots).`);
|
|
24
|
-
const childIds = Object.keys(itemSlots);
|
|
25
|
-
if (childIds.length === 1 && SCALAR_SLOT_TYPES.has(itemSlots[childIds[0]].type ?? "")) throw new Error(`Section "${sectionKey}": object_list "${slotId}" has a single scalar slot "${childIds[0]}" — use a \`list\` slot instead.`);
|
|
26
|
-
for (const [childId, child] of Object.entries(itemSlots)) if (child.type === "object_list") throw new Error(`Section "${sectionKey}": object_list "${slotId}" item "${childId}" is itself an object_list — object_list is depth-1 (a scalar \`list\` is allowed).`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
//#endregion
|
|
31
|
-
export { assertObjectListContract };
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { SectionSchema } from "../../../../schema/section-schema.js";
|
|
2
|
-
import { FillSpec } from "../../../../schema/fill-spec.js";
|
|
3
|
-
import { FixtureModuleShape } from "../../../../schema/fixture-schema.js";
|
|
4
|
-
import { TemplateManifest } from "../../../../schema/manifest.js";
|
|
5
|
-
import { loadSection } from "../../loader.js";
|
|
6
|
-
import { flattenLayout } from "../../../../schema/rows.js";
|
|
7
|
-
import { parseTreatment } from "../../../../contracts/fill-treatments.js";
|
|
8
|
-
import { resolveSectionRef } from "../../../../schema/resolve-section-ref.js";
|
|
9
|
-
import { assertCatalogExhaustiveness } from "./catalog-exhaustiveness.js";
|
|
10
|
-
import { FACTS_ARRAY_FIELDS, FACTS_READABLE } from "./facts.js";
|
|
11
|
-
import { assertFormatterContract } from "./formatter-contract.js";
|
|
12
|
-
import { assertGroupContract } from "./group-contract.js";
|
|
13
|
-
import { assertListScalarContract } from "./list-scalar-contract.js";
|
|
14
|
-
import { assertNestedSlotTypes } from "./nested-slot-contract.js";
|
|
15
|
-
import { assertObjectListContract } from "./object-list-contract.js";
|
|
16
|
-
import { assertProducedByContract } from "./produced-by-contract.js";
|
|
17
|
-
import { assertRowContract } from "./row-contract.js";
|
|
18
|
-
import { assertSelectContract } from "./select-contract.js";
|
|
19
|
-
import { assertSourceContract } from "./source-contract.js";
|
|
20
|
-
import { assertVariantContract } from "./variant-contract.js";
|
|
21
|
-
import { isDeepStrictEqual } from "node:util";
|
|
22
|
-
import { readFile } from "node:fs/promises";
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
|
|
25
|
-
//#region src/cli/check/stages/validate/orchestrator.ts
|
|
26
|
-
function serializeVariants(variants) {
|
|
27
|
-
const out = {};
|
|
28
|
-
for (const [axis, def] of Object.entries(variants)) out[axis] = {
|
|
29
|
-
cases: def.cases,
|
|
30
|
-
default: def.default
|
|
31
|
-
};
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
function flattenMeta(authored, sectionName) {
|
|
35
|
-
const { meta, ...rest } = authored;
|
|
36
|
-
return {
|
|
37
|
-
...rest,
|
|
38
|
-
anchor: meta.anchor ?? sectionName,
|
|
39
|
-
...meta.variants ? { variants: serializeVariants(meta.variants) } : {},
|
|
40
|
-
...meta.groups ? { groups: meta.groups } : {}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Zod-parses a loaded section's schema + fill-spec and materializes the
|
|
45
|
-
* bookkeeping fields (slot_order, item_order, flattened meta) every contract
|
|
46
|
-
* and consumer expects. Throws on the first parse/shape failure — there is no
|
|
47
|
-
* schema to check contracts against until this succeeds.
|
|
48
|
-
*/
|
|
49
|
-
function materializeSection(key, loaded) {
|
|
50
|
-
const authored = SectionSchema.parse(loaded.schema);
|
|
51
|
-
authored.slot_order = Object.keys(authored.slots);
|
|
52
|
-
for (const slot of Object.values(authored.slots)) if (slot.type === "object_list" && slot.item) slot.item_order = slot.item_layout ? flattenLayout(slot.item_layout) : Object.keys(slot.item);
|
|
53
|
-
return {
|
|
54
|
-
key,
|
|
55
|
-
schema: flattenMeta(authored, key),
|
|
56
|
-
fillSpec: FillSpec.parse(loaded.fillSpec),
|
|
57
|
-
displayName: authored.meta.displayName
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Zod-parses the section's fixtures module against the fixture meta-schema,
|
|
62
|
-
* collecting one message per violation. Its own gate (template-kit/fixtures-invalid)
|
|
63
|
-
* because it is a different authored artifact from the schema: the render stage
|
|
64
|
-
* consumes this module, and until it was parsed here an invalid one surfaced only
|
|
65
|
-
* as a raw crash mid-render.
|
|
66
|
-
*/
|
|
67
|
-
function validateFixtureModule(key, loaded) {
|
|
68
|
-
const parsed = FixtureModuleShape.safeParse(loaded.fixtures);
|
|
69
|
-
if (parsed.success) return [];
|
|
70
|
-
return parsed.error.issues.map((issue) => {
|
|
71
|
-
const path = issue.path.join(".");
|
|
72
|
-
return `Section "${key}": fixtures.ts — ${path === "" ? "the fixture module" : `"${path}"`}: ${issue.message}.`;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
/** Runs `fn`, pushing its thrown message onto `messages` instead of propagating. */
|
|
76
|
-
function guard(messages, fn) {
|
|
77
|
-
try {
|
|
78
|
-
fn();
|
|
79
|
-
} catch (cause) {
|
|
80
|
-
messages.push(cause instanceof Error ? cause.message : String(cause));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Runs every cross-reference + contract check against an already-loaded
|
|
85
|
-
* section, collecting each violation's message rather than stopping at the
|
|
86
|
-
* first. Returns an empty array when the section is clean.
|
|
87
|
-
*/
|
|
88
|
-
function validateOneSection(key, loaded) {
|
|
89
|
-
const messages = [];
|
|
90
|
-
let materialized;
|
|
91
|
-
try {
|
|
92
|
-
materialized = materializeSection(key, loaded);
|
|
93
|
-
} catch (cause) {
|
|
94
|
-
messages.push(cause instanceof Error ? cause.message : String(cause));
|
|
95
|
-
return messages;
|
|
96
|
-
}
|
|
97
|
-
const { schema, fillSpec } = materialized;
|
|
98
|
-
guard(messages, () => {
|
|
99
|
-
assertCatalogExhaustiveness();
|
|
100
|
-
});
|
|
101
|
-
for (const r of fillSpec.reads ?? []) guard(messages, () => {
|
|
102
|
-
const base = r.split(".")[0];
|
|
103
|
-
if (!FACTS_READABLE.has(base)) throw new Error(`Section "${key}": fill-spec reads "${r}" is not an author-declarable facts key (allowed: ${[...FACTS_READABLE].join(", ")}).`);
|
|
104
|
-
});
|
|
105
|
-
for (const d of fillSpec.decisions) {
|
|
106
|
-
for (const produced of d.produces) guard(messages, () => {
|
|
107
|
-
if (!(produced in schema.slots)) throw new Error(`Section "${key}": fill-spec decision "${d.id}" produces unknown slot "${produced}".`);
|
|
108
|
-
});
|
|
109
|
-
if (d.type === "list-extract" || d.type === "structured-list") {
|
|
110
|
-
const decisionType = d.type;
|
|
111
|
-
const decisionId = d.id;
|
|
112
|
-
const iterate = d.iterate;
|
|
113
|
-
const itemCount = d.item_count;
|
|
114
|
-
guard(messages, () => {
|
|
115
|
-
if (typeof iterate === "string" === (itemCount !== void 0)) throw new Error(`Section "${key}": ${decisionType} decision "${decisionId}" must specify exactly one of \`iterate\` or \`item_count\`.`);
|
|
116
|
-
});
|
|
117
|
-
if (typeof iterate === "string") guard(messages, () => {
|
|
118
|
-
if (!FACTS_ARRAY_FIELDS.has(iterate)) throw new Error(`Section "${key}": ${decisionType} decision "${decisionId}" iterate="${iterate}" is not an iterable facts array (allowed: ${[...FACTS_ARRAY_FIELDS].join(", ")}).`);
|
|
119
|
-
});
|
|
120
|
-
if (d.type === "structured-list") {
|
|
121
|
-
const itemFields = d.item_fields;
|
|
122
|
-
for (const [field, raw] of Object.entries(itemFields)) guard(messages, () => {
|
|
123
|
-
if (parseTreatment(raw) === null) throw new Error(`Section "${key}": decision "${decisionId}" item_field "${field}" has invalid treatment "${raw}".`);
|
|
124
|
-
});
|
|
125
|
-
const routeBy = d.route_by;
|
|
126
|
-
const catalog = d.catalog;
|
|
127
|
-
if (typeof routeBy === "string") {
|
|
128
|
-
const slots = new Set((catalog ?? []).map((event) => event.slot).filter((s) => typeof s === "string"));
|
|
129
|
-
for (const p of d.produces) guard(messages, () => {
|
|
130
|
-
if (!slots.has(p)) throw new Error(`Section "${key}": decision "${decisionId}" routes to slot "${p}" but no catalog entry declares slot="${p}".`);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
guard(messages, () => {
|
|
137
|
-
assertProducedByContract(schema, fillSpec, key);
|
|
138
|
-
});
|
|
139
|
-
guard(messages, () => {
|
|
140
|
-
assertVariantContract(schema, key);
|
|
141
|
-
});
|
|
142
|
-
const decisionIds = new Set(fillSpec.decisions.map((d) => d.id));
|
|
143
|
-
guard(messages, () => {
|
|
144
|
-
assertSourceContract({
|
|
145
|
-
key,
|
|
146
|
-
slots: schema.slots
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
guard(messages, () => {
|
|
150
|
-
assertFormatterContract({
|
|
151
|
-
key,
|
|
152
|
-
slots: schema.slots
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
guard(messages, () => {
|
|
156
|
-
assertObjectListContract(schema, key);
|
|
157
|
-
});
|
|
158
|
-
guard(messages, () => {
|
|
159
|
-
assertNestedSlotTypes(schema, key);
|
|
160
|
-
});
|
|
161
|
-
guard(messages, () => {
|
|
162
|
-
assertListScalarContract(schema, key);
|
|
163
|
-
});
|
|
164
|
-
guard(messages, () => {
|
|
165
|
-
assertSelectContract(schema, key);
|
|
166
|
-
});
|
|
167
|
-
guard(messages, () => {
|
|
168
|
-
assertGroupContract(schema, key);
|
|
169
|
-
});
|
|
170
|
-
guard(messages, () => {
|
|
171
|
-
assertRowContract(schema, key);
|
|
172
|
-
});
|
|
173
|
-
for (const [optName, opt] of Object.entries(schema.options)) {
|
|
174
|
-
for (const dId of opt.affects_decisions) guard(messages, () => {
|
|
175
|
-
if (!decisionIds.has(dId)) throw new Error(`Section "${key}": option "${optName}" affects_decisions contains unknown decision "${dId}".`);
|
|
176
|
-
});
|
|
177
|
-
for (const slotId of opt.affects_slots) guard(messages, () => {
|
|
178
|
-
if (!(slotId in schema.slots)) throw new Error(`Section "${key}": option "${optName}" affects_slots contains unknown slot "${slotId}".`);
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return messages;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Re-materializes every section a template composes (loadSection is
|
|
185
|
-
* memoized, so this is cheap) and resolves + cross-checks the template's
|
|
186
|
-
* manifest.json: the manifest key matches the folder name, every section
|
|
187
|
-
* reference resolves to a real (locally-authored, per resolveSectionRef's D3
|
|
188
|
-
* rule) section, every overridden option exists and is template_overridable,
|
|
189
|
-
* and no override merely restates the section's own default. Finishes with
|
|
190
|
-
* the whole-composition nav contract (nav-contract.ts). Collects every
|
|
191
|
-
* violation rather than stopping at the first.
|
|
192
|
-
*/
|
|
193
|
-
async function validateTemplateManifest(workspaceRoot, template) {
|
|
194
|
-
const messages = [];
|
|
195
|
-
let manifest;
|
|
196
|
-
try {
|
|
197
|
-
const raw = JSON.parse(await readFile(join(template.dir, "manifest.json"), "utf8"));
|
|
198
|
-
manifest = TemplateManifest.parse(raw);
|
|
199
|
-
} catch (cause) {
|
|
200
|
-
messages.push(cause instanceof Error ? cause.message : String(cause));
|
|
201
|
-
return messages;
|
|
202
|
-
}
|
|
203
|
-
guard(messages, () => {
|
|
204
|
-
if (manifest.key !== template.key) throw new Error(`Template "${template.key}": manifest key "${manifest.key}" does not match the template folder name "${template.key}".`);
|
|
205
|
-
});
|
|
206
|
-
const sectionByName = /* @__PURE__ */ new Map();
|
|
207
|
-
for (const section of template.sections) try {
|
|
208
|
-
const loaded = await loadSection(workspaceRoot, section);
|
|
209
|
-
sectionByName.set(section.name, materializeSection(section.name, loaded));
|
|
210
|
-
} catch {}
|
|
211
|
-
for (const inst of manifest.sections) {
|
|
212
|
-
const resolvedKey = resolveSectionRef(manifest.key, inst.section);
|
|
213
|
-
const bareName = resolvedKey.split("/").pop();
|
|
214
|
-
const target = resolvedKey === `${manifest.key}/${bareName}` ? sectionByName.get(bareName) : void 0;
|
|
215
|
-
if (!target) {
|
|
216
|
-
guard(messages, () => {
|
|
217
|
-
throw new Error(`Template "${manifest.key}": section instance "${inst.instance_id}" references unknown section "${inst.section}" (resolved "${resolvedKey}").`);
|
|
218
|
-
});
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
for (const [optName, overrideVal] of Object.entries(inst.options)) guard(messages, () => {
|
|
222
|
-
const sectOpt = target.schema.options[optName];
|
|
223
|
-
if (!sectOpt) throw new Error(`Template "${manifest.key}": section instance "${inst.instance_id}" overrides option "${optName}", which the section "${inst.section}" does not declare.`);
|
|
224
|
-
if (sectOpt.template_overridable === false) throw new Error(`Template "${manifest.key}": section instance "${inst.instance_id}" overrides option "${optName}", but the section declares template_overridable: false.`);
|
|
225
|
-
if (isDeepStrictEqual(overrideVal, sectOpt.default)) throw new Error(`Template "${manifest.key}": section instance "${inst.instance_id}" overrides option "${optName}" with a value equal to the section default — drop it (only genuine overrides belong in the manifest).`);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
const { assertNavContract } = await import("./nav-contract.js");
|
|
229
|
-
await guardAsync(messages, () => assertNavContract(workspaceRoot, template));
|
|
230
|
-
return [...new Set(messages)];
|
|
231
|
-
}
|
|
232
|
-
async function guardAsync(messages, fn) {
|
|
233
|
-
try {
|
|
234
|
-
await fn();
|
|
235
|
-
} catch (cause) {
|
|
236
|
-
messages.push(cause instanceof Error ? cause.message : String(cause));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
//#endregion
|
|
241
|
-
export { flattenMeta, materializeSection, validateFixtureModule, validateOneSection, validateTemplateManifest };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//#region src/cli/check/stages/validate/produced-by-contract.ts
|
|
2
|
-
function assertProducedByContract(schema, fillSpec, sectionKey) {
|
|
3
|
-
const decisionIds = new Set(fillSpec.decisions.map((d) => d.id));
|
|
4
|
-
for (const [slotId, slot] of Object.entries(schema.slots)) {
|
|
5
|
-
const srcKind = slot.source?.kind;
|
|
6
|
-
if (srcKind === "direct" || srcKind === "derived") {
|
|
7
|
-
if (slot.produced_by) throw new Error(`Section "${sectionKey}": slot "${slotId}" is source-bound; remove the redundant produced_by="${slot.produced_by}".`);
|
|
8
|
-
continue;
|
|
9
|
-
}
|
|
10
|
-
if (!slot.produced_by) throw new Error(`Section "${sectionKey}": slot "${slotId}" of type ${slot.type} must declare a \`source\` binding or a \`produced_by\` decision.`);
|
|
11
|
-
if (!decisionIds.has(slot.produced_by)) throw new Error(`Section "${sectionKey}": slot "${slotId}" produced_by="${slot.produced_by}" does not match any decision id.`);
|
|
12
|
-
const producer = fillSpec.decisions.find((d) => d.id === slot.produced_by);
|
|
13
|
-
if (producer && !producer.produces.includes(slotId)) throw new Error(`Section "${sectionKey}": slot "${slotId}" declares produced_by="${slot.produced_by}", but decision "${slot.produced_by}" does not list "${slotId}" in its produces [${producer.produces.join(", ")}].`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
//#endregion
|
|
18
|
-
export { assertProducedByContract };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { flattenLayout, isShortSlot } from "../../../../schema/rows.js";
|
|
2
|
-
|
|
3
|
-
//#region src/cli/check/stages/validate/row-contract.ts
|
|
4
|
-
function assertRowContract(schema, sectionKey) {
|
|
5
|
-
for (const group of schema.groups ?? []) assertRowPairs(group.members, schema.slots, `group "${group.id}"`, sectionKey);
|
|
6
|
-
for (const [slotId, slot] of Object.entries(schema.slots)) {
|
|
7
|
-
if (slot.type !== "object_list" || !slot.item_layout) continue;
|
|
8
|
-
const itemFields = slot.item ?? {};
|
|
9
|
-
assertRowPairs(slot.item_layout, itemFields, `object_list "${slotId}"`, sectionKey);
|
|
10
|
-
const flat = flattenLayout(slot.item_layout);
|
|
11
|
-
const dup = flat.find((id, i) => flat.indexOf(id) !== i);
|
|
12
|
-
if (dup) throw new Error(`Section "${sectionKey}": object_list "${slotId}" item_layout lists "${dup}" more than once.`);
|
|
13
|
-
for (const id of flat) if (!(id in itemFields)) throw new Error(`Section "${sectionKey}": object_list "${slotId}" item_layout names unknown field "${id}".`);
|
|
14
|
-
for (const key of Object.keys(itemFields)) if (!flat.includes(key)) throw new Error(`Section "${sectionKey}": object_list "${slotId}" item_layout omits field "${key}".`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function assertRowPairs(layout, slots, ctx, sectionKey) {
|
|
18
|
-
const order = Object.keys(slots);
|
|
19
|
-
for (const entry of layout) {
|
|
20
|
-
if (typeof entry === "string") continue;
|
|
21
|
-
const [a, b] = entry;
|
|
22
|
-
for (const id of [a, b]) {
|
|
23
|
-
if (!(id in slots)) throw new Error(`Section "${sectionKey}": ${ctx} row names unknown slot "${id}".`);
|
|
24
|
-
if (!isShortSlot(slots[id])) throw new Error(`Section "${sectionKey}": ${ctx} row pairs non-short slot "${id}" (type "${slots[id].type}"); only number/short-text slots may share a row.`);
|
|
25
|
-
}
|
|
26
|
-
if (order.indexOf(b) !== order.indexOf(a) + 1) throw new Error(`Section "${sectionKey}": ${ctx} row [${a}, ${b}] must be adjacent and in order; reorder the declaration so "${b}" immediately follows "${a}".`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
//#endregion
|
|
31
|
-
export { assertRowContract };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region src/cli/check/stages/validate/select-contract.ts
|
|
2
|
-
function assertSelectContract(schema, sectionKey) {
|
|
3
|
-
for (const [slotId, slotUnknown] of Object.entries(schema.slots)) {
|
|
4
|
-
const slot = slotUnknown;
|
|
5
|
-
if (slot.type !== "select") continue;
|
|
6
|
-
const hasValues = slot.values != null;
|
|
7
|
-
const hasOptions = slot.options != null;
|
|
8
|
-
if (!(slot.catalog != null) && hasValues === hasOptions) throw new Error(`Section "${sectionKey}": select "${slotId}" needs exactly one of \`values\` or \`options\` (or a \`catalog\`).`);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
//#endregion
|
|
13
|
-
export { assertSelectContract };
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { SOURCE_FIELDS } from "../../../../contracts/slot-catalog.js";
|
|
2
|
-
import { TRANSFORMS } from "../../../../contracts/transforms.js";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/check/stages/validate/source-contract.ts
|
|
5
|
-
function assertSourceContract(schema) {
|
|
6
|
-
for (const [name, slot] of Object.entries(schema.slots)) {
|
|
7
|
-
const src = slot.source;
|
|
8
|
-
if (!src) continue;
|
|
9
|
-
if (src.kind === "direct") {
|
|
10
|
-
const def = SOURCE_FIELDS[src.field ?? ""];
|
|
11
|
-
if (!def) throw new Error(`${schema.key}.${name}: unknown direct source field "${String(src.field)}"`);
|
|
12
|
-
if (def.image && slot.type !== "image" && slot.type !== "object_list") throw new Error(`${schema.key}.${name}: image source field "${src.field}" must sit on an image slot`);
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (src.kind !== "derived") continue;
|
|
16
|
-
if (!(src.transform !== void 0 && Object.hasOwn(TRANSFORMS, src.transform))) throw new Error(`${schema.key}.${name}: unknown transform "${String(src.transform)}" (add it to platform/contracts/transforms.ts)`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
export { assertSourceContract };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
//#region src/cli/check/stages/validate/variant-contract.ts
|
|
2
|
-
function assertVariantContract(schema, sectionKey) {
|
|
3
|
-
const axes = schema.variants ?? {};
|
|
4
|
-
for (const [axisName, axis] of Object.entries(axes)) if (!axis.cases.includes(axis.default)) throw new Error(`Section "${sectionKey}": variant axis "${axisName}" default "${axis.default}" is not one of its cases.`);
|
|
5
|
-
for (const [slotId, slot] of Object.entries(schema.slots)) {
|
|
6
|
-
const v = slot.variant;
|
|
7
|
-
if (v == null) continue;
|
|
8
|
-
const axis = axes[v.axis];
|
|
9
|
-
if (!axis) throw new Error(`Section "${sectionKey}": slot "${slotId}" gates on unknown axis "${v.axis}".`);
|
|
10
|
-
for (const c of v.cases) if (!axis.cases.includes(c)) throw new Error(`Section "${sectionKey}": slot "${slotId}" gates on case "${c}" not declared by axis "${v.axis}".`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
//#endregion
|
|
15
|
-
export { assertVariantContract };
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/check/workspace.ts
|
|
5
|
-
const isDir = async (p) => {
|
|
6
|
-
try {
|
|
7
|
-
return (await stat(p)).isDirectory();
|
|
8
|
-
} catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
const isFile = async (p) => {
|
|
13
|
-
try {
|
|
14
|
-
return (await stat(p)).isFile();
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
/** Nearest ancestor holding both a package.json and a templates/ dir. Throws if there is none. */
|
|
20
|
-
async function findWorkspaceRoot(from) {
|
|
21
|
-
let dir = resolve(from);
|
|
22
|
-
for (;;) {
|
|
23
|
-
if (await isFile(join(dir, "package.json")) && await isDir(join(dir, "templates"))) return dir;
|
|
24
|
-
const parent = dirname(dir);
|
|
25
|
-
if (parent === dir) throw new Error("not inside a workspace — no parent directory has both a package.json and a templates/ folder.\nRun this from your workspace root, or create one with `npm create @homepages/workspace`.");
|
|
26
|
-
dir = parent;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* The kit version the workspace DECLARES in package.json — deliberately not what is
|
|
31
|
-
* installed. The link overlay never touches package.json, so declared-vs-installed is
|
|
32
|
-
* precisely the gap the check warning exists to surface.
|
|
33
|
-
*/
|
|
34
|
-
async function declaredKitPin(root) {
|
|
35
|
-
try {
|
|
36
|
-
const raw = await readFile(join(root, "package.json"), "utf8");
|
|
37
|
-
return JSON.parse(raw).dependencies?.["@homepages/template-kit"] ?? "unknown";
|
|
38
|
-
} catch {
|
|
39
|
-
return "unknown";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
async function discoverTemplates(root) {
|
|
43
|
-
const templatesDir = join(root, "templates");
|
|
44
|
-
const entries = await readdir(templatesDir, { withFileTypes: true });
|
|
45
|
-
const templates = [];
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
if (!entry.isDirectory()) continue;
|
|
48
|
-
const dir = join(templatesDir, entry.name);
|
|
49
|
-
const sectionsDir = join(dir, "sections");
|
|
50
|
-
const sections = [];
|
|
51
|
-
if (await isDir(sectionsDir)) {
|
|
52
|
-
for (const s of await readdir(sectionsDir, { withFileTypes: true })) if (s.isDirectory()) sections.push({
|
|
53
|
-
name: s.name,
|
|
54
|
-
dir: join(sectionsDir, s.name)
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
sections.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
-
templates.push({
|
|
59
|
-
key: entry.name,
|
|
60
|
-
dir,
|
|
61
|
-
sections
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
templates.sort((a, b) => a.key.localeCompare(b.key));
|
|
65
|
-
return templates;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Which templates this invocation gates. A bare invocation inside a template folder
|
|
69
|
-
* infers that template; at the root with several templates it asks for a name.
|
|
70
|
-
* `command` is the invoking subcommand (`check`/`theme`), named back in the
|
|
71
|
-
* "several templates" guidance so it points at the command the author actually ran.
|
|
72
|
-
*/
|
|
73
|
-
async function selectTemplates(root, named, all, command) {
|
|
74
|
-
const templates = await discoverTemplates(root);
|
|
75
|
-
const keys = templates.map((t) => t.key);
|
|
76
|
-
if (all) return templates;
|
|
77
|
-
if (named !== void 0) {
|
|
78
|
-
const found = templates.find((t) => t.key === named);
|
|
79
|
-
if (!found) throw new Error(`unknown template "${named}". This workspace has: ${keys.join(", ") || "(none)"}`);
|
|
80
|
-
return [found];
|
|
81
|
-
}
|
|
82
|
-
if (templates.length === 1) return templates;
|
|
83
|
-
if (templates.length === 0) return [];
|
|
84
|
-
throw new Error(`this workspace has several templates (${keys.join(", ")}).\nName one — \`template-kit ${command} <template>\` — or run them all with \`template-kit ${command} --all\`.`);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* The key of the template whose folder `cwd` sits inside (`<root>/templates/<key>/…`),
|
|
88
|
-
* or `undefined` when `cwd` is not under any template. Lets a bare `check`/`theme`
|
|
89
|
-
* infer its target from the current directory.
|
|
90
|
-
*/
|
|
91
|
-
function templateFromCwd(root, cwd) {
|
|
92
|
-
const prefix = `${root}/templates/`;
|
|
93
|
-
if (!cwd.startsWith(prefix)) return void 0;
|
|
94
|
-
const [key] = cwd.slice(prefix.length).split("/");
|
|
95
|
-
return key === "" ? void 0 : key;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
//#endregion
|
|
99
|
-
export { declaredKitPin, discoverTemplates, findWorkspaceRoot, selectTemplates, templateFromCwd };
|