create-ailk 0.8.0 → 0.10.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/README.md +71 -3
- package/component-catalog.json +279 -45
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +104 -5
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +48 -32
- package/dist/lib/module-license-gate.js +117 -8
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +67 -23
- package/dist/parse-args.d.ts +12 -2
- package/dist/parse-args.js +74 -13
- package/dist/programmatic.d.ts +30 -6
- package/dist/programmatic.js +216 -19
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +141 -8
- package/dist/starter-grid.d.ts +16 -63
- package/dist/starter-grid.js +102 -84
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +200 -83
- package/dist/template-manifest.d.ts +30 -13
- package/dist/template-manifest.js +53 -28
- package/package.json +6 -3
- package/release-scope.json +25 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +1 -1
- package/site-templates/designs/standard/DESIGN.md +98 -6
- package/site-templates/marketing-site/DESIGN.md +98 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
- package/site-templates/marketing-site-free/DESIGN.md +98 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/saas-product/DESIGN.md +98 -6
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
- package/site-templates/shapes/person-service/ailk-template.json +1 -1
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
- package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
- package/templates/.claude/agents/web.md +10 -8
- package/templates/.claude/rules/architecture.md +17 -7
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-auth/SKILL.md +142 -90
- package/templates/.claude/skills/add-auth/references/env-vars.md +32 -17
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +17 -8
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +94 -86
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +51 -18
- package/templates/.claude/skills/check-site/references/common-errors.md +12 -9
- package/templates/.claude/skills/check-site/references/findings-glossary.md +4 -4
- package/templates/.claude/skills/check-site/references/severity-levels.md +3 -3
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- package/templates/.claude/skills/setup-deploy/SKILL.md +75 -38
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.env.example +23 -2
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +98 -6
- package/templates/README.md +50 -38
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/lib/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +35 -19
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +14 -13
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +23 -9
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +40 -1
- package/templates/content/en/HomePage/home.mdx +2 -6
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +4 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +0 -506
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +0 -175
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +0 -8
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +0 -12
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +0 -12
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
package/dist/site-chrome.d.ts
CHANGED
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
* block, gets these defaults written, byte-identical to the monorepo's own
|
|
22
22
|
* committed `apps/web/lib/site-chrome.ts`.
|
|
23
23
|
*
|
|
24
|
-
* A
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
24
|
+
* A design supplies layout knobs too (#6638, widened #6813 D4): its OWN
|
|
25
|
+
* `layout` (a `DesignPackage`'s, read by `design-scaffold.ts`'s
|
|
26
|
+
* `resolveDesign`), passed as `resolveSiteChrome`'s optional `designLayout`
|
|
27
|
+
* argument between an explicit `layout` and these defaults. This module
|
|
28
|
+
* imports nothing from `template-manifest.ts` (the cycle that import used to
|
|
29
|
+
* close): an unrecognized design NAME now fails in `resolveDesign`, before
|
|
30
|
+
* this function ever sees one.
|
|
27
31
|
*/
|
|
28
|
-
import { type StarterDesign } from "./template-manifest.js";
|
|
29
32
|
/** The `nav` knob's vocabulary. `mark-only` → the layout passes Nav an empty
|
|
30
33
|
* `navItems` (brand mark + the existing actions/theme/locale controls only). */
|
|
31
34
|
export type SiteChromeNav = "full" | "mark-only";
|
|
@@ -75,12 +78,19 @@ export declare class TemplateLayoutError extends Error {
|
|
|
75
78
|
*/
|
|
76
79
|
export declare function validateTemplateLayout(value: unknown, subject: string): TemplateLayout;
|
|
77
80
|
/**
|
|
78
|
-
* Fill every field a `TemplateLayout` leaves unset — from `
|
|
79
|
-
*
|
|
80
|
-
* `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by
|
|
81
|
-
* template can wear a design and still override one knob of it.
|
|
81
|
+
* Fill every field a `TemplateLayout` leaves unset — from `designLayout`
|
|
82
|
+
* (a design package's own resolved `layout`, #6638/#6813 D4) when one is
|
|
83
|
+
* given, then from `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by
|
|
84
|
+
* field, so a template can wear a design and still override one knob of it.
|
|
85
|
+
*
|
|
86
|
+
* `designLayout` is typed `TemplateLayout`, but a `.mjs` caller
|
|
87
|
+
* (`build-template-manifest.mjs`) cannot be compile-time checked the way a
|
|
88
|
+
* `.ts` caller can, so the type alone cannot stop a caller passing a design
|
|
89
|
+
* NAME (a string) rather than its resolved layout: a runtime guard throws a
|
|
90
|
+
* `TypeError` naming what was given, rather than silently resolving to the
|
|
91
|
+
* defaults.
|
|
82
92
|
*/
|
|
83
|
-
export declare function resolveSiteChrome(layout?: TemplateLayout,
|
|
93
|
+
export declare function resolveSiteChrome(layout?: TemplateLayout, designLayout?: TemplateLayout): ResolvedSiteChrome;
|
|
84
94
|
/**
|
|
85
95
|
* Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
|
|
86
96
|
* GENERATED-header idiom `renderNavConfig` uses. `layout.tsx` imports
|
package/dist/site-chrome.js
CHANGED
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
* block, gets these defaults written, byte-identical to the monorepo's own
|
|
22
22
|
* committed `apps/web/lib/site-chrome.ts`.
|
|
23
23
|
*
|
|
24
|
-
* A
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
24
|
+
* A design supplies layout knobs too (#6638, widened #6813 D4): its OWN
|
|
25
|
+
* `layout` (a `DesignPackage`'s, read by `design-scaffold.ts`'s
|
|
26
|
+
* `resolveDesign`), passed as `resolveSiteChrome`'s optional `designLayout`
|
|
27
|
+
* argument between an explicit `layout` and these defaults. This module
|
|
28
|
+
* imports nothing from `template-manifest.ts` (the cycle that import used to
|
|
29
|
+
* close): an unrecognized design NAME now fails in `resolveDesign`, before
|
|
30
|
+
* this function ever sees one.
|
|
27
31
|
*/
|
|
28
|
-
import { DESIGN_LAYOUTS, TemplateError, } from "./template-manifest.js";
|
|
29
32
|
/** Defaults (#5612 D1; banner off since #6469). */
|
|
30
33
|
export const DEFAULT_SITE_CHROME = {
|
|
31
34
|
nav: "full",
|
|
@@ -92,17 +95,24 @@ export function validateTemplateLayout(value, subject) {
|
|
|
92
95
|
return result;
|
|
93
96
|
}
|
|
94
97
|
/**
|
|
95
|
-
* Fill every field a `TemplateLayout` leaves unset — from `
|
|
96
|
-
*
|
|
97
|
-
* `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by
|
|
98
|
-
* template can wear a design and still override one knob of it.
|
|
98
|
+
* Fill every field a `TemplateLayout` leaves unset — from `designLayout`
|
|
99
|
+
* (a design package's own resolved `layout`, #6638/#6813 D4) when one is
|
|
100
|
+
* given, then from `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by
|
|
101
|
+
* field, so a template can wear a design and still override one knob of it.
|
|
102
|
+
*
|
|
103
|
+
* `designLayout` is typed `TemplateLayout`, but a `.mjs` caller
|
|
104
|
+
* (`build-template-manifest.mjs`) cannot be compile-time checked the way a
|
|
105
|
+
* `.ts` caller can, so the type alone cannot stop a caller passing a design
|
|
106
|
+
* NAME (a string) rather than its resolved layout: a runtime guard throws a
|
|
107
|
+
* `TypeError` naming what was given, rather than silently resolving to the
|
|
108
|
+
* defaults.
|
|
99
109
|
*/
|
|
100
|
-
export function resolveSiteChrome(layout,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new TemplateError(`unknown design "${design}". Declared: ${Object.keys(DESIGN_LAYOUTS).join(", ")}`);
|
|
110
|
+
export function resolveSiteChrome(layout, designLayout) {
|
|
111
|
+
if (designLayout !== undefined &&
|
|
112
|
+
(typeof designLayout !== "object" || designLayout === null || Array.isArray(designLayout))) {
|
|
113
|
+
throw new TypeError(`resolveSiteChrome: designLayout must be a layout object, got ${JSON.stringify(designLayout)}`);
|
|
105
114
|
}
|
|
115
|
+
const fromDesign = designLayout ?? {};
|
|
106
116
|
return {
|
|
107
117
|
nav: layout?.nav ?? fromDesign.nav ?? DEFAULT_SITE_CHROME.nav,
|
|
108
118
|
footer: layout?.footer ?? fromDesign.footer ?? DEFAULT_SITE_CHROME.footer,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Level } from "./lib/module-tier.js";
|
|
2
|
+
import type { ModuleName } from "./parse-args.js";
|
|
3
|
+
import { type SiteRoster } from "./site-roster.js";
|
|
4
|
+
import type { MarkerMap } from "./substitute.js";
|
|
5
|
+
export interface PlanSiteManifestOptions {
|
|
6
|
+
targetDir: string;
|
|
7
|
+
/** The installed manifest, read after every layer landed. */
|
|
8
|
+
roster: SiteRoster;
|
|
9
|
+
/** The effective module set the scaffold installed. */
|
|
10
|
+
modules: readonly ModuleName[];
|
|
11
|
+
/** The level the key path derived, or undefined when it derived none. */
|
|
12
|
+
level: Level | undefined;
|
|
13
|
+
siteTemplatesDir: string;
|
|
14
|
+
templatesDir: string;
|
|
15
|
+
markers: MarkerMap;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The manifest's next text: `modules` (and `level`, when derived) after
|
|
19
|
+
* `requires`, and a row for every route present on disk.
|
|
20
|
+
*
|
|
21
|
+
* @throws AddModuleRefusedError when the file already records `modules` or
|
|
22
|
+
* `level`, or its layout is not the generator's (`rows` last, `requires`
|
|
23
|
+
* at the top level) — guessing would silently mis-record the site.
|
|
24
|
+
*/
|
|
25
|
+
export declare function planSiteManifest(opts: PlanSiteManifestOptions): string;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site manifest's record of what the scaffold IS (#6944) — written into
|
|
3
|
+
* `<site>/.ailk-site-manifest.json` by `scaffold()` once every layer has
|
|
4
|
+
* landed.
|
|
5
|
+
*
|
|
6
|
+
* A template scaffold's manifest arrives as its template's generated
|
|
7
|
+
* `TEMPLATE-MANIFEST.json` (#6658 D7): the template's own `requires` and
|
|
8
|
+
* rows, and nothing about the run that installed it. A Pro `--shape` run of a
|
|
9
|
+
* Free template recorded `requires: ["marketing"]` and the Free rows, missing
|
|
10
|
+
* every page its module bundles landed. `planSiteManifest` adds:
|
|
11
|
+
*
|
|
12
|
+
* - `modules` — the effective module set, the one `architecture.yaml`
|
|
13
|
+
* declares. Skills read it for what the site can render;
|
|
14
|
+
* - `level` — ONLY when the licence-key path derived one
|
|
15
|
+
* (`resolveModuleSet` / `deriveLevel`). Never inferred from `modules`:
|
|
16
|
+
* the level comes only from the key. An alias or `--modules` keep run
|
|
17
|
+
* derives none and records none;
|
|
18
|
+
* - a row for every route present on disk — each module's contributed rows
|
|
19
|
+
* through `planRosterAppend` (add-module's own append), the sample pages
|
|
20
|
+
* a module's bundle installs (`sampleRows`), and a code-route row for any
|
|
21
|
+
* other page route under `apps/web/app/[locale]/` (`codeRouteRows`).
|
|
22
|
+
*
|
|
23
|
+
* Every byte already in the file is kept; the new rows and keys are rendered
|
|
24
|
+
* the way prettier prints the generator's JSON, so the file stays formatted.
|
|
25
|
+
*
|
|
26
|
+
* Pure module: no `import.meta.url`. Package-rooted directories are supplied
|
|
27
|
+
* by the caller, the same split as `site-roster.ts` / `programmatic.ts`.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
30
|
+
import { join, relative, sep } from "node:path";
|
|
31
|
+
import { isDeepStrictEqual } from "node:util";
|
|
32
|
+
import { AddModuleRefusedError } from "./add-module-core.js";
|
|
33
|
+
import { MODULE_SLICES, normalizeModules } from "./module-architecture.js";
|
|
34
|
+
import { moduleRosterRows, planRosterAppend, } from "./site-roster.js";
|
|
35
|
+
import { TEMPLATE_MANIFESTS, rosterDeclares, templateTreeDir, } from "./template-manifest.js";
|
|
36
|
+
/** Prettier's default print width — the generator formats the manifest with it. */
|
|
37
|
+
const PRINT_WIDTH = 80;
|
|
38
|
+
/** Where the site's App Router pages live. */
|
|
39
|
+
const ROUTE_ROOT = join("apps", "web", "app", "[locale]");
|
|
40
|
+
const PAGE_FILE = /^page\.(tsx|ts|jsx|js|mdx)$/;
|
|
41
|
+
/**
|
|
42
|
+
* The generator's row for a content-less route (`build-template-manifest.mjs`:
|
|
43
|
+
* every row renders under the locale layout's MarketingShell, chrome on).
|
|
44
|
+
*/
|
|
45
|
+
const CODE_ROUTE_ROW = {
|
|
46
|
+
kind: "route",
|
|
47
|
+
pageType: null,
|
|
48
|
+
schema: null,
|
|
49
|
+
shell: "MarketingShell",
|
|
50
|
+
chrome: "on",
|
|
51
|
+
sections: [],
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The manifest's next text: `modules` (and `level`, when derived) after
|
|
55
|
+
* `requires`, and a row for every route present on disk.
|
|
56
|
+
*
|
|
57
|
+
* @throws AddModuleRefusedError when the file already records `modules` or
|
|
58
|
+
* `level`, or its layout is not the generator's (`rows` last, `requires`
|
|
59
|
+
* at the top level) — guessing would silently mis-record the site.
|
|
60
|
+
*/
|
|
61
|
+
export function planSiteManifest(opts) {
|
|
62
|
+
const { roster } = opts;
|
|
63
|
+
for (const key of ["modules", "level"]) {
|
|
64
|
+
if (key in roster.doc) {
|
|
65
|
+
throw new AddModuleRefusedError(`${roster.path} already records "${key}"`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const plan = planRosterAppend({
|
|
69
|
+
targetDir: opts.targetDir,
|
|
70
|
+
roster,
|
|
71
|
+
modules: opts.modules,
|
|
72
|
+
references: opts.modules.flatMap((m) => moduleRosterRows(m, opts.siteTemplatesDir)),
|
|
73
|
+
siteTemplatesDir: opts.siteTemplatesDir,
|
|
74
|
+
templatesDir: opts.templatesDir,
|
|
75
|
+
markers: opts.markers,
|
|
76
|
+
});
|
|
77
|
+
const appended = plan.next ?? roster.text;
|
|
78
|
+
const declared = new Set(JSON.parse(appended).rows.map((row) => row["route"]));
|
|
79
|
+
const extra = [
|
|
80
|
+
...sampleRows(opts, declared),
|
|
81
|
+
...codeRouteRows(opts.targetDir, declared),
|
|
82
|
+
];
|
|
83
|
+
const withRows = appendRows(appended, extra, roster);
|
|
84
|
+
return insertRecord(withRows, opts.modules, opts.level, roster.path);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The rows for the sample pages a module's bundle installs through its
|
|
88
|
+
* `installMap` (`signal-collectors`' /projects family) that no row names yet:
|
|
89
|
+
* each is the row a template requiring the module declares for that content,
|
|
90
|
+
* as the generator wrote it into the template's shipped manifest. A
|
|
91
|
+
* `contentDir` row lists the site's own seeds. `declared` gains each route.
|
|
92
|
+
*/
|
|
93
|
+
function sampleRows(opts, declared) {
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const moduleName of normalizeModules([...opts.modules])) {
|
|
96
|
+
const installed = Object.values(MODULE_SLICES[moduleName].installMap ?? {})
|
|
97
|
+
.filter((to) => to.startsWith("content/en/") && to.endsWith(".mdx"))
|
|
98
|
+
.filter((to) => existsSync(join(opts.targetDir, to)));
|
|
99
|
+
if (installed.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
for (const template of Object.values(TEMPLATE_MANIFESTS)) {
|
|
102
|
+
if (!template.requires.includes(moduleName))
|
|
103
|
+
continue;
|
|
104
|
+
const shippedPath = join(opts.siteTemplatesDir, templateTreeDir(template.name), "TEMPLATE-MANIFEST.json");
|
|
105
|
+
if (!existsSync(shippedPath))
|
|
106
|
+
continue;
|
|
107
|
+
const shipped = JSON.parse(readFileSync(shippedPath, "utf8")).rows;
|
|
108
|
+
for (const row of template.roster) {
|
|
109
|
+
if (declared.has(row.route))
|
|
110
|
+
continue;
|
|
111
|
+
if (!installed.some((path) => rosterDeclares([row], path)))
|
|
112
|
+
continue;
|
|
113
|
+
const value = shipped.find((r) => r["route"] === row.route);
|
|
114
|
+
if (value === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
declared.add(row.route);
|
|
117
|
+
out.push(row.contentDir !== undefined && "seeds" in value
|
|
118
|
+
? {
|
|
119
|
+
...value,
|
|
120
|
+
seeds: listMdx(join(opts.targetDir, row.contentDir), opts.targetDir),
|
|
121
|
+
}
|
|
122
|
+
: value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** A code-route row for every page route on disk that `declared` does not name. */
|
|
129
|
+
function codeRouteRows(targetDir, declared) {
|
|
130
|
+
return routesOnDisk(targetDir)
|
|
131
|
+
.filter((route) => !declared.has(route))
|
|
132
|
+
.map((route) => ({ route, ...CODE_ROUTE_ROW }));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Every route the site's App Router serves from a page file, sorted: route
|
|
136
|
+
* groups `(x)` dropped, a trailing optional catch-all `[[...x]]` folded into
|
|
137
|
+
* its parent (docs-blog's `/blog/[[...slug]]` serves `/blog`). The root
|
|
138
|
+
* content renderers (`/[slug]`, `/[slug]/[detail]`) are not routes of their
|
|
139
|
+
* own — they are how the content rows render — so they are left out.
|
|
140
|
+
*/
|
|
141
|
+
function routesOnDisk(targetDir) {
|
|
142
|
+
const root = join(targetDir, ROUTE_ROOT);
|
|
143
|
+
const routes = new Set();
|
|
144
|
+
const stack = existsSync(root) ? [root] : [];
|
|
145
|
+
for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
|
|
146
|
+
const entries = readdirSync(cur, { withFileTypes: true });
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (entry.isDirectory())
|
|
149
|
+
stack.push(join(cur, entry.name));
|
|
150
|
+
}
|
|
151
|
+
if (!entries.some((entry) => entry.isFile() && PAGE_FILE.test(entry.name))) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const segments = relative(root, cur)
|
|
155
|
+
.split(sep)
|
|
156
|
+
.filter((segment) => segment !== "" && !/^\(.+\)$/.test(segment));
|
|
157
|
+
const last = segments[segments.length - 1];
|
|
158
|
+
if (last !== undefined && /^\[\[\.\.\..+\]\]$/.test(last))
|
|
159
|
+
segments.pop();
|
|
160
|
+
if (segments[0]?.startsWith("["))
|
|
161
|
+
continue;
|
|
162
|
+
routes.add(`/${segments.join("/")}`);
|
|
163
|
+
}
|
|
164
|
+
return [...routes].sort();
|
|
165
|
+
}
|
|
166
|
+
/** Every `.mdx` under `dir`, as sorted `/`-separated paths relative to `base`. */
|
|
167
|
+
function listMdx(dir, base) {
|
|
168
|
+
const out = [];
|
|
169
|
+
const stack = existsSync(dir) ? [dir] : [];
|
|
170
|
+
for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
|
|
171
|
+
for (const entry of readdirSync(cur, { withFileTypes: true })) {
|
|
172
|
+
const abs = join(cur, entry.name);
|
|
173
|
+
if (entry.isDirectory())
|
|
174
|
+
stack.push(abs);
|
|
175
|
+
else if (entry.isFile() && entry.name.endsWith(".mdx")) {
|
|
176
|
+
out.push(relative(base, abs).split(sep).join("/"));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out.sort();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `value` as prettier prints the generator's JSON with its value starting at
|
|
184
|
+
* `column` after a `prefix`-wide key: objects expanded one key per line, an
|
|
185
|
+
* array of scalars inline when the line fits, one element per line otherwise.
|
|
186
|
+
*/
|
|
187
|
+
function renderJson(value, column, prefix = 0, trailing = 0) {
|
|
188
|
+
const pad = " ".repeat(column + 2);
|
|
189
|
+
const close = " ".repeat(column);
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
if (value.length === 0)
|
|
192
|
+
return "[]";
|
|
193
|
+
if (value.every((item) => item === null || typeof item !== "object")) {
|
|
194
|
+
const inline = `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
|
|
195
|
+
if (column + prefix + inline.length + trailing <= PRINT_WIDTH)
|
|
196
|
+
return inline;
|
|
197
|
+
}
|
|
198
|
+
const items = value.map((item, i) => pad + renderJson(item, column + 2, 0, i < value.length - 1 ? 1 : 0));
|
|
199
|
+
return `[\n${items.join(",\n")}\n${close}]`;
|
|
200
|
+
}
|
|
201
|
+
if (value !== null && typeof value === "object") {
|
|
202
|
+
const entries = Object.entries(value);
|
|
203
|
+
if (entries.length === 0)
|
|
204
|
+
return "{}";
|
|
205
|
+
const lines = entries.map(([key, item], i) => {
|
|
206
|
+
const name = `${JSON.stringify(key)}: `;
|
|
207
|
+
return (pad +
|
|
208
|
+
name +
|
|
209
|
+
renderJson(item, column + 2, name.length, i < entries.length - 1 ? 1 : 0));
|
|
210
|
+
});
|
|
211
|
+
return `{\n${lines.join(",\n")}\n${close}}`;
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(value);
|
|
214
|
+
}
|
|
215
|
+
/** `text` with `rows` appended to its `rows` array — which the generator writes last. */
|
|
216
|
+
function appendRows(text, rows, roster) {
|
|
217
|
+
if (rows.length === 0)
|
|
218
|
+
return text;
|
|
219
|
+
const keys = Object.keys(JSON.parse(text));
|
|
220
|
+
if (keys[keys.length - 1] !== "rows") {
|
|
221
|
+
throw new AddModuleRefusedError(`${roster.path} does not end with its "rows" array, so rows cannot be appended`);
|
|
222
|
+
}
|
|
223
|
+
const close = text.lastIndexOf("]");
|
|
224
|
+
let end = close;
|
|
225
|
+
while (end > 0 && /\s/.test(text[end - 1]))
|
|
226
|
+
end--;
|
|
227
|
+
const empty = text[end - 1] === "[";
|
|
228
|
+
const column = roster.lead.length - roster.lead.lastIndexOf("\n") - 1;
|
|
229
|
+
const rendered = rows
|
|
230
|
+
.map((row) => roster.lead + renderJson(row, column))
|
|
231
|
+
.join(",");
|
|
232
|
+
return empty
|
|
233
|
+
? text.slice(0, end) + rendered + roster.trail + text.slice(close)
|
|
234
|
+
: text.slice(0, end) + "," + rendered + text.slice(end);
|
|
235
|
+
}
|
|
236
|
+
/** `text` with `modules` (and `level`, when derived) inserted after the top-level `requires`. */
|
|
237
|
+
function insertRecord(text, modules, level, label) {
|
|
238
|
+
const anchor = '\n "requires": ';
|
|
239
|
+
const at = text.indexOf(anchor);
|
|
240
|
+
if (at === -1 || text.indexOf(anchor, at + 1) !== -1) {
|
|
241
|
+
throw new AddModuleRefusedError(`${label} has no single top-level "requires" key to record the module set beside`);
|
|
242
|
+
}
|
|
243
|
+
const end = text.indexOf("]", at) + 1;
|
|
244
|
+
const name = '"modules": ';
|
|
245
|
+
const record = `,\n ${name}${renderJson([...modules], 2, name.length, 1)}` +
|
|
246
|
+
(level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "");
|
|
247
|
+
const next = text.slice(0, end) + record + text.slice(end);
|
|
248
|
+
const doc = JSON.parse(next);
|
|
249
|
+
if (!isDeepStrictEqual(doc["modules"], [...modules]) ||
|
|
250
|
+
doc["level"] !== level) {
|
|
251
|
+
throw new AddModuleRefusedError(`${label}: the module set could not be recorded beside "requires"`);
|
|
252
|
+
}
|
|
253
|
+
return next;
|
|
254
|
+
}
|
package/dist/site-roster.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { CompiledDesignTheme } from "./design-compile.js";
|
|
1
2
|
import { type PageRosterRow } from "./module-architecture.js";
|
|
2
3
|
import type { ModuleName } from "./parse-args.js";
|
|
4
|
+
import type { ResolvedSiteChrome } from "./site-chrome.js";
|
|
3
5
|
import { type MarkerMap } from "./substitute.js";
|
|
4
6
|
import { type TemplateManifest } from "./template-manifest.js";
|
|
5
7
|
/** The site-root file that is the roster of record (#6658 D7). */
|
|
@@ -55,6 +57,25 @@ export interface ModuleRosterRow {
|
|
|
55
57
|
* not declare is refused: guessing would silently mis-roster the site.
|
|
56
58
|
*/
|
|
57
59
|
export declare function readSiteRoster(targetDir: string): SiteRoster | undefined;
|
|
60
|
+
/** What `planSiteManifestDesign` writes when it rewrites the manifest. */
|
|
61
|
+
export interface SiteManifestDesignNext {
|
|
62
|
+
design: string;
|
|
63
|
+
layout: ResolvedSiteChrome;
|
|
64
|
+
theme: CompiledDesignTheme;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Plan the site manifest's `design`/`layout`/`theme` keys as a design's
|
|
68
|
+
* applied values (#6813 D6) — `undefined` when the file already carries
|
|
69
|
+
* `next` (no rewrite, so the file keeps its bytes byte-for-byte), else the
|
|
70
|
+
* text with each CHANGED key's value spliced in place, located with the same
|
|
71
|
+
* `findKey`/`endOfValue` scanner `planRosterAppend` uses. `rows`,
|
|
72
|
+
* `generatedHash` and every other byte stay untouched.
|
|
73
|
+
*
|
|
74
|
+
* @throws AddModuleRefusedError when `text` is not a JSON object, or is
|
|
75
|
+
* missing `design`, `layout` or `theme` — a manifest predating #6658 D7
|
|
76
|
+
* never reaches this function (`scaffold()` guards on the file's presence).
|
|
77
|
+
*/
|
|
78
|
+
export declare function planSiteManifestDesign(text: string, next: SiteManifestDesignNext, label: string): string | undefined;
|
|
58
79
|
/**
|
|
59
80
|
* The generator's row for each of a module's `pageContributions`, in
|
|
60
81
|
* declaration order — read from the committed manifest of the first
|
|
@@ -63,6 +84,26 @@ export declare function readSiteRoster(targetDir: string): SiteRoster | undefine
|
|
|
63
84
|
* of its forms. A route no shipped manifest carries is refused.
|
|
64
85
|
*/
|
|
65
86
|
export declare function moduleRosterRows(moduleName: ModuleName, siteTemplatesDir: string): ModuleRosterRow[];
|
|
87
|
+
/**
|
|
88
|
+
* The Free `/meet` placeholder seeds on `route` (#6942): the keyless Free
|
|
89
|
+
* template's own `content` on a route the `scheduling` module contributes
|
|
90
|
+
* (`FREE_MEET_PLACEHOLDER_ROWS`, `starter-grid.ts`, read through the one
|
|
91
|
+
* registry that composes it). Empty for any other route. Throws when the Free
|
|
92
|
+
* template carries none at all, so a renamed template or a moved row fails
|
|
93
|
+
* loudly instead of re-opening the prune #6942 closed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function meetPlaceholderSeeds(route: string): readonly string[];
|
|
96
|
+
/**
|
|
97
|
+
* `roster` with the Free `/meet` placeholder seeds folded onto the
|
|
98
|
+
* `scheduling` module's code-route rows (#6942) — a row on a placeholder
|
|
99
|
+
* route that names no `content` of its own. Unconfigured, the module's
|
|
100
|
+
* `/meet` and `/meet/[slug]` pages hand off to the placeholder routes, so a
|
|
101
|
+
* seed there is still served and still declared: the scaffold's seed prune
|
|
102
|
+
* (`programmatic.ts`) and the site roster check read the roster through
|
|
103
|
+
* this. A row with its own `content` (the Free placeholder row itself) and a
|
|
104
|
+
* row on any other route are returned as is.
|
|
105
|
+
*/
|
|
106
|
+
export declare function withMeetPlaceholderSeeds(roster: readonly PageRosterRow[]): PageRosterRow[];
|
|
66
107
|
/** What becomes of a superseded template row's seed file. */
|
|
67
108
|
export type SupersededSeed = {
|
|
68
109
|
path: string;
|
package/dist/site-roster.js
CHANGED
|
@@ -30,7 +30,7 @@ import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-cor
|
|
|
30
30
|
import { diffTwoStrings } from "./lib/extract-module-bundle.js";
|
|
31
31
|
import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
|
|
32
32
|
import { substituteMarkers } from "./substitute.js";
|
|
33
|
-
import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, templateTreeDir, } from "./template-manifest.js";
|
|
33
|
+
import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, shippedManifestPath, templateTreeDir, } from "./template-manifest.js";
|
|
34
34
|
/** The site-root file that is the roster of record (#6658 D7). */
|
|
35
35
|
export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
|
|
36
36
|
/**
|
|
@@ -39,8 +39,6 @@ export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
|
|
|
39
39
|
* `SITE_ROSTER_PATH` is absent, and written back in place.
|
|
40
40
|
*/
|
|
41
41
|
export const LEGACY_SITE_ROSTER_PATH = "TEMPLATE-MANIFEST.json";
|
|
42
|
-
/** A template tree's own generated manifest — the file the site copy is made from. */
|
|
43
|
-
const SHIPPED_MANIFEST_PATH = "TEMPLATE-MANIFEST.json";
|
|
44
42
|
/** Prettier's default print width — the generator formats the manifest with it. */
|
|
45
43
|
const PRINT_WIDTH = 80;
|
|
46
44
|
// ── A string-aware JSON scanner, for the text splice ──────────────────────
|
|
@@ -183,13 +181,67 @@ export function readSiteRoster(targetDir) {
|
|
|
183
181
|
}
|
|
184
182
|
return { ...file, template, path };
|
|
185
183
|
}
|
|
184
|
+
/** `value`, rendered the way prettier prints an object-only JSON value nested one level under the manifest's root object. */
|
|
185
|
+
function renderManifestValue(value) {
|
|
186
|
+
return JSON.stringify(value, null, 2)
|
|
187
|
+
.split("\n")
|
|
188
|
+
.map((line, i) => (i === 0 ? line : ` ${line}`))
|
|
189
|
+
.join("\n");
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Plan the site manifest's `design`/`layout`/`theme` keys as a design's
|
|
193
|
+
* applied values (#6813 D6) — `undefined` when the file already carries
|
|
194
|
+
* `next` (no rewrite, so the file keeps its bytes byte-for-byte), else the
|
|
195
|
+
* text with each CHANGED key's value spliced in place, located with the same
|
|
196
|
+
* `findKey`/`endOfValue` scanner `planRosterAppend` uses. `rows`,
|
|
197
|
+
* `generatedHash` and every other byte stay untouched.
|
|
198
|
+
*
|
|
199
|
+
* @throws AddModuleRefusedError when `text` is not a JSON object, or is
|
|
200
|
+
* missing `design`, `layout` or `theme` — a manifest predating #6658 D7
|
|
201
|
+
* never reaches this function (`scaffold()` guards on the file's presence).
|
|
202
|
+
*/
|
|
203
|
+
export function planSiteManifestDesign(text, next, label) {
|
|
204
|
+
let doc;
|
|
205
|
+
try {
|
|
206
|
+
doc = JSON.parse(text);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
throw new AddModuleRefusedError(`${label} is not JSON (${err.message})`);
|
|
210
|
+
}
|
|
211
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
212
|
+
throw new AddModuleRefusedError(`${label} is not a JSON object`);
|
|
213
|
+
}
|
|
214
|
+
const parsed = doc;
|
|
215
|
+
const keys = ["design", "layout", "theme"];
|
|
216
|
+
for (const key of keys) {
|
|
217
|
+
if (!(key in parsed)) {
|
|
218
|
+
throw new AddModuleRefusedError(`${label} has no "${key}" key`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (keys.every((key) => isDeepStrictEqual(parsed[key], next[key]))) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
let result = text;
|
|
225
|
+
for (const key of keys) {
|
|
226
|
+
if (isDeepStrictEqual(parsed[key], next[key]))
|
|
227
|
+
continue;
|
|
228
|
+
const at = findKey(result, result.indexOf("{"), key);
|
|
229
|
+
if (at === undefined) {
|
|
230
|
+
throw new AddModuleRefusedError(`${label} has a "${key}" key the scanner cannot locate`);
|
|
231
|
+
}
|
|
232
|
+
const end = endOfValue(result, at.valueStart);
|
|
233
|
+
result = result.slice(0, at.valueStart) + renderManifestValue(next[key]) + result.slice(end);
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
186
237
|
// ── D2: a module's rows, from the shipped manifests ───────────────────────
|
|
187
238
|
/** A template's shipped manifest, or undefined when its tree ships none. */
|
|
188
239
|
function readShippedManifest(siteTemplatesDir, templateName) {
|
|
189
|
-
const
|
|
240
|
+
const relPath = shippedManifestPath(templateName);
|
|
241
|
+
const path = join(siteTemplatesDir, relPath);
|
|
190
242
|
if (!existsSync(path))
|
|
191
243
|
return undefined;
|
|
192
|
-
return parseRosterFile(readFileSync(path, "utf8"), `site-templates/${
|
|
244
|
+
return parseRosterFile(readFileSync(path, "utf8"), `site-templates/${relPath}`);
|
|
193
245
|
}
|
|
194
246
|
/**
|
|
195
247
|
* The generator's row for each of a module's `pageContributions`, in
|
|
@@ -216,14 +268,53 @@ export function moduleRosterRows(moduleName, siteTemplatesDir) {
|
|
|
216
268
|
};
|
|
217
269
|
return contributions.map((contribution) => {
|
|
218
270
|
const isDir = contribution.contentDir !== undefined;
|
|
219
|
-
const row = find(contribution.route, isDir ? "route" : undefined);
|
|
220
271
|
const seeded = isDir ? find(contribution.route, "page") : undefined;
|
|
272
|
+
let row = find(contribution.route, isDir ? "route" : undefined);
|
|
273
|
+
if (row === undefined && isDir && seeded !== undefined) {
|
|
274
|
+
const contentDir = contribution.contentDir;
|
|
275
|
+
const installMap = MODULE_SLICES[moduleName].installMap ?? {};
|
|
276
|
+
const selfSeeds = Object.values(installMap).some((dest) => dest.startsWith(`${contentDir}/`));
|
|
277
|
+
if (selfSeeds)
|
|
278
|
+
row = deriveRouteRow(contribution.route, seeded);
|
|
279
|
+
}
|
|
221
280
|
if (row === undefined || (isDir && seeded === undefined)) {
|
|
222
281
|
throw new AddModuleRefusedError(`module "${moduleName}" contributes ${contribution.route}, but no shipped template manifest carries ${isDir ? "both forms of " : ""}its row, so it cannot be added to ${SITE_ROSTER_PATH}`);
|
|
223
282
|
}
|
|
224
283
|
return { module: moduleName, contribution, row, ...(seeded && { seeded }) };
|
|
225
284
|
});
|
|
226
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Derive a `contentDir` contribution's code-route form from its seeded page
|
|
288
|
+
* form (spec 6892 D9) — for a SELF-SEEDING contribution, one whose
|
|
289
|
+
* contributing module's own `installMap` lands at least one file under the
|
|
290
|
+
* directory, so no template supplies a route form for it (every template
|
|
291
|
+
* taking the module gets the module's seeds, never zero of them). Returns
|
|
292
|
+
* field-for-field the route form `build-template-manifest.mjs` emits for a
|
|
293
|
+
* template that supplies no seeds of its own: `kind: "route"`, no
|
|
294
|
+
* `pageType`/`schema`/`contentDir`/`seeds`, the seeded form's own `shell` and
|
|
295
|
+
* `chrome`, no sections. Rendered the way a generated manifest's row-array
|
|
296
|
+
* element is indented (four spaces beyond `JSON.stringify`'s own two), so a
|
|
297
|
+
* derived row's `text` is byte-identical to the route-form slice a template
|
|
298
|
+
* manifest carries for the same contribution.
|
|
299
|
+
*/
|
|
300
|
+
function deriveRouteRow(route, seeded) {
|
|
301
|
+
const value = {
|
|
302
|
+
route,
|
|
303
|
+
kind: "route",
|
|
304
|
+
pageType: null,
|
|
305
|
+
schema: null,
|
|
306
|
+
shell: seeded.value["shell"],
|
|
307
|
+
chrome: seeded.value["chrome"],
|
|
308
|
+
sections: [],
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
value,
|
|
312
|
+
text: JSON.stringify(value, null, 2)
|
|
313
|
+
.split("\n")
|
|
314
|
+
.map((line, i) => (i === 0 ? line : ` ${line}`))
|
|
315
|
+
.join("\n"),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
227
318
|
/**
|
|
228
319
|
* Every `.mdx` under `dir`, as `/`-separated paths relative to `base`. Walked
|
|
229
320
|
* by hand: `Dirent.parentPath` and a reliable recursive `readdirSync` post-date
|
|
@@ -281,6 +372,40 @@ function expectedRow(ref, targetDir) {
|
|
|
281
372
|
const seeds = listMdx(join(targetDir, dir), targetDir);
|
|
282
373
|
return seeds.length > 0 ? withSeeds(ref.seeded, seeds) : ref.row;
|
|
283
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* The Free `/meet` placeholder seeds on `route` (#6942): the keyless Free
|
|
377
|
+
* template's own `content` on a route the `scheduling` module contributes
|
|
378
|
+
* (`FREE_MEET_PLACEHOLDER_ROWS`, `starter-grid.ts`, read through the one
|
|
379
|
+
* registry that composes it). Empty for any other route. Throws when the Free
|
|
380
|
+
* template carries none at all, so a renamed template or a moved row fails
|
|
381
|
+
* loudly instead of re-opening the prune #6942 closed.
|
|
382
|
+
*/
|
|
383
|
+
export function meetPlaceholderSeeds(route) {
|
|
384
|
+
const scheduling = new Set((MODULE_SLICES.scheduling.pageContributions ?? []).map((row) => row.route));
|
|
385
|
+
const placeholders = (TEMPLATE_MANIFESTS["marketing-site-free"]?.roster ?? []).filter((row) => scheduling.has(row.route) && row.content !== undefined);
|
|
386
|
+
if (placeholders.length === 0) {
|
|
387
|
+
throw new Error('meetPlaceholderSeeds: "marketing-site-free" carries no /meet placeholder row on a scheduling route');
|
|
388
|
+
}
|
|
389
|
+
return placeholders.find((row) => row.route === route)?.content ?? [];
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* `roster` with the Free `/meet` placeholder seeds folded onto the
|
|
393
|
+
* `scheduling` module's code-route rows (#6942) — a row on a placeholder
|
|
394
|
+
* route that names no `content` of its own. Unconfigured, the module's
|
|
395
|
+
* `/meet` and `/meet/[slug]` pages hand off to the placeholder routes, so a
|
|
396
|
+
* seed there is still served and still declared: the scaffold's seed prune
|
|
397
|
+
* (`programmatic.ts`) and the site roster check read the roster through
|
|
398
|
+
* this. A row with its own `content` (the Free placeholder row itself) and a
|
|
399
|
+
* row on any other route are returned as is.
|
|
400
|
+
*/
|
|
401
|
+
export function withMeetPlaceholderSeeds(roster) {
|
|
402
|
+
return roster.map((row) => {
|
|
403
|
+
if (row.content !== undefined || row.contentDir !== undefined)
|
|
404
|
+
return row;
|
|
405
|
+
const seeds = meetPlaceholderSeeds(row.route);
|
|
406
|
+
return seeds.length === 0 ? row : { ...row, content: seeds };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
284
409
|
/** The bytes a scaffold installed at `path` from the template, base or overlay layer. */
|
|
285
410
|
function installedSeed(opts, path) {
|
|
286
411
|
const layers = [
|
|
@@ -313,7 +438,7 @@ export function planRosterAppend(opts) {
|
|
|
313
438
|
const inSet = new Set(opts.modules);
|
|
314
439
|
const refs = opts.references.filter((ref) => inSet.has(ref.module));
|
|
315
440
|
const contributed = new Set(refs.map((ref) => ref.contribution.route));
|
|
316
|
-
const shipped = readShippedManifest(opts.siteTemplatesDir,
|
|
441
|
+
const shipped = readShippedManifest(opts.siteTemplatesDir, roster.template.name);
|
|
317
442
|
const templateRows = [];
|
|
318
443
|
const moduleRows = new Map();
|
|
319
444
|
const superseded = [];
|
|
@@ -350,7 +475,13 @@ export function planRosterAppend(opts) {
|
|
|
350
475
|
}
|
|
351
476
|
const seeds = [];
|
|
352
477
|
for (const row of superseded) {
|
|
478
|
+
// The module's row on this route still declares a Free `/meet`
|
|
479
|
+
// placeholder seed (#6942): its unconfigured pages serve it, so the seed
|
|
480
|
+
// is kept, never removed or reported.
|
|
481
|
+
const kept = meetPlaceholderSeeds(row.route);
|
|
353
482
|
for (const path of row.content ?? []) {
|
|
483
|
+
if (kept.includes(path))
|
|
484
|
+
continue;
|
|
354
485
|
const abs = join(targetDir, path);
|
|
355
486
|
if (!existsSync(abs))
|
|
356
487
|
continue;
|
|
@@ -487,11 +618,13 @@ export function siteRosterProblems(opts) {
|
|
|
487
618
|
}),
|
|
488
619
|
};
|
|
489
620
|
});
|
|
621
|
+
// #6942 — the Free /meet placeholder seeds the scheduling module's rows keep.
|
|
622
|
+
const declared = withMeetPlaceholderSeeds(effective);
|
|
490
623
|
const installed = [...layerInstalledSeeds(roster.template, modules, opts)]
|
|
491
624
|
.filter((path) => existsSync(join(opts.targetDir, path)))
|
|
492
625
|
.sort();
|
|
493
626
|
for (const path of installed) {
|
|
494
|
-
if (!rosterDeclares(
|
|
627
|
+
if (!rosterDeclares(declared, path)) {
|
|
495
628
|
problems.push(`${path}: installed but not declared by any roster row`);
|
|
496
629
|
}
|
|
497
630
|
}
|