create-ailk 0.3.0 → 0.5.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/component-catalog.json +11939 -0
- package/component-catalog.md +176 -64
- package/dist/cli.js +48 -10
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +88 -1
- package/dist/lib/extract-module-bundle.js +375 -80
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.d.ts +20 -0
- package/dist/lib/fetch-template.js +133 -0
- package/dist/lib/module-license-gate.d.ts +8 -0
- package/dist/lib/module-license-gate.js +12 -4
- package/dist/module-architecture.d.ts +116 -7
- package/dist/module-architecture.js +465 -94
- package/dist/parse-args.d.ts +8 -1
- package/dist/parse-args.js +49 -2
- package/dist/programmatic.d.ts +36 -1
- package/dist/programmatic.js +216 -5
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +140 -23
- package/dist/template-manifest.d.ts +144 -0
- package/dist/template-manifest.js +337 -0
- package/dist/template-names.d.ts +41 -0
- package/dist/template-names.js +45 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
- package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
- package/site-templates/marketing-site-free/ailk-template.json +9 -0
- package/site-templates/marketing-site-free/provenance.json +4 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
- package/site-templates/minimalism-site/ailk-template.json +21 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +32 -7
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/CLAUDE.md +4 -1
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/package.json +6 -2
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
- package/templates/apps/api/src/config/modules.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/server.ts +122 -3
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/default-tenant.ts +261 -0
- package/templates/apps/api/src/services/lead-promotion.ts +161 -0
- package/templates/apps/api/src/services/leads-inbox.ts +263 -0
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/mcp/CLAUDE.md +1 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/package.json +6 -2
- package/templates/apps/mcp/src/server.ts +2 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/index.ts +2 -2
- package/templates/apps/web/.env.example +3 -3
- package/templates/apps/web/CLAUDE.md +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +214 -17
- package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +12 -15
- package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +29 -13
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
- package/templates/apps/web/messages/en.json +86 -1
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +6 -2
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
package/dist/parse-args.d.ts
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
* composition is downstream work, so the default (`marketing`) reproduces
|
|
8
8
|
* today's fixed-tree scaffold exactly.
|
|
9
9
|
*/
|
|
10
|
-
export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs-blog", "multi-tenant", "scheduling", "signal-collectors", "commerce"];
|
|
10
|
+
export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs-blog", "multi-tenant", "workspace", "scheduling", "signal-collectors", "commerce"];
|
|
11
11
|
export type ModuleName = (typeof RECOGNIZED_MODULES)[number];
|
|
12
12
|
export interface CliOptions {
|
|
13
13
|
/** Positional target directory (the dir to scaffold into). Optional only
|
|
14
14
|
* when `help` or `version` is set. */
|
|
15
15
|
target: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The site template to install (`--template`), validated against
|
|
18
|
+
* `RECOGNIZED_TEMPLATES` at parse time. Undefined when the flag is absent —
|
|
19
|
+
* and a run naming no template scaffolds exactly what it did before the
|
|
20
|
+
* flag existed (spec 5481 § 3 DD3).
|
|
21
|
+
*/
|
|
22
|
+
template: string | undefined;
|
|
16
23
|
install: boolean;
|
|
17
24
|
help: boolean;
|
|
18
25
|
version: boolean;
|
package/dist/parse-args.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
import { RECOGNIZED_SURFACES, parseSurfaces } from "./surfaces.js";
|
|
12
|
+
import { RECOGNIZED_TEMPLATES } from "./template-names.js";
|
|
12
13
|
/**
|
|
13
14
|
* The recognized module vocabulary for the `--modules` selector.
|
|
14
15
|
*
|
|
@@ -22,6 +23,7 @@ export const RECOGNIZED_MODULES = [
|
|
|
22
23
|
"marketing",
|
|
23
24
|
"docs-blog",
|
|
24
25
|
"multi-tenant",
|
|
26
|
+
"workspace",
|
|
25
27
|
"scheduling",
|
|
26
28
|
"signal-collectors",
|
|
27
29
|
"commerce",
|
|
@@ -51,9 +53,23 @@ Options:
|
|
|
51
53
|
Defaults to \`marketing\`, which reproduces today's
|
|
52
54
|
scaffold. A module is a composable unit across the
|
|
53
55
|
three tiers (data · API · presentation).
|
|
56
|
+
--template <name> The site template to install (recognized:
|
|
57
|
+
${RECOGNIZED_TEMPLATES.join(", ")}).
|
|
58
|
+
A template is a page roster plus seed content and
|
|
59
|
+
config — never code. Its required modules are ADDED
|
|
60
|
+
to whatever \`--modules\` names, so the two compose.
|
|
61
|
+
A template needing only free modules installs from
|
|
62
|
+
the tree this package ships, with no key and no
|
|
63
|
+
network call; one needing a paid module needs
|
|
64
|
+
AILK_LICENSE_KEY to cover every module it requires,
|
|
65
|
+
and refuses naming whichever it does not.
|
|
54
66
|
--surfaces <csv> Comma-separated route surfaces to scaffold IN
|
|
55
|
-
ADDITION to the core set (/, /about, /contact, /faq
|
|
56
|
-
/thanks
|
|
67
|
+
ADDITION to the core set (/, /about, /contact, /faq).
|
|
68
|
+
/thanks/{slug} ships with every scaffold too, but it
|
|
69
|
+
is not a surface you select here — it is a per-form
|
|
70
|
+
confirmation page, authored when the form that needs
|
|
71
|
+
it exists (there is no /thanks hub). Recognized:
|
|
72
|
+
${RECOGNIZED_SURFACES.join(", ")}.
|
|
57
73
|
Surfaces the declared architecture does not carry are
|
|
58
74
|
NOT scaffolded — their routes are absent, so a fresh
|
|
59
75
|
site never serves a stock page with placeholder
|
|
@@ -102,6 +118,7 @@ export function parseCliArgs(argv) {
|
|
|
102
118
|
allowPositionals: true,
|
|
103
119
|
options: {
|
|
104
120
|
modules: { type: "string" },
|
|
121
|
+
template: { type: "string" },
|
|
105
122
|
surfaces: { type: "string" },
|
|
106
123
|
"sync-architecture": { type: "string" },
|
|
107
124
|
"sync-catalog": { type: "boolean", default: false },
|
|
@@ -119,6 +136,7 @@ export function parseCliArgs(argv) {
|
|
|
119
136
|
const version = parsed.values.version === true;
|
|
120
137
|
const install = parsed.values.install === true;
|
|
121
138
|
const modules = parseModules(parsed.values.modules);
|
|
139
|
+
const template = parseTemplate(parsed.values.template);
|
|
122
140
|
const surfaces = parseSurfaceList(parsed.values.surfaces);
|
|
123
141
|
const syncArchitecture = parsed.values["sync-architecture"];
|
|
124
142
|
const syncCatalog = parsed.values["sync-catalog"] === true;
|
|
@@ -138,6 +156,7 @@ export function parseCliArgs(argv) {
|
|
|
138
156
|
if (help || version) {
|
|
139
157
|
return {
|
|
140
158
|
target: undefined,
|
|
159
|
+
template: undefined,
|
|
141
160
|
install,
|
|
142
161
|
help,
|
|
143
162
|
version,
|
|
@@ -155,6 +174,7 @@ export function parseCliArgs(argv) {
|
|
|
155
174
|
}
|
|
156
175
|
return {
|
|
157
176
|
target: parsed.positionals[0],
|
|
177
|
+
template,
|
|
158
178
|
install,
|
|
159
179
|
help,
|
|
160
180
|
version,
|
|
@@ -173,6 +193,7 @@ export function parseCliArgs(argv) {
|
|
|
173
193
|
}
|
|
174
194
|
return {
|
|
175
195
|
target: parsed.positionals[0],
|
|
196
|
+
template,
|
|
176
197
|
install,
|
|
177
198
|
help,
|
|
178
199
|
version,
|
|
@@ -183,6 +204,32 @@ export function parseCliArgs(argv) {
|
|
|
183
204
|
syncArchitecture: undefined,
|
|
184
205
|
};
|
|
185
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Validate the `--template` value against the declared template set
|
|
209
|
+
* (spec 5481 § 3 DD1).
|
|
210
|
+
*
|
|
211
|
+
* Validated HERE, in the parser, so an unrecognized template fails at parse
|
|
212
|
+
* time as a `ParseError` exactly like an unrecognized module or surface does.
|
|
213
|
+
* Splitting it out to `programmatic.ts` would make `--template` the one flag
|
|
214
|
+
* in this CLI whose bad value surfaces somewhere else.
|
|
215
|
+
*
|
|
216
|
+
* The names come from `template-names.ts` rather than from
|
|
217
|
+
* `TEMPLATE_MANIFESTS` itself, and that file's doc comment records the
|
|
218
|
+
* measured reason: importing the registry here closes a runtime cycle
|
|
219
|
+
* through `lib/module-tier.ts`. `TEMPLATE_MANIFESTS` remains the registry;
|
|
220
|
+
* the two sets are asserted equal by the suite.
|
|
221
|
+
*
|
|
222
|
+
* @throws ParseError on an undeclared template name, naming the declared set.
|
|
223
|
+
*/
|
|
224
|
+
function parseTemplate(raw) {
|
|
225
|
+
if (raw === undefined)
|
|
226
|
+
return undefined;
|
|
227
|
+
const name = raw.trim();
|
|
228
|
+
if (!RECOGNIZED_TEMPLATES.includes(name)) {
|
|
229
|
+
throw new ParseError(`unknown template "${raw}". Recognized: ${RECOGNIZED_TEMPLATES.join(", ")}`);
|
|
230
|
+
}
|
|
231
|
+
return name;
|
|
232
|
+
}
|
|
186
233
|
/**
|
|
187
234
|
* Parse + validate the `--surfaces` CSV, re-throwing an unrecognized-surface
|
|
188
235
|
* failure as a ParseError so every CLI parse failure reaches the orchestrator's
|
package/dist/programmatic.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { CATALOG_TARGET_PATH } from "./component-catalog.js";
|
|
19
19
|
import { type ModuleName } from "./parse-args.js";
|
|
20
|
+
import { type TemplateLayout } from "./site-chrome.js";
|
|
20
21
|
import { type SyncRoutesResult } from "./sync-routes.js";
|
|
21
22
|
export interface ScaffoldOptions {
|
|
22
23
|
/** Absolute or relative path to the target directory. Must not exist or be empty. */
|
|
@@ -28,6 +29,21 @@ export interface ScaffoldOptions {
|
|
|
28
29
|
* Ignored at runtime until named presets are implemented.
|
|
29
30
|
*/
|
|
30
31
|
preset?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The site template to install (`--template`), a `TEMPLATE_MANIFESTS` key.
|
|
34
|
+
*
|
|
35
|
+
* Consumed ONLY by the async `scaffold()` below. `scaffoldSync` stays
|
|
36
|
+
* template-oblivious for exactly the reason it stays licensing-oblivious
|
|
37
|
+
* (spec 5481 § 3 DD3): a call naming no template takes a code path with no
|
|
38
|
+
* new statement in it, which is a stronger byte-identity guarantee than a
|
|
39
|
+
* test asserting the output happens to match.
|
|
40
|
+
*
|
|
41
|
+
* What a template DOES change is `modules` — `scaffold()` unions the
|
|
42
|
+
* template's `requires` with whatever `--modules` named and passes THAT
|
|
43
|
+
* list to `scaffoldSync` (DD2), so the resolved surfaces, the emitted
|
|
44
|
+
* `architecture.yaml` and the generated nav are all functions of the union.
|
|
45
|
+
*/
|
|
46
|
+
template?: string;
|
|
31
47
|
/**
|
|
32
48
|
* Selected modules (recognized: marketing, docs-blog, multi-tenant,
|
|
33
49
|
* scheduling). Defaults to
|
|
@@ -70,8 +86,19 @@ export interface ScaffoldOptions {
|
|
|
70
86
|
licenseKey?: string;
|
|
71
87
|
/** Overrides `AILK_MODULE_REGISTRY_URL` / the built-in module-distribution default. */
|
|
72
88
|
moduleRegistryUrl?: string;
|
|
89
|
+
/** Overrides `AILK_TEMPLATE_REGISTRY_URL` / the built-in template-distribution default. */
|
|
90
|
+
templateRegistryUrl?: string;
|
|
73
91
|
/** Injected fetch for tests. Never a live network call in CI or in this repo's evidence runs. */
|
|
74
92
|
fetchImpl?: typeof fetch;
|
|
93
|
+
/**
|
|
94
|
+
* Resolved template layout knobs (#5612 D1/D2) — INTERNAL: populated by
|
|
95
|
+
* `scaffold()` from the resolved template's `layout` block (the code
|
|
96
|
+
* registry's `TemplateManifest.layout`) before calling `scaffoldSync`. Never
|
|
97
|
+
* set by a CLI/MCP caller directly. `scaffoldSync` fills every unset field
|
|
98
|
+
* with `DEFAULT_SITE_CHROME`, so a call naming no `layout` (every existing
|
|
99
|
+
* caller) writes the same defaults `site-chrome.ts` always has.
|
|
100
|
+
*/
|
|
101
|
+
layout?: TemplateLayout;
|
|
75
102
|
}
|
|
76
103
|
export interface ScaffoldResult {
|
|
77
104
|
/** Resolved absolute path to the scaffolded directory. */
|
|
@@ -98,9 +125,17 @@ export declare function scaffoldSync(opts: ScaffoldOptions): ScaffoldResult;
|
|
|
98
125
|
* read and no network call (the regression floor). A paid module with
|
|
99
126
|
* no key, or a licence the module distribution rejects, throws
|
|
100
127
|
* `LicenseRequiredError` HERE — before anything is written to disk.
|
|
101
|
-
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy
|
|
128
|
+
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy, over the
|
|
129
|
+
* EFFECTIVE module set (see 0 below).
|
|
102
130
|
* 4. Merge each resolved paid module's fetched bundle into the freshly
|
|
103
131
|
* scaffolded tree (`extractModuleBundle`).
|
|
132
|
+
* 5. Install the named site template LAST (#5481; parent record D9), from
|
|
133
|
+
* its fetched bundle or — when it requires only free modules — from the
|
|
134
|
+
* `site-templates/` tree this package ships.
|
|
135
|
+
*
|
|
136
|
+
* And, ahead of all five when `opts.template` is set (0): resolve the
|
|
137
|
+
* template and union its `requires` with `opts.modules`, so every step above
|
|
138
|
+
* runs over the EFFECTIVE module set rather than the requested one.
|
|
104
139
|
*
|
|
105
140
|
* `scaffoldSync` itself stays licensing-oblivious on purpose: every existing
|
|
106
141
|
* synchronous caller (this package's own test suite, overwhelmingly) is
|
package/dist/programmatic.js
CHANGED
|
@@ -21,15 +21,27 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog.js";
|
|
22
22
|
import { copyTemplate } from "./copy.js";
|
|
23
23
|
import { deriveArchitecture } from "./derive-architecture.js";
|
|
24
|
-
import { extractModuleBundle } from "./lib/extract-module-bundle.js";
|
|
24
|
+
import { extractModuleBundle, installBundleTree, } from "./lib/extract-module-bundle.js";
|
|
25
|
+
import { fetchTemplate } from "./lib/fetch-template.js";
|
|
25
26
|
import { resolvePaidModules } from "./lib/module-license-gate.js";
|
|
26
27
|
import { classifyModules } from "./lib/module-tier.js";
|
|
27
28
|
import { normalizeModules } from "./module-architecture.js";
|
|
28
|
-
import {
|
|
29
|
+
import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
|
|
30
|
+
import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
29
31
|
import { syncRoutes } from "./sync-routes.js";
|
|
32
|
+
import { BASE_LAYER_DIR, mergeRoster, resolveTemplate, resolveTemplateLineage, templateDelivery, } from "./template-manifest.js";
|
|
30
33
|
import { validateTarget } from "./validate.js";
|
|
31
34
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
35
|
const TEMPLATES_DIR = resolve(PKG_ROOT, "templates");
|
|
36
|
+
/**
|
|
37
|
+
* Where a Free site template's files are read from on the keyless path — and
|
|
38
|
+
* why it is BESIDE `templates/` rather than inside it (parent record D14):
|
|
39
|
+
* `copyTemplate` copies the whole `templates/` tree into EVERY scaffold, so a
|
|
40
|
+
* per-template subtree there would land in scaffolds that asked for a
|
|
41
|
+
* different template, or for none. It reaches the published tarball through
|
|
42
|
+
* `package.json` `files` instead.
|
|
43
|
+
*/
|
|
44
|
+
const SITE_TEMPLATES_DIR = resolve(PKG_ROOT, "site-templates");
|
|
33
45
|
const VERSIONS_FILE = resolve(PKG_ROOT, "versions.json");
|
|
34
46
|
function readVersionMap() {
|
|
35
47
|
if (!existsSync(VERSIONS_FILE)) {
|
|
@@ -84,6 +96,10 @@ export function scaffoldSync(opts) {
|
|
|
84
96
|
PROJECT_NAME: projectName,
|
|
85
97
|
COMPANY_NAME: "Your Company",
|
|
86
98
|
COMPANY_DOMAIN: "example.com",
|
|
99
|
+
// The social-profile handle the Free chrome's footer links carry
|
|
100
|
+
// (`https://x.com/{{COMPANY_HANDLE}}`, #5469 D5) — a placeholder the
|
|
101
|
+
// site owner replaces, never the kit vendor's own handle.
|
|
102
|
+
COMPANY_HANDLE: "yourcompany",
|
|
87
103
|
},
|
|
88
104
|
});
|
|
89
105
|
// Derive + write the module-set-driven architecture.yaml (spec #3787). This
|
|
@@ -97,6 +113,12 @@ export function scaffoldSync(opts) {
|
|
|
97
113
|
// the template's copy in place would link /docs and /blog sitewide in a
|
|
98
114
|
// scaffold that carries neither route.
|
|
99
115
|
writeFileSync(join(targetAbs, NAV_CONFIG_PATH), renderNavConfig(enabledSurfaces));
|
|
116
|
+
// Site-level chrome knobs (#5612 D2) — beside nav-config.ts, same GENERATED
|
|
117
|
+
// idiom. `opts.layout` is set only by `scaffold()`, from the resolved
|
|
118
|
+
// template's `layout` block; a bare `scaffoldSync` call (every existing
|
|
119
|
+
// synchronous caller) writes DEFAULT_SITE_CHROME, byte-identical to the
|
|
120
|
+
// monorepo's own committed apps/web/lib/site-chrome.ts.
|
|
121
|
+
writeFileSync(join(targetAbs, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(opts.layout)));
|
|
100
122
|
// Opt-in only (#4636). Every write above this line is unconditional, so a
|
|
101
123
|
// scaffold with `withCatalog` unset touches exactly the files it did before.
|
|
102
124
|
if (opts.withCatalog === true)
|
|
@@ -114,9 +136,17 @@ export function scaffoldSync(opts) {
|
|
|
114
136
|
* read and no network call (the regression floor). A paid module with
|
|
115
137
|
* no key, or a licence the module distribution rejects, throws
|
|
116
138
|
* `LicenseRequiredError` HERE — before anything is written to disk.
|
|
117
|
-
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy
|
|
139
|
+
* 3. `scaffoldSync` — unchanged, today's local `templates/` copy, over the
|
|
140
|
+
* EFFECTIVE module set (see 0 below).
|
|
118
141
|
* 4. Merge each resolved paid module's fetched bundle into the freshly
|
|
119
142
|
* scaffolded tree (`extractModuleBundle`).
|
|
143
|
+
* 5. Install the named site template LAST (#5481; parent record D9), from
|
|
144
|
+
* its fetched bundle or — when it requires only free modules — from the
|
|
145
|
+
* `site-templates/` tree this package ships.
|
|
146
|
+
*
|
|
147
|
+
* And, ahead of all five when `opts.template` is set (0): resolve the
|
|
148
|
+
* template and union its `requires` with `opts.modules`, so every step above
|
|
149
|
+
* runs over the EFFECTIVE module set rather than the requested one.
|
|
120
150
|
*
|
|
121
151
|
* `scaffoldSync` itself stays licensing-oblivious on purpose: every existing
|
|
122
152
|
* synchronous caller (this package's own test suite, overwhelmingly) is
|
|
@@ -124,31 +154,212 @@ export function scaffoldSync(opts) {
|
|
|
124
154
|
* with a paid module in `opts.modules` exercises the gate at all.
|
|
125
155
|
*/
|
|
126
156
|
export async function scaffold(opts) {
|
|
127
|
-
|
|
157
|
+
// Resolved first, and by the registry rather than re-validated here: the
|
|
158
|
+
// name already passed `parse-args.ts`, and `resolveTemplate` throws a
|
|
159
|
+
// `TemplateError` for a programmatic caller that skipped the parser.
|
|
160
|
+
const template = opts.template !== undefined ? resolveTemplate(opts.template) : undefined;
|
|
161
|
+
// #5618 D2 — the `extends` chain is resolved in the client registry before
|
|
162
|
+
// any fetch: an unknown parent or a cycle is refused here, with the target
|
|
163
|
+
// still non-existent, like the four refusals below.
|
|
164
|
+
if (template !== undefined)
|
|
165
|
+
resolveTemplateLineage(template.name);
|
|
166
|
+
// The effective module set: union(template.requires, parsed --modules),
|
|
167
|
+
// normalized through `normalizeModules` — the single ordering authority
|
|
168
|
+
// (parent record D7). Order-independence falls out rather than being
|
|
169
|
+
// engineered: it returns `RECOGNIZED_MODULES.filter(...)`, declaration-
|
|
170
|
+
// ordered and deduped, so the same SET in any argument order is the same
|
|
171
|
+
// list.
|
|
172
|
+
const modules = normalizeModules([
|
|
173
|
+
...(template?.requires ?? []),
|
|
174
|
+
...(opts.modules ?? ["marketing"]),
|
|
175
|
+
]);
|
|
176
|
+
const { paid } = classifyModules(modules);
|
|
128
177
|
const bundles = await resolvePaidModules({
|
|
129
178
|
paidModules: paid,
|
|
130
179
|
licenseKey: opts.licenseKey,
|
|
180
|
+
...(template !== undefined ? { templateName: template.name } : {}),
|
|
131
181
|
...(opts.moduleRegistryUrl !== undefined
|
|
132
182
|
? { registryUrl: opts.moduleRegistryUrl }
|
|
133
183
|
: {}),
|
|
134
184
|
...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
|
|
135
185
|
});
|
|
136
|
-
|
|
186
|
+
// The template's OWN bundle, fetched here — before `scaffoldSync` — so the
|
|
187
|
+
// fourth refusal (the template's bundle unpublished, parent record D17 /
|
|
188
|
+
// spec 5481 § 3 DD5 row 4) throws with the target still non-existent, like
|
|
189
|
+
// the three module refusals above it (DD6). It is EXTRACTED last, below.
|
|
190
|
+
//
|
|
191
|
+
// The keyless path never reaches this at all: a template requiring only
|
|
192
|
+
// free modules classifies to an empty paid set, so `resolvePaidModules`
|
|
193
|
+
// returned at its first branch without reading `process.env` or the
|
|
194
|
+
// network, and `templateDelivery` sends us past this too (DD7). That is
|
|
195
|
+
// what makes record D11's no-network guarantee structural rather than
|
|
196
|
+
// merely likely.
|
|
197
|
+
const templateBundle = template !== undefined && templateDelivery(template) === "paid"
|
|
198
|
+
? await fetchTemplate({
|
|
199
|
+
templateName: template.name,
|
|
200
|
+
token: opts.licenseKey ?? process.env["AILK_LICENSE_KEY"] ?? "",
|
|
201
|
+
...(opts.templateRegistryUrl !== undefined
|
|
202
|
+
? { registryUrl: opts.templateRegistryUrl }
|
|
203
|
+
: {}),
|
|
204
|
+
...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
|
|
205
|
+
})
|
|
206
|
+
: undefined;
|
|
207
|
+
// `modules` — the union — is what `scaffoldSync` receives, not `opts`
|
|
208
|
+
// (spec 5481 § 3 DD2). `resolveSurfaces`, `deriveArchitecture` and
|
|
209
|
+
// `renderNavConfig` are all computed INSIDE `scaffoldSync` from
|
|
210
|
+
// `opts.modules`, so a union computed for the licence classification alone
|
|
211
|
+
// would gate correctly and emit the wrong surfaces, the wrong
|
|
212
|
+
// `architecture.yaml` and the wrong nav — a silent wrong result, not a
|
|
213
|
+
// failure.
|
|
214
|
+
//
|
|
215
|
+
// The template's roster ENABLES its surfaces (#5469 D10, the #5554 finding).
|
|
216
|
+
// A template's module-less rows (`/pricing`, `/privacy`, `/terms`) name
|
|
217
|
+
// `GATEABLE_SURFACES` entries no module contributes, and until this line
|
|
218
|
+
// `mergeRoster` was dead on this path: the content landed, the route
|
|
219
|
+
// directories were skipped, and the three pages 404ed. The merged roster's
|
|
220
|
+
// routes select their surface entries — matched on each surface's own
|
|
221
|
+
// `routes[]`, never on a new field — and that set rides `opts.surfaces`
|
|
222
|
+
// into `scaffoldSync`'s existing `resolveSurfaces` call, so the copied
|
|
223
|
+
// tree, the emitted `architecture.yaml` and the generated nav all follow.
|
|
224
|
+
// A scaffold naming no template takes no new statement (parent § D16): the
|
|
225
|
+
// roster-derived set is only ever computed when `template` is set.
|
|
226
|
+
const surfaces = template !== undefined
|
|
227
|
+
? [...new Set([...(opts.surfaces ?? []), ...rosterSurfaces(template)])].sort()
|
|
228
|
+
: opts.surfaces;
|
|
229
|
+
const result = scaffoldSync({
|
|
230
|
+
...opts,
|
|
231
|
+
modules,
|
|
232
|
+
...(surfaces !== undefined ? { surfaces } : {}),
|
|
233
|
+
// #5612 D1/D2 — the code registry's own `layout` block, known
|
|
234
|
+
// synchronously (unlike a bundle's `ailk-template.json`, only readable
|
|
235
|
+
// after install below). A template declaring none resolves to the same
|
|
236
|
+
// defaults `scaffoldSync` already writes for a templateless scaffold.
|
|
237
|
+
...(template?.layout !== undefined ? { layout: template.layout } : {}),
|
|
238
|
+
});
|
|
137
239
|
// Threaded to extractModuleBundle for `package.json` patch targets'
|
|
138
240
|
// `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
|
|
139
241
|
// `scaffoldSync` already reads for `copyTemplate`, re-read here since
|
|
140
242
|
// `scaffold()` doesn't share `scaffoldSync`'s call frame.
|
|
141
243
|
const versions = readVersionMap();
|
|
244
|
+
// #5618 D4 — the file-grain ownership registry for THIS scaffold: which
|
|
245
|
+
// layer wrote which destination. Call-scoped (a fresh Map per scaffold(),
|
|
246
|
+
// never module-level), threaded through every layer install below in the
|
|
247
|
+
// composition order base → modules → template. A module writing a path the
|
|
248
|
+
// base or an earlier module owns is refused before its first write unless
|
|
249
|
+
// it is a declared patch; the template, the customer's copy, may overwrite
|
|
250
|
+
// anything. Starts empty at the base-layer boundary: the `templates/`
|
|
251
|
+
// snapshot (layer 0) is deliberately outside it.
|
|
252
|
+
const ownership = new Map();
|
|
253
|
+
// The BASE LAYER lands first (#5618 D1) — the seed files every template
|
|
254
|
+
// reaches by `extends` (today the three module-less base pages), read off
|
|
255
|
+
// the `site-templates/_base/` tree this package ships, through the same
|
|
256
|
+
// `installBundleTree` the keyless Free path uses below. Inside the
|
|
257
|
+
// `template !== undefined` guard, so a scaffold naming no template
|
|
258
|
+
// executes no new statement — the keyless regression floor holds
|
|
259
|
+
// structurally, exactly as spec 5462 D16 argues for `rosterSurfaces` above.
|
|
260
|
+
// Never fetched: it ships in the npm tarball, so the Free path keeps making
|
|
261
|
+
// zero requests. Every module bundle and then the template land ON TOP of
|
|
262
|
+
// it, later layers winning file by file.
|
|
263
|
+
if (template !== undefined) {
|
|
264
|
+
installBundleTree({
|
|
265
|
+
sourceRoot: join(SITE_TEMPLATES_DIR, BASE_LAYER_DIR),
|
|
266
|
+
targetDir: result.targetDir,
|
|
267
|
+
name: BASE_LAYER_DIR,
|
|
268
|
+
templateRequires: [],
|
|
269
|
+
versions,
|
|
270
|
+
ownership,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
142
273
|
for (const [moduleName, bytes] of bundles) {
|
|
143
274
|
await extractModuleBundle({
|
|
144
275
|
bytes,
|
|
145
276
|
targetDir: result.targetDir,
|
|
146
277
|
moduleName,
|
|
147
278
|
versions,
|
|
279
|
+
ownership,
|
|
148
280
|
});
|
|
149
281
|
}
|
|
282
|
+
// The template lands LAST — after `scaffoldSync` and after every module
|
|
283
|
+
// bundle (parent record D9) — so its content and config files sit on top of
|
|
284
|
+
// everything derived. It shapes the derived artifacts by DECLARING (its
|
|
285
|
+
// `requires` are in the union above; its roster rows are what the nav is
|
|
286
|
+
// generated from), never by landing a code file: the nav is already written
|
|
287
|
+
// by `scaffoldSync` from `renderNavConfig(enabledSurfaces)`, and record
|
|
288
|
+
// §5.2 forbids a `.ts` in any `site-templates/` `installMap` destination.
|
|
289
|
+
//
|
|
290
|
+
// One install step, two source roots (DD7): an extracted staging dir on the
|
|
291
|
+
// paid path, `site-templates/<name>/` on disk on the Free one.
|
|
292
|
+
if (template !== undefined) {
|
|
293
|
+
// #5612 D1/D2 — the INSTALLED template's own `ailk-template.json` layout
|
|
294
|
+
// (validated by `readAilkTemplateManifest`), if it declares one. Read
|
|
295
|
+
// only now: it lives in the bundle/tree, unreachable before install. When
|
|
296
|
+
// declared, it is authoritative over the code registry's `template.layout`
|
|
297
|
+
// already baked into `result`'s site-chrome.ts above — overwritten below.
|
|
298
|
+
// Absent (every shipped template today), the registry-resolved write from
|
|
299
|
+
// `scaffoldSync` stands untouched.
|
|
300
|
+
const installed = templateBundle !== undefined
|
|
301
|
+
? await extractModuleBundle({
|
|
302
|
+
bytes: templateBundle,
|
|
303
|
+
targetDir: result.targetDir,
|
|
304
|
+
moduleName: template.name,
|
|
305
|
+
templateRequires: template.requires,
|
|
306
|
+
templateExtends: template.extends,
|
|
307
|
+
versions,
|
|
308
|
+
ownership,
|
|
309
|
+
})
|
|
310
|
+
: (() => {
|
|
311
|
+
const sourceRoot = join(SITE_TEMPLATES_DIR, template.name);
|
|
312
|
+
if (!existsSync(sourceRoot)) {
|
|
313
|
+
throw new Error(`template "${template.name}": ${sourceRoot} is missing — the published package must carry site-templates/ in its "files" list`);
|
|
314
|
+
}
|
|
315
|
+
return installBundleTree({
|
|
316
|
+
sourceRoot,
|
|
317
|
+
targetDir: result.targetDir,
|
|
318
|
+
name: template.name,
|
|
319
|
+
templateRequires: template.requires,
|
|
320
|
+
templateExtends: template.extends,
|
|
321
|
+
versions,
|
|
322
|
+
ownership,
|
|
323
|
+
});
|
|
324
|
+
})();
|
|
325
|
+
if (installed.layout !== undefined) {
|
|
326
|
+
writeFileSync(join(result.targetDir, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(installed.layout)));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
150
329
|
return result;
|
|
151
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* The MODULE-LESS gateable-surface ids a template's merged roster names
|
|
333
|
+
* (#5469 D10): every `GATEABLE_SURFACES` entry no module contributes
|
|
334
|
+
* (`MODULE_SURFACES`) one of whose `routes[]` directories is the route
|
|
335
|
+
* directory of a merged roster row. Module-owned surfaces are left to the
|
|
336
|
+
* module set — a module's own rows (docs-blog's `/blog`) already enable
|
|
337
|
+
* theirs through `resolveSurfaces`, and the Free `/blog` teaser row must NOT
|
|
338
|
+
* enable the docs-blog `blog` surface: keyless there is no `blog/` route
|
|
339
|
+
* directory to copy, and the teaser renders through `[slug]` (D3).
|
|
340
|
+
*
|
|
341
|
+
* Merged over the template's OWN `requires`, not the scaffold's full module
|
|
342
|
+
* set: a module-less surface can only ever come from the template's base
|
|
343
|
+
* rows, and merging the Free template with an ADDED docs-blog would make
|
|
344
|
+
* `mergeRoster` refuse `/blog` (the teaser and the module's listing both
|
|
345
|
+
* claim it — parent § D8). That composition is legal at the route level
|
|
346
|
+
* precisely because the module's `blog/` directory takes precedence over
|
|
347
|
+
* the `[slug]` teaser, so it is resolved there, never in the registry.
|
|
348
|
+
*
|
|
349
|
+
* Route → directory: a roster route is site-relative (`/pricing`,
|
|
350
|
+
* `/flows/[slug]/start`); a surface owns template-relative route directories
|
|
351
|
+
* (`apps/web/app/[locale]/pricing`). A row matches a surface when its first
|
|
352
|
+
* path segment is the surface's top-level route directory.
|
|
353
|
+
*/
|
|
354
|
+
function rosterSurfaces(template) {
|
|
355
|
+
const routeRoot = "apps/web/app/[locale]";
|
|
356
|
+
const moduleOwned = new Set(Object.values(MODULE_SURFACES).flat());
|
|
357
|
+
const topSegments = new Set(mergeRoster(template, template.requires)
|
|
358
|
+
.map((row) => row.route.split("/").filter(Boolean)[0])
|
|
359
|
+
.filter((segment) => segment !== undefined));
|
|
360
|
+
return GATEABLE_SURFACES.filter((surface) => !moduleOwned.has(surface.id) &&
|
|
361
|
+
surface.routes.some((dir) => topSegments.has(dir.slice(routeRoot.length + 1)))).map((surface) => surface.id);
|
|
362
|
+
}
|
|
152
363
|
// Re-exported so a consumer of the async `scaffold()` above can type its own
|
|
153
364
|
// wrapper (e.g. the MCP scaffold-site tool) without reaching into `lib/`.
|
|
154
365
|
export { LicenseRequiredError, } from "./lib/module-license-gate.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template layout knobs (#5612) — the resolved chrome facts a scaffold's
|
|
3
|
+
* `apps/web/app/[locale]/layout.tsx` reads to mount Nav/Footer/Banner/
|
|
4
|
+
* MarketingShell: `nav` (full nav links vs. brand-mark-only), `footer`
|
|
5
|
+
* (columns vs. the existing centered variant), `announcementBanner` (mount
|
|
6
|
+
* the home-only announcement Banner or not) and `home` (the default
|
|
7
|
+
* document-scroll home vs. the scroll-snap deck, #5270/#5426).
|
|
8
|
+
*
|
|
9
|
+
* A site template is content + an `installMap`, never a route/layout/`.ts`
|
|
10
|
+
* file (spec 5462 D9/D16) — these are SITE-LEVEL facts a template DECLARES
|
|
11
|
+
* (on `TemplateManifest.layout` in the code registry, or on a bundle's
|
|
12
|
+
* `ailk-template.json` `layout` field), never a file it ships. This module is
|
|
13
|
+
* the single place that declares the vocabulary, fills defaults, validates an
|
|
14
|
+
* untrusted (bundle-sourced) value, and renders the generated
|
|
15
|
+
* `apps/web/lib/site-chrome.ts` every scaffold gets — beside `nav-config.ts`,
|
|
16
|
+
* the same GENERATED-header idiom (`surfaces.ts` `renderNavConfig`).
|
|
17
|
+
*
|
|
18
|
+
* Every field is optional and DEFAULTS to today's render (`full` / `columns`
|
|
19
|
+
* / `true` / `default`) — a scaffold naming no template, or a template naming
|
|
20
|
+
* no `layout` block, gets these defaults written, byte-identical to the
|
|
21
|
+
* monorepo's own committed `apps/web/lib/site-chrome.ts`.
|
|
22
|
+
*/
|
|
23
|
+
/** The `nav` knob's vocabulary. `mark-only` → the layout passes Nav an empty
|
|
24
|
+
* `navItems` (brand mark + the existing actions/theme/locale controls only). */
|
|
25
|
+
export type SiteChromeNav = "full" | "mark-only";
|
|
26
|
+
/** The `footer` knob's vocabulary. `centered` → `Footer layout="centered"`
|
|
27
|
+
* (the existing third variant: nav row + social row + FooterBottomBar). */
|
|
28
|
+
export type SiteChromeFooter = "columns" | "centered";
|
|
29
|
+
/** The `home` knob's vocabulary. `deck` → the locale-root route mounts
|
|
30
|
+
* MarketingShell with `snap="mandatory"` + `deckHero` (spec 5426 D7). */
|
|
31
|
+
export type SiteChromeHome = "default" | "deck";
|
|
32
|
+
/**
|
|
33
|
+
* A template's declared layout block — every field optional. Lives on
|
|
34
|
+
* `TemplateManifest.layout` (the code registry, `template-manifest.ts`) and
|
|
35
|
+
* on a template bundle's `ailk-template.json` `layout` field, same shape.
|
|
36
|
+
*/
|
|
37
|
+
export interface TemplateLayout {
|
|
38
|
+
nav?: SiteChromeNav;
|
|
39
|
+
footer?: SiteChromeFooter;
|
|
40
|
+
announcementBanner?: boolean;
|
|
41
|
+
home?: SiteChromeHome;
|
|
42
|
+
}
|
|
43
|
+
/** `TemplateLayout` with every field resolved — what `site-chrome.ts` exports. */
|
|
44
|
+
export interface ResolvedSiteChrome {
|
|
45
|
+
nav: SiteChromeNav;
|
|
46
|
+
footer: SiteChromeFooter;
|
|
47
|
+
announcementBanner: boolean;
|
|
48
|
+
home: SiteChromeHome;
|
|
49
|
+
}
|
|
50
|
+
/** Byte-identical-to-today defaults (#5612 D1). */
|
|
51
|
+
export declare const DEFAULT_SITE_CHROME: ResolvedSiteChrome;
|
|
52
|
+
/** Template-relative path of the generated site-chrome registry — beside `NAV_CONFIG_PATH`. */
|
|
53
|
+
export declare const SITE_CHROME_PATH = "apps/web/lib/site-chrome.ts";
|
|
54
|
+
/** Thrown by `validateTemplateLayout` on an unrecognized value, naming the field. */
|
|
55
|
+
export declare class TemplateLayoutError extends Error {
|
|
56
|
+
constructor(message: string);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validate an UNTRUSTED `layout` value (a bundle's `ailk-template.json`
|
|
60
|
+
* `layout` field — arbitrary JSON, never compile-time checked the way the
|
|
61
|
+
* code registry's `TemplateManifest.layout` is) against the vocabulary above.
|
|
62
|
+
* Refuses on the first unrecognized field value, naming the field and the
|
|
63
|
+
* value (#5612 D1). `undefined`/`null` is valid (no block declared); a
|
|
64
|
+
* non-object value, or a recognized field holding the wrong TYPE, is refused
|
|
65
|
+
* the same way as an unrecognized enum value — both are "not a value this
|
|
66
|
+
* field accepts".
|
|
67
|
+
*
|
|
68
|
+
* @param subject Caller-supplied context for the error message (e.g. `template "foo"`).
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateTemplateLayout(value: unknown, subject: string): TemplateLayout;
|
|
71
|
+
/** Fill every field a `TemplateLayout` leaves unset with `DEFAULT_SITE_CHROME`'s value. */
|
|
72
|
+
export declare function resolveSiteChrome(layout?: TemplateLayout): ResolvedSiteChrome;
|
|
73
|
+
/**
|
|
74
|
+
* Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
|
|
75
|
+
* GENERATED-header idiom `renderNavConfig` uses. `layout.tsx` imports
|
|
76
|
+
* `siteChrome` to decide Nav's `navItems`, Footer's `layout` prop, whether
|
|
77
|
+
* the announcement Banner mounts, and whether the locale-root route passes
|
|
78
|
+
* `snap="mandatory"` + `deckHero` to MarketingShell.
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderSiteChrome(resolved: ResolvedSiteChrome): string;
|