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
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
/** Byte-identical-to-today defaults (#5612 D1). */
|
|
24
|
+
export const DEFAULT_SITE_CHROME = {
|
|
25
|
+
nav: "full",
|
|
26
|
+
footer: "columns",
|
|
27
|
+
announcementBanner: true,
|
|
28
|
+
home: "default",
|
|
29
|
+
};
|
|
30
|
+
/** Template-relative path of the generated site-chrome registry — beside `NAV_CONFIG_PATH`. */
|
|
31
|
+
export const SITE_CHROME_PATH = "apps/web/lib/site-chrome.ts";
|
|
32
|
+
/** Thrown by `validateTemplateLayout` on an unrecognized value, naming the field. */
|
|
33
|
+
export class TemplateLayoutError extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "TemplateLayoutError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const NAV_VALUES = ["full", "mark-only"];
|
|
40
|
+
const FOOTER_VALUES = ["columns", "centered"];
|
|
41
|
+
const HOME_VALUES = ["default", "deck"];
|
|
42
|
+
/**
|
|
43
|
+
* Validate an UNTRUSTED `layout` value (a bundle's `ailk-template.json`
|
|
44
|
+
* `layout` field — arbitrary JSON, never compile-time checked the way the
|
|
45
|
+
* code registry's `TemplateManifest.layout` is) against the vocabulary above.
|
|
46
|
+
* Refuses on the first unrecognized field value, naming the field and the
|
|
47
|
+
* value (#5612 D1). `undefined`/`null` is valid (no block declared); a
|
|
48
|
+
* non-object value, or a recognized field holding the wrong TYPE, is refused
|
|
49
|
+
* the same way as an unrecognized enum value — both are "not a value this
|
|
50
|
+
* field accepts".
|
|
51
|
+
*
|
|
52
|
+
* @param subject Caller-supplied context for the error message (e.g. `template "foo"`).
|
|
53
|
+
*/
|
|
54
|
+
export function validateTemplateLayout(value, subject) {
|
|
55
|
+
if (value === undefined || value === null)
|
|
56
|
+
return {};
|
|
57
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
throw new TemplateLayoutError(`${subject}: "layout" must be an object, got ${JSON.stringify(value)}`);
|
|
59
|
+
}
|
|
60
|
+
const raw = value;
|
|
61
|
+
const result = {};
|
|
62
|
+
if ("nav" in raw && raw.nav !== undefined) {
|
|
63
|
+
if (!NAV_VALUES.includes(raw.nav)) {
|
|
64
|
+
throw new TemplateLayoutError(`${subject}: layout.nav must be one of ${NAV_VALUES.join(", ")} — got ${JSON.stringify(raw.nav)}`);
|
|
65
|
+
}
|
|
66
|
+
result.nav = raw.nav;
|
|
67
|
+
}
|
|
68
|
+
if ("footer" in raw && raw.footer !== undefined) {
|
|
69
|
+
if (!FOOTER_VALUES.includes(raw.footer)) {
|
|
70
|
+
throw new TemplateLayoutError(`${subject}: layout.footer must be one of ${FOOTER_VALUES.join(", ")} — got ${JSON.stringify(raw.footer)}`);
|
|
71
|
+
}
|
|
72
|
+
result.footer = raw.footer;
|
|
73
|
+
}
|
|
74
|
+
if ("announcementBanner" in raw && raw.announcementBanner !== undefined) {
|
|
75
|
+
if (typeof raw.announcementBanner !== "boolean") {
|
|
76
|
+
throw new TemplateLayoutError(`${subject}: layout.announcementBanner must be a boolean — got ${JSON.stringify(raw.announcementBanner)}`);
|
|
77
|
+
}
|
|
78
|
+
result.announcementBanner = raw.announcementBanner;
|
|
79
|
+
}
|
|
80
|
+
if ("home" in raw && raw.home !== undefined) {
|
|
81
|
+
if (!HOME_VALUES.includes(raw.home)) {
|
|
82
|
+
throw new TemplateLayoutError(`${subject}: layout.home must be one of ${HOME_VALUES.join(", ")} — got ${JSON.stringify(raw.home)}`);
|
|
83
|
+
}
|
|
84
|
+
result.home = raw.home;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/** Fill every field a `TemplateLayout` leaves unset with `DEFAULT_SITE_CHROME`'s value. */
|
|
89
|
+
export function resolveSiteChrome(layout) {
|
|
90
|
+
return {
|
|
91
|
+
nav: layout?.nav ?? DEFAULT_SITE_CHROME.nav,
|
|
92
|
+
footer: layout?.footer ?? DEFAULT_SITE_CHROME.footer,
|
|
93
|
+
announcementBanner: layout?.announcementBanner ?? DEFAULT_SITE_CHROME.announcementBanner,
|
|
94
|
+
home: layout?.home ?? DEFAULT_SITE_CHROME.home,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
|
|
99
|
+
* GENERATED-header idiom `renderNavConfig` uses. `layout.tsx` imports
|
|
100
|
+
* `siteChrome` to decide Nav's `navItems`, Footer's `layout` prop, whether
|
|
101
|
+
* the announcement Banner mounts, and whether the locale-root route passes
|
|
102
|
+
* `snap="mandatory"` + `deckHero` to MarketingShell.
|
|
103
|
+
*/
|
|
104
|
+
export function renderSiteChrome(resolved) {
|
|
105
|
+
return `// site-chrome.ts — resolved template layout knobs.
|
|
106
|
+
//
|
|
107
|
+
// GENERATED by create-ailk from the declared template's \`layout\` block
|
|
108
|
+
// (#5612). A scaffold with no template, or a template with no \`layout\`
|
|
109
|
+
// block, gets these defaults — byte-identical to every scaffold before this
|
|
110
|
+
// file existed. \`apps/web/app/[locale]/layout.tsx\` reads this to decide:
|
|
111
|
+
// - nav: "mark-only" → Nav renders the brand mark + right-hand controls
|
|
112
|
+
// only (no nav links).
|
|
113
|
+
// - footer: "centered" → Footer renders its existing centered variant.
|
|
114
|
+
// - announcementBanner: false → the home-only announcement Banner is not
|
|
115
|
+
// mounted (the cookie-consent Banner is unaffected — it is not an
|
|
116
|
+
// announcement).
|
|
117
|
+
// - home: "deck" → the locale-root route mounts MarketingShell with
|
|
118
|
+
// snap="mandatory" + deckHero (spec 5426 D7); every other route is
|
|
119
|
+
// unaffected.
|
|
120
|
+
|
|
121
|
+
export interface SiteChrome {
|
|
122
|
+
nav: "full" | "mark-only";
|
|
123
|
+
footer: "columns" | "centered";
|
|
124
|
+
announcementBanner: boolean;
|
|
125
|
+
home: "default" | "deck";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const siteChrome: SiteChrome = {
|
|
129
|
+
nav: ${JSON.stringify(resolved.nav)},
|
|
130
|
+
footer: ${JSON.stringify(resolved.footer)},
|
|
131
|
+
announcementBanner: ${JSON.stringify(resolved.announcementBanner)},
|
|
132
|
+
home: ${JSON.stringify(resolved.home)},
|
|
133
|
+
};
|
|
134
|
+
`;
|
|
135
|
+
}
|
package/dist/surfaces.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface Surface {
|
|
|
51
51
|
export declare const GATEABLE_SURFACES: readonly Surface[];
|
|
52
52
|
/** The recognized `--surfaces` vocabulary, sorted. */
|
|
53
53
|
export declare const RECOGNIZED_SURFACES: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Retired surface ids, kept for ONE release (#5563). Removal rides the same
|
|
56
|
+
* trigger as the `/schedule` legacyRoutes entries in routes-map.ts — see the
|
|
57
|
+
* comment on that row. Resolving through an alias prints a deprecation notice.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SURFACE_ALIASES: Readonly<Record<string, string>>;
|
|
54
60
|
/**
|
|
55
61
|
* Surfaces each recognized module contributes.
|
|
56
62
|
*
|
|
@@ -176,12 +182,21 @@ export declare const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
|
|
|
176
182
|
* exists: two dead links, sitewide, by default. So the nav follows the declared
|
|
177
183
|
* surface set for the same reason the route tree does.
|
|
178
184
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
185
|
+
* The base items are the Free marketing-site chrome (#5469 D5, the approved
|
|
186
|
+
* chrome diff recorded on that card):
|
|
187
|
+
* a Product flyout (How it works · Features · FAQ), then How it works, Pricing
|
|
188
|
+
* and About. Every one of those pages is in EVERY scaffold — the four core
|
|
189
|
+
* routes plus the `[slug]` route over the overlay's LandingPage MDX — except
|
|
190
|
+
* Pricing, which is a gateable surface: its item is emitted only when the
|
|
191
|
+
* `pricing` surface is enabled (a template roster enables it, #5469 D10), so
|
|
192
|
+
* the nav still never links a page the scaffold does not carry.
|
|
193
|
+
*
|
|
194
|
+
* Only gateable surfaces carrying a `navLabelKey` are linked after the base
|
|
195
|
+
* items, because a nav entry needs a translated label. Emitting an entry whose
|
|
196
|
+
* `labelKey` has no translation would trade a dead link for a missing-message
|
|
197
|
+
* error.
|
|
183
198
|
*
|
|
184
|
-
* Deterministic: entries follow GATEABLE_SURFACES order (alphabetical
|
|
185
|
-
* after the
|
|
199
|
+
* Deterministic: gateable entries follow GATEABLE_SURFACES order (alphabetical
|
|
200
|
+
* by id), after the fixed base items.
|
|
186
201
|
*/
|
|
187
202
|
export declare function renderNavConfig(enabled: string[]): string;
|
package/dist/surfaces.js
CHANGED
|
@@ -49,6 +49,12 @@ export const CORE_SURFACE_PATHS = [
|
|
|
49
49
|
"/contact",
|
|
50
50
|
"/faq",
|
|
51
51
|
"/thanks/{slug}",
|
|
52
|
+
// #5469 D2 — the one generic content route: `app/[locale]/[slug]/page.tsx`
|
|
53
|
+
// renders `content/<locale>/LandingPage/<slug>.mdx` and 404s otherwise.
|
|
54
|
+
// Next.js resolves a static segment before a dynamic one, so it never
|
|
55
|
+
// shadows a scaffolded route directory — which is exactly what lets a
|
|
56
|
+
// module's own route (the docs-blog `blog/`) win over a same-named MDX.
|
|
57
|
+
"/{slug}",
|
|
52
58
|
];
|
|
53
59
|
/** Template-relative route directories that are always copied. */
|
|
54
60
|
export const CORE_ROUTE_DIRS = [
|
|
@@ -61,6 +67,11 @@ export const CORE_ROUTE_DIRS = [
|
|
|
61
67
|
// so joining CORE_ROUTE_DIRS only makes the REGISTRY match what already
|
|
62
68
|
// ships — it does not change what a fresh scaffold carries.
|
|
63
69
|
`${ROUTE_ROOT}/sign-in`,
|
|
70
|
+
// #5469 D2 — always copied: it is what serves the Free template's
|
|
71
|
+
// /how-it-works, /features and /blog pages, and it is inert for every slug
|
|
72
|
+
// that has no LandingPage MDX (notFound), so a scaffold carrying it and no
|
|
73
|
+
// content is byte-for-byte a scaffold with three more 404s, nothing else.
|
|
74
|
+
`${ROUTE_ROOT}/[slug]`,
|
|
64
75
|
];
|
|
65
76
|
/**
|
|
66
77
|
* Directory names skipped by `listTemplateRouteDirs`'s walker even though they
|
|
@@ -159,11 +170,40 @@ export const GATEABLE_SURFACES = [
|
|
|
159
170
|
routes: [`${ROUTE_ROOT}/launch-service`],
|
|
160
171
|
content: [],
|
|
161
172
|
},
|
|
173
|
+
{
|
|
174
|
+
// The `scheduling` MODULE's presentation tier — the booking page, the
|
|
175
|
+
// per-booking-type page and the confirmation page the module OWNS
|
|
176
|
+
// (`/meet`, `/meet/:slug`, `/meet/confirmed`, one directory, so one entry
|
|
177
|
+
// gates all three; renamed from the schedule/ directory by #5563).
|
|
178
|
+
// API-backed like `flows`: the booker reads its slots from apps/api's
|
|
179
|
+
// `/v1/schedule/*` routes and the event name from `SCHEDULING_CAL_LINK`,
|
|
180
|
+
// so there is no MDX of its own.
|
|
181
|
+
id: "meet",
|
|
182
|
+
routes: [`${ROUTE_ROOT}/meet`],
|
|
183
|
+
content: [],
|
|
184
|
+
// `navLabelKey` names COPY, not a route — D1 R2 (#5563): renaming it would
|
|
185
|
+
// orphan an operator's customised `messages/<locale>.json` for zero
|
|
186
|
+
// functional gain, since no route derives from a message key.
|
|
187
|
+
navLabelKey: "schedule",
|
|
188
|
+
},
|
|
162
189
|
{
|
|
163
190
|
id: "partners",
|
|
164
191
|
routes: [`${ROUTE_ROOT}/partners`],
|
|
165
192
|
content: ["content/<locale>/PartnersList", "content/<locale>/Partner"],
|
|
166
193
|
},
|
|
194
|
+
{
|
|
195
|
+
// Module-LESS by construction (#5479 D16): no MODULE_SURFACES value names
|
|
196
|
+
// it, so a site template may enable it by roster alone. Same for
|
|
197
|
+
// `privacy` and `terms` below.
|
|
198
|
+
id: "pricing",
|
|
199
|
+
routes: [`${ROUTE_ROOT}/pricing`],
|
|
200
|
+
content: ["content/<locale>/Pricing"],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "privacy",
|
|
204
|
+
routes: [`${ROUTE_ROOT}/privacy`],
|
|
205
|
+
content: ["content/<locale>/PrivacyPage"],
|
|
206
|
+
},
|
|
167
207
|
{
|
|
168
208
|
id: "products",
|
|
169
209
|
routes: [`${ROUTE_ROOT}/products`],
|
|
@@ -182,17 +222,6 @@ export const GATEABLE_SURFACES = [
|
|
|
182
222
|
"content/<locale>/ResourceDetail",
|
|
183
223
|
],
|
|
184
224
|
},
|
|
185
|
-
{
|
|
186
|
-
// The `scheduling` MODULE's presentation tier — the booking page and the
|
|
187
|
-
// confirmation page the module OWNS (`/schedule` + `/schedule/confirmed`,
|
|
188
|
-
// one directory, so one entry gates both). API-backed like `flows`: the
|
|
189
|
-
// booker reads its slots from apps/api's `/v1/schedule/*` routes and the
|
|
190
|
-
// event name from `SCHEDULING_CAL_LINK`, so there is no MDX of its own.
|
|
191
|
-
id: "schedule",
|
|
192
|
-
routes: [`${ROUTE_ROOT}/schedule`],
|
|
193
|
-
content: [],
|
|
194
|
-
navLabelKey: "schedule",
|
|
195
|
-
},
|
|
196
225
|
{
|
|
197
226
|
id: "services",
|
|
198
227
|
routes: [`${ROUTE_ROOT}/services`],
|
|
@@ -208,6 +237,11 @@ export const GATEABLE_SURFACES = [
|
|
|
208
237
|
routes: [`${ROUTE_ROOT}/tech-articles`],
|
|
209
238
|
content: ["content/<locale>/TechArticle"],
|
|
210
239
|
},
|
|
240
|
+
{
|
|
241
|
+
id: "terms",
|
|
242
|
+
routes: [`${ROUTE_ROOT}/terms`],
|
|
243
|
+
content: ["content/<locale>/TermsPage"],
|
|
244
|
+
},
|
|
211
245
|
{
|
|
212
246
|
id: "testimonials",
|
|
213
247
|
routes: [`${ROUTE_ROOT}/testimonials`],
|
|
@@ -226,6 +260,18 @@ export const GATEABLE_SURFACES = [
|
|
|
226
260
|
];
|
|
227
261
|
/** The recognized `--surfaces` vocabulary, sorted. */
|
|
228
262
|
export const RECOGNIZED_SURFACES = GATEABLE_SURFACES.map((s) => s.id);
|
|
263
|
+
/**
|
|
264
|
+
* Retired surface ids, kept for ONE release (#5563). Removal rides the same
|
|
265
|
+
* trigger as the `/schedule` legacyRoutes entries in routes-map.ts — see the
|
|
266
|
+
* comment on that row. Resolving through an alias prints a deprecation notice.
|
|
267
|
+
*/
|
|
268
|
+
export const SURFACE_ALIASES = {
|
|
269
|
+
schedule: "meet",
|
|
270
|
+
};
|
|
271
|
+
/** Canonical id for a `--surfaces` value, resolving a retired alias. */
|
|
272
|
+
function canonicalizeSurface(s) {
|
|
273
|
+
return SURFACE_ALIASES[s] ?? s;
|
|
274
|
+
}
|
|
229
275
|
/**
|
|
230
276
|
* Surfaces each recognized module contributes.
|
|
231
277
|
*
|
|
@@ -247,7 +293,13 @@ export const MODULE_SURFACES = {
|
|
|
247
293
|
marketing: [],
|
|
248
294
|
"docs-blog": ["docs", "blog"],
|
|
249
295
|
"multi-tenant": [],
|
|
250
|
-
|
|
296
|
+
// `workspace` contributes zero gateable surfaces (#5371 D4): every route it
|
|
297
|
+
// owns lives in the `(authed)` segment, never in the public marketing route
|
|
298
|
+
// tree a template's roster composes — the same reason `multi-tenant` above
|
|
299
|
+
// carries an empty list. The entry itself is still required: surface-gating's
|
|
300
|
+
// vocabulary assertion covers exactly the recognized module set.
|
|
301
|
+
workspace: [],
|
|
302
|
+
scheduling: ["meet"],
|
|
251
303
|
"signal-collectors": ["flows"],
|
|
252
304
|
commerce: [],
|
|
253
305
|
};
|
|
@@ -273,7 +325,7 @@ export function parseSurfaces(raw) {
|
|
|
273
325
|
return [];
|
|
274
326
|
const requested = raw
|
|
275
327
|
.split(",")
|
|
276
|
-
.map((s) => s.trim().toLowerCase())
|
|
328
|
+
.map((s) => canonicalizeSurface(s.trim().toLowerCase()))
|
|
277
329
|
.filter(Boolean);
|
|
278
330
|
for (const s of requested) {
|
|
279
331
|
if (!RECOGNIZED_SURFACES.includes(s)) {
|
|
@@ -302,7 +354,8 @@ export function resolveSurfaces(modules = ["marketing"], explicit = []) {
|
|
|
302
354
|
set.add(s);
|
|
303
355
|
}
|
|
304
356
|
const selected = new Set(modules.map((m) => m.trim().toLowerCase()));
|
|
305
|
-
for (const
|
|
357
|
+
for (const raw of explicit) {
|
|
358
|
+
const s = canonicalizeSurface(raw);
|
|
306
359
|
if (!RECOGNIZED_SURFACES.includes(s)) {
|
|
307
360
|
throw new SurfaceError(`unknown surface "${s}". Recognized: ${RECOGNIZED_SURFACES.join(", ")}`);
|
|
308
361
|
}
|
|
@@ -407,8 +460,19 @@ export const ROUTE_SINCE = {
|
|
|
407
460
|
integrations: "0.0.0",
|
|
408
461
|
"integrations/[slug]": "0.0.0",
|
|
409
462
|
"launch-service": "0.0.0",
|
|
463
|
+
meet: "0.0.0",
|
|
464
|
+
// #5563 — the genuinely new row: a per-booking-type page with no
|
|
465
|
+
// predecessor, so it attaches at the create-ailk minor this rename ships
|
|
466
|
+
// in rather than the "predates the manifest" 0.0.0 above.
|
|
467
|
+
"meet/[slug]": "0.5.0",
|
|
468
|
+
"meet/confirmed": "0.0.0",
|
|
410
469
|
partners: "0.0.0",
|
|
411
470
|
"partners/[slug]": "0.0.0",
|
|
471
|
+
// #5479 — three genuinely new route directories, so they attach at today's
|
|
472
|
+
// fixed-group version rather than the "predates the manifest" 0.0.0 above,
|
|
473
|
+
// the same rule #5048 ("projects/apply") and #5014 ("(authed)") applied.
|
|
474
|
+
pricing: "0.22.0",
|
|
475
|
+
privacy: "0.22.0",
|
|
412
476
|
products: "0.0.0",
|
|
413
477
|
"products/[slug]": "0.0.0",
|
|
414
478
|
projects: "0.0.0",
|
|
@@ -420,14 +484,13 @@ export const ROUTE_SINCE = {
|
|
|
420
484
|
"projects/apply": "0.18.0",
|
|
421
485
|
resources: "0.0.0",
|
|
422
486
|
"resources/[slug]": "0.0.0",
|
|
423
|
-
schedule: "0.0.0",
|
|
424
|
-
"schedule/confirmed": "0.0.0",
|
|
425
487
|
services: "0.0.0",
|
|
426
488
|
"services/[slug]": "0.0.0",
|
|
427
489
|
software: "0.0.0",
|
|
428
490
|
"software/[slug]": "0.0.0",
|
|
429
491
|
"tech-articles": "0.0.0",
|
|
430
492
|
"tech-articles/[slug]": "0.0.0",
|
|
493
|
+
terms: "0.22.0",
|
|
431
494
|
testimonials: "0.0.0",
|
|
432
495
|
"thanks/[slug]": "0.14.0",
|
|
433
496
|
"use-cases": "0.0.0",
|
|
@@ -444,10 +507,31 @@ export const ROUTE_SINCE = {
|
|
|
444
507
|
// choice #5048's "projects/apply" entry already made for the same reason.
|
|
445
508
|
"(authed)": "0.18.0",
|
|
446
509
|
"(authed)/account": "0.18.0",
|
|
510
|
+
// The leads inbox (#5678). The ROUTE ships in every scaffold, Free
|
|
511
|
+
// included — lead capture is a never-gate surface, so reading a lead back
|
|
512
|
+
// is one too. Whether it shows a ROW is a separate question with two
|
|
513
|
+
// deployment preconditions (a resolvable workspace, and a `siteId` that
|
|
514
|
+
// equals a workspace id); both fail empty, never wide. See
|
|
515
|
+
// `apps/api/src/services/leads-inbox.ts` § "What it takes for this to
|
|
516
|
+
// return anything".
|
|
517
|
+
"(authed)/leads": "0.24.0",
|
|
518
|
+
// #5371 — the workspace module's three settings routes. Genuinely new route
|
|
519
|
+
// directories, so they attach at today's fixed-group version rather than the
|
|
520
|
+
// "predates the manifest" 0.0.0 above, the same rule #5479 and #5048 applied.
|
|
521
|
+
// Directory renamed apps/web/.../(authed)/workspace -> (authed)/admin by
|
|
522
|
+
// #5637 (Admin-vocabulary rename); the ROUTE_SINCE version is unchanged —
|
|
523
|
+
// it tracks when the route was introduced, not when it was renamed.
|
|
524
|
+
"(authed)/admin": "0.22.1",
|
|
525
|
+
"(authed)/admin/site": "0.22.1",
|
|
526
|
+
"(authed)/admin/team": "0.22.1",
|
|
447
527
|
"(authed)/waitlist": "0.18.0",
|
|
448
528
|
"(authed)/waitlist/[experimentId]": "0.18.0",
|
|
449
529
|
"(authed)/waitlist/[experimentId]/signups/[leadId]": "0.18.0",
|
|
450
530
|
"sign-in": "0.18.0",
|
|
531
|
+
// #5469 D2 — the generic `[slug]` content route. A genuinely new route
|
|
532
|
+
// directory, so it attaches at today's fixed-group version, the same rule
|
|
533
|
+
// #5479, #5371 and #5048 applied.
|
|
534
|
+
"[slug]": "0.23.0",
|
|
451
535
|
};
|
|
452
536
|
/**
|
|
453
537
|
* Surface id of the registry that owns `routeDir` (ROUTE_ROOT-relative): the
|
|
@@ -490,6 +574,10 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
490
574
|
`${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
|
|
491
575
|
`${ROUTE_ROOT}/(authed)/waitlist/format.ts`,
|
|
492
576
|
],
|
|
577
|
+
"(authed)/admin": ["apps/web/lib/server-api.ts"],
|
|
578
|
+
"(authed)/leads": ["apps/web/lib/server-api.ts"],
|
|
579
|
+
"(authed)/admin/site": ["apps/web/lib/server-api.ts"],
|
|
580
|
+
"(authed)/admin/team": ["apps/web/lib/server-api.ts"],
|
|
493
581
|
"(authed)/waitlist": ["apps/web/lib/server-api.ts"],
|
|
494
582
|
};
|
|
495
583
|
/**
|
|
@@ -504,6 +592,10 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
504
592
|
export const ROUTE_MESSAGE_NAMESPACES = {
|
|
505
593
|
"sign-in": ["signIn"],
|
|
506
594
|
"(authed)/account": ["account"],
|
|
595
|
+
"(authed)/admin": ["admin"],
|
|
596
|
+
"(authed)/leads": ["leadsInbox"],
|
|
597
|
+
"(authed)/admin/site": ["admin"],
|
|
598
|
+
"(authed)/admin/team": ["admin"],
|
|
507
599
|
"(authed)/waitlist": ["waitlistDashboard.gate"],
|
|
508
600
|
};
|
|
509
601
|
/** Template-relative path of the generated site-nav registry. */
|
|
@@ -517,19 +609,43 @@ export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
|
|
|
517
609
|
* exists: two dead links, sitewide, by default. So the nav follows the declared
|
|
518
610
|
* surface set for the same reason the route tree does.
|
|
519
611
|
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
612
|
+
* The base items are the Free marketing-site chrome (#5469 D5, the approved
|
|
613
|
+
* chrome diff recorded on that card):
|
|
614
|
+
* a Product flyout (How it works · Features · FAQ), then How it works, Pricing
|
|
615
|
+
* and About. Every one of those pages is in EVERY scaffold — the four core
|
|
616
|
+
* routes plus the `[slug]` route over the overlay's LandingPage MDX — except
|
|
617
|
+
* Pricing, which is a gateable surface: its item is emitted only when the
|
|
618
|
+
* `pricing` surface is enabled (a template roster enables it, #5469 D10), so
|
|
619
|
+
* the nav still never links a page the scaffold does not carry.
|
|
620
|
+
*
|
|
621
|
+
* Only gateable surfaces carrying a `navLabelKey` are linked after the base
|
|
622
|
+
* items, because a nav entry needs a translated label. Emitting an entry whose
|
|
623
|
+
* `labelKey` has no translation would trade a dead link for a missing-message
|
|
624
|
+
* error.
|
|
524
625
|
*
|
|
525
|
-
* Deterministic: entries follow GATEABLE_SURFACES order (alphabetical
|
|
526
|
-
* after the
|
|
626
|
+
* Deterministic: gateable entries follow GATEABLE_SURFACES order (alphabetical
|
|
627
|
+
* by id), after the fixed base items.
|
|
527
628
|
*/
|
|
528
629
|
export function renderNavConfig(enabled) {
|
|
529
630
|
const on = new Set(enabled);
|
|
530
631
|
const linked = GATEABLE_SURFACES.filter((s) => s.navLabelKey !== undefined && on.has(s.id));
|
|
531
632
|
const entries = [
|
|
532
|
-
|
|
633
|
+
" {",
|
|
634
|
+
' labelKey: "product",',
|
|
635
|
+
" flyout: {",
|
|
636
|
+
' presentation: "list",',
|
|
637
|
+
' width: "trigger",',
|
|
638
|
+
" features: [",
|
|
639
|
+
' { labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },',
|
|
640
|
+
' { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },',
|
|
641
|
+
' { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },',
|
|
642
|
+
" ],",
|
|
643
|
+
' actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],',
|
|
644
|
+
" },",
|
|
645
|
+
" },",
|
|
646
|
+
' { labelKey: "howItWorks", path: "/how-it-works" },',
|
|
647
|
+
...(on.has("pricing") ? [' { labelKey: "pricing", path: "/pricing" },'] : []),
|
|
648
|
+
' { labelKey: "about", path: "/about" },',
|
|
533
649
|
...linked.map((s) => ` { labelKey: "${s.navLabelKey}", path: "/${s.id}" },`),
|
|
534
650
|
];
|
|
535
651
|
// The emitted file imports the consumer's `NavItem` type from the UI package.
|
|
@@ -554,6 +670,7 @@ export function renderNavConfig(enabled) {
|
|
|
554
670
|
// \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
|
|
555
671
|
// them with the active locale at render.
|
|
556
672
|
${navItemImport}
|
|
673
|
+
import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
|
|
557
674
|
|
|
558
675
|
export const navItems: NavItem[] = [
|
|
559
676
|
${entries.join("\n")}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { type PageRosterRow } from "./module-architecture.js";
|
|
2
|
+
import type { ModuleName } from "./parse-args.js";
|
|
3
|
+
import type { TemplateLayout } from "./site-chrome.js";
|
|
4
|
+
/**
|
|
5
|
+
* One site template: its fixed module list and the page rows it brings itself.
|
|
6
|
+
*
|
|
7
|
+
* THERE IS NO `tier`, `level` OR `price` MEMBER, and there never is one
|
|
8
|
+
* (parent § D4). Delivery tier is DERIVED from `requires` — by the client via
|
|
9
|
+
* `templateDelivery` below, and by the issuer via `TIER_SPECS` (#5480) — so
|
|
10
|
+
* declaring it here would be a second authority for a fact the module list
|
|
11
|
+
* already settles. The type, not a convention, is what makes adding one a
|
|
12
|
+
* compile error: a convention is a thing someone must remember.
|
|
13
|
+
*
|
|
14
|
+
* There is likewise no optional-module member (parent § D12). A template's
|
|
15
|
+
* level must be readable from ONE fixed list; a soft list would make the same
|
|
16
|
+
* template Free or Pro depending on the key.
|
|
17
|
+
*/
|
|
18
|
+
export interface TemplateManifest {
|
|
19
|
+
/** Template name. Equals this manifest's `TEMPLATE_MANIFESTS` key. */
|
|
20
|
+
name: string;
|
|
21
|
+
/**
|
|
22
|
+
* The modules whose PAGES this template's roster uses (parent § D15) — not
|
|
23
|
+
* the modules its level bundles. Typed against `ModuleName`, so an
|
|
24
|
+
* unrecognized name fails type-check rather than a runtime validation.
|
|
25
|
+
*/
|
|
26
|
+
requires: readonly ModuleName[];
|
|
27
|
+
/**
|
|
28
|
+
* The template this one EXTENDS — its lineage, never its level (#5618 D2).
|
|
29
|
+
* A single parent, not a list: "later layers win file by file" needs a
|
|
30
|
+
* total order, and a list would make the winner depend on the list's order
|
|
31
|
+
* (the failure `normalizeModules` exists to prevent for modules). The
|
|
32
|
+
* chain terminates at `BASE_LAYER_DIR`, the one layer every template
|
|
33
|
+
* reaches; `resolveTemplateLineage` walks and validates it. The delivery
|
|
34
|
+
* level stays derived from `requires` — `saas-product` extends `_base`, not
|
|
35
|
+
* `marketing-site`, precisely because "Pro plus multi-tenant" is a level
|
|
36
|
+
* fact `requires` already states.
|
|
37
|
+
*/
|
|
38
|
+
extends: string;
|
|
39
|
+
/** Base rows, before any module's contribution is merged. */
|
|
40
|
+
roster: readonly PageRosterRow[];
|
|
41
|
+
/** Bundle-relative source → scaffold-relative destination. */
|
|
42
|
+
installMap?: Record<string, string>;
|
|
43
|
+
/**
|
|
44
|
+
* Site-level chrome knobs (#5612 D1) — nav/footer/banner/home. Every field
|
|
45
|
+
* optional; a template declaring none (all four settled entries below)
|
|
46
|
+
* renders byte-identically to before this field existed. See
|
|
47
|
+
* `site-chrome.ts` for the vocabulary, defaults and the generated
|
|
48
|
+
* `apps/web/lib/site-chrome.ts` this resolves into.
|
|
49
|
+
*/
|
|
50
|
+
layout?: TemplateLayout;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The base layer's directory under `site-templates/` (#5618 D1) — the seed
|
|
54
|
+
* files every template reaches by declaring `extends`, installed by
|
|
55
|
+
* `installBundleTree` before any module bundle and only when `--template`
|
|
56
|
+
* is named, so a scaffold naming no template is byte-identical to before it
|
|
57
|
+
* existed. It is NOT a template: it has no `TEMPLATE_MANIFESTS` entry (so
|
|
58
|
+
* `--template _base` stays unrecognized — `RECOGNIZED_TEMPLATES` is exactly
|
|
59
|
+
* that object's key set) and is never published as a registry bundle. The
|
|
60
|
+
* leading underscore is what `build-module-bundles.mjs`'s template loop and
|
|
61
|
+
* the directory-scanning tests key their skip on.
|
|
62
|
+
*/
|
|
63
|
+
export declare const BASE_LAYER_DIR = "_base";
|
|
64
|
+
/** Thrown on an undeclared template name or a route claimed twice. Mirrors `SurfaceError`'s shape. */
|
|
65
|
+
export declare class TemplateError extends Error {
|
|
66
|
+
constructor(message: string);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The three settled templates and their fixed `requires` lists — settled by
|
|
70
|
+
* Sam, 2026-09-14 (parent § D12). The plain name is the Pro one: it is the
|
|
71
|
+
* flagship and the demo.
|
|
72
|
+
*
|
|
73
|
+
* `workspace` appears in none of them. It is a Pro module, but an authed app
|
|
74
|
+
* surface contributing no PUBLIC page to a marketing site, and `requires`
|
|
75
|
+
* names what the roster uses (parent § D15). A Pro key unlocks it regardless
|
|
76
|
+
* of any template. `ModuleName` could express it either way — `workspace` is
|
|
77
|
+
* one of the seven `RECOGNIZED_MODULES` names; it is simply not one any
|
|
78
|
+
* template's fixed `requires` list names.
|
|
79
|
+
*
|
|
80
|
+
* `commerce` is likewise in none of them: it is a per-site add-on available at
|
|
81
|
+
* any level, so a template for it would contradict the offering model and
|
|
82
|
+
* multiply the set combinatorially (parent § D13). It still contributes rows
|
|
83
|
+
* to whichever template takes it, through `mergeRoster` below.
|
|
84
|
+
*
|
|
85
|
+
* Frozen (#5507): this registry is also the issuer's authorization allowlist
|
|
86
|
+
* (spec 5480 § C7) — a mutable global there could be monkey-patched at
|
|
87
|
+
* runtime. A caller needing a different registry for a test injects its own
|
|
88
|
+
* fixture rather than mutating this one.
|
|
89
|
+
*/
|
|
90
|
+
export declare const TEMPLATE_MANIFESTS: Readonly<Record<string, TemplateManifest>>;
|
|
91
|
+
/** Look up a declared template. @throws TemplateError on an undeclared name. */
|
|
92
|
+
export declare function resolveTemplate(name: string): TemplateManifest;
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a template's `extends` chain — `[name, …ancestors, "_base"]` — in
|
|
95
|
+
* the client registry, before any fetch (#5618 D2). `_base` is the only
|
|
96
|
+
* valid terminus: an `extends` naming neither it nor a declared template is
|
|
97
|
+
* refused naming the field, the value and the recognized set, in
|
|
98
|
+
* `resolveTemplate`'s message shape; a cycle is refused naming the whole
|
|
99
|
+
* chain. The registry is a parameter for the same reason `TEMPLATE_MANIFESTS`
|
|
100
|
+
* is frozen (#5507): a test needing a cyclic or dangling fixture injects its
|
|
101
|
+
* own rather than mutating the shipped one.
|
|
102
|
+
*
|
|
103
|
+
* @throws TemplateError on an undeclared name, an unknown parent or a cycle.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveTemplateLineage(name: string, registry?: Readonly<Record<string, TemplateManifest>>): string[];
|
|
106
|
+
/**
|
|
107
|
+
* Compose a template's roster: its base rows, then each taken module's
|
|
108
|
+
* contributed rows in `normalizeModules` order (parent § D7).
|
|
109
|
+
*
|
|
110
|
+
* `normalizeModules` is REUSED rather than re-sorted here. It already returns
|
|
111
|
+
* the unique set ordered by `RECOGNIZED_MODULES`, and `scaffold()` already
|
|
112
|
+
* calls it — a second sort at this call site would be a second ordering
|
|
113
|
+
* authority that could drift from the one `deriveArchitecture` depends on.
|
|
114
|
+
* That also makes the merge a pure function of the module SET: the same set in
|
|
115
|
+
* any argument order returns identical rows.
|
|
116
|
+
*
|
|
117
|
+
* The merge is pure and writes nothing. Staging a contributed row's seed
|
|
118
|
+
* content is the bundle producer's job (`build-module-bundles.mjs`, #5481);
|
|
119
|
+
* doing it here would put a filesystem dependency in a function three sibling
|
|
120
|
+
* units import for its return value.
|
|
121
|
+
*
|
|
122
|
+
* @param modules the modules this scaffold takes. A template's `requires` are
|
|
123
|
+
* taken by definition — a required module whose bundle cannot be had refuses
|
|
124
|
+
* the install outright (parent § D17, #5481) rather than merging short.
|
|
125
|
+
* @throws TemplateError when two sources claim one route (parent § D8). Both
|
|
126
|
+
* the base roster and every contribution are compile-time known, so the
|
|
127
|
+
* collision is caught by a test before a bundle exists — never resolved at
|
|
128
|
+
* runtime by a base-wins or module-wins precedence rule, either of which
|
|
129
|
+
* silently produces a site missing a page someone declared.
|
|
130
|
+
*/
|
|
131
|
+
export declare function mergeRoster(manifest: TemplateManifest, modules: readonly string[]): PageRosterRow[];
|
|
132
|
+
/**
|
|
133
|
+
* Whether a template installs on the keyless path or needs a verified
|
|
134
|
+
* `AILK_LICENSE_KEY` — derived from `requires`, never declared (parent § D3).
|
|
135
|
+
*
|
|
136
|
+
* The derivation goes through the existing `classifyModules` rather than a
|
|
137
|
+
* second list of paid names, which would drift. This is deliberately the
|
|
138
|
+
* CLIENT half of a two-derivation split: the issuer derives the commercial
|
|
139
|
+
* LEVEL from the same `requires` via `TIER_SPECS` (#5480). One shared
|
|
140
|
+
* derivation would force this package to import
|
|
141
|
+
* `@working-theory/license-manifest` — a `restricted` package — breaking
|
|
142
|
+
* `pnpm install` for every Free consumer, not merely gating the paid path.
|
|
143
|
+
*/
|
|
144
|
+
export declare function templateDelivery(manifest: TemplateManifest): "free" | "paid";
|