create-ailk 0.22.0 → 0.24.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 +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +68 -5
- package/dist/sync-routes.js +455 -29
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1426 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +6 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +348 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +975 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +369 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +64 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
package/dist/sync-routes.js
CHANGED
|
@@ -19,26 +19,32 @@
|
|
|
19
19
|
* - Expected bytes are what `copyTemplate` would write. The copier's
|
|
20
20
|
* text/binary + marker rules are NOT re-implemented here: the template's
|
|
21
21
|
* route root is copied once into a scratch dir and compared from there.
|
|
22
|
-
* - Never overwrite, never delete
|
|
23
|
-
* reported as modified and left alone; `--apply`
|
|
24
|
-
* that do not exist, so a second run is a no-op.
|
|
25
|
-
* its structured-data test, a route `--apply`
|
|
26
|
-
* exempts there is reported with the exact
|
|
27
|
-
* entry to add — the test itself is never
|
|
22
|
+
* - Never overwrite, never delete — with one named exception. A route dir
|
|
23
|
+
* the site has changed is reported as modified and left alone; `--apply`
|
|
24
|
+
* only creates directories that do not exist, so a second run is a no-op.
|
|
25
|
+
* When the site has edited its structured-data test, a route `--apply`
|
|
26
|
+
* adds that the template exempts there is reported with the exact
|
|
27
|
+
* `NO_STRUCTURED_DATA_ROUTES` entry to add — the test itself is never
|
|
28
|
+
* written (#8704). The exception is the retired legacy spoke checkout
|
|
29
|
+
* (#9142): on a spoke, `--apply` deletes its files that are still
|
|
30
|
+
* byte-identical to the template record and rewrites the generated
|
|
31
|
+
* billing action off them (`retireLegacySpokeCheckout`); an edited or
|
|
32
|
+
* unrecorded file is reported and left alone.
|
|
28
33
|
*
|
|
29
34
|
* Pure module: no `import.meta.url`, so jest can import it directly. The
|
|
30
35
|
* package's own `templates/` dir is supplied by programmatic.ts.
|
|
31
36
|
*/
|
|
32
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
37
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
33
38
|
import { tmpdir } from "node:os";
|
|
34
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
39
|
+
import { basename, dirname, join, posix, resolve, sep } from "node:path";
|
|
35
40
|
import { parseArgs } from "node:util";
|
|
36
|
-
import { copyTemplate } from "./copy.js";
|
|
41
|
+
import { copyTemplate, renderTemplateFile } from "./copy.js";
|
|
42
|
+
import { SPOKE_BILLING_ACTION_PATH } from "./derive-architecture.js";
|
|
37
43
|
import { appendEnvRows } from "./lib/declared-env.js";
|
|
38
44
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
39
|
-
import {
|
|
45
|
+
import { BILLING_ACTION, BILLING_ACTION_MARKER, RETIRED_CHECKOUT_E2E_MARKER, RETIRED_CHECKOUT_E2E_WORKFLOW_PATH, RETIRED_SPOKE_CHECKOUT_PATHS, } from "./spoke-checkout.js";
|
|
40
46
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
41
|
-
import { TEMPLATE_RECORD_PATH, parseTemplateRecord, sha256Hex } from "./template-record.js";
|
|
47
|
+
import { TEMPLATE_RECORD_PATH, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
42
48
|
import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
|
|
43
49
|
/** Template-relative prefix every `apps/web` route directory sits under. */
|
|
44
50
|
const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
@@ -50,12 +56,184 @@ export const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data
|
|
|
50
56
|
* `projects/apply` (#5048) is signal-collectors' own owned file
|
|
51
57
|
* (`ListingApplicationClient.tsx`, D-48) — its module bundle restores it
|
|
52
58
|
* whenever signal-collectors is selected, independent of the unrelated
|
|
53
|
-
* "projects" content surface's own selection.
|
|
54
|
-
* `
|
|
59
|
+
* "projects" content surface's own selection. `careers/apply` and
|
|
60
|
+
* `careers/[slug]` (#8840 D13) are the same case: signal-collectors' bundle
|
|
61
|
+
* lands the Job application walks (`careers/apply/page.tsx`,
|
|
62
|
+
* `careers/[slug]/apply/**`) whenever the module is selected, so a site that
|
|
63
|
+
* turned the careers pages off must not read as carrying "careers" — a site
|
|
64
|
+
* that carries them keeps `careers/page.tsx`, which still counts. See the doc
|
|
65
|
+
* comment on `syncRoutes`'s `carried` computation.
|
|
55
66
|
*/
|
|
56
67
|
const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
|
|
57
68
|
"projects/apply",
|
|
69
|
+
"careers/apply",
|
|
70
|
+
"careers/[slug]",
|
|
58
71
|
]);
|
|
72
|
+
/** Extensions whose files are scanned for import specifiers (#9406 DD1). */
|
|
73
|
+
const SOURCE_FILE = /\.(?:ts|tsx|js|jsx|mjs|mts)$/;
|
|
74
|
+
/**
|
|
75
|
+
* Static import and re-export (`import … from`, `export … from`), side-effect
|
|
76
|
+
* (`import "…"`) and dynamic (`import("…")`) specifiers (#9406 DD1). Matched
|
|
77
|
+
* over the whole text: `[^'"]*?` spans newlines, so a multi-line
|
|
78
|
+
* `import {\n a,\n} from "…"` is caught from its `import` line.
|
|
79
|
+
*/
|
|
80
|
+
const IMPORT_PATTERNS = [
|
|
81
|
+
/^\s*(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]/gm,
|
|
82
|
+
/^\s*import\s*['"]([^'"]+)['"]/gm,
|
|
83
|
+
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
84
|
+
];
|
|
85
|
+
/** The site-local (`./`, `../`) import specifiers of a source text, deduplicated. */
|
|
86
|
+
function relativeImportSpecifiers(text) {
|
|
87
|
+
const out = new Set();
|
|
88
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
89
|
+
for (const match of text.matchAll(pattern)) {
|
|
90
|
+
const specifier = match[1];
|
|
91
|
+
if (specifier !== undefined && (specifier.startsWith("./") || specifier.startsWith("../"))) {
|
|
92
|
+
out.add(specifier);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...out];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The scaffold-relative files `specifier`, imported from `importer`, may name —
|
|
100
|
+
* in #9406 DD2's probe order: a `.js`/`.jsx` specifier's `.ts`/`.tsx` spelling
|
|
101
|
+
* first, then the exact path, the extension probes and the directory index.
|
|
102
|
+
* Empty when the path climbs out of the scaffold root.
|
|
103
|
+
*/
|
|
104
|
+
function importCandidates(importer, specifier) {
|
|
105
|
+
const base = posix.normalize(posix.join(posix.dirname(importer), specifier));
|
|
106
|
+
if (base === ".." || base.startsWith("../"))
|
|
107
|
+
return [];
|
|
108
|
+
const out = [];
|
|
109
|
+
const js = /\.(jsx?)$/.exec(base);
|
|
110
|
+
if (js !== null) {
|
|
111
|
+
const stem = base.slice(0, -js[0].length);
|
|
112
|
+
if (js[1] === "js")
|
|
113
|
+
out.push(`${stem}.ts`);
|
|
114
|
+
out.push(`${stem}.tsx`);
|
|
115
|
+
}
|
|
116
|
+
out.push(base);
|
|
117
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".mjs"])
|
|
118
|
+
out.push(base + ext);
|
|
119
|
+
out.push(`${base}/index.ts`, `${base}/index.tsx`);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** Whether `root`/`rel` (`/`-separated) is an existing regular file. */
|
|
123
|
+
function isRegularFile(root, rel) {
|
|
124
|
+
try {
|
|
125
|
+
return statSync(join(root, ...rel.split("/"))).isFile();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The deepest route in `missingRoutes` a scaffold-relative path sits under —
|
|
133
|
+
* the route whose own copy writes it — or undefined.
|
|
134
|
+
*/
|
|
135
|
+
function ownerRoute(path, missingRoutes) {
|
|
136
|
+
if (!path.startsWith(`${ROUTE_ROOT}/`))
|
|
137
|
+
return undefined;
|
|
138
|
+
const rel = path.slice(ROUTE_ROOT.length + 1);
|
|
139
|
+
let owner;
|
|
140
|
+
for (const m of missingRoutes) {
|
|
141
|
+
if (rel.startsWith(`${m}/`) && (owner === undefined || m.length > owner.length))
|
|
142
|
+
owner = m;
|
|
143
|
+
}
|
|
144
|
+
return owner;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the site-local imports of every file `route`'s own copy writes, and
|
|
148
|
+
* transitively of every helper that brings (#9406 DD1–DD3). Each import is
|
|
149
|
+
* resolved against the site first, then the template. A file the site has
|
|
150
|
+
* ends the walk there (the site owns its closure); a file another missing
|
|
151
|
+
* route's copy brings is recorded for DD4's propagation; a file only the
|
|
152
|
+
* template has is a copy — unless the spoke drops it or its surface is not
|
|
153
|
+
* carried, which refuses the route.
|
|
154
|
+
*/
|
|
155
|
+
function walkRouteImports(route, ctx) {
|
|
156
|
+
const inScratch = (path) => path.startsWith(`${ROUTE_ROOT}/`) ? path.slice(ROUTE_ROOT.length + 1) : undefined;
|
|
157
|
+
const templateHas = (path) => {
|
|
158
|
+
const rel = inScratch(path);
|
|
159
|
+
return rel !== undefined
|
|
160
|
+
? isRegularFile(ctx.scratch, rel)
|
|
161
|
+
: isRegularFile(ctx.templatesDir, path);
|
|
162
|
+
};
|
|
163
|
+
// The bytes `copyTemplate` (a route file) or `copyDependencyFile` (any
|
|
164
|
+
// other) would write for `path`.
|
|
165
|
+
const templateBytes = (path) => {
|
|
166
|
+
const rel = inScratch(path);
|
|
167
|
+
if (rel !== undefined)
|
|
168
|
+
return readFileSync(join(ctx.scratch, ...rel.split("/")));
|
|
169
|
+
return Buffer.from(renderTemplateFile(readFileSync(join(ctx.templatesDir, ...path.split("/"))), path, ctx.versions, ctx.markers));
|
|
170
|
+
};
|
|
171
|
+
const copies = new Set();
|
|
172
|
+
const differs = new Set();
|
|
173
|
+
const unresolved = [];
|
|
174
|
+
const reached = [];
|
|
175
|
+
const queue = regularFilesUnder(ctx.scratch, route)
|
|
176
|
+
.map((rel) => `${ROUTE_ROOT}/${rel}`)
|
|
177
|
+
.filter((path) => ownerRoute(path, ctx.missingRoutes) === route);
|
|
178
|
+
const visited = new Set(queue);
|
|
179
|
+
for (let from = queue.shift(); from !== undefined; from = queue.shift()) {
|
|
180
|
+
if (!SOURCE_FILE.test(from))
|
|
181
|
+
continue;
|
|
182
|
+
const importer = from;
|
|
183
|
+
for (const specifier of relativeImportSpecifiers(templateBytes(importer).toString("utf8"))) {
|
|
184
|
+
const candidates = importCandidates(importer, specifier);
|
|
185
|
+
const inSite = candidates.find((c) => isRegularFile(ctx.targetAbs, c));
|
|
186
|
+
const path = inSite ?? candidates.find(templateHas);
|
|
187
|
+
if (path === undefined) {
|
|
188
|
+
unresolved.push({ specifier, from: importer, reason: "not in the template" });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const owner = ownerRoute(path, ctx.missingRoutes);
|
|
192
|
+
if (owner !== undefined) {
|
|
193
|
+
if (owner !== route)
|
|
194
|
+
reached.push({ path, owner, specifier, from: importer });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (inSite !== undefined) {
|
|
198
|
+
const siteBytes = readFileSync(join(ctx.targetAbs, ...path.split("/")));
|
|
199
|
+
if (templateHas(path) && !templateBytes(path).equals(siteBytes))
|
|
200
|
+
differs.add(path);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ctx.spoke !== undefined && isSpokeExcluded(path, ctx.spoke)) {
|
|
204
|
+
unresolved.push({ specifier, from: importer, reason: "the spoke does not carry it" });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const routeRel = inScratch(path);
|
|
208
|
+
const surface = routeRel === undefined ? "core" : surfaceForRouteDir(routeRel);
|
|
209
|
+
if (surface === undefined || !ctx.carried.has(surface)) {
|
|
210
|
+
unresolved.push({
|
|
211
|
+
specifier,
|
|
212
|
+
from: importer,
|
|
213
|
+
reason: `its surface (${surface ?? "none"}) is not carried by this site`,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
copies.add(path);
|
|
218
|
+
if (!visited.has(path)) {
|
|
219
|
+
visited.add(path);
|
|
220
|
+
queue.push(path);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
route,
|
|
226
|
+
copies: [...copies].sort(),
|
|
227
|
+
differs: [...differs].sort(),
|
|
228
|
+
unresolved: sortUnresolved(unresolved),
|
|
229
|
+
reached,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** `unresolved` rows sorted by `from`, then `specifier`. */
|
|
233
|
+
function sortUnresolved(rows) {
|
|
234
|
+
const key = (row) => `${row.from}\u0000${row.specifier}`;
|
|
235
|
+
return [...rows].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
|
236
|
+
}
|
|
59
237
|
/**
|
|
60
238
|
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
61
239
|
* brace scan that skips string literals and comments. Empty when the
|
|
@@ -229,6 +407,141 @@ function isModified(expectedDir, targetDir, skip = () => false) {
|
|
|
229
407
|
}
|
|
230
408
|
return false;
|
|
231
409
|
}
|
|
410
|
+
/** Directories a retirement never removes, even when it leaves them empty. */
|
|
411
|
+
const RETIRE_STOP_DIRS = new Set([
|
|
412
|
+
"apps/web/app/api",
|
|
413
|
+
"apps/web/lib",
|
|
414
|
+
"apps/web/lib/__tests__",
|
|
415
|
+
".github/workflows",
|
|
416
|
+
]);
|
|
417
|
+
/** Every regular file at or under `rel` in `targetAbs`, `/`-separated. A symlink is skipped, never followed. */
|
|
418
|
+
function regularFilesUnder(targetAbs, rel) {
|
|
419
|
+
const abs = join(targetAbs, ...rel.split("/"));
|
|
420
|
+
let stat;
|
|
421
|
+
try {
|
|
422
|
+
stat = lstatSync(abs);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
if (stat.isFile())
|
|
428
|
+
return [rel];
|
|
429
|
+
if (!stat.isDirectory())
|
|
430
|
+
return [];
|
|
431
|
+
return readdirSync(abs)
|
|
432
|
+
.sort()
|
|
433
|
+
.flatMap((name) => regularFilesUnder(targetAbs, `${rel}/${name}`));
|
|
434
|
+
}
|
|
435
|
+
/** The first line of a text file. */
|
|
436
|
+
function firstLine(text) {
|
|
437
|
+
const eol = text.indexOf("\n");
|
|
438
|
+
return eol === -1 ? text : text.slice(0, eol);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Retire the legacy env-key spoke checkout from a spoke (#9142): its buy
|
|
442
|
+
* route, Stripe webhook route, `lib/checkout.ts`, their tests
|
|
443
|
+
* (`RETIRED_SPOKE_CHECKOUT_PATHS`) and the generated checkout e2e workflow.
|
|
444
|
+
*
|
|
445
|
+
* - A copied file is deleted only when the template record lists it and its
|
|
446
|
+
* bytes still hash to the recorded copy — the rule `create-ailk update`
|
|
447
|
+
* uses for a file the template dropped; its record row goes with it. An
|
|
448
|
+
* edited file, or one the record does not list, is reported in `kept`.
|
|
449
|
+
* - The workflow, never recorded, is deleted only when its first line is
|
|
450
|
+
* still create-ailk's generated marker.
|
|
451
|
+
* - The billing action (a spoke that signs in through its hub) imported
|
|
452
|
+
* `lib/checkout.ts`; when its first line is still the generated marker and
|
|
453
|
+
* it differs from the current render, it is rewritten to `BILLING_ACTION`.
|
|
454
|
+
* An edited one that still imports `lib/checkout` is reported in `kept`.
|
|
455
|
+
*
|
|
456
|
+
* Without `apply` nothing is written: the lists say what `--apply` would do.
|
|
457
|
+
* Directories the deletes leave empty are removed, up to `RETIRE_STOP_DIRS`.
|
|
458
|
+
*/
|
|
459
|
+
export function retireLegacySpokeCheckout(targetAbs, apply) {
|
|
460
|
+
const retired = [];
|
|
461
|
+
const kept = [];
|
|
462
|
+
const rewritten = [];
|
|
463
|
+
const recordPath = join(targetAbs, TEMPLATE_RECORD_PATH);
|
|
464
|
+
let record;
|
|
465
|
+
if (existsSync(recordPath)) {
|
|
466
|
+
try {
|
|
467
|
+
record = parseTemplateRecord(readFileSync(recordPath, "utf8"));
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
record = undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const path of RETIRED_SPOKE_CHECKOUT_PATHS.flatMap((rel) => regularFilesUnder(targetAbs, rel))) {
|
|
474
|
+
const recorded = record?.files[path]?.sha256;
|
|
475
|
+
if (recorded === undefined) {
|
|
476
|
+
kept.push({
|
|
477
|
+
path,
|
|
478
|
+
reason: record === undefined
|
|
479
|
+
? "no readable template record, so an edit cannot be told apart"
|
|
480
|
+
: "not in the template record",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
else if (sha256Hex(readFileSync(join(targetAbs, ...path.split("/")))) !== recorded) {
|
|
484
|
+
kept.push({ path, reason: "edited here" });
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
retired.push(path);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const workflow = regularFilesUnder(targetAbs, RETIRED_CHECKOUT_E2E_WORKFLOW_PATH);
|
|
491
|
+
for (const path of workflow) {
|
|
492
|
+
const text = readFileSync(join(targetAbs, ...path.split("/")), "utf8");
|
|
493
|
+
if (firstLine(text) === RETIRED_CHECKOUT_E2E_MARKER)
|
|
494
|
+
retired.push(path);
|
|
495
|
+
else
|
|
496
|
+
kept.push({ path, reason: "edited here" });
|
|
497
|
+
}
|
|
498
|
+
const billingAbs = join(targetAbs, ...SPOKE_BILLING_ACTION_PATH.split("/"));
|
|
499
|
+
if (regularFilesUnder(targetAbs, SPOKE_BILLING_ACTION_PATH).length === 1) {
|
|
500
|
+
const text = readFileSync(billingAbs, "utf8");
|
|
501
|
+
if (text !== BILLING_ACTION) {
|
|
502
|
+
if (firstLine(text) === BILLING_ACTION_MARKER) {
|
|
503
|
+
rewritten.push(SPOKE_BILLING_ACTION_PATH);
|
|
504
|
+
}
|
|
505
|
+
else if (/lib\/checkout["'`]/.test(text)) {
|
|
506
|
+
kept.push({
|
|
507
|
+
path: SPOKE_BILLING_ACTION_PATH,
|
|
508
|
+
reason: "edited here, and it imports the retired lib/checkout",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (apply) {
|
|
514
|
+
for (const path of retired) {
|
|
515
|
+
const abs = resolve(join(targetAbs, ...path.split("/")));
|
|
516
|
+
if (!abs.startsWith(targetAbs + sep)) {
|
|
517
|
+
throw new Error(`refusing to delete outside target dir: ${abs}`);
|
|
518
|
+
}
|
|
519
|
+
rmSync(abs);
|
|
520
|
+
let dir = dirname(path);
|
|
521
|
+
while (dir !== "." && !RETIRE_STOP_DIRS.has(dir)) {
|
|
522
|
+
const dirAbs = join(targetAbs, ...dir.split("/"));
|
|
523
|
+
if (!existsSync(dirAbs) || readdirSync(dirAbs).length > 0)
|
|
524
|
+
break;
|
|
525
|
+
rmdirSync(dirAbs);
|
|
526
|
+
dir = dirname(dir);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (rewritten.length > 0)
|
|
530
|
+
writeFileSync(billingAbs, BILLING_ACTION);
|
|
531
|
+
if (record !== undefined && retired.some((path) => path in record.files)) {
|
|
532
|
+
const files = { ...record.files };
|
|
533
|
+
for (const path of retired)
|
|
534
|
+
delete files[path];
|
|
535
|
+
writeFileSync(recordPath, serializeTemplateRecord({ ...record, files }));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const byPath = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
539
|
+
return {
|
|
540
|
+
retired: retired.sort(byPath),
|
|
541
|
+
kept: kept.sort((a, b) => byPath(a.path, b.path)),
|
|
542
|
+
rewritten,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
232
545
|
/**
|
|
233
546
|
* Compare an already-scaffolded repo's route tree with the template's and,
|
|
234
547
|
* when `apply` is set, copy in the route dirs it is missing.
|
|
@@ -313,6 +626,7 @@ export function syncRoutes(opts) {
|
|
|
313
626
|
const targetRouteRoot = join(targetAbs, ROUTE_ROOT);
|
|
314
627
|
const missing = [];
|
|
315
628
|
const modified = [];
|
|
629
|
+
let walks = [];
|
|
316
630
|
const scratch = mkdtempSync(join(tmpdir(), "ailk-sync-routes-"));
|
|
317
631
|
try {
|
|
318
632
|
copyTemplate({
|
|
@@ -330,18 +644,73 @@ export function syncRoutes(opts) {
|
|
|
330
644
|
modified.push(report);
|
|
331
645
|
}
|
|
332
646
|
}
|
|
647
|
+
// #9406 — resolve each missing route's site-local imports while the
|
|
648
|
+
// scratch render is still on disk.
|
|
649
|
+
walks = missing.map((r) => walkRouteImports(r.route, {
|
|
650
|
+
scratch,
|
|
651
|
+
templatesDir: opts.templatesDir,
|
|
652
|
+
targetAbs,
|
|
653
|
+
missingRoutes: missing.map((m) => m.route),
|
|
654
|
+
versions: opts.versions,
|
|
655
|
+
markers,
|
|
656
|
+
spoke,
|
|
657
|
+
carried: new Set(["core", "authed", ...carried]),
|
|
658
|
+
}));
|
|
333
659
|
}
|
|
334
660
|
finally {
|
|
335
661
|
rmSync(scratch, { recursive: true, force: true });
|
|
336
662
|
}
|
|
663
|
+
// #9406 DD4 — a route refuses when an import resolves nowhere; refusal
|
|
664
|
+
// inherits to its sub-routes and to any route importing a file only a
|
|
665
|
+
// refused route's copy would write, iterated to a fixpoint. Never upward:
|
|
666
|
+
// an ancestor is still copied without the refused sub-tree.
|
|
667
|
+
const missingRoutes = missing.map((r) => r.route);
|
|
668
|
+
const refusedSet = new Set(walks.filter((w) => w.unresolved.length > 0).map((w) => w.route));
|
|
669
|
+
for (let changed = true; changed;) {
|
|
670
|
+
changed = false;
|
|
671
|
+
for (const walk of walks) {
|
|
672
|
+
if (refusedSet.has(walk.route))
|
|
673
|
+
continue;
|
|
674
|
+
if (missingRoutes.some((a) => refusedSet.has(a) && walk.route.startsWith(`${a}/`))) {
|
|
675
|
+
refusedSet.add(walk.route);
|
|
676
|
+
changed = true;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const dangling = walk.reached.filter((r) => refusedSet.has(r.owner));
|
|
680
|
+
if (dangling.length > 0) {
|
|
681
|
+
walk.unresolved = sortUnresolved([
|
|
682
|
+
...walk.unresolved,
|
|
683
|
+
...dangling.map(({ path, owner, specifier, from }) => ({
|
|
684
|
+
specifier,
|
|
685
|
+
from,
|
|
686
|
+
reason: `${path} is brought by ${owner}, which is refused`,
|
|
687
|
+
})),
|
|
688
|
+
]);
|
|
689
|
+
refusedSet.add(walk.route);
|
|
690
|
+
changed = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const refused = missingRoutes.filter((r) => refusedSet.has(r)).sort();
|
|
695
|
+
const routeImports = walks
|
|
696
|
+
.filter((w) => w.copies.length + w.differs.length + w.unresolved.length > 0)
|
|
697
|
+
.map(({ route, copies, differs, unresolved }) => ({ route, copies, differs, unresolved }))
|
|
698
|
+
.sort((a, b) => (a.route < b.route ? -1 : a.route > b.route ? 1 : 0));
|
|
337
699
|
// #5014 third clause: a route's ROUTE_DEPENDENCIES files must exist in the
|
|
338
700
|
// target too, or `--apply` leaves a route that cannot compile. Computed
|
|
339
701
|
// from `missing` only — a route already present is assumed to already have
|
|
340
702
|
// brought its own dependencies along (or the site owner has otherwise
|
|
341
703
|
// satisfied them); re-checking every IN-SCOPE route's dependencies on every
|
|
342
704
|
// run would report a pre-existing, unrelated gap as if this sync caused it.
|
|
705
|
+
// #9406 — unioned with the copies each route's imports resolve to, and
|
|
706
|
+
// never for a refused route: its helpers are neither required nor copied.
|
|
343
707
|
const missingDependencies = [
|
|
344
|
-
...new Set(missing
|
|
708
|
+
...new Set(missing
|
|
709
|
+
.filter((r) => !refusedSet.has(r.route))
|
|
710
|
+
.flatMap((r) => [
|
|
711
|
+
...r.dependencies.filter((dep) => !existsSync(join(targetAbs, dep))),
|
|
712
|
+
...(walks.find((w) => w.route === r.route)?.copies ?? []),
|
|
713
|
+
])),
|
|
345
714
|
].sort();
|
|
346
715
|
const missingMessageNamespaces = [
|
|
347
716
|
...new Set(missing.flatMap((r) => r.messageNamespaces.filter((ns) => !hasMessageNamespace(targetAbs, ns)))),
|
|
@@ -349,11 +718,11 @@ export function syncRoutes(opts) {
|
|
|
349
718
|
const applied = [];
|
|
350
719
|
const appliedDependencies = [];
|
|
351
720
|
if (opts.apply === true) {
|
|
352
|
-
const
|
|
353
|
-
for (const route of
|
|
721
|
+
const writable = missingRoutes.filter((r) => !refusedSet.has(r));
|
|
722
|
+
for (const route of writable) {
|
|
354
723
|
applied.push(route);
|
|
355
724
|
// An ancestor route dir's subtree copy already brings this one.
|
|
356
|
-
if (
|
|
725
|
+
if (writable.some((m) => route.startsWith(m + "/")))
|
|
357
726
|
continue;
|
|
358
727
|
const dst = resolve(join(targetRouteRoot, route));
|
|
359
728
|
if (!dst.startsWith(targetAbs + sep)) {
|
|
@@ -364,6 +733,10 @@ export function syncRoutes(opts) {
|
|
|
364
733
|
targetDir: dst,
|
|
365
734
|
versions: opts.versions,
|
|
366
735
|
markers,
|
|
736
|
+
// #9406 DD4 — hold back each refused sub-route from the ancestor's copy.
|
|
737
|
+
excludePaths: refused
|
|
738
|
+
.filter((r) => r.startsWith(route + "/"))
|
|
739
|
+
.map((r) => r.slice(route.length + 1)),
|
|
367
740
|
});
|
|
368
741
|
}
|
|
369
742
|
for (const dep of missingDependencies) {
|
|
@@ -371,7 +744,7 @@ export function syncRoutes(opts) {
|
|
|
371
744
|
if (!dst.startsWith(targetAbs + sep)) {
|
|
372
745
|
throw new Error(`refusing to write outside target dir: ${dst}`);
|
|
373
746
|
}
|
|
374
|
-
copyDependencyFile(
|
|
747
|
+
copyDependencyFile(opts.templatesDir, dep, dst, opts.versions, markers);
|
|
375
748
|
appliedDependencies.push(dep);
|
|
376
749
|
}
|
|
377
750
|
// #7874 D9 — a spoke declares what the copied routes read, as the last
|
|
@@ -387,11 +760,16 @@ export function syncRoutes(opts) {
|
|
|
387
760
|
missing,
|
|
388
761
|
modified,
|
|
389
762
|
applied,
|
|
763
|
+
refused,
|
|
764
|
+
routeImports,
|
|
390
765
|
missingDependencies,
|
|
391
766
|
appliedDependencies,
|
|
392
767
|
missingMessageNamespaces,
|
|
393
768
|
skippedSurfaces,
|
|
394
769
|
structuredDataEntries: structuredDataEntriesFor(targetAbs, opts.templatesDir, applied),
|
|
770
|
+
retiredCheckout: spoke !== undefined
|
|
771
|
+
? retireLegacySpokeCheckout(targetAbs, opts.apply === true)
|
|
772
|
+
: { retired: [], kept: [], rewritten: [] },
|
|
395
773
|
};
|
|
396
774
|
}
|
|
397
775
|
/**
|
|
@@ -420,19 +798,20 @@ function hasMessageNamespace(targetAbs, namespace) {
|
|
|
420
798
|
}
|
|
421
799
|
}
|
|
422
800
|
/**
|
|
423
|
-
* Copy one dependency file from the template into
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* safe to call on
|
|
801
|
+
* Copy one dependency file (`rel`, template-relative) from the template into
|
|
802
|
+
* the target through `renderTemplateFile` — the renderer `copyTemplate` uses,
|
|
803
|
+
* so a text file gets its markers and a non-text asset an import reached
|
|
804
|
+
* (`./logo.png`, #9406) is copied byte-for-byte. Never overwrites: the caller
|
|
805
|
+
* (`syncRoutes`) only calls this for a path already confirmed absent from the
|
|
806
|
+
* target, but the check is repeated here so this helper is safe to call on
|
|
807
|
+
* its own.
|
|
429
808
|
*/
|
|
430
|
-
function copyDependencyFile(
|
|
809
|
+
function copyDependencyFile(templatesDir, rel, dst, versions, markers) {
|
|
431
810
|
if (existsSync(dst))
|
|
432
811
|
return;
|
|
433
812
|
mkdirSync(dirname(dst), { recursive: true });
|
|
434
|
-
const
|
|
435
|
-
writeFileSync(dst,
|
|
813
|
+
const bytes = readFileSync(join(templatesDir, ...rel.split("/")));
|
|
814
|
+
writeFileSync(dst, renderTemplateFile(bytes, rel, versions, markers));
|
|
436
815
|
}
|
|
437
816
|
/** Render the report the CLI prints after a run. Ends with a newline. */
|
|
438
817
|
export function formatSyncRoutesReport(result, apply) {
|
|
@@ -446,7 +825,7 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
446
825
|
}
|
|
447
826
|
else if (apply) {
|
|
448
827
|
lines.push(`[create-ailk] Copied ${result.applied.length} route(s):`);
|
|
449
|
-
lines.push(...result.missing.map(entry));
|
|
828
|
+
lines.push(...result.missing.filter((r) => result.applied.includes(r.route)).map(entry));
|
|
450
829
|
}
|
|
451
830
|
else {
|
|
452
831
|
lines.push(`[create-ailk] ${result.missing.length} template route(s) missing:`);
|
|
@@ -459,6 +838,32 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
459
838
|
if (result.skippedSurfaces.length > 0) {
|
|
460
839
|
lines.push(`[create-ailk] Surfaces this site does not carry (skipped): ${result.skippedSurfaces.join(", ")}`);
|
|
461
840
|
}
|
|
841
|
+
// #9406 DD5 — per missing route, what its imports copy, leave, or cannot
|
|
842
|
+
// resolve; then the routes a dangling import holds back.
|
|
843
|
+
if (result.routeImports.length > 0) {
|
|
844
|
+
lines.push("[create-ailk] Local modules the missing route(s) import:");
|
|
845
|
+
for (const { route, copies, differs, unresolved } of result.routeImports) {
|
|
846
|
+
lines.push(` ${route}`);
|
|
847
|
+
if (copies.length > 0) {
|
|
848
|
+
lines.push(" copies:");
|
|
849
|
+
lines.push(...copies.map((path) => ` ${path}`));
|
|
850
|
+
}
|
|
851
|
+
if (differs.length > 0) {
|
|
852
|
+
lines.push(" left as-is (differs from the template):");
|
|
853
|
+
lines.push(...differs.map((path) => ` ${path}`));
|
|
854
|
+
}
|
|
855
|
+
if (unresolved.length > 0) {
|
|
856
|
+
lines.push(" cannot resolve:");
|
|
857
|
+
lines.push(...unresolved.map(({ specifier, from, reason }) => ` ${specifier} (from ${from}): ${reason}`));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (result.refused.length > 0) {
|
|
862
|
+
lines.push(apply
|
|
863
|
+
? `[create-ailk] ${result.refused.length} route(s) not copied — an import does not resolve:`
|
|
864
|
+
: `[create-ailk] ${result.refused.length} route(s) would not be copied — an import does not resolve:`);
|
|
865
|
+
lines.push(...result.refused.map((route) => ` ${route}`));
|
|
866
|
+
}
|
|
462
867
|
// #5014 third clause: the routes/messages/lib a synced route needs to
|
|
463
868
|
// compile, reported (and, for lib files, applied) alongside the route.
|
|
464
869
|
if (result.missingDependencies.length > 0) {
|
|
@@ -478,6 +883,25 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
478
883
|
lines.push(...entry.split("\n").map((line) => ` ${line}`));
|
|
479
884
|
}
|
|
480
885
|
}
|
|
886
|
+
// #9142 — the retired legacy spoke checkout, named so the operator sees
|
|
887
|
+
// exactly what left and what stays for them to delete.
|
|
888
|
+
const retiredCheckout = result.retiredCheckout;
|
|
889
|
+
if (retiredCheckout.retired.length > 0) {
|
|
890
|
+
lines.push(apply
|
|
891
|
+
? `[create-ailk] Removed ${retiredCheckout.retired.length} file(s) of the retired legacy spoke checkout (a spoke sells through its hub's Offerings buy route):`
|
|
892
|
+
: `[create-ailk] ${retiredCheckout.retired.length} file(s) of the retired legacy spoke checkout to remove with --apply (a spoke sells through its hub's Offerings buy route):`);
|
|
893
|
+
lines.push(...retiredCheckout.retired.map((path) => ` ${path}`));
|
|
894
|
+
}
|
|
895
|
+
if (retiredCheckout.rewritten.length > 0) {
|
|
896
|
+
lines.push(apply
|
|
897
|
+
? "[create-ailk] Rewrote the generated billing action off the retired lib/checkout:"
|
|
898
|
+
: "[create-ailk] The generated billing action imports the retired lib/checkout; --apply rewrites it:");
|
|
899
|
+
lines.push(...retiredCheckout.rewritten.map((path) => ` ${path}`));
|
|
900
|
+
}
|
|
901
|
+
if (retiredCheckout.kept.length > 0) {
|
|
902
|
+
lines.push(`[create-ailk] ${retiredCheckout.kept.length} file(s) of the retired legacy spoke checkout left in place — delete or rewrite by hand:`);
|
|
903
|
+
lines.push(...retiredCheckout.kept.map(({ path, reason }) => ` ${path} (${reason})`));
|
|
904
|
+
}
|
|
481
905
|
if (result.missing.length > 0 && !apply) {
|
|
482
906
|
lines.push("Run `create-ailk sync-routes <target> --apply` to copy the missing routes.");
|
|
483
907
|
}
|
|
@@ -500,7 +924,9 @@ Options:
|
|
|
500
924
|
|
|
501
925
|
Checks every core route plus the routes of each surface the site carries; a
|
|
502
926
|
surface gated off at scaffold time is skipped, not drift. A route the site has
|
|
503
|
-
changed locally is reported and never overwritten. Nothing is
|
|
927
|
+
changed locally is reported and never overwritten. Nothing is deleted except,
|
|
928
|
+
on a spoke, the retired legacy checkout's files that still match create-ailk's
|
|
929
|
+
copy: --apply removes them and rewrites the generated billing action (#9142).
|
|
504
930
|
`;
|
|
505
931
|
/**
|
|
506
932
|
* Parse the argv AFTER the `sync-routes` word. Strict `node:util` parseArgs,
|
|
@@ -3,7 +3,7 @@ export declare const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
|
3
3
|
/**
|
|
4
4
|
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
5
5
|
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
6
|
-
* registry, the four spoke seams, and a commerce spoke's checkout config,
|
|
6
|
+
* registry and its module-presence flags (#8843 D10), the four spoke seams, and a commerce spoke's checkout config,
|
|
7
7
|
* which the checkout writer stamps and the site owns from then on (#8748
|
|
8
8
|
* D2). Composed from each writer's own exported path constant, so a renamed
|
|
9
9
|
* constant carries its path along here too (#8522 D2).
|
package/dist/template-record.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
import { PAID_SECTIONS_PATH } from "./paid-sections.js";
|
|
21
|
+
import { MODULE_FLAGS_PATH, PAID_SECTIONS_PATH } from "./paid-sections.js";
|
|
22
22
|
import { SITE_CHROME_PATH } from "./site-chrome.js";
|
|
23
23
|
import { SITE_FONTS_PATH } from "./site-fonts.js";
|
|
24
24
|
import { SPOKE_CHECKOUT_CONFIG_PATH } from "./spoke-checkout.js";
|
|
@@ -29,7 +29,7 @@ export const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
|
29
29
|
/**
|
|
30
30
|
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
31
31
|
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
32
|
-
* registry, the four spoke seams, and a commerce spoke's checkout config,
|
|
32
|
+
* registry and its module-presence flags (#8843 D10), the four spoke seams, and a commerce spoke's checkout config,
|
|
33
33
|
* which the checkout writer stamps and the site owns from then on (#8748
|
|
34
34
|
* D2). Composed from each writer's own exported path constant, so a renamed
|
|
35
35
|
* constant carries its path along here too (#8522 D2).
|
|
@@ -39,6 +39,7 @@ export const REGENERATED_APP_PATHS = [
|
|
|
39
39
|
SITE_CHROME_PATH,
|
|
40
40
|
SITE_FONTS_PATH,
|
|
41
41
|
PAID_SECTIONS_PATH,
|
|
42
|
+
MODULE_FLAGS_PATH,
|
|
42
43
|
...SPOKE_SEAM_FILES,
|
|
43
44
|
SPOKE_CHECKOUT_CONFIG_PATH,
|
|
44
45
|
];
|
package/dist/update.js
CHANGED
|
@@ -26,6 +26,7 @@ import { AddModuleRefusedError, readArchitectureHeader } from "./add-module-core
|
|
|
26
26
|
import { buildScaffoldMarkers, isExcluded, isTextFile, renderTemplateFile, templateDestination } from "./copy.js";
|
|
27
27
|
import { fetchTemplateSnapshot } from "./lib/fetch-template-snapshot.js";
|
|
28
28
|
import { mergeThreeWay } from "./lib/merge-three-way.js";
|
|
29
|
+
import { MODULE_FLAGS_PATH, writeModuleFlags } from "./paid-sections.js";
|
|
29
30
|
import { ParseError } from "./parse-args.js";
|
|
30
31
|
import { SITE_ROSTER_PATH } from "./site-roster.js";
|
|
31
32
|
import { SPOKE_CHECKOUT_TEMPLATE_PATHS } from "./spoke-checkout.js";
|
|
@@ -514,6 +515,15 @@ export async function updateSite(opts) {
|
|
|
514
515
|
if (recordChanged) {
|
|
515
516
|
writeFileSync(recordPath, serializeTemplateRecord({ schema: 1, projectName: record.projectName, files: nextFiles }));
|
|
516
517
|
}
|
|
518
|
+
// #8843 D10 — the module-presence flags are generated, never copied
|
|
519
|
+
// (REGENERATED_APP_PATHS): render them from this site's own
|
|
520
|
+
// apps/web/package.json on every run, so a site updated onto a layout
|
|
521
|
+
// that imports them has them before it builds. Not recorded.
|
|
522
|
+
const flags = writeModuleFlags(targetDir);
|
|
523
|
+
if (flags === "added")
|
|
524
|
+
moved.added.push(MODULE_FLAGS_PATH);
|
|
525
|
+
else if (flags === "updated")
|
|
526
|
+
moved.updated.push(MODULE_FLAGS_PATH);
|
|
517
527
|
return {
|
|
518
528
|
targetDir,
|
|
519
529
|
version,
|