create-ailk 0.4.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 +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- 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.js +4 -0
- package/dist/module-architecture.d.ts +59 -8
- package/dist/module-architecture.js +334 -115
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- 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 +107 -32
- package/dist/template-manifest.d.ts +55 -3
- package/dist/template-manifest.js +181 -12
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +9 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -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 +2 -1
- 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 +8 -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 +82 -6
- 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/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +7 -15
- 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 +1 -1
- 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 +29 -2
- 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 +7 -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/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
package/dist/parse-args.js
CHANGED
|
@@ -64,8 +64,12 @@ Options:
|
|
|
64
64
|
AILK_LICENSE_KEY to cover every module it requires,
|
|
65
65
|
and refuses naming whichever it does not.
|
|
66
66
|
--surfaces <csv> Comma-separated route surfaces to scaffold IN
|
|
67
|
-
ADDITION to the core set (/, /about, /contact, /faq
|
|
68
|
-
/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(", ")}.
|
|
69
73
|
Surfaces the declared architecture does not carry are
|
|
70
74
|
NOT scaffolded — their routes are absent, so a fresh
|
|
71
75
|
site never serves a stock page with placeholder
|
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. */
|
|
@@ -89,6 +90,15 @@ export interface ScaffoldOptions {
|
|
|
89
90
|
templateRegistryUrl?: string;
|
|
90
91
|
/** Injected fetch for tests. Never a live network call in CI or in this repo's evidence runs. */
|
|
91
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;
|
|
92
102
|
}
|
|
93
103
|
export interface ScaffoldResult {
|
|
94
104
|
/** Resolved absolute path to the scaffolded directory. */
|
package/dist/programmatic.js
CHANGED
|
@@ -26,9 +26,10 @@ import { fetchTemplate } from "./lib/fetch-template.js";
|
|
|
26
26
|
import { resolvePaidModules } from "./lib/module-license-gate.js";
|
|
27
27
|
import { classifyModules } from "./lib/module-tier.js";
|
|
28
28
|
import { normalizeModules } from "./module-architecture.js";
|
|
29
|
-
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";
|
|
30
31
|
import { syncRoutes } from "./sync-routes.js";
|
|
31
|
-
import { resolveTemplate, templateDelivery } from "./template-manifest.js";
|
|
32
|
+
import { BASE_LAYER_DIR, mergeRoster, resolveTemplate, resolveTemplateLineage, templateDelivery, } from "./template-manifest.js";
|
|
32
33
|
import { validateTarget } from "./validate.js";
|
|
33
34
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
35
|
const TEMPLATES_DIR = resolve(PKG_ROOT, "templates");
|
|
@@ -95,6 +96,10 @@ export function scaffoldSync(opts) {
|
|
|
95
96
|
PROJECT_NAME: projectName,
|
|
96
97
|
COMPANY_NAME: "Your Company",
|
|
97
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",
|
|
98
103
|
},
|
|
99
104
|
});
|
|
100
105
|
// Derive + write the module-set-driven architecture.yaml (spec #3787). This
|
|
@@ -108,6 +113,12 @@ export function scaffoldSync(opts) {
|
|
|
108
113
|
// the template's copy in place would link /docs and /blog sitewide in a
|
|
109
114
|
// scaffold that carries neither route.
|
|
110
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)));
|
|
111
122
|
// Opt-in only (#4636). Every write above this line is unconditional, so a
|
|
112
123
|
// scaffold with `withCatalog` unset touches exactly the files it did before.
|
|
113
124
|
if (opts.withCatalog === true)
|
|
@@ -147,6 +158,11 @@ export async function scaffold(opts) {
|
|
|
147
158
|
// name already passed `parse-args.ts`, and `resolveTemplate` throws a
|
|
148
159
|
// `TemplateError` for a programmatic caller that skipped the parser.
|
|
149
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);
|
|
150
166
|
// The effective module set: union(template.requires, parsed --modules),
|
|
151
167
|
// normalized through `normalizeModules` — the single ordering authority
|
|
152
168
|
// (parent record D7). Order-independence falls out rather than being
|
|
@@ -195,18 +211,72 @@ export async function scaffold(opts) {
|
|
|
195
211
|
// would gate correctly and emit the wrong surfaces, the wrong
|
|
196
212
|
// `architecture.yaml` and the wrong nav — a silent wrong result, not a
|
|
197
213
|
// failure.
|
|
198
|
-
|
|
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
|
+
});
|
|
199
239
|
// Threaded to extractModuleBundle for `package.json` patch targets'
|
|
200
240
|
// `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
|
|
201
241
|
// `scaffoldSync` already reads for `copyTemplate`, re-read here since
|
|
202
242
|
// `scaffold()` doesn't share `scaffoldSync`'s call frame.
|
|
203
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
|
+
}
|
|
204
273
|
for (const [moduleName, bytes] of bundles) {
|
|
205
274
|
await extractModuleBundle({
|
|
206
275
|
bytes,
|
|
207
276
|
targetDir: result.targetDir,
|
|
208
277
|
moduleName,
|
|
209
278
|
versions,
|
|
279
|
+
ownership,
|
|
210
280
|
});
|
|
211
281
|
}
|
|
212
282
|
// The template lands LAST — after `scaffoldSync` and after every module
|
|
@@ -220,31 +290,76 @@ export async function scaffold(opts) {
|
|
|
220
290
|
// One install step, two source roots (DD7): an extracted staging dir on the
|
|
221
291
|
// paid path, `site-templates/<name>/` on disk on the Free one.
|
|
222
292
|
if (template !== undefined) {
|
|
223
|
-
|
|
224
|
-
|
|
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({
|
|
225
302
|
bytes: templateBundle,
|
|
226
303
|
targetDir: result.targetDir,
|
|
227
304
|
moduleName: template.name,
|
|
228
305
|
templateRequires: template.requires,
|
|
306
|
+
templateExtends: template.extends,
|
|
229
307
|
versions,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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)));
|
|
244
327
|
}
|
|
245
328
|
}
|
|
246
329
|
return result;
|
|
247
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
|
+
}
|
|
248
363
|
// Re-exported so a consumer of the async `scaffold()` above can type its own
|
|
249
364
|
// wrapper (e.g. the MCP scaffold-site tool) without reaching into `lib/`.
|
|
250
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;
|
|
@@ -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;
|