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/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,6 +170,22 @@ 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`],
|
|
@@ -195,17 +222,6 @@ export const GATEABLE_SURFACES = [
|
|
|
195
222
|
"content/<locale>/ResourceDetail",
|
|
196
223
|
],
|
|
197
224
|
},
|
|
198
|
-
{
|
|
199
|
-
// The `scheduling` MODULE's presentation tier — the booking page and the
|
|
200
|
-
// confirmation page the module OWNS (`/schedule` + `/schedule/confirmed`,
|
|
201
|
-
// one directory, so one entry gates both). API-backed like `flows`: the
|
|
202
|
-
// booker reads its slots from apps/api's `/v1/schedule/*` routes and the
|
|
203
|
-
// event name from `SCHEDULING_CAL_LINK`, so there is no MDX of its own.
|
|
204
|
-
id: "schedule",
|
|
205
|
-
routes: [`${ROUTE_ROOT}/schedule`],
|
|
206
|
-
content: [],
|
|
207
|
-
navLabelKey: "schedule",
|
|
208
|
-
},
|
|
209
225
|
{
|
|
210
226
|
id: "services",
|
|
211
227
|
routes: [`${ROUTE_ROOT}/services`],
|
|
@@ -244,6 +260,18 @@ export const GATEABLE_SURFACES = [
|
|
|
244
260
|
];
|
|
245
261
|
/** The recognized `--surfaces` vocabulary, sorted. */
|
|
246
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
|
+
}
|
|
247
275
|
/**
|
|
248
276
|
* Surfaces each recognized module contributes.
|
|
249
277
|
*
|
|
@@ -271,7 +299,7 @@ export const MODULE_SURFACES = {
|
|
|
271
299
|
// carries an empty list. The entry itself is still required: surface-gating's
|
|
272
300
|
// vocabulary assertion covers exactly the recognized module set.
|
|
273
301
|
workspace: [],
|
|
274
|
-
scheduling: ["
|
|
302
|
+
scheduling: ["meet"],
|
|
275
303
|
"signal-collectors": ["flows"],
|
|
276
304
|
commerce: [],
|
|
277
305
|
};
|
|
@@ -297,7 +325,7 @@ export function parseSurfaces(raw) {
|
|
|
297
325
|
return [];
|
|
298
326
|
const requested = raw
|
|
299
327
|
.split(",")
|
|
300
|
-
.map((s) => s.trim().toLowerCase())
|
|
328
|
+
.map((s) => canonicalizeSurface(s.trim().toLowerCase()))
|
|
301
329
|
.filter(Boolean);
|
|
302
330
|
for (const s of requested) {
|
|
303
331
|
if (!RECOGNIZED_SURFACES.includes(s)) {
|
|
@@ -326,7 +354,8 @@ export function resolveSurfaces(modules = ["marketing"], explicit = []) {
|
|
|
326
354
|
set.add(s);
|
|
327
355
|
}
|
|
328
356
|
const selected = new Set(modules.map((m) => m.trim().toLowerCase()));
|
|
329
|
-
for (const
|
|
357
|
+
for (const raw of explicit) {
|
|
358
|
+
const s = canonicalizeSurface(raw);
|
|
330
359
|
if (!RECOGNIZED_SURFACES.includes(s)) {
|
|
331
360
|
throw new SurfaceError(`unknown surface "${s}". Recognized: ${RECOGNIZED_SURFACES.join(", ")}`);
|
|
332
361
|
}
|
|
@@ -431,6 +460,12 @@ export const ROUTE_SINCE = {
|
|
|
431
460
|
integrations: "0.0.0",
|
|
432
461
|
"integrations/[slug]": "0.0.0",
|
|
433
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",
|
|
434
469
|
partners: "0.0.0",
|
|
435
470
|
"partners/[slug]": "0.0.0",
|
|
436
471
|
// #5479 — three genuinely new route directories, so they attach at today's
|
|
@@ -449,8 +484,6 @@ export const ROUTE_SINCE = {
|
|
|
449
484
|
"projects/apply": "0.18.0",
|
|
450
485
|
resources: "0.0.0",
|
|
451
486
|
"resources/[slug]": "0.0.0",
|
|
452
|
-
schedule: "0.0.0",
|
|
453
|
-
"schedule/confirmed": "0.0.0",
|
|
454
487
|
services: "0.0.0",
|
|
455
488
|
"services/[slug]": "0.0.0",
|
|
456
489
|
software: "0.0.0",
|
|
@@ -474,16 +507,31 @@ export const ROUTE_SINCE = {
|
|
|
474
507
|
// choice #5048's "projects/apply" entry already made for the same reason.
|
|
475
508
|
"(authed)": "0.18.0",
|
|
476
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",
|
|
477
518
|
// #5371 — the workspace module's three settings routes. Genuinely new route
|
|
478
519
|
// directories, so they attach at today's fixed-group version rather than the
|
|
479
520
|
// "predates the manifest" 0.0.0 above, the same rule #5479 and #5048 applied.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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",
|
|
483
527
|
"(authed)/waitlist": "0.18.0",
|
|
484
528
|
"(authed)/waitlist/[experimentId]": "0.18.0",
|
|
485
529
|
"(authed)/waitlist/[experimentId]/signups/[leadId]": "0.18.0",
|
|
486
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",
|
|
487
535
|
};
|
|
488
536
|
/**
|
|
489
537
|
* Surface id of the registry that owns `routeDir` (ROUTE_ROOT-relative): the
|
|
@@ -526,9 +574,10 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
526
574
|
`${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
|
|
527
575
|
`${ROUTE_ROOT}/(authed)/waitlist/format.ts`,
|
|
528
576
|
],
|
|
529
|
-
"(authed)/
|
|
530
|
-
"(authed)/
|
|
531
|
-
"(authed)/
|
|
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"],
|
|
532
581
|
"(authed)/waitlist": ["apps/web/lib/server-api.ts"],
|
|
533
582
|
};
|
|
534
583
|
/**
|
|
@@ -543,9 +592,10 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
543
592
|
export const ROUTE_MESSAGE_NAMESPACES = {
|
|
544
593
|
"sign-in": ["signIn"],
|
|
545
594
|
"(authed)/account": ["account"],
|
|
546
|
-
"(authed)/
|
|
547
|
-
"(authed)/
|
|
548
|
-
"(authed)/
|
|
595
|
+
"(authed)/admin": ["admin"],
|
|
596
|
+
"(authed)/leads": ["leadsInbox"],
|
|
597
|
+
"(authed)/admin/site": ["admin"],
|
|
598
|
+
"(authed)/admin/team": ["admin"],
|
|
549
599
|
"(authed)/waitlist": ["waitlistDashboard.gate"],
|
|
550
600
|
};
|
|
551
601
|
/** Template-relative path of the generated site-nav registry. */
|
|
@@ -559,19 +609,43 @@ export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
|
|
|
559
609
|
* exists: two dead links, sitewide, by default. So the nav follows the declared
|
|
560
610
|
* surface set for the same reason the route tree does.
|
|
561
611
|
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
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.
|
|
566
625
|
*
|
|
567
|
-
* Deterministic: entries follow GATEABLE_SURFACES order (alphabetical
|
|
568
|
-
* after the
|
|
626
|
+
* Deterministic: gateable entries follow GATEABLE_SURFACES order (alphabetical
|
|
627
|
+
* by id), after the fixed base items.
|
|
569
628
|
*/
|
|
570
629
|
export function renderNavConfig(enabled) {
|
|
571
630
|
const on = new Set(enabled);
|
|
572
631
|
const linked = GATEABLE_SURFACES.filter((s) => s.navLabelKey !== undefined && on.has(s.id));
|
|
573
632
|
const entries = [
|
|
574
|
-
|
|
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" },',
|
|
575
649
|
...linked.map((s) => ` { labelKey: "${s.navLabelKey}", path: "/${s.id}" },`),
|
|
576
650
|
];
|
|
577
651
|
// The emitted file imports the consumer's `NavItem` type from the UI package.
|
|
@@ -596,6 +670,7 @@ export function renderNavConfig(enabled) {
|
|
|
596
670
|
// \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
|
|
597
671
|
// them with the active locale at render.
|
|
598
672
|
${navItemImport}
|
|
673
|
+
import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
|
|
599
674
|
|
|
600
675
|
export const navItems: NavItem[] = [
|
|
601
676
|
${entries.join("\n")}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PageRosterRow } from "./module-architecture.js";
|
|
2
2
|
import type { ModuleName } from "./parse-args.js";
|
|
3
|
+
import type { TemplateLayout } from "./site-chrome.js";
|
|
3
4
|
/**
|
|
4
5
|
* One site template: its fixed module list and the page rows it brings itself.
|
|
5
6
|
*
|
|
@@ -23,11 +24,43 @@ export interface TemplateManifest {
|
|
|
23
24
|
* unrecognized name fails type-check rather than a runtime validation.
|
|
24
25
|
*/
|
|
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;
|
|
26
39
|
/** Base rows, before any module's contribution is merged. */
|
|
27
40
|
roster: readonly PageRosterRow[];
|
|
28
41
|
/** Bundle-relative source → scaffold-relative destination. */
|
|
29
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;
|
|
30
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";
|
|
31
64
|
/** Thrown on an undeclared template name or a route claimed twice. Mirrors `SurfaceError`'s shape. */
|
|
32
65
|
export declare class TemplateError extends Error {
|
|
33
66
|
constructor(message: string);
|
|
@@ -40,17 +73,36 @@ export declare class TemplateError extends Error {
|
|
|
40
73
|
* `workspace` appears in none of them. It is a Pro module, but an authed app
|
|
41
74
|
* surface contributing no PUBLIC page to a marketing site, and `requires`
|
|
42
75
|
* names what the roster uses (parent § D15). A Pro key unlocks it regardless
|
|
43
|
-
* of any template. `ModuleName` could
|
|
44
|
-
*
|
|
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.
|
|
45
79
|
*
|
|
46
80
|
* `commerce` is likewise in none of them: it is a per-site add-on available at
|
|
47
81
|
* any level, so a template for it would contradict the offering model and
|
|
48
82
|
* multiply the set combinatorially (parent § D13). It still contributes rows
|
|
49
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.
|
|
50
89
|
*/
|
|
51
|
-
export declare const TEMPLATE_MANIFESTS: Record<string, TemplateManifest
|
|
90
|
+
export declare const TEMPLATE_MANIFESTS: Readonly<Record<string, TemplateManifest>>;
|
|
52
91
|
/** Look up a declared template. @throws TemplateError on an undeclared name. */
|
|
53
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[];
|
|
54
106
|
/**
|
|
55
107
|
* Compose a template's roster: its base rows, then each taken module's
|
|
56
108
|
* contributed rows in `normalizeModules` order (parent § D7).
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { classifyModules } from "./lib/module-tier.js";
|
|
18
18
|
import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
|
|
19
|
+
/**
|
|
20
|
+
* The base layer's directory under `site-templates/` (#5618 D1) — the seed
|
|
21
|
+
* files every template reaches by declaring `extends`, installed by
|
|
22
|
+
* `installBundleTree` before any module bundle and only when `--template`
|
|
23
|
+
* is named, so a scaffold naming no template is byte-identical to before it
|
|
24
|
+
* existed. It is NOT a template: it has no `TEMPLATE_MANIFESTS` entry (so
|
|
25
|
+
* `--template _base` stays unrecognized — `RECOGNIZED_TEMPLATES` is exactly
|
|
26
|
+
* that object's key set) and is never published as a registry bundle. The
|
|
27
|
+
* leading underscore is what `build-module-bundles.mjs`'s template loop and
|
|
28
|
+
* the directory-scanning tests key their skip on.
|
|
29
|
+
*/
|
|
30
|
+
export const BASE_LAYER_DIR = "_base";
|
|
19
31
|
/** Thrown on an undeclared template name or a route claimed twice. Mirrors `SurfaceError`'s shape. */
|
|
20
32
|
export class TemplateError extends Error {
|
|
21
33
|
constructor(message) {
|
|
@@ -24,17 +36,92 @@ export class TemplateError extends Error {
|
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
39
|
+
* The ten rows every declared template brings itself — one declared page set,
|
|
40
|
+
* whichever mechanism scaffolds each (the Free manifest § 3, #5469 D4).
|
|
41
|
+
*
|
|
42
|
+
* Two mechanisms, one roster:
|
|
43
|
+
*
|
|
44
|
+
* - `/`, `/how-it-works`, `/features`, `/about`, `/faq`, `/contact` and
|
|
45
|
+
* `/sign-in` are in EVERY scaffold, template or not: their content is the
|
|
46
|
+
* always-copied overlay (`template-overlay/content/en/**`, copied whole by
|
|
47
|
+
* `build-templates.mjs`) and their routes are core (`CORE_ROUTE_DIRS` —
|
|
48
|
+
* the three LandingPage pages render through the generic `[slug]` route,
|
|
49
|
+
* #5469 D2). They are declared here anyway, so the manifest a template
|
|
50
|
+
* publishes (`build-template-manifest.mjs`) lists the whole page set and
|
|
51
|
+
* the roster is what a template IS, not merely what it adds.
|
|
52
|
+
* - `/pricing`, `/privacy` and `/terms` are module-less by construction
|
|
53
|
+
* (parent § D16): no module owns these surfaces, their route files live in
|
|
54
|
+
* `apps/web` and are snapshotted into the starter like every other route,
|
|
55
|
+
* and a template enables them by NAMING them in its roster — `scaffold()`
|
|
56
|
+
* selects their `GATEABLE_SURFACES` entries from the merged roster's
|
|
57
|
+
* routes (#5469 D10). Their seed content ships ONCE, in the base layer
|
|
58
|
+
* (`site-templates/_base/content/**`, #5618 D1), which every template
|
|
59
|
+
* reaches by declaring `extends` — installed before the module bundles
|
|
60
|
+
* and the template's own tree, so a template may override a base page
|
|
61
|
+
* by shipping the same path.
|
|
29
62
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
63
|
+
* `/sign-in` carries no `content`: it is a code route (the SignInCard island),
|
|
64
|
+
* like the API-backed rows the scheduling module contributes.
|
|
65
|
+
*
|
|
66
|
+
* `/blog` is deliberately NOT here (#5469 D3): the docs-blog module
|
|
67
|
+
* contributes its own `/blog` row, so a base row would make `mergeRoster`
|
|
68
|
+
* refuse every template that takes that module. The Free teaser row is
|
|
69
|
+
* declared on `marketing-site-free` alone, below.
|
|
35
70
|
*/
|
|
36
71
|
const TEMPLATE_BASE_ROSTER = [
|
|
72
|
+
{ route: "/", content: ["content/en/HomePage/home.mdx"] },
|
|
73
|
+
{
|
|
74
|
+
route: "/how-it-works",
|
|
75
|
+
content: ["content/en/LandingPage/how-it-works.mdx"],
|
|
76
|
+
},
|
|
77
|
+
{ route: "/features", content: ["content/en/LandingPage/features.mdx"] },
|
|
37
78
|
{ route: "/pricing", content: ["content/en/Pricing/pricing.mdx"] },
|
|
79
|
+
{ route: "/about", content: ["content/en/AboutPage/about.mdx"] },
|
|
80
|
+
{ route: "/faq", content: ["content/en/FAQPage/faq.mdx"] },
|
|
81
|
+
{ route: "/contact", content: ["content/en/ContactPage/contact.mdx"] },
|
|
82
|
+
{ route: "/sign-in" },
|
|
83
|
+
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
84
|
+
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* The Free `/blog` teaser (#5469 D3) — one static LandingPage (`schema: Blog`)
|
|
88
|
+
* that says what the Pro docs-blog module gives you. It is served by the
|
|
89
|
+
* generic `[slug]` route over `content/en/LandingPage/blog.mdx`, which ships
|
|
90
|
+
* in the always-copied overlay. `/blog` degrades by DIRECTORY PRECEDENCE, not
|
|
91
|
+
* by a module gate: in a keyed docs-blog scaffold the module's own
|
|
92
|
+
* `blog/[[...slug]]/page.tsx` directory exists and Next.js resolves the static
|
|
93
|
+
* segment first, so the teaser file is inert; keyless, no `blog/` directory
|
|
94
|
+
* exists and the teaser renders. Declared on the Free template only — on
|
|
95
|
+
* `marketing-site` the docs-blog contribution claims the route.
|
|
96
|
+
*/
|
|
97
|
+
const FREE_BLOG_TEASER_ROW = {
|
|
98
|
+
route: "/blog",
|
|
99
|
+
content: ["content/en/LandingPage/blog.mdx"],
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* `minimalism-site`'s own rows (#5589) — the one declared template that does
|
|
103
|
+
* not reuse `TEMPLATE_BASE_ROSTER`. Its page set is the issue's ROSTER, and
|
|
104
|
+
* the rows it does NOT carry are as deliberate as the ones it does: no
|
|
105
|
+
* `/how-it-works`, `/features` or `/pricing` (a reports-first site sells
|
|
106
|
+
* nothing from a pricing table), and no `/sign-in` (a code route every
|
|
107
|
+
* scaffold carries regardless — `CORE_ROUTE_DIRS` — which this template
|
|
108
|
+
* simply does not advertise as one of its pages).
|
|
109
|
+
*
|
|
110
|
+
* `/meet`, `/meet/[slug]`, `/meet/confirmed`, `/reports/[slug]` and the three
|
|
111
|
+
* `/flows/*` rows are absent here on purpose: they are the `scheduling` and
|
|
112
|
+
* `signal-collectors` modules' own `pageContributions`, merged in by
|
|
113
|
+
* `mergeRoster`. Declaring one here would make that merge refuse the
|
|
114
|
+
* template outright (§ D8's both-claimants refusal).
|
|
115
|
+
*/
|
|
116
|
+
const MINIMALISM_ROSTER = [
|
|
117
|
+
{ route: "/", content: ["content/en/HomePage/home.mdx"] },
|
|
118
|
+
{ route: "/reports", content: ["content/en/LandingPage/reports.mdx"] },
|
|
119
|
+
{ route: "/about", content: ["content/en/AboutPage/about.mdx"] },
|
|
120
|
+
{ route: "/services", content: ["content/en/ServicesList/services.mdx"] },
|
|
121
|
+
{ route: "/services/[slug]" },
|
|
122
|
+
{ route: "/contact", content: ["content/en/ContactPage/contact.mdx"] },
|
|
123
|
+
{ route: "/referral", content: ["content/en/LandingPage/referral.mdx"] },
|
|
124
|
+
{ route: "/faq", content: ["content/en/FAQPage/faq.mdx"] },
|
|
38
125
|
{ route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
|
|
39
126
|
{ route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
|
|
40
127
|
];
|
|
@@ -46,15 +133,21 @@ const TEMPLATE_BASE_ROSTER = [
|
|
|
46
133
|
* `workspace` appears in none of them. It is a Pro module, but an authed app
|
|
47
134
|
* surface contributing no PUBLIC page to a marketing site, and `requires`
|
|
48
135
|
* names what the roster uses (parent § D15). A Pro key unlocks it regardless
|
|
49
|
-
* of any template. `ModuleName` could
|
|
50
|
-
*
|
|
136
|
+
* of any template. `ModuleName` could express it either way — `workspace` is
|
|
137
|
+
* one of the seven `RECOGNIZED_MODULES` names; it is simply not one any
|
|
138
|
+
* template's fixed `requires` list names.
|
|
51
139
|
*
|
|
52
140
|
* `commerce` is likewise in none of them: it is a per-site add-on available at
|
|
53
141
|
* any level, so a template for it would contradict the offering model and
|
|
54
142
|
* multiply the set combinatorially (parent § D13). It still contributes rows
|
|
55
143
|
* to whichever template takes it, through `mergeRoster` below.
|
|
144
|
+
*
|
|
145
|
+
* Frozen (#5507): this registry is also the issuer's authorization allowlist
|
|
146
|
+
* (spec 5480 § C7) — a mutable global there could be monkey-patched at
|
|
147
|
+
* runtime. A caller needing a different registry for a test injects its own
|
|
148
|
+
* fixture rather than mutating this one.
|
|
56
149
|
*/
|
|
57
|
-
export const TEMPLATE_MANIFESTS = {
|
|
150
|
+
export const TEMPLATE_MANIFESTS = Object.freeze({
|
|
58
151
|
/**
|
|
59
152
|
* The Free template, and the mechanism's own proof (parent § D11): it
|
|
60
153
|
* requires only `marketing`, so the keyless merge path is exercised by a
|
|
@@ -63,7 +156,8 @@ export const TEMPLATE_MANIFESTS = {
|
|
|
63
156
|
"marketing-site-free": {
|
|
64
157
|
name: "marketing-site-free",
|
|
65
158
|
requires: ["marketing"],
|
|
66
|
-
|
|
159
|
+
extends: BASE_LAYER_DIR,
|
|
160
|
+
roster: [...TEMPLATE_BASE_ROSTER, FREE_BLOG_TEASER_ROW],
|
|
67
161
|
},
|
|
68
162
|
/**
|
|
69
163
|
* The Pro flagship. Its module list is OFFERINGS § 4 offering 3's tiers line
|
|
@@ -73,6 +167,7 @@ export const TEMPLATE_MANIFESTS = {
|
|
|
73
167
|
"marketing-site": {
|
|
74
168
|
name: "marketing-site",
|
|
75
169
|
requires: ["marketing", "docs-blog", "scheduling", "signal-collectors"],
|
|
170
|
+
extends: BASE_LAYER_DIR,
|
|
76
171
|
roster: TEMPLATE_BASE_ROSTER,
|
|
77
172
|
},
|
|
78
173
|
/**
|
|
@@ -93,9 +188,50 @@ export const TEMPLATE_MANIFESTS = {
|
|
|
93
188
|
"signal-collectors",
|
|
94
189
|
"multi-tenant",
|
|
95
190
|
],
|
|
191
|
+
extends: BASE_LAYER_DIR,
|
|
96
192
|
roster: TEMPLATE_BASE_ROSTER,
|
|
97
193
|
},
|
|
98
|
-
|
|
194
|
+
/**
|
|
195
|
+
* The second Pro template (#5589) — the reports-first, scroll-snap deck
|
|
196
|
+
* site. NOT `marketing-site` plus a module: its `requires` omits
|
|
197
|
+
* `docs-blog`, so the two Pro templates are siblings under `_base`
|
|
198
|
+
* (#5618 D2), never a lineage. Level still derives from `requires`:
|
|
199
|
+
* `scheduling` and `signal-collectors` are paid, so `templateDelivery`
|
|
200
|
+
* returns `"paid"` exactly as it does for `marketing-site`.
|
|
201
|
+
*
|
|
202
|
+
* It declares its OWN roster rather than reusing `TEMPLATE_BASE_ROSTER`
|
|
203
|
+
* (the only declared template that does): the deck home replaces the
|
|
204
|
+
* document-scroll home, and `/how-it-works`, `/features`, `/pricing` and
|
|
205
|
+
* `/sign-in` are not pages this site has. `/pricing`'s absence is not a
|
|
206
|
+
* missing file — the `_base` layer still lands its seed content, and the
|
|
207
|
+
* roster is what the template IS, so an unrostered page simply has no
|
|
208
|
+
* route directory copied (`rosterSurfaces`).
|
|
209
|
+
*
|
|
210
|
+
* `/reports` and `/referral` carry no `GATEABLE_SURFACES` entry and need
|
|
211
|
+
* none: both are single-segment `LandingPage` slugs served by the generic
|
|
212
|
+
* `[slug]` route (#5469 D2), the same mechanism Free's `/how-it-works`
|
|
213
|
+
* uses. `/reports/[slug]` (the reader) is the signal-collectors module's
|
|
214
|
+
* own contributed row.
|
|
215
|
+
*
|
|
216
|
+
* `/services/[slug]` declares no `content`: its seed is a whole DIRECTORY
|
|
217
|
+
* (`content/en/Service`, as `routes-map.ts` records it), and a roster
|
|
218
|
+
* row's `content` names files the manifest generator reads frontmatter
|
|
219
|
+
* from. The three samples ride the template tree unmapped, landing at
|
|
220
|
+
* that directory.
|
|
221
|
+
*/
|
|
222
|
+
"minimalism-site": {
|
|
223
|
+
name: "minimalism-site",
|
|
224
|
+
requires: ["marketing", "scheduling", "signal-collectors"],
|
|
225
|
+
extends: BASE_LAYER_DIR,
|
|
226
|
+
roster: MINIMALISM_ROSTER,
|
|
227
|
+
layout: {
|
|
228
|
+
nav: "mark-only",
|
|
229
|
+
footer: "centered",
|
|
230
|
+
announcementBanner: false,
|
|
231
|
+
home: "deck",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
99
235
|
/** Look up a declared template. @throws TemplateError on an undeclared name. */
|
|
100
236
|
export function resolveTemplate(name) {
|
|
101
237
|
const manifest = TEMPLATE_MANIFESTS[name];
|
|
@@ -104,6 +240,39 @@ export function resolveTemplate(name) {
|
|
|
104
240
|
}
|
|
105
241
|
return manifest;
|
|
106
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a template's `extends` chain — `[name, …ancestors, "_base"]` — in
|
|
245
|
+
* the client registry, before any fetch (#5618 D2). `_base` is the only
|
|
246
|
+
* valid terminus: an `extends` naming neither it nor a declared template is
|
|
247
|
+
* refused naming the field, the value and the recognized set, in
|
|
248
|
+
* `resolveTemplate`'s message shape; a cycle is refused naming the whole
|
|
249
|
+
* chain. The registry is a parameter for the same reason `TEMPLATE_MANIFESTS`
|
|
250
|
+
* is frozen (#5507): a test needing a cyclic or dangling fixture injects its
|
|
251
|
+
* own rather than mutating the shipped one.
|
|
252
|
+
*
|
|
253
|
+
* @throws TemplateError on an undeclared name, an unknown parent or a cycle.
|
|
254
|
+
*/
|
|
255
|
+
export function resolveTemplateLineage(name, registry = TEMPLATE_MANIFESTS) {
|
|
256
|
+
const chain = [];
|
|
257
|
+
let current = name;
|
|
258
|
+
for (;;) {
|
|
259
|
+
if (chain.includes(current)) {
|
|
260
|
+
throw new TemplateError(`template "${name}": cyclic "extends" chain ${[...chain, current].join(" → ")}`);
|
|
261
|
+
}
|
|
262
|
+
chain.push(current);
|
|
263
|
+
if (current === BASE_LAYER_DIR)
|
|
264
|
+
return chain;
|
|
265
|
+
const manifest = registry[current];
|
|
266
|
+
if (!manifest) {
|
|
267
|
+
const declared = [...Object.keys(registry), BASE_LAYER_DIR].join(", ");
|
|
268
|
+
if (chain.length === 1) {
|
|
269
|
+
throw new TemplateError(`unknown template "${current}". Declared: ${declared}`);
|
|
270
|
+
}
|
|
271
|
+
throw new TemplateError(`template "${chain[chain.length - 2]}": "extends" names unknown template "${current}". Declared: ${declared}`);
|
|
272
|
+
}
|
|
273
|
+
current = manifest.extends;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
107
276
|
/**
|
|
108
277
|
* Compose a template's roster: its base rows, then each taken module's
|
|
109
278
|
* contributed rows in `normalizeModules` order (parent § D7).
|
package/dist/template-names.d.ts
CHANGED
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
* is consumed as a plain array across the package.
|
|
38
38
|
*/
|
|
39
39
|
/** The declared site templates, in `TEMPLATE_MANIFESTS` declaration order. */
|
|
40
|
-
export declare const RECOGNIZED_TEMPLATES: readonly ["marketing-site-free", "marketing-site", "saas-product"];
|
|
40
|
+
export declare const RECOGNIZED_TEMPLATES: readonly ["marketing-site-free", "marketing-site", "saas-product", "minimalism-site"];
|
|
41
41
|
export type TemplateName = (typeof RECOGNIZED_TEMPLATES)[number];
|
package/dist/template-names.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"dist",
|
|
13
13
|
"templates",
|
|
14
14
|
"site-templates",
|
|
15
|
+
"versions.json",
|
|
16
|
+
"component-catalog.json",
|
|
15
17
|
"component-catalog.md",
|
|
16
18
|
"README.md",
|
|
17
19
|
"LICENSE"
|
|
@@ -22,6 +24,9 @@
|
|
|
22
24
|
"dependencies": {
|
|
23
25
|
"tar": "^7.5.21"
|
|
24
26
|
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@working-theory/templates": "0.24.0"
|
|
29
|
+
},
|
|
25
30
|
"repository": {
|
|
26
31
|
"type": "git",
|
|
27
32
|
"url": "git+https://github.com/working-theory-labs/ai-launch-kit.git",
|
|
@@ -50,6 +55,8 @@
|
|
|
50
55
|
"test:coverage": "jest --coverage",
|
|
51
56
|
"build-templates": "tsc -b && node scripts/build-templates.mjs",
|
|
52
57
|
"build-catalog": "node scripts/build-component-catalog.mjs",
|
|
53
|
-
"check-catalog": "node scripts/build-component-catalog.mjs --check"
|
|
58
|
+
"check-catalog": "node scripts/build-component-catalog.mjs --check",
|
|
59
|
+
"manifest:build": "node scripts/build-template-manifest.mjs --template marketing-site-free",
|
|
60
|
+
"manifest:check": "node scripts/build-template-manifest.mjs --template marketing-site-free --check && node scripts/build-template-manifest.mjs --template marketing-site --check"
|
|
54
61
|
}
|
|
55
62
|
}
|