create-ailk 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
// i18n S2 — Middleware locale detection priority chain tests (#949)
|
|
5
|
+
//
|
|
6
|
+
// Tests that the composed next-intl + security-headers middleware preserves
|
|
7
|
+
// security headers on every response, and that the middleware config exports
|
|
8
|
+
// the expected matcher pattern excluding static assets.
|
|
9
|
+
//
|
|
10
|
+
// Note: next-intl middleware runs in the Next.js Edge runtime and relies on
|
|
11
|
+
// Next.js internals (URL rewriting, response piping) that cannot be fully
|
|
12
|
+
// exercised in a jest/node environment. We test:
|
|
13
|
+
// 1. Security headers are preserved (S16 preservation guarantee)
|
|
14
|
+
// 2. Matcher config excludes _next/static, _next/image, favicon.ico
|
|
15
|
+
// 3. The module initialises without throwing (locales read correctly)
|
|
16
|
+
//
|
|
17
|
+
// Locale list is now read from NEXT_PUBLIC_ENABLED_LOCALES env var (baked at
|
|
18
|
+
// build time by next.config.mjs). Tests control the env var directly rather
|
|
19
|
+
// than mocking lib/locales (which no longer runs in the Edge middleware path).
|
|
20
|
+
|
|
21
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
22
|
+
|
|
23
|
+
// Mock next-intl middleware so it returns a plain NextResponse (no Edge runtime needed).
|
|
24
|
+
jest.mock("next-intl/middleware", () => {
|
|
25
|
+
return function createMiddleware() {
|
|
26
|
+
return function (_req: unknown) {
|
|
27
|
+
return NextResponse.next();
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
import { middleware, config } from "../../middleware";
|
|
33
|
+
|
|
34
|
+
function makeRequest(
|
|
35
|
+
path: string,
|
|
36
|
+
headers: Record<string, string> = {},
|
|
37
|
+
): NextRequest {
|
|
38
|
+
return new NextRequest(new URL(path, "http://localhost:3000"), {
|
|
39
|
+
headers: { host: "localhost:3000", ...headers },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("i18n middleware — security headers preserved (S16 contract)", () => {
|
|
44
|
+
it("applies Content-Security-Policy to every response", async () => {
|
|
45
|
+
const res = await middleware(makeRequest("/"));
|
|
46
|
+
expect(res.headers.get("Content-Security-Policy")).not.toBeNull();
|
|
47
|
+
expect(res.headers.get("Content-Security-Policy")).toContain(
|
|
48
|
+
"frame-ancestors 'none'",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("applies Strict-Transport-Security", async () => {
|
|
53
|
+
const res = await middleware(makeRequest("/"));
|
|
54
|
+
expect(res.headers.get("Strict-Transport-Security")).toContain(
|
|
55
|
+
"max-age=63072000",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("applies X-Content-Type-Options: nosniff", async () => {
|
|
60
|
+
const res = await middleware(makeRequest("/"));
|
|
61
|
+
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("applies X-Frame-Options: DENY", async () => {
|
|
65
|
+
const res = await middleware(makeRequest("/"));
|
|
66
|
+
expect(res.headers.get("X-Frame-Options")).toBe("DENY");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("applies Referrer-Policy", async () => {
|
|
70
|
+
const res = await middleware(makeRequest("/"));
|
|
71
|
+
expect(res.headers.get("Referrer-Policy")).toBe(
|
|
72
|
+
"strict-origin-when-cross-origin",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("applies Permissions-Policy", async () => {
|
|
77
|
+
const res = await middleware(makeRequest("/about"));
|
|
78
|
+
expect(res.headers.get("Permissions-Policy")).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("i18n middleware — locale config reads from NEXT_PUBLIC_ENABLED_LOCALES", () => {
|
|
83
|
+
it("middleware initialises without throwing when env var is unset (falls back to [en])", async () => {
|
|
84
|
+
// NEXT_PUBLIC_ENABLED_LOCALES is not set in test env — middleware falls back to ["en"].
|
|
85
|
+
const res = await middleware(makeRequest("/"));
|
|
86
|
+
expect(res).toBeDefined();
|
|
87
|
+
expect(res.headers.get("Content-Security-Policy")).not.toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("middleware does not throw for two-locale config", async () => {
|
|
91
|
+
// Middleware reads env at module load time. This test verifies the
|
|
92
|
+
// middleware still handles requests regardless of locale path.
|
|
93
|
+
const res = await middleware(makeRequest("/es/about"));
|
|
94
|
+
expect(res).toBeDefined();
|
|
95
|
+
expect(res.headers.get("Content-Security-Policy")).not.toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("i18n middleware — matcher config", () => {
|
|
100
|
+
it("excludes _next/static from matcher", () => {
|
|
101
|
+
const matcher = Array.isArray(config.matcher)
|
|
102
|
+
? config.matcher.join(",")
|
|
103
|
+
: config.matcher;
|
|
104
|
+
expect(matcher).toContain("_next");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("excludes favicon.ico from matcher", () => {
|
|
108
|
+
const matcher = Array.isArray(config.matcher)
|
|
109
|
+
? config.matcher.join(",")
|
|
110
|
+
: config.matcher;
|
|
111
|
+
expect(matcher).toContain("favicon.ico");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("matcher is a string (single pattern) or array", () => {
|
|
115
|
+
expect(
|
|
116
|
+
typeof config.matcher === "string" || Array.isArray(config.matcher),
|
|
117
|
+
).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
// i18n S2 — Routing integration tests (#949)
|
|
5
|
+
//
|
|
6
|
+
// Tests the 5 AC routing cases from the spec §5.1 at the middleware level.
|
|
7
|
+
// Full curl integration verification (live HTTP server) is in the PR checklist.
|
|
8
|
+
//
|
|
9
|
+
// What we test in unit scope:
|
|
10
|
+
// - Middleware does not throw for any of the 5 locale path shapes
|
|
11
|
+
// - Security headers are always applied (S16 preservation)
|
|
12
|
+
// - NEXT_PUBLIC_ENABLED_LOCALES controls the locale list (env var, baked at build)
|
|
13
|
+
// - Middleware config has the correct matcher (static assets excluded)
|
|
14
|
+
|
|
15
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
16
|
+
|
|
17
|
+
// Stub next-intl so we can test middleware composition without Edge runtime.
|
|
18
|
+
// next-intl's actual locale detection is tested by the library's own suite;
|
|
19
|
+
// our job is to verify the composition (security headers applied, no throws).
|
|
20
|
+
jest.mock("next-intl/middleware", () => {
|
|
21
|
+
return function createMiddleware() {
|
|
22
|
+
return function (_req: unknown) {
|
|
23
|
+
return NextResponse.next();
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeRequest(
|
|
29
|
+
path: string,
|
|
30
|
+
headers: Record<string, string> = {},
|
|
31
|
+
): NextRequest {
|
|
32
|
+
return new NextRequest(new URL(path, "http://localhost:3000"), {
|
|
33
|
+
headers: { host: "localhost:3000", ...headers },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// AC-1: curl http://localhost:3000/ returns 200 with English content + lang=en
|
|
38
|
+
describe("AC-1: bare default-locale URL / → middleware OK", () => {
|
|
39
|
+
it("does not throw for bare / path", async () => {
|
|
40
|
+
const { middleware } = await import("../../middleware");
|
|
41
|
+
const res = await middleware(makeRequest("/"));
|
|
42
|
+
expect(res.status).not.toBe(500);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("security headers present on /", async () => {
|
|
46
|
+
const { middleware } = await import("../../middleware");
|
|
47
|
+
const res = await middleware(makeRequest("/"));
|
|
48
|
+
expect(res.headers.get("Content-Security-Policy")).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// AC-2: curl http://localhost:3000/en/ returns 200 with English content + lang=en
|
|
53
|
+
describe("AC-2: /en/ prefix → middleware OK", () => {
|
|
54
|
+
it("does not throw for /en/ path", async () => {
|
|
55
|
+
const { middleware } = await import("../../middleware");
|
|
56
|
+
const res = await middleware(makeRequest("/en/"));
|
|
57
|
+
expect(res.status).not.toBe(500);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("security headers present on /en/", async () => {
|
|
61
|
+
const { middleware } = await import("../../middleware");
|
|
62
|
+
const res = await middleware(makeRequest("/en/"));
|
|
63
|
+
expect(res.headers.get("Strict-Transport-Security")).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// AC-3: curl -H "Accept-Language: es" http://localhost:3000/ → 200 English fallback
|
|
68
|
+
describe("AC-3: Accept-Language: es on / → no error (English fallback)", () => {
|
|
69
|
+
it("does not throw with es accept-language when only en is enabled", async () => {
|
|
70
|
+
const { middleware } = await import("../../middleware");
|
|
71
|
+
const res = await middleware(
|
|
72
|
+
makeRequest("/", { "accept-language": "es,en;q=0.8" }),
|
|
73
|
+
);
|
|
74
|
+
expect(res.status).not.toBe(500);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("security headers still applied with es accept-language", async () => {
|
|
78
|
+
const { middleware } = await import("../../middleware");
|
|
79
|
+
const res = await middleware(makeRequest("/", { "accept-language": "es" }));
|
|
80
|
+
expect(res.headers.get("Content-Security-Policy")).not.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// AC-4: /es/ with locales:[en,es] → 200 English fallback + lang=es
|
|
85
|
+
// NEXT_PUBLIC_ENABLED_LOCALES is baked at build time; in tests we verify
|
|
86
|
+
// middleware handles the path without throwing (actual locale routing is
|
|
87
|
+
// tested via next-intl's own suite and the curl checklist).
|
|
88
|
+
describe("AC-4: /es/ path → middleware handles without throwing", () => {
|
|
89
|
+
it("does not throw for /es/ path", async () => {
|
|
90
|
+
const { middleware } = await import("../../middleware");
|
|
91
|
+
const res = await middleware(makeRequest("/es/"));
|
|
92
|
+
expect(res.status).not.toBe(500);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// AC-5: /es/ with locales:[en] only → 404 from next-intl's router
|
|
97
|
+
// In unit test scope, we verify the middleware doesn't throw — the actual 404
|
|
98
|
+
// from the Next.js router (not middleware layer) is verified via curl checklist.
|
|
99
|
+
describe("AC-5: /es/ with locales:[en] only → middleware does not throw", () => {
|
|
100
|
+
it("middleware processes /es/ request without throwing", async () => {
|
|
101
|
+
const { middleware } = await import("../../middleware");
|
|
102
|
+
// Middleware itself doesn't throw — Next.js router returns 404
|
|
103
|
+
const res = await middleware(makeRequest("/es/"));
|
|
104
|
+
expect(res).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("locale list falls back to [en] when NEXT_PUBLIC_ENABLED_LOCALES unset", () => {
|
|
108
|
+
// In test env, NEXT_PUBLIC_ENABLED_LOCALES is not set → middleware uses ["en"].
|
|
109
|
+
// This mirrors the next.config.mjs fallback behavior at build time.
|
|
110
|
+
const localeEnv = process.env.NEXT_PUBLIC_ENABLED_LOCALES;
|
|
111
|
+
const locales = (localeEnv ?? "en").split(",");
|
|
112
|
+
expect(locales).toEqual(["en"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
// i18n S2 — Translation API tests (#949)
|
|
5
|
+
//
|
|
6
|
+
// Tests that:
|
|
7
|
+
// 1. messages/en.json has the required namespace structure (consent + common)
|
|
8
|
+
// 2. All leaf values are non-empty strings
|
|
9
|
+
// 3. The i18n.ts module exports a function (getRequestConfig wrapper)
|
|
10
|
+
// 4. The catalog loads correctly for the 'en' locale via the i18n loader
|
|
11
|
+
// 5. Missing locale catalog falls back to English (no catalog for 'es')
|
|
12
|
+
|
|
13
|
+
// next-intl/server relies on the Next.js request context and cannot be called
|
|
14
|
+
// directly in jest. We test i18n.ts's internal loader logic by mocking the
|
|
15
|
+
// server context and verifying the messages returned.
|
|
16
|
+
|
|
17
|
+
// Mock next-intl/server so getRequestConfig is a passthrough that calls our fn.
|
|
18
|
+
jest.mock("next-intl/server", () => ({
|
|
19
|
+
getRequestConfig: (fn: unknown) => fn,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import messages from "../../messages/en.json";
|
|
23
|
+
|
|
24
|
+
describe("message catalog — en.json structure", () => {
|
|
25
|
+
it("has top-level 'consent' namespace", () => {
|
|
26
|
+
expect(messages).toHaveProperty("consent");
|
|
27
|
+
expect(typeof messages.consent).toBe("object");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("has top-level 'common' namespace", () => {
|
|
31
|
+
expect(messages).toHaveProperty("common");
|
|
32
|
+
expect(typeof messages.common).toBe("object");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("consent namespace contains required keys for S3 ConsentBanner", () => {
|
|
36
|
+
const { consent } = messages;
|
|
37
|
+
expect(consent).toHaveProperty("title");
|
|
38
|
+
expect(consent).toHaveProperty("body");
|
|
39
|
+
expect(consent).toHaveProperty("acceptLabel");
|
|
40
|
+
expect(consent).toHaveProperty("rejectLabel");
|
|
41
|
+
expect(consent).toHaveProperty("policyLinkLabel");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("common namespace contains siteTitle key", () => {
|
|
45
|
+
const { common } = messages;
|
|
46
|
+
expect(common).toHaveProperty("siteTitle");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("all leaf values are non-empty strings", () => {
|
|
50
|
+
// Walk the object tree asserting every leaf is a non-empty string.
|
|
51
|
+
// JSON.parse produces unknown, so we use a typed entry helper.
|
|
52
|
+
const parsed: unknown = JSON.parse(JSON.stringify(messages));
|
|
53
|
+
function assertLeafStrings(obj: unknown, path = ""): void {
|
|
54
|
+
if (typeof obj !== "object" || obj === null) {
|
|
55
|
+
expect(typeof obj).toBe("string");
|
|
56
|
+
expect((obj as string).length).toBeGreaterThan(0);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const [key, value] of Object.entries(
|
|
60
|
+
obj as Record<string, unknown>,
|
|
61
|
+
)) {
|
|
62
|
+
assertLeafStrings(value, path ? `${path}.${key}` : key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
assertLeafStrings(parsed);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("i18n.ts — getRequestConfig loader", () => {
|
|
70
|
+
it("module default export is a function (the getRequestConfig callback)", async () => {
|
|
71
|
+
const i18nModule = await import("../../i18n/request");
|
|
72
|
+
expect(typeof i18nModule.default).toBe("function");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("loader returns messages object for 'en' locale", async () => {
|
|
76
|
+
const i18nModule = await import("../../i18n/request");
|
|
77
|
+
const loader = i18nModule.default as (args: {
|
|
78
|
+
requestLocale: Promise<string>;
|
|
79
|
+
}) => Promise<{ locale: string; messages: typeof messages }>;
|
|
80
|
+
|
|
81
|
+
const result = await loader({
|
|
82
|
+
requestLocale: Promise.resolve("en"),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result).toHaveProperty("locale", "en");
|
|
86
|
+
expect(result).toHaveProperty("messages");
|
|
87
|
+
expect(result.messages).toHaveProperty("consent");
|
|
88
|
+
expect(result.messages).toHaveProperty("common");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("loader falls back to English messages for unknown locale 'es'", async () => {
|
|
92
|
+
const i18nModule = await import("../../i18n/request");
|
|
93
|
+
const loader = i18nModule.default as (args: {
|
|
94
|
+
requestLocale: Promise<string>;
|
|
95
|
+
}) => Promise<{ locale: string; messages: Record<string, unknown> }>;
|
|
96
|
+
|
|
97
|
+
// 'es' has no messages/es.json — should return en.json contents
|
|
98
|
+
const result = await loader({
|
|
99
|
+
requestLocale: Promise.resolve("es"),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result).toHaveProperty("locale", "es");
|
|
103
|
+
expect(result).toHaveProperty("messages");
|
|
104
|
+
expect(result.messages).toHaveProperty("consent");
|
|
105
|
+
expect(result.messages).toHaveProperty("common");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("TypeScript type safety — compile-time key checking", () => {
|
|
110
|
+
it("messages structure satisfies the declared IntlMessages shape at runtime", () => {
|
|
111
|
+
// The TypeScript contract is declared in types/messages.d.ts.
|
|
112
|
+
// A deliberately mis-typed key (e.g. t('consnt.title')) is caught by
|
|
113
|
+
// tsc --noEmit — verified manually per AC checklist.
|
|
114
|
+
// Here we assert the runtime structure is internally consistent.
|
|
115
|
+
type ExpectedMessages = {
|
|
116
|
+
consent: {
|
|
117
|
+
title: string;
|
|
118
|
+
body: string;
|
|
119
|
+
acceptLabel: string;
|
|
120
|
+
rejectLabel: string;
|
|
121
|
+
policyLinkLabel: string;
|
|
122
|
+
};
|
|
123
|
+
common: {
|
|
124
|
+
siteTitle: string;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const typed = messages as ExpectedMessages;
|
|
129
|
+
expect(typed.consent.title.length).toBeGreaterThan(0);
|
|
130
|
+
expect(typed.consent.acceptLabel.length).toBeGreaterThan(0);
|
|
131
|
+
expect(typed.consent.rejectLabel.length).toBeGreaterThan(0);
|
|
132
|
+
expect(typed.common.siteTitle.length).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("i18n/request.ts — outer catch fallback (lines 31-32)", () => {
|
|
137
|
+
// Tests the outer catch block of i18n/request.ts: when import('../messages/en.json')
|
|
138
|
+
// on line 27 (inner fallback) rejects, the outer catch on lines 30-32 fires
|
|
139
|
+
// and attempts its own import of en.json.
|
|
140
|
+
//
|
|
141
|
+
// We cover the branch by isolating i18n/request.ts under jest.isolateModules and
|
|
142
|
+
// injecting a module-registry mock that makes the second en.json import
|
|
143
|
+
// (line 27) throw, so execution falls into the outer catch (line 31).
|
|
144
|
+
|
|
145
|
+
it("outer catch returns English messages when inner fallback import throws", async () => {
|
|
146
|
+
let result: { locale: string; messages: Record<string, unknown> } | undefined;
|
|
147
|
+
|
|
148
|
+
// Track how many times en.json is imported so we can make the second one
|
|
149
|
+
// fail (simulating the line-27 inner fallback rejection) while the third
|
|
150
|
+
// (line-31 outer catch) succeeds.
|
|
151
|
+
let enImportCount = 0;
|
|
152
|
+
|
|
153
|
+
await jest.isolateModulesAsync(async () => {
|
|
154
|
+
// Mock next-intl/server in the isolated scope.
|
|
155
|
+
jest.mock("next-intl/server", () => ({
|
|
156
|
+
getRequestConfig: (fn: unknown) => fn,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// Mock the messages directory so we can control what each import returns.
|
|
160
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock required; controls dynamic import resolution in i18n.ts outer-catch coverage; tracked in #507
|
|
161
|
+
jest.mock("../../messages/en.json", () => {
|
|
162
|
+
enImportCount++;
|
|
163
|
+
if (enImportCount === 1) {
|
|
164
|
+
// First call — from line 27 (inner fallback after locale miss) —
|
|
165
|
+
// throw to force execution into the outer catch on line 31.
|
|
166
|
+
throw new Error("simulated en.json load failure");
|
|
167
|
+
}
|
|
168
|
+
// Second call — from line 31 (outer catch) — return real content.
|
|
169
|
+
// i18n.ts does `fallback.default ?? fallback`, so put content at top level.
|
|
170
|
+
return { consent: { title: "t", body: "b", acceptLabel: "a", rejectLabel: "r", policyLinkLabel: "p" }, common: { siteTitle: "s" } };
|
|
171
|
+
},
|
|
172
|
+
{ virtual: true },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const i18nMod = await import("../../i18n/request");
|
|
176
|
+
const loader = i18nMod.default as (args: {
|
|
177
|
+
requestLocale: Promise<string>;
|
|
178
|
+
}) => Promise<{ locale: string; messages: Record<string, unknown> }>;
|
|
179
|
+
|
|
180
|
+
// Use locale "es" (no catalog) → locale import → null → line 27 runs →
|
|
181
|
+
// throws (mocked) → outer catch fires → line 31 runs → returns stub.
|
|
182
|
+
result = await loader({
|
|
183
|
+
requestLocale: Promise.resolve("es"),
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result).toBeDefined();
|
|
188
|
+
expect(result!.messages).toHaveProperty("consent");
|
|
189
|
+
expect(result!.messages).toHaveProperty("common");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for pack-aware pageToJsonLd and getPackOnce.
|
|
3
|
+
*
|
|
4
|
+
* U3 contract:
|
|
5
|
+
* - pageToJsonLd is async; calls getPackOnce() then buildJsonLdWithPack(page, pack).
|
|
6
|
+
* - Empty-pack passthrough: output is byte-identical to the pre-U3 synchronous
|
|
7
|
+
* buildJsonLd(page) call when the pack has no patches for the page type.
|
|
8
|
+
* - With-pack: output reflects the pack's builder patch for the page type.
|
|
9
|
+
* - getPackOnce memoizes getCurrentPack — called at most once per module load.
|
|
10
|
+
*
|
|
11
|
+
* @working-theory/schema-pack-registry is globally mocked in setup.ts (prevents the
|
|
12
|
+
* jose/license chain from loading in the JSDOM test environment). Individual
|
|
13
|
+
* tests configure `getCurrentPack` and `applyPack` return values as needed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Pack } from "@working-theory/schema-pack-registry";
|
|
17
|
+
import { getCurrentPack, applyPack } from "@working-theory/schema-pack-registry";
|
|
18
|
+
import { buildJsonLd } from "@working-theory/schema";
|
|
19
|
+
import { pageToJsonLd } from "@working-theory/page-renderer";
|
|
20
|
+
import type { ContentPage } from "@working-theory/content-adapters";
|
|
21
|
+
|
|
22
|
+
const mockGetCurrentPack = getCurrentPack as jest.MockedFunction<
|
|
23
|
+
typeof getCurrentPack
|
|
24
|
+
>;
|
|
25
|
+
const mockApplyPack = applyPack as jest.MockedFunction<typeof applyPack>;
|
|
26
|
+
|
|
27
|
+
const emptyPack: Pack = {
|
|
28
|
+
version: "2026.01.01",
|
|
29
|
+
schemaApiVersion: 1,
|
|
30
|
+
publishedAt: "2026-01-01T00:00:00.000Z",
|
|
31
|
+
patches: {},
|
|
32
|
+
registryDeltas: [],
|
|
33
|
+
signature: "built-in",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const homePageContent: ContentPage = {
|
|
37
|
+
type: "HomePage",
|
|
38
|
+
slug: "home",
|
|
39
|
+
title: "Example",
|
|
40
|
+
description: "An example site.",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("pageToJsonLd — pack-aware async", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
mockGetCurrentPack.mockReset();
|
|
46
|
+
mockApplyPack.mockReset();
|
|
47
|
+
// Default: applyPack is a passthrough (no patches applied).
|
|
48
|
+
mockApplyPack.mockImplementation(
|
|
49
|
+
(_name: string, output: Record<string, unknown>) => ({
|
|
50
|
+
result: output,
|
|
51
|
+
fellBack: false,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("is async (returns a Promise)", async () => {
|
|
57
|
+
mockGetCurrentPack.mockResolvedValue(emptyPack);
|
|
58
|
+
const result = pageToJsonLd(homePageContent, "en");
|
|
59
|
+
expect(result).toBeInstanceOf(Promise);
|
|
60
|
+
await result;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("empty-pack: output byte-identical to buildJsonLd(page) with matching locale", async () => {
|
|
64
|
+
mockGetCurrentPack.mockResolvedValue(emptyPack);
|
|
65
|
+
const result = await pageToJsonLd(homePageContent, "en");
|
|
66
|
+
const input = {
|
|
67
|
+
type: homePageContent.type,
|
|
68
|
+
slug: homePageContent.slug,
|
|
69
|
+
title: homePageContent.title,
|
|
70
|
+
description: homePageContent.description,
|
|
71
|
+
locale: "en",
|
|
72
|
+
} as Parameters<typeof buildJsonLd>[0];
|
|
73
|
+
const baseline = buildJsonLd(input);
|
|
74
|
+
expect(JSON.stringify(result)).toBe(JSON.stringify(baseline));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("with-pack: applyPack is called and its result is returned", async () => {
|
|
78
|
+
mockGetCurrentPack.mockResolvedValue(emptyPack);
|
|
79
|
+
// Simulate applyPack injecting a custom field.
|
|
80
|
+
mockApplyPack.mockImplementation(
|
|
81
|
+
(_name: string, output: Record<string, unknown>) => ({
|
|
82
|
+
result: { ...output, customField: "pack-injected" },
|
|
83
|
+
fellBack: false,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
const result = await pageToJsonLd(homePageContent, "en");
|
|
87
|
+
expect(result).toHaveProperty("customField", "pack-injected");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("getCurrentPack is memoized — called at most once per module load", async () => {
|
|
91
|
+
mockGetCurrentPack.mockResolvedValue(emptyPack);
|
|
92
|
+
await pageToJsonLd(homePageContent, "en");
|
|
93
|
+
const callCount = mockGetCurrentPack.mock.calls.length;
|
|
94
|
+
await pageToJsonLd(homePageContent, "en");
|
|
95
|
+
// Second call must not invoke getCurrentPack again — memoized.
|
|
96
|
+
expect(mockGetCurrentPack.mock.calls.length).toBe(callCount);
|
|
97
|
+
});
|
|
98
|
+
});
|