create-ailk 0.7.0 → 0.9.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 +112 -10
- package/component-catalog.json +798 -49
- package/component-catalog.md +9 -6
- 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 +114 -28
- 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/extract-module-bundle.js +5 -1
- 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 +89 -0
- package/dist/lib/module-license-gate.js +171 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- 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 +64 -21
- package/dist/parse-args.d.ts +88 -10
- package/dist/parse-args.js +272 -21
- package/dist/programmatic.d.ts +66 -12
- package/dist/programmatic.js +361 -34
- 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 +149 -11
- package/dist/starter-grid.d.ts +232 -0
- package/dist/starter-grid.js +545 -0
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +268 -92
- package/dist/template-manifest.d.ts +65 -29
- package/dist/template-manifest.js +161 -382
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +5 -4
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
- package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/designs/standard/DESIGN.md +103 -6
- package/site-templates/marketing-site/DESIGN.md +103 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
- package/site-templates/marketing-site-free/DESIGN.md +103 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/saas-product/DESIGN.md +103 -6
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/rules/architecture.md +7 -0
- package/templates/.claude/skills/README.md +5 -3
- package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +85 -72
- 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 +23 -0
- 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-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- 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/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +103 -6
- package/templates/README.md +63 -43
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- 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/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -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/openapi/spec.ts +63 -12
- 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/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- 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 +35 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -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/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- 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]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
- 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 +65 -17
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
- 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 +50 -37
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-identity.ts +14 -0
- 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 +45 -5
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- 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 +43 -34
- package/templates/content/en/HomePage/home.mdx +10 -22
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
- 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 -53
- 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/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/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
- /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
- /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/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Free vs paid module classification (K1, #5089 — REL-015.3 "AI Launch Kit —
|
|
3
|
-
* Commercial line", D-17 "Flavor 3").
|
|
4
|
-
*
|
|
5
|
-
* The commercial line is decided: the paid modules do NOT ship on public npm
|
|
6
|
-
* — delivery itself is the gate. `create-ailk` scaffolds Free's modules from
|
|
7
|
-
* `templates/` with no key (today's path, unchanged); a paid module is
|
|
8
|
-
* FETCHED from the internal module distribution when `AILK_LICENSE_KEY`
|
|
9
|
-
* verifies (see `fetch-module.ts` + `module-license-gate.ts`).
|
|
10
|
-
*
|
|
11
|
-
* `FREE_MODULES` is the one hand-authored list (today just `marketing`, the
|
|
12
|
-
* always-available default). `PAID_MODULES` is derived from it — every other
|
|
13
|
-
* `RECOGNIZED_MODULES` entry — so adding a new module to the vocabulary
|
|
14
|
-
* (`parse-args.ts`) defaults it to "paid" until someone deliberately adds it
|
|
15
|
-
* to `FREE_MODULES`, rather than silently shipping it free.
|
|
16
|
-
*/
|
|
17
1
|
import { type ModuleName } from "../parse-args.js";
|
|
18
2
|
/** Modules scaffolded from `templates/` with no license — today's free path. */
|
|
19
3
|
export declare const FREE_MODULES: readonly ModuleName[];
|
|
@@ -24,6 +8,51 @@ export declare const FREE_MODULES: readonly ModuleName[];
|
|
|
24
8
|
* from `RECOGNIZED_MODULES` silently.
|
|
25
9
|
*/
|
|
26
10
|
export declare const PAID_MODULES: readonly ModuleName[];
|
|
11
|
+
/**
|
|
12
|
+
* The levels a key can be at (#6759 D2). A value, never read from a flag, a
|
|
13
|
+
* manifest or a key claim — it is always DERIVED, by `deriveLevel`
|
|
14
|
+
* (`module-license-gate.ts`), from what the registry actually answers.
|
|
15
|
+
*/
|
|
16
|
+
export declare const LEVELS: readonly ["free", "pro", "pro-plus"];
|
|
17
|
+
export type Level = (typeof LEVELS)[number];
|
|
18
|
+
export declare const LEVEL_LABEL: Readonly<Record<Level, string>>;
|
|
19
|
+
/**
|
|
20
|
+
* Bought per site on top of a paid level — never switched on BY a level
|
|
21
|
+
* (OFFERINGS §4a). Naming one in `--modules` adds it without trimming
|
|
22
|
+
* (`composeModuleSet`), and it is refused below Pro naming the level it is
|
|
23
|
+
* an add-on ON TOP of.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ADD_ONS: readonly ModuleName[];
|
|
26
|
+
/**
|
|
27
|
+
* Included at Pro and above (the licence manifest's `pro` tier), but not
|
|
28
|
+
* switched on by default at any level (D1) — an authed app surface
|
|
29
|
+
* contributing no PUBLIC page to a marketing site. Naming it in `--modules`
|
|
30
|
+
* adds it without trimming, exactly like an add-on.
|
|
31
|
+
*/
|
|
32
|
+
export declare const OPT_IN_MODULES: readonly ModuleName[];
|
|
33
|
+
/**
|
|
34
|
+
* What a key at each level switches on with no `--modules` (D2). Derived —
|
|
35
|
+
* every list is `RECOGNIZED_MODULES.filter(...)`, so declaration order is
|
|
36
|
+
* preserved and a module added to the vocabulary later lands in `pro` by
|
|
37
|
+
* derivation (loud: `module-tier.test.ts`'s table test pins the exact sets).
|
|
38
|
+
*/
|
|
39
|
+
export declare const LEVEL_MODULES: Readonly<Record<Level, readonly ModuleName[]>>;
|
|
40
|
+
/**
|
|
41
|
+
* Which level (or add-on status) a module belongs to (D2). `"free"` for
|
|
42
|
+
* `FREE_MODULES`, `"add-on"` for `ADD_ONS`, `"pro-plus"` for
|
|
43
|
+
* `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — D1). Pure.
|
|
44
|
+
*/
|
|
45
|
+
export declare function levelOf(m: ModuleName): Level | "add-on";
|
|
46
|
+
/**
|
|
47
|
+
* The pure core of FR-2/FR-3 (D2): compose the effective module set from
|
|
48
|
+
* what `--modules` named and the key's derived level. Naming any module that
|
|
49
|
+
* is not an addition (`ADDITIONS`) is a KEEP — it names exactly the set,
|
|
50
|
+
* unchanged from today (NFR-2), and the level plays no part. Naming only
|
|
51
|
+
* additions (or nothing) takes the level's default plus those additions.
|
|
52
|
+
* Never refuses — refusals belong to the gate (`resolveModuleSet`), because
|
|
53
|
+
* whether a named module is covered is a registry answer, not a table lookup.
|
|
54
|
+
*/
|
|
55
|
+
export declare function composeModuleSet(named: readonly ModuleName[] | undefined, level: Level): ModuleName[];
|
|
27
56
|
export interface ModuleClassification {
|
|
28
57
|
free: ModuleName[];
|
|
29
58
|
paid: ModuleName[];
|
package/dist/lib/module-tier.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* (`parse-args.ts`) defaults it to "paid" until someone deliberately adds it
|
|
15
15
|
* to `FREE_MODULES`, rather than silently shipping it free.
|
|
16
16
|
*/
|
|
17
|
+
import { normalizeModules } from "../module-architecture.js";
|
|
17
18
|
import { RECOGNIZED_MODULES } from "../parse-args.js";
|
|
18
19
|
/** Modules scaffolded from `templates/` with no license — today's free path. */
|
|
19
20
|
export const FREE_MODULES = ["marketing"];
|
|
@@ -24,6 +25,81 @@ export const FREE_MODULES = ["marketing"];
|
|
|
24
25
|
* from `RECOGNIZED_MODULES` silently.
|
|
25
26
|
*/
|
|
26
27
|
export const PAID_MODULES = RECOGNIZED_MODULES.filter((m) => !FREE_MODULES.includes(m));
|
|
28
|
+
/**
|
|
29
|
+
* The levels a key can be at (#6759 D2). A value, never read from a flag, a
|
|
30
|
+
* manifest or a key claim — it is always DERIVED, by `deriveLevel`
|
|
31
|
+
* (`module-license-gate.ts`), from what the registry actually answers.
|
|
32
|
+
*/
|
|
33
|
+
export const LEVELS = ["free", "pro", "pro-plus"];
|
|
34
|
+
export const LEVEL_LABEL = {
|
|
35
|
+
free: "Free",
|
|
36
|
+
pro: "Pro",
|
|
37
|
+
"pro-plus": "Pro Plus",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Bought per site on top of a paid level — never switched on BY a level
|
|
41
|
+
* (OFFERINGS §4a). Naming one in `--modules` adds it without trimming
|
|
42
|
+
* (`composeModuleSet`), and it is refused below Pro naming the level it is
|
|
43
|
+
* an add-on ON TOP of.
|
|
44
|
+
*/
|
|
45
|
+
export const ADD_ONS = ["commerce"];
|
|
46
|
+
/**
|
|
47
|
+
* Included at Pro and above (the licence manifest's `pro` tier), but not
|
|
48
|
+
* switched on by default at any level (D1) — an authed app surface
|
|
49
|
+
* contributing no PUBLIC page to a marketing site. Naming it in `--modules`
|
|
50
|
+
* adds it without trimming, exactly like an add-on.
|
|
51
|
+
*/
|
|
52
|
+
export const OPT_IN_MODULES = ["workspace"];
|
|
53
|
+
/** The Pro Plus membership: what Pro Plus switches on beyond Pro. */
|
|
54
|
+
const PRO_PLUS_MODULES = ["multi-tenant"];
|
|
55
|
+
/** Named modules that never trim the level default when composing a set (D2). */
|
|
56
|
+
const ADDITIONS = [...ADD_ONS, ...OPT_IN_MODULES];
|
|
57
|
+
const PRO_EXCLUDED = new Set([...ADDITIONS, ...PRO_PLUS_MODULES]);
|
|
58
|
+
/**
|
|
59
|
+
* What a key at each level switches on with no `--modules` (D2). Derived —
|
|
60
|
+
* every list is `RECOGNIZED_MODULES.filter(...)`, so declaration order is
|
|
61
|
+
* preserved and a module added to the vocabulary later lands in `pro` by
|
|
62
|
+
* derivation (loud: `module-tier.test.ts`'s table test pins the exact sets).
|
|
63
|
+
*/
|
|
64
|
+
export const LEVEL_MODULES = Object.freeze({
|
|
65
|
+
free: [...FREE_MODULES],
|
|
66
|
+
pro: RECOGNIZED_MODULES.filter((m) => FREE_MODULES.includes(m) ||
|
|
67
|
+
(PAID_MODULES.includes(m) && !PRO_EXCLUDED.has(m))),
|
|
68
|
+
"pro-plus": RECOGNIZED_MODULES.filter((m) => FREE_MODULES.includes(m) ||
|
|
69
|
+
(PAID_MODULES.includes(m) && !ADDITIONS.includes(m))),
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* Which level (or add-on status) a module belongs to (D2). `"free"` for
|
|
73
|
+
* `FREE_MODULES`, `"add-on"` for `ADD_ONS`, `"pro-plus"` for
|
|
74
|
+
* `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — D1). Pure.
|
|
75
|
+
*/
|
|
76
|
+
export function levelOf(m) {
|
|
77
|
+
if (FREE_MODULES.includes(m))
|
|
78
|
+
return "free";
|
|
79
|
+
if (ADD_ONS.includes(m))
|
|
80
|
+
return "add-on";
|
|
81
|
+
if (PRO_PLUS_MODULES.includes(m))
|
|
82
|
+
return "pro-plus";
|
|
83
|
+
return "pro";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The pure core of FR-2/FR-3 (D2): compose the effective module set from
|
|
87
|
+
* what `--modules` named and the key's derived level. Naming any module that
|
|
88
|
+
* is not an addition (`ADDITIONS`) is a KEEP — it names exactly the set,
|
|
89
|
+
* unchanged from today (NFR-2), and the level plays no part. Naming only
|
|
90
|
+
* additions (or nothing) takes the level's default plus those additions.
|
|
91
|
+
* Never refuses — refusals belong to the gate (`resolveModuleSet`), because
|
|
92
|
+
* whether a named module is covered is a registry answer, not a table lookup.
|
|
93
|
+
*/
|
|
94
|
+
export function composeModuleSet(named, level) {
|
|
95
|
+
const namedList = [...(named ?? [])];
|
|
96
|
+
const keep = namedList.filter((m) => !ADDITIONS.includes(m));
|
|
97
|
+
if (keep.length > 0) {
|
|
98
|
+
return normalizeModules(namedList);
|
|
99
|
+
}
|
|
100
|
+
const add = namedList.filter((m) => ADDITIONS.includes(m));
|
|
101
|
+
return normalizeModules([...LEVEL_MODULES[level], ...add]);
|
|
102
|
+
}
|
|
27
103
|
/**
|
|
28
104
|
* Split a requested (already-validated, `ModuleName`) module set into its
|
|
29
105
|
* free and paid subsets, each preserving the input's relative order.
|
package/dist/lib/pro-callout.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* and a licensed install already owns the tier it pitches.
|
|
10
10
|
*/
|
|
11
11
|
import { normalizeModules } from "../module-architecture.js";
|
|
12
|
-
import { resolveTemplate } from "../template-manifest.js";
|
|
12
|
+
import { SHAPE_STARTERS, resolveTemplate } from "../template-manifest.js";
|
|
13
13
|
import { classifyModules } from "./module-tier.js";
|
|
14
14
|
export function proCallout() {
|
|
15
15
|
return [
|
|
@@ -34,7 +34,15 @@ export const LICENSED_PREREQUISITES_NOTE = "No database and no vendor API key ar
|
|
|
34
34
|
* `templateDelivery` derives "paid" from `requires` alone.
|
|
35
35
|
*/
|
|
36
36
|
export function isLicensedInstall(opts) {
|
|
37
|
-
|
|
37
|
+
// #6757 D6 row 4 — a bare-shape starter's name (the shape itself, passed
|
|
38
|
+
// here after #6754 D4 item 3 resolves it) is looked up through
|
|
39
|
+
// SHAPE_STARTERS first; `resolveTemplate` alone throws `unknown template`
|
|
40
|
+
// for one (NFR-3).
|
|
41
|
+
const requires = opts.template !== undefined
|
|
42
|
+
? Object.prototype.hasOwnProperty.call(SHAPE_STARTERS, opts.template)
|
|
43
|
+
? SHAPE_STARTERS[opts.template].requires
|
|
44
|
+
: resolveTemplate(opts.template).requires
|
|
45
|
+
: [];
|
|
38
46
|
const modules = normalizeModules([
|
|
39
47
|
...requires,
|
|
40
48
|
...(opts.modules ?? ["marketing"]),
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** What the driver observed of one route's render. */
|
|
2
|
+
export interface RenderObservation {
|
|
3
|
+
/** The requested route, e.g. `/` or `/about`. */
|
|
4
|
+
route: string;
|
|
5
|
+
/** The message `page.goto` threw with, when it threw. */
|
|
6
|
+
gotoError?: string;
|
|
7
|
+
/** The status of the response `page.goto` resolved; undefined when it resolved none. */
|
|
8
|
+
status: number | undefined;
|
|
9
|
+
/** `page.url()` after navigation settled. */
|
|
10
|
+
finalUrl: string;
|
|
11
|
+
/** The screenshot's size on disk; undefined when none was written. */
|
|
12
|
+
pngBytes: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
export type RenderVerdict = {
|
|
15
|
+
ok: true;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
reason: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether `route` rendered. Checked in order: a navigation error (a
|
|
22
|
+
* net error — `ERR_TOO_MANY_REDIRECTS` included — or a timeout), a final
|
|
23
|
+
* status other than 200, a final pathname other than the requested route (a
|
|
24
|
+
* redirect elsewhere is not a render of this route), and a missing or empty
|
|
25
|
+
* PNG. Pathnames are compared, never whole URLs, so a query or hash is fine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assessRender(observation: RenderObservation): RenderVerdict;
|
|
28
|
+
/** The slice of a Playwright `Page` that `renderRoute` uses. */
|
|
29
|
+
export interface RenderPage {
|
|
30
|
+
goto(url: string, options: {
|
|
31
|
+
waitUntil: "load";
|
|
32
|
+
timeout: number;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
status(): number;
|
|
35
|
+
} | null>;
|
|
36
|
+
url(): string;
|
|
37
|
+
screenshot(options: {
|
|
38
|
+
path: string;
|
|
39
|
+
fullPage: boolean;
|
|
40
|
+
}): Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** How long one navigation may take before it counts as a failed render. */
|
|
43
|
+
export declare const RENDER_TIMEOUT_MS = 60000;
|
|
44
|
+
/**
|
|
45
|
+
* Navigate `page` to `base + route`, screenshot it to `pngPath`, and return
|
|
46
|
+
* `assessRender`'s verdict on what happened. `status` comes from the response
|
|
47
|
+
* `page.goto` itself resolved and `finalUrl` from `page.url()` — never the
|
|
48
|
+
* requested URL, never a side `fetch` — so a redirect cannot pass as a render.
|
|
49
|
+
* A failed navigation takes no screenshot; a screenshot that throws counts
|
|
50
|
+
* as no PNG written — never a stale one left at `pngPath` by an earlier run.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderRoute(page: RenderPage, base: string, route: string, pngPath: string): Promise<RenderVerdict>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdict on one browser render of a scaffold route (#6821, spec
|
|
3
|
+
* docs/specs/6821-the-shape-design-render-matrix-spec.md D4).
|
|
4
|
+
*
|
|
5
|
+
* A design leg of `scripts/verify-scaffold.mjs` renders its scaffold's home
|
|
6
|
+
* route and one inner route in Chromium and stores a screenshot of each — the
|
|
7
|
+
* shape × design render matrix. A render that did not happen must fail the
|
|
8
|
+
* leg, and proving that rule must not cost a scaffold build, so it lives here
|
|
9
|
+
* as a pure function (`assessRender`, the `api-boot-assertion.ts` precedent)
|
|
10
|
+
* plus the one function that gathers its facts (`renderRoute`), which touches
|
|
11
|
+
* Playwright only through the `page` it is handed. The driver loads both from
|
|
12
|
+
* `dist/`; `__tests__/verify-scaffold-render.test.ts` drives them with a fake
|
|
13
|
+
* page. Nothing here imports `@playwright/test`.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, rmSync, statSync } from "node:fs";
|
|
16
|
+
/**
|
|
17
|
+
* Decide whether `route` rendered. Checked in order: a navigation error (a
|
|
18
|
+
* net error — `ERR_TOO_MANY_REDIRECTS` included — or a timeout), a final
|
|
19
|
+
* status other than 200, a final pathname other than the requested route (a
|
|
20
|
+
* redirect elsewhere is not a render of this route), and a missing or empty
|
|
21
|
+
* PNG. Pathnames are compared, never whole URLs, so a query or hash is fine.
|
|
22
|
+
*/
|
|
23
|
+
export function assessRender(observation) {
|
|
24
|
+
const { route, gotoError, status, finalUrl, pngBytes } = observation;
|
|
25
|
+
if (gotoError !== undefined) {
|
|
26
|
+
return { ok: false, reason: `navigation failed: ${gotoError}` };
|
|
27
|
+
}
|
|
28
|
+
if (status !== 200) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: status === undefined ? "navigation resolved no response" : `final status ${status}, expected 200`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const finalPath = new URL(finalUrl).pathname;
|
|
35
|
+
if (finalPath !== route) {
|
|
36
|
+
return { ok: false, reason: `requested "${route}" but the page settled on "${finalPath}"` };
|
|
37
|
+
}
|
|
38
|
+
if (typeof pngBytes !== "number" || pngBytes <= 0) {
|
|
39
|
+
return { ok: false, reason: pngBytes === 0 ? "the screenshot PNG is empty" : "no screenshot PNG was written" };
|
|
40
|
+
}
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
/** How long one navigation may take before it counts as a failed render. */
|
|
44
|
+
export const RENDER_TIMEOUT_MS = 60_000;
|
|
45
|
+
/**
|
|
46
|
+
* Navigate `page` to `base + route`, screenshot it to `pngPath`, and return
|
|
47
|
+
* `assessRender`'s verdict on what happened. `status` comes from the response
|
|
48
|
+
* `page.goto` itself resolved and `finalUrl` from `page.url()` — never the
|
|
49
|
+
* requested URL, never a side `fetch` — so a redirect cannot pass as a render.
|
|
50
|
+
* A failed navigation takes no screenshot; a screenshot that throws counts
|
|
51
|
+
* as no PNG written — never a stale one left at `pngPath` by an earlier run.
|
|
52
|
+
*/
|
|
53
|
+
export async function renderRoute(page, base, route, pngPath) {
|
|
54
|
+
let status;
|
|
55
|
+
try {
|
|
56
|
+
const response = await page.goto(base + route, { waitUntil: "load", timeout: RENDER_TIMEOUT_MS });
|
|
57
|
+
status = response === null ? undefined : response.status();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return assessRender({
|
|
61
|
+
route,
|
|
62
|
+
gotoError: err instanceof Error ? err.message : String(err),
|
|
63
|
+
status: undefined,
|
|
64
|
+
finalUrl: page.url(),
|
|
65
|
+
pngBytes: undefined,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
rmSync(pngPath, { force: true });
|
|
69
|
+
try {
|
|
70
|
+
await page.screenshot({ path: pngPath, fullPage: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Falls through to the missing-PNG check below.
|
|
74
|
+
}
|
|
75
|
+
const pngBytes = existsSync(pngPath) ? statSync(pngPath).size : undefined;
|
|
76
|
+
return assessRender({ route, status, finalUrl: page.url(), pngBytes });
|
|
77
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** One redirect the driver followed: the status and the raw `Location`. */
|
|
2
|
+
export interface RedirectHop {
|
|
3
|
+
status: number;
|
|
4
|
+
location: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface SweepVerdict {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
/** A passing page verdict names the own-content marker that matched. */
|
|
11
|
+
export interface PageRenderVerdict extends SweepVerdict {
|
|
12
|
+
marker?: PageMarker;
|
|
13
|
+
}
|
|
14
|
+
export type PageMarker = "h1" | "auth-card" | "sign-in-gate";
|
|
15
|
+
/** More hops than this is a redirect chain, not a render. */
|
|
16
|
+
export declare const MAX_REDIRECTS = 5;
|
|
17
|
+
/**
|
|
18
|
+
* Routes that are correct to answer 404 in every leg, and the licence claim
|
|
19
|
+
* that gates them. No leg's served app holds a signed licence — the driver
|
|
20
|
+
* scrubs `AILK_LICENSE_KEY`, and the mock key is a fixture string, not a JWT —
|
|
21
|
+
* so each entry must answer exactly 404. A 200 means the gate stopped gating.
|
|
22
|
+
*/
|
|
23
|
+
export declare const CLAIM_GATED_ROUTES: Readonly<Record<string, string>>;
|
|
24
|
+
/**
|
|
25
|
+
* A route whose live page needs an env var the driver never sets (#6146).
|
|
26
|
+
* While it is unset the route serves its Free placeholder seed instead
|
|
27
|
+
* (#6942): a 200 whose `<h1>` carries the `CONFIG_GATED_PLACEHOLDERS`
|
|
28
|
+
* heading. A 404 is the bug — the site's own nav link dead — and a rendered
|
|
29
|
+
* page without that heading means the gate stopped gating. Unlike
|
|
30
|
+
* `CLAIM_GATED_ROUTES`, a route absent from the route table is not stale: the
|
|
31
|
+
* module that owns it may simply not be installed in this leg.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CONFIG_GATED_ROUTES: Readonly<Record<string, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* The placeholder heading each `CONFIG_GATED_ROUTES` entry renders while its
|
|
36
|
+
* env var is unset — for `/meet`, the hero headline of the overlay's
|
|
37
|
+
* `content/en/LandingPage/meet.mdx` seed. A fresh scaffold never edits it.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CONFIG_GATED_PLACEHOLDERS: Readonly<Record<string, string>>;
|
|
40
|
+
/** Reachability only — for routes that are not pages (`/og`, `/llms.txt`, …). */
|
|
41
|
+
export declare function assessRouteStatus(input: {
|
|
42
|
+
route: string;
|
|
43
|
+
hops: RedirectHop[];
|
|
44
|
+
status: number | null;
|
|
45
|
+
}): SweepVerdict;
|
|
46
|
+
/**
|
|
47
|
+
* A `CLAIM_GATED_ROUTES` entry: it must be in the route table (a missing one
|
|
48
|
+
* is stale) and answer exactly 404.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assessClaimGatedRoute(input: {
|
|
51
|
+
route: string;
|
|
52
|
+
routeTable: string[];
|
|
53
|
+
hops: RedirectHop[];
|
|
54
|
+
status: number | null;
|
|
55
|
+
}): SweepVerdict;
|
|
56
|
+
/**
|
|
57
|
+
* Decide whether a page route rendered, under the real locale, with its own
|
|
58
|
+
* content. The first failed rule is the reason:
|
|
59
|
+
*
|
|
60
|
+
* 1. no self-redirect, and at most `MAX_REDIRECTS` hops;
|
|
61
|
+
* 2. a final 200;
|
|
62
|
+
* 3. an HTML response;
|
|
63
|
+
* 4. `<html lang>` equal to `locale` — a page under a bogus locale fails;
|
|
64
|
+
* 5. no streamed error or not-found digest;
|
|
65
|
+
* 6. an own-content marker — a non-empty `<h1>` (a content page), an
|
|
66
|
+
* `auth-card` testid (an `(auth)` page), or a `sign-in-gate` testid (an
|
|
67
|
+
* `(authed)` page seen anonymously). The layout carries none of them.
|
|
68
|
+
*/
|
|
69
|
+
export declare function assessPageRender(input: {
|
|
70
|
+
route: string;
|
|
71
|
+
hops: RedirectHop[];
|
|
72
|
+
status: number | null;
|
|
73
|
+
contentType: string | null;
|
|
74
|
+
html: string;
|
|
75
|
+
locale: string;
|
|
76
|
+
}): PageRenderVerdict;
|
|
77
|
+
/**
|
|
78
|
+
* A `CONFIG_GATED_ROUTES` entry. Unlike `assessClaimGatedRoute`, a route
|
|
79
|
+
* absent from the route table is not stale — the module that owns it may
|
|
80
|
+
* simply not be installed in this leg — so it PASSES, skipped, with no
|
|
81
|
+
* network call needed to decide that (mirroring how `assessClaimGatedRoute`
|
|
82
|
+
* is called with sentinel `hops`/`status` for its own absent case, never
|
|
83
|
+
* fetched). Once the route IS present it is scored as an ordinary page via
|
|
84
|
+
* `assessPageRender`; with its gating env var unset it must ALSO be the
|
|
85
|
+
* placeholder — an `<h1>` carrying its `CONFIG_GATED_PLACEHOLDERS` heading.
|
|
86
|
+
* Every failure with the env var unset names it, pinning the cause.
|
|
87
|
+
*/
|
|
88
|
+
export declare function assessConfigGatedRoute(input: {
|
|
89
|
+
route: string;
|
|
90
|
+
envVar: string;
|
|
91
|
+
envSet: boolean;
|
|
92
|
+
routeTable: string[];
|
|
93
|
+
hops: RedirectHop[];
|
|
94
|
+
status: number | null;
|
|
95
|
+
contentType: string | null;
|
|
96
|
+
html: string;
|
|
97
|
+
locale: string;
|
|
98
|
+
}): SweepVerdict;
|
|
99
|
+
/**
|
|
100
|
+
* Every FILE under `dir`, recursing into directories, as sorted slash-joined
|
|
101
|
+
* paths relative to it — so `public/.well-known/security.txt` is listed and
|
|
102
|
+
* the `.well-known` directory never is. Anything that is neither (a symlink,
|
|
103
|
+
* a socket) throws rather than being silently followed or skipped.
|
|
104
|
+
*/
|
|
105
|
+
export declare function listPublicFiles(dir: string): string[];
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdicts of the scaffold render sweep (#6146).
|
|
3
|
+
*
|
|
4
|
+
* `scripts/verify-scaffold.mjs` serves a built scaffold and fetches every
|
|
5
|
+
* declared route. Until #6146 it served on `-H 127.0.0.1`, which made Next
|
|
6
|
+
* proxy next-intl's locale rewrite as an EXTERNAL one back into the same
|
|
7
|
+
* server, so every locale page answered a 307 to itself — and the driver
|
|
8
|
+
* recorded each as a "known gap" and passed. No request in a leg rendered a
|
|
9
|
+
* page, and a scaffold that 500'd on every page (#6107) stayed green.
|
|
10
|
+
*
|
|
11
|
+
* The rules that decide whether a route really rendered live here, apart from
|
|
12
|
+
* the driver, for the reason `api-boot-assertion.ts` does: proving them must
|
|
13
|
+
* not cost a templates build, an install and a `next build`. Each is a pure
|
|
14
|
+
* function of what the driver observed, so
|
|
15
|
+
* `__tests__/verify-scaffold-render-sweep.test.ts` drives them directly. The
|
|
16
|
+
* driver loads this module from `dist/`. Only `listPublicFiles` touches the
|
|
17
|
+
* filesystem, and none of it touches the network.
|
|
18
|
+
*/
|
|
19
|
+
import { readdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
/** More hops than this is a redirect chain, not a render. */
|
|
22
|
+
export const MAX_REDIRECTS = 5;
|
|
23
|
+
/**
|
|
24
|
+
* Routes that are correct to answer 404 in every leg, and the licence claim
|
|
25
|
+
* that gates them. No leg's served app holds a signed licence — the driver
|
|
26
|
+
* scrubs `AILK_LICENSE_KEY`, and the mock key is a fixture string, not a JWT —
|
|
27
|
+
* so each entry must answer exactly 404. A 200 means the gate stopped gating.
|
|
28
|
+
*/
|
|
29
|
+
export const CLAIM_GATED_ROUTES = {
|
|
30
|
+
"/purchase/success": "commerce",
|
|
31
|
+
"/purchase/cancel": "commerce",
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A route whose live page needs an env var the driver never sets (#6146).
|
|
35
|
+
* While it is unset the route serves its Free placeholder seed instead
|
|
36
|
+
* (#6942): a 200 whose `<h1>` carries the `CONFIG_GATED_PLACEHOLDERS`
|
|
37
|
+
* heading. A 404 is the bug — the site's own nav link dead — and a rendered
|
|
38
|
+
* page without that heading means the gate stopped gating. Unlike
|
|
39
|
+
* `CLAIM_GATED_ROUTES`, a route absent from the route table is not stale: the
|
|
40
|
+
* module that owns it may simply not be installed in this leg.
|
|
41
|
+
*/
|
|
42
|
+
export const CONFIG_GATED_ROUTES = {
|
|
43
|
+
"/meet": "SCHEDULING_CAL_LINK",
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The placeholder heading each `CONFIG_GATED_ROUTES` entry renders while its
|
|
47
|
+
* env var is unset — for `/meet`, the hero headline of the overlay's
|
|
48
|
+
* `content/en/LandingPage/meet.mdx` seed. A fresh scaffold never edits it.
|
|
49
|
+
*/
|
|
50
|
+
export const CONFIG_GATED_PLACEHOLDERS = {
|
|
51
|
+
"/meet": "{Book a time with us}",
|
|
52
|
+
};
|
|
53
|
+
const LOOPBACK_BASE = "http://localhost";
|
|
54
|
+
function pathnameOf(location, relativeTo) {
|
|
55
|
+
return new URL(location, new URL(relativeTo, LOOPBACK_BASE)).pathname;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rules 1 and 2 of every verdict: no self-redirect, at most `MAX_REDIRECTS`
|
|
59
|
+
* hops, and a final 200. A hop is a self-redirect when its `Location`
|
|
60
|
+
* pathname equals the pathname that hop answered — the route itself for the
|
|
61
|
+
* first hop, the previous hop's target after that. Query and host are not
|
|
62
|
+
* compared: the next-intl loop this catches differs from its request in
|
|
63
|
+
* neither.
|
|
64
|
+
*/
|
|
65
|
+
function assessReachable(route, hops, status) {
|
|
66
|
+
let answered = route;
|
|
67
|
+
for (const hop of hops) {
|
|
68
|
+
if (hop.location === null)
|
|
69
|
+
break;
|
|
70
|
+
const target = pathnameOf(hop.location, answered);
|
|
71
|
+
if (target === new URL(answered, LOOPBACK_BASE).pathname) {
|
|
72
|
+
return { ok: false, reason: `${answered} -> ${hop.status} self-redirect` };
|
|
73
|
+
}
|
|
74
|
+
answered = target;
|
|
75
|
+
}
|
|
76
|
+
if (hops.length > MAX_REDIRECTS) {
|
|
77
|
+
return { ok: false, reason: `${route} -> redirect chain of more than ${MAX_REDIRECTS} hops` };
|
|
78
|
+
}
|
|
79
|
+
if (status !== 200)
|
|
80
|
+
return { ok: false, reason: `${route} -> status ${status}` };
|
|
81
|
+
return { ok: true, reason: "200" };
|
|
82
|
+
}
|
|
83
|
+
/** Reachability only — for routes that are not pages (`/og`, `/llms.txt`, …). */
|
|
84
|
+
export function assessRouteStatus(input) {
|
|
85
|
+
return assessReachable(input.route, input.hops, input.status);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A `CLAIM_GATED_ROUTES` entry: it must be in the route table (a missing one
|
|
89
|
+
* is stale) and answer exactly 404.
|
|
90
|
+
*/
|
|
91
|
+
export function assessClaimGatedRoute(input) {
|
|
92
|
+
const { route, routeTable, hops, status } = input;
|
|
93
|
+
const claim = CLAIM_GATED_ROUTES[route];
|
|
94
|
+
if (!routeTable.some((r) => (r.replace("/[locale]", "") || "/") === route)) {
|
|
95
|
+
return { ok: false, reason: `${route} -> stale CLAIM_GATED_ROUTES entry: not in the route table` };
|
|
96
|
+
}
|
|
97
|
+
if (hops.length > 0 || status !== 404) {
|
|
98
|
+
const first = hops[0];
|
|
99
|
+
const seen = first ? `${first.status} redirect` : `status ${status}`;
|
|
100
|
+
const why = status === 200 ? "gate open in a keyless serve" : `expected 404, got ${seen}`;
|
|
101
|
+
return { ok: false, reason: `${route} -> ${why} (claim-gated: ${claim})` };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, reason: `404 (claim-gated: ${claim})` };
|
|
104
|
+
}
|
|
105
|
+
/** The `lang` of the document's `<html>` element, or null when it has none. */
|
|
106
|
+
function htmlLang(html) {
|
|
107
|
+
return /<html\b[^>]*\slang="([^"]*)"/i.exec(html)?.[1] ?? null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The first React Server Components error digest in the flight payload, or
|
|
111
|
+
* null. It is serialized inside a script string as `\"digest\":\"<value>\"`.
|
|
112
|
+
* An error or not-found boundary streamed after the shell answers 200 and can
|
|
113
|
+
* carry its own `<h1>`; this entry is the one reliable trace it leaves.
|
|
114
|
+
*/
|
|
115
|
+
function flightDigest(html) {
|
|
116
|
+
return /\\"digest\\":\\"([^"\\]*)\\"/.exec(html)?.[1] ?? null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Whether an `<h1>`'s inner HTML carries a name: text once tags are stripped,
|
|
120
|
+
* or an image with a non-empty `alt` (the Hero's lockup renders the wordmark
|
|
121
|
+
* inside the `<h1>`, and the heading takes its accessible name from it).
|
|
122
|
+
*/
|
|
123
|
+
function headingHasName(inner) {
|
|
124
|
+
if (inner.replace(/<[^>]*>/g, "").trim() !== "")
|
|
125
|
+
return true;
|
|
126
|
+
return [...inner.matchAll(/<img\b[^>]*\salt="([^"]*)"/gi)].some((m) => (m[1] ?? "").trim() !== "");
|
|
127
|
+
}
|
|
128
|
+
/** The first own-content marker the page carries, in the order checked. */
|
|
129
|
+
function ownContentMarker(html) {
|
|
130
|
+
for (const m of html.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi)) {
|
|
131
|
+
if (headingHasName(m[1] ?? ""))
|
|
132
|
+
return "h1";
|
|
133
|
+
}
|
|
134
|
+
if (html.includes('data-testid="auth-card"'))
|
|
135
|
+
return "auth-card";
|
|
136
|
+
if (html.includes('data-testid="sign-in-gate'))
|
|
137
|
+
return "sign-in-gate";
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Decide whether a page route rendered, under the real locale, with its own
|
|
142
|
+
* content. The first failed rule is the reason:
|
|
143
|
+
*
|
|
144
|
+
* 1. no self-redirect, and at most `MAX_REDIRECTS` hops;
|
|
145
|
+
* 2. a final 200;
|
|
146
|
+
* 3. an HTML response;
|
|
147
|
+
* 4. `<html lang>` equal to `locale` — a page under a bogus locale fails;
|
|
148
|
+
* 5. no streamed error or not-found digest;
|
|
149
|
+
* 6. an own-content marker — a non-empty `<h1>` (a content page), an
|
|
150
|
+
* `auth-card` testid (an `(auth)` page), or a `sign-in-gate` testid (an
|
|
151
|
+
* `(authed)` page seen anonymously). The layout carries none of them.
|
|
152
|
+
*/
|
|
153
|
+
export function assessPageRender(input) {
|
|
154
|
+
const { route, hops, status, contentType, html, locale } = input;
|
|
155
|
+
const reachable = assessReachable(route, hops, status);
|
|
156
|
+
if (!reachable.ok)
|
|
157
|
+
return reachable;
|
|
158
|
+
if (!(contentType ?? "").startsWith("text/html")) {
|
|
159
|
+
return { ok: false, reason: `${route} -> not html (${contentType})` };
|
|
160
|
+
}
|
|
161
|
+
const lang = htmlLang(html);
|
|
162
|
+
if (lang !== locale)
|
|
163
|
+
return { ok: false, reason: `${route} -> rendered under locale "${lang}"` };
|
|
164
|
+
const digest = flightDigest(html);
|
|
165
|
+
if (digest !== null) {
|
|
166
|
+
const why = digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;404")
|
|
167
|
+
? "streamed not-found"
|
|
168
|
+
: `streamed error (digest ${digest})`;
|
|
169
|
+
return { ok: false, reason: `${route} -> ${why}` };
|
|
170
|
+
}
|
|
171
|
+
const marker = ownContentMarker(html);
|
|
172
|
+
if (marker === null) {
|
|
173
|
+
return { ok: false, reason: `${route} -> no own content (no <h1>, auth-card or sign-in-gate)` };
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, reason: marker, marker };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* A `CONFIG_GATED_ROUTES` entry. Unlike `assessClaimGatedRoute`, a route
|
|
179
|
+
* absent from the route table is not stale — the module that owns it may
|
|
180
|
+
* simply not be installed in this leg — so it PASSES, skipped, with no
|
|
181
|
+
* network call needed to decide that (mirroring how `assessClaimGatedRoute`
|
|
182
|
+
* is called with sentinel `hops`/`status` for its own absent case, never
|
|
183
|
+
* fetched). Once the route IS present it is scored as an ordinary page via
|
|
184
|
+
* `assessPageRender`; with its gating env var unset it must ALSO be the
|
|
185
|
+
* placeholder — an `<h1>` carrying its `CONFIG_GATED_PLACEHOLDERS` heading.
|
|
186
|
+
* Every failure with the env var unset names it, pinning the cause.
|
|
187
|
+
*/
|
|
188
|
+
export function assessConfigGatedRoute(input) {
|
|
189
|
+
const { route, envVar, envSet, routeTable, hops, status, contentType, html, locale } = input;
|
|
190
|
+
if (!routeTable.some((r) => (r.replace("/[locale]", "") || "/") === route)) {
|
|
191
|
+
return { ok: true, reason: `${route} -> skipped: not in the route table (module not installed)` };
|
|
192
|
+
}
|
|
193
|
+
const rendered = assessPageRender({ route, hops, status, contentType, html, locale });
|
|
194
|
+
if (envSet)
|
|
195
|
+
return rendered;
|
|
196
|
+
if (!rendered.ok)
|
|
197
|
+
return { ok: false, reason: `${rendered.reason} (${envVar} unset)` };
|
|
198
|
+
const placeholder = CONFIG_GATED_PLACEHOLDERS[route];
|
|
199
|
+
if (placeholder === undefined) {
|
|
200
|
+
return { ok: false, reason: `${route} -> no CONFIG_GATED_PLACEHOLDERS entry to check with ${envVar} unset` };
|
|
201
|
+
}
|
|
202
|
+
if (!headingTexts(html).some((text) => text.includes(placeholder))) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
reason: `${route} -> expected the "${placeholder}" placeholder with ${envVar} unset, rendered another page`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { ok: true, reason: `placeholder (${envVar} unset)` };
|
|
209
|
+
}
|
|
210
|
+
/** The text of every `<h1>` in the document, tags stripped. */
|
|
211
|
+
function headingTexts(html) {
|
|
212
|
+
return [...html.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi)].map((m) => (m[1] ?? "").replace(/<[^>]*>/g, ""));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Every FILE under `dir`, recursing into directories, as sorted slash-joined
|
|
216
|
+
* paths relative to it — so `public/.well-known/security.txt` is listed and
|
|
217
|
+
* the `.well-known` directory never is. Anything that is neither (a symlink,
|
|
218
|
+
* a socket) throws rather than being silently followed or skipped.
|
|
219
|
+
*/
|
|
220
|
+
export function listPublicFiles(dir) {
|
|
221
|
+
return listFilesUnder(dir, "");
|
|
222
|
+
}
|
|
223
|
+
function listFilesUnder(dir, relPrefix) {
|
|
224
|
+
const out = [];
|
|
225
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
228
|
+
if (entry.isDirectory())
|
|
229
|
+
out.push(...listFilesUnder(join(dir, entry.name), rel));
|
|
230
|
+
else if (entry.isFile())
|
|
231
|
+
out.push(rel);
|
|
232
|
+
else
|
|
233
|
+
throw new Error(`public/${rel} is neither a file nor a directory`);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|