create-ailk 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- 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 +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ModulePatch } from "../module-architecture.js";
|
|
2
|
+
import { type WorkspaceVersionMap } from "../substitute.js";
|
|
3
|
+
export interface ApplyModulePatchesOptions {
|
|
4
|
+
versions: WorkspaceVersionMap;
|
|
5
|
+
}
|
|
6
|
+
/** One target whose content changed — the output of `computeModulePatches`. */
|
|
7
|
+
export interface ComputedModulePatch {
|
|
8
|
+
/** Target path, exactly as it appeared in the manifest (`ModulePatch.target`). */
|
|
9
|
+
target: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ComputeModulePatchesOptions extends ApplyModulePatchesOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Returns the "before" bytes for `target`, or `undefined` if it does not
|
|
15
|
+
* exist (D4 class 2, missing target). The caller decides WHERE those bytes
|
|
16
|
+
* come from — an already-scaffolded `targetDir` file, or (when the same
|
|
17
|
+
* bundle also delivers that target via its plain content or `installMap`)
|
|
18
|
+
* the staged copy — so that this function never has to know about staging
|
|
19
|
+
* at all. Called at most once per distinct target.
|
|
20
|
+
*/
|
|
21
|
+
readTarget: (target: string) => string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute every patched target's new content in memory — throws on the
|
|
25
|
+
* first D4 class 1-3 failure encountered (deterministic order — targets in
|
|
26
|
+
* `patches` array order) and writes nothing anywhere; returns only the
|
|
27
|
+
* targets whose content actually changed. Pure with respect to the
|
|
28
|
+
* filesystem: reads go through `options.readTarget`, nothing is written.
|
|
29
|
+
* Pair with `writeModulePatches` for the second pass, deferred until every
|
|
30
|
+
* other part of the caller's own operation (e.g. `copyTree`/`installMap`)
|
|
31
|
+
* has ALSO succeeded, so a patch failure can never leave partial output
|
|
32
|
+
* from an earlier step in place either.
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeModulePatches(patches: readonly ModulePatch[], options: ComputeModulePatchesOptions): ComputedModulePatch[];
|
|
35
|
+
/** Write every `computeModulePatches` result to disk, resolved against `baseDir`, once. */
|
|
36
|
+
export declare function writeModulePatches(computed: readonly ComputedModulePatch[], baseDir: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Apply every patch in `patches` onto the scaffold at `targetDir` — reads
|
|
39
|
+
* every target directly from `targetDir` (no staging tree involved) and
|
|
40
|
+
* writes once everything computes cleanly. The convenience form for a
|
|
41
|
+
* patch set whose targets are ALL pre-existing scaffold files; a caller
|
|
42
|
+
* whose bundle also delivers some of those targets itself (via plain
|
|
43
|
+
* content or `installMap`) should use `computeModulePatches`/
|
|
44
|
+
* `writeModulePatches` directly so a patch failure can roll back the
|
|
45
|
+
* whole merge, not just the patch step.
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyModulePatches(patches: readonly ModulePatch[], targetDir: string, options: ApplyModulePatchesOptions): void;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies a module bundle's declarative `patches` (spec 5418 § 3 D1) onto an
|
|
3
|
+
* already-scaffolded target — the only mechanism that edits a file the
|
|
4
|
+
* buyer's scaffold already has (`installMap`/`postInstall`, spec 5373 § D7,
|
|
5
|
+
* can only add a file or print a message). Computes every patched file in
|
|
6
|
+
* memory first; only after every target's in-memory pass succeeds does a
|
|
7
|
+
* second pass write the changed files back to disk, once (D4) — so a
|
|
8
|
+
* conflict, a missing target, or an unparseable target anywhere in the
|
|
9
|
+
* manifest's patch set leaves the scaffold exactly as `copyTree`/`installMap`
|
|
10
|
+
* left it, nothing partial.
|
|
11
|
+
*
|
|
12
|
+
* `kind` decides how a target is read and re-serialized:
|
|
13
|
+
* - "json" — `package.json` (object-shaped fields, `merge-keys`) and
|
|
14
|
+
* `tsconfig.json` (array-shaped, `append-unique`). Parsed with
|
|
15
|
+
* `JSON.parse`, mutated in place, re-serialized with
|
|
16
|
+
* `JSON.stringify(obj, null, 2) + "\n"` (D2). A `package.json` target
|
|
17
|
+
* then runs through `substituteWorkspaceVersions`, mirroring
|
|
18
|
+
* `copy.ts`'s `isPackageJson` branch for a freshly-copied file.
|
|
19
|
+
* - "next-config" / "jest-config" — treated as text, never parsed or
|
|
20
|
+
* evaluated (NFR1 — no AST dependency). Each op is one of five named,
|
|
21
|
+
* idempotent, anchored transforms (D3): checked "already applied" →
|
|
22
|
+
* "anchor found, apply" → "not found, unparseable" — never a
|
|
23
|
+
* best-effort splice.
|
|
24
|
+
*
|
|
25
|
+
* Every failure mode here is one of D4's classes 1-3 (conflict, missing
|
|
26
|
+
* target, unparseable target). Class 4 (unknown kind/op) is caught earlier,
|
|
27
|
+
* at manifest validation (D6, `extract-module-bundle.ts`), before this
|
|
28
|
+
* function is ever called — every op here is assumed to be a recognized
|
|
29
|
+
* vocabulary member for its patch's `kind`.
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { basename, resolve } from "node:path";
|
|
33
|
+
import { substituteWorkspaceVersions, } from "../substitute.js";
|
|
34
|
+
/** D4 class 1 — a merge-keys key, or a JS-config anchor's already-applied value, disagrees. */
|
|
35
|
+
function conflict(target, locator, existing, wanted) {
|
|
36
|
+
return new Error(`patch conflict in ${target}: "${locator}" is ${JSON.stringify(existing)}, module wants ${JSON.stringify(wanted)}`);
|
|
37
|
+
}
|
|
38
|
+
/** D4 class 2 — `resolve(targetDir, target)` does not exist on disk. */
|
|
39
|
+
function missingTarget(target) {
|
|
40
|
+
return new Error(`patch target missing: ${target} does not exist in this scaffold`);
|
|
41
|
+
}
|
|
42
|
+
/** D4 class 3 — a json target fails JSON.parse. */
|
|
43
|
+
function unparseableJson(target) {
|
|
44
|
+
return new Error(`patch target unparseable: ${target} is not valid JSON`);
|
|
45
|
+
}
|
|
46
|
+
/** D4 class 3 — a next-config/jest-config anchor (or a JSON `path`) cannot be located at all. */
|
|
47
|
+
function unparseableAnchor(target, anchor) {
|
|
48
|
+
return new Error(`patch target unparseable: ${target} — could not locate "${anchor}"`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Navigate `path` into `root`, requiring every intermediate segment to
|
|
52
|
+
* already resolve to a plain object. Returns the immediate parent + final
|
|
53
|
+
* key, or `null` if any intermediate segment is missing / not an object —
|
|
54
|
+
* the caller decides what "the final key's own container is missing or the
|
|
55
|
+
* wrong shape" means for its own op (D2: never create structure the buyer
|
|
56
|
+
* never had; a missing/wrong-shape container is `unparseableAnchor`, the
|
|
57
|
+
* same disposition the "if under-specified" resolution gives `merge-keys`
|
|
58
|
+
* and, by symmetry, `append-unique`).
|
|
59
|
+
*/
|
|
60
|
+
function resolvePathParent(root, path) {
|
|
61
|
+
let cur = root;
|
|
62
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
63
|
+
const seg = path[i];
|
|
64
|
+
if (typeof cur !== "object" ||
|
|
65
|
+
cur === null ||
|
|
66
|
+
Array.isArray(cur) ||
|
|
67
|
+
!Object.prototype.hasOwnProperty.call(cur, seg)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
cur = cur[seg];
|
|
71
|
+
}
|
|
72
|
+
if (typeof cur !== "object" || cur === null || Array.isArray(cur))
|
|
73
|
+
return null;
|
|
74
|
+
return { parent: cur, key: path[path.length - 1] };
|
|
75
|
+
}
|
|
76
|
+
function deepEqual(a, b) {
|
|
77
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve `workspace:*` to the concrete published semver BEFORE the
|
|
81
|
+
* merge-keys comparison/write, not only at final text-level serialization
|
|
82
|
+
* (D2 mirrors `copy.ts`'s `isPackageJson` branch, but that branch runs on a
|
|
83
|
+
* FRESHLY-COPIED file that never gets re-read; a patch target does — a
|
|
84
|
+
* second `applyModulePatches` run reads back the ALREADY-substituted `^x.y.z`
|
|
85
|
+
* value, so comparing it against a still-literal `"workspace:*"` `entries`
|
|
86
|
+
* value would misreport idempotent re-application as a conflict). Reuses
|
|
87
|
+
* `substituteWorkspaceVersions` itself (same regex, same throw-on-missing
|
|
88
|
+
* behavior) rather than re-deriving the caret-prefix rule.
|
|
89
|
+
*/
|
|
90
|
+
function resolveEntryValue(key, value, versions) {
|
|
91
|
+
if (value !== "workspace:*" || !key.startsWith("@working-theory/"))
|
|
92
|
+
return value;
|
|
93
|
+
const substituted = substituteWorkspaceVersions(`"${key}": "workspace:*"`, versions);
|
|
94
|
+
const match = /:\s*"([^"]+)"$/.exec(substituted);
|
|
95
|
+
return match ? match[1] : value;
|
|
96
|
+
}
|
|
97
|
+
function applyJsonMergeKeys(target, root, op, versions, isPackageJson) {
|
|
98
|
+
const resolved = resolvePathParent(root, op.path);
|
|
99
|
+
const container = resolved ? resolved.parent[resolved.key] : undefined;
|
|
100
|
+
if (!resolved || typeof container !== "object" || container === null || Array.isArray(container)) {
|
|
101
|
+
throw unparseableAnchor(target, op.path.join("."));
|
|
102
|
+
}
|
|
103
|
+
const obj = container;
|
|
104
|
+
for (const [key, rawValue] of Object.entries(op.entries)) {
|
|
105
|
+
const value = isPackageJson ? resolveEntryValue(key, rawValue, versions) : rawValue;
|
|
106
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
107
|
+
obj[key] = value;
|
|
108
|
+
}
|
|
109
|
+
else if (!deepEqual(obj[key], value)) {
|
|
110
|
+
throw conflict(target, [...op.path, key].join("."), obj[key], value);
|
|
111
|
+
}
|
|
112
|
+
// Present with the identical value — no-op (idempotent).
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function applyJsonAppendUnique(target, root, op) {
|
|
116
|
+
const resolved = resolvePathParent(root, op.path);
|
|
117
|
+
const container = resolved ? resolved.parent[resolved.key] : undefined;
|
|
118
|
+
if (!resolved || !Array.isArray(container)) {
|
|
119
|
+
throw unparseableAnchor(target, op.path.join("."));
|
|
120
|
+
}
|
|
121
|
+
for (const item of op.items) {
|
|
122
|
+
if (!container.some((existing) => deepEqual(existing, item))) {
|
|
123
|
+
container.push(item);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function applyJsonOps(target, text, ops, versions, isPackageJson) {
|
|
128
|
+
let root;
|
|
129
|
+
try {
|
|
130
|
+
root = JSON.parse(text);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw unparseableJson(target);
|
|
134
|
+
}
|
|
135
|
+
for (const op of ops) {
|
|
136
|
+
if (op.op === "merge-keys")
|
|
137
|
+
applyJsonMergeKeys(target, root, op, versions, isPackageJson);
|
|
138
|
+
else if (op.op === "append-unique")
|
|
139
|
+
applyJsonAppendUnique(target, root, op);
|
|
140
|
+
else {
|
|
141
|
+
throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${target}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
145
|
+
}
|
|
146
|
+
/** The `[...]` span of the array literal opened right after `anchorLiteral`, or `null` if the anchor/array can't be located. */
|
|
147
|
+
function findArraySpan(text, anchorLiteral) {
|
|
148
|
+
const anchorIdx = text.indexOf(anchorLiteral);
|
|
149
|
+
if (anchorIdx === -1)
|
|
150
|
+
return null;
|
|
151
|
+
const openIdx = text.indexOf("[", anchorIdx);
|
|
152
|
+
if (openIdx === -1)
|
|
153
|
+
return null;
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let closeIdx = -1;
|
|
156
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
157
|
+
if (text[i] === "[")
|
|
158
|
+
depth++;
|
|
159
|
+
else if (text[i] === "]") {
|
|
160
|
+
depth--;
|
|
161
|
+
if (depth === 0) {
|
|
162
|
+
closeIdx = i;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (closeIdx === -1)
|
|
168
|
+
return null;
|
|
169
|
+
return { openIdx, closeIdx };
|
|
170
|
+
}
|
|
171
|
+
/** Whether `elementLine` already occurs WITHIN the array literal opened after `anchorLiteral` (not merely somewhere in the file). */
|
|
172
|
+
function arrayContainsElement(text, anchorLiteral, elementLine) {
|
|
173
|
+
const span = findArraySpan(text, anchorLiteral);
|
|
174
|
+
if (span === null)
|
|
175
|
+
return false;
|
|
176
|
+
return text.slice(span.openIdx, span.closeIdx).includes(elementLine);
|
|
177
|
+
}
|
|
178
|
+
/** Insert `elementLine` (already including its own trailing "\n") as the last element of the array literal opened right after `anchorLiteral`. */
|
|
179
|
+
function insertAsLastArrayElement(text, anchorLiteral, elementLine) {
|
|
180
|
+
const span = findArraySpan(text, anchorLiteral);
|
|
181
|
+
if (span === null)
|
|
182
|
+
return null;
|
|
183
|
+
const { openIdx, closeIdx } = span;
|
|
184
|
+
const body = text.slice(openIdx + 1, closeIdx);
|
|
185
|
+
const trailingMatch = /\n[ \t]*$/.exec(body);
|
|
186
|
+
const trailingWs = trailingMatch ? trailingMatch[0] : "\n";
|
|
187
|
+
const bodyCore = trailingMatch ? body.slice(0, -trailingWs.length) : body;
|
|
188
|
+
const newBody = `${bodyCore}\n${elementLine.replace(/\n$/, "")}${trailingWs}`;
|
|
189
|
+
return text.slice(0, openIdx + 1) + newBody + text.slice(closeIdx);
|
|
190
|
+
}
|
|
191
|
+
/** Insert `entryText` (including its own trailing "\n") immediately after the line containing `anchorLiteral`. */
|
|
192
|
+
function insertAfterAnchorLine(text, anchorLiteral, entryText) {
|
|
193
|
+
const anchorIdx = text.indexOf(anchorLiteral);
|
|
194
|
+
if (anchorIdx === -1)
|
|
195
|
+
return null;
|
|
196
|
+
const lineEnd = text.indexOf("\n", anchorIdx);
|
|
197
|
+
if (lineEnd === -1)
|
|
198
|
+
return null;
|
|
199
|
+
return text.slice(0, lineEnd + 1) + entryText + text.slice(lineEnd + 1);
|
|
200
|
+
}
|
|
201
|
+
function applyAddTranspilePackage(target, text, op) {
|
|
202
|
+
const elementLine = ` "${op.package}",\n`;
|
|
203
|
+
if (arrayContainsElement(text, "transpilePackages: [", elementLine))
|
|
204
|
+
return text; // already applied — idempotent no-op.
|
|
205
|
+
const next = insertAsLastArrayElement(text, "transpilePackages: [", elementLine);
|
|
206
|
+
if (next === null)
|
|
207
|
+
throw unparseableAnchor(target, "transpilePackages: [");
|
|
208
|
+
return next;
|
|
209
|
+
}
|
|
210
|
+
function applyWrapExportWith(target, text, op) {
|
|
211
|
+
let next = text;
|
|
212
|
+
if (!next.includes(op.importLine)) {
|
|
213
|
+
const re = /^import\b.*\n/gm;
|
|
214
|
+
let lastEnd = -1;
|
|
215
|
+
let m;
|
|
216
|
+
while ((m = re.exec(next)))
|
|
217
|
+
lastEnd = m.index + m[0].length;
|
|
218
|
+
if (lastEnd === -1)
|
|
219
|
+
throw unparseableAnchor(target, op.importLine.trimEnd());
|
|
220
|
+
next = next.slice(0, lastEnd) + op.importLine + next.slice(lastEnd);
|
|
221
|
+
}
|
|
222
|
+
if (next.includes(op.replaceWith))
|
|
223
|
+
return next; // already applied — idempotent no-op.
|
|
224
|
+
if (!next.includes(op.find))
|
|
225
|
+
throw unparseableAnchor(target, op.find);
|
|
226
|
+
return next.replace(op.find, op.replaceWith);
|
|
227
|
+
}
|
|
228
|
+
function applyAddTransformIgnorePattern(target, text, op) {
|
|
229
|
+
if (text.includes(op.replaceWith))
|
|
230
|
+
return text; // already applied — idempotent no-op.
|
|
231
|
+
if (!text.includes(op.find))
|
|
232
|
+
throw unparseableAnchor(target, op.find);
|
|
233
|
+
return text.replace(op.find, op.replaceWith);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Shared by add-module-name-mapper / add-transform: a keyed entry inside an
|
|
237
|
+
* object literal. `renderedValue` is the exact FILE-TEXT form of the wanted
|
|
238
|
+
* value (quoted for a mapper's string value; verbatim for a transform's raw
|
|
239
|
+
* source text) — used only for the idempotence text comparison and the
|
|
240
|
+
* insertion. `plainWantedValue` is the LOGICAL value — used for the D4
|
|
241
|
+
* class-1 message, which JSON.stringifies it itself (D4's `<newValueJSON>`
|
|
242
|
+
* shape); passing the already-quoted `renderedValue` there would double-quote it.
|
|
243
|
+
* `unwrapQuotes` strips one layer of literal `"…"` off the captured
|
|
244
|
+
* existing-file text before it goes into the message, for the same reason —
|
|
245
|
+
* on for add-module-name-mapper (always a quoted string), off for
|
|
246
|
+
* add-transform (verbatim source, not necessarily quoted at all).
|
|
247
|
+
*/
|
|
248
|
+
function applyKeyedEntry(target, text, anchorLiteral, locatorPrefix, pattern, renderedValue, plainWantedValue, unwrapQuotes, entryText) {
|
|
249
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
250
|
+
const keyRe = new RegExp(`"${escaped}":\\s*(.+?),`, "s");
|
|
251
|
+
const match = keyRe.exec(text);
|
|
252
|
+
if (match) {
|
|
253
|
+
const existingRaw = match[1].trim();
|
|
254
|
+
if (existingRaw === renderedValue.trim())
|
|
255
|
+
return text; // already applied — idempotent no-op.
|
|
256
|
+
const plainExisting = unwrapQuotes
|
|
257
|
+
? existingRaw.replace(/^"([\s\S]*)"$/, "$1")
|
|
258
|
+
: existingRaw;
|
|
259
|
+
throw conflict(target, `${locatorPrefix}["${pattern}"]`, plainExisting, plainWantedValue);
|
|
260
|
+
}
|
|
261
|
+
const next = insertAfterAnchorLine(text, anchorLiteral, entryText);
|
|
262
|
+
if (next === null)
|
|
263
|
+
throw unparseableAnchor(target, anchorLiteral);
|
|
264
|
+
return next;
|
|
265
|
+
}
|
|
266
|
+
function applyAddModuleNameMapper(target, text, op) {
|
|
267
|
+
const renderedValue = `"${op.value}"`;
|
|
268
|
+
const entryText = ` "${op.pattern}":\n ${renderedValue},\n`;
|
|
269
|
+
return applyKeyedEntry(target, text, "moduleNameMapper: {", "moduleNameMapper", op.pattern, renderedValue, op.value, true, entryText);
|
|
270
|
+
}
|
|
271
|
+
function applyAddTransform(target, text, op) {
|
|
272
|
+
const entryText = ` "${op.pattern}": ${op.value},\n`;
|
|
273
|
+
return applyKeyedEntry(target, text, "transform: {", "transform", op.pattern, op.value, op.value, false, entryText);
|
|
274
|
+
}
|
|
275
|
+
function applyTextOps(target, text, ops) {
|
|
276
|
+
let next = text;
|
|
277
|
+
for (const op of ops) {
|
|
278
|
+
if (op.op === "add-transpile-package")
|
|
279
|
+
next = applyAddTranspilePackage(target, next, op);
|
|
280
|
+
else if (op.op === "wrap-export-with")
|
|
281
|
+
next = applyWrapExportWith(target, next, op);
|
|
282
|
+
else if (op.op === "add-transform-ignore-pattern")
|
|
283
|
+
next = applyAddTransformIgnorePattern(target, next, op);
|
|
284
|
+
else if (op.op === "add-module-name-mapper")
|
|
285
|
+
next = applyAddModuleNameMapper(target, next, op);
|
|
286
|
+
else if (op.op === "add-transform")
|
|
287
|
+
next = applyAddTransform(target, next, op);
|
|
288
|
+
else {
|
|
289
|
+
throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${target}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return next;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Compute every patched target's new content in memory — throws on the
|
|
296
|
+
* first D4 class 1-3 failure encountered (deterministic order — targets in
|
|
297
|
+
* `patches` array order) and writes nothing anywhere; returns only the
|
|
298
|
+
* targets whose content actually changed. Pure with respect to the
|
|
299
|
+
* filesystem: reads go through `options.readTarget`, nothing is written.
|
|
300
|
+
* Pair with `writeModulePatches` for the second pass, deferred until every
|
|
301
|
+
* other part of the caller's own operation (e.g. `copyTree`/`installMap`)
|
|
302
|
+
* has ALSO succeeded, so a patch failure can never leave partial output
|
|
303
|
+
* from an earlier step in place either.
|
|
304
|
+
*/
|
|
305
|
+
export function computeModulePatches(patches, options) {
|
|
306
|
+
if (patches.length === 0)
|
|
307
|
+
return [];
|
|
308
|
+
// Group by target — a manifest MAY list the same target across multiple
|
|
309
|
+
// ModulePatch entries; every op targeting a file applies, in array order,
|
|
310
|
+
// to that one file's in-memory buffer.
|
|
311
|
+
const byTarget = new Map();
|
|
312
|
+
for (const patch of patches) {
|
|
313
|
+
const existing = byTarget.get(patch.target);
|
|
314
|
+
if (existing)
|
|
315
|
+
existing.ops.push(...patch.ops);
|
|
316
|
+
else
|
|
317
|
+
byTarget.set(patch.target, { kind: patch.kind, ops: [...patch.ops] });
|
|
318
|
+
}
|
|
319
|
+
const computed = [];
|
|
320
|
+
for (const [target, { kind, ops }] of byTarget) {
|
|
321
|
+
const original = options.readTarget(target);
|
|
322
|
+
if (original === undefined)
|
|
323
|
+
throw missingTarget(target);
|
|
324
|
+
let content;
|
|
325
|
+
if (kind === "json") {
|
|
326
|
+
const isPackageJson = basename(target) === "package.json";
|
|
327
|
+
content = applyJsonOps(target, original, ops, options.versions, isPackageJson);
|
|
328
|
+
if (isPackageJson) {
|
|
329
|
+
content = substituteWorkspaceVersions(content, options.versions);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (kind === "next-config" || kind === "jest-config") {
|
|
333
|
+
content = applyTextOps(target, original, ops);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
throw new Error(`invalid module manifest: unknown patch kind "${kind}" for target ${target}`);
|
|
337
|
+
}
|
|
338
|
+
if (content !== original)
|
|
339
|
+
computed.push({ target, content });
|
|
340
|
+
}
|
|
341
|
+
return computed;
|
|
342
|
+
}
|
|
343
|
+
/** Write every `computeModulePatches` result to disk, resolved against `baseDir`, once. */
|
|
344
|
+
export function writeModulePatches(computed, baseDir) {
|
|
345
|
+
const baseAbs = resolve(baseDir);
|
|
346
|
+
for (const { target, content } of computed) {
|
|
347
|
+
writeFileSync(resolve(baseAbs, target), content);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Apply every patch in `patches` onto the scaffold at `targetDir` — reads
|
|
352
|
+
* every target directly from `targetDir` (no staging tree involved) and
|
|
353
|
+
* writes once everything computes cleanly. The convenience form for a
|
|
354
|
+
* patch set whose targets are ALL pre-existing scaffold files; a caller
|
|
355
|
+
* whose bundle also delivers some of those targets itself (via plain
|
|
356
|
+
* content or `installMap`) should use `computeModulePatches`/
|
|
357
|
+
* `writeModulePatches` directly so a patch failure can roll back the
|
|
358
|
+
* whole merge, not just the patch step.
|
|
359
|
+
*/
|
|
360
|
+
export function applyModulePatches(patches, targetDir, options) {
|
|
361
|
+
const targetAbs = resolve(targetDir);
|
|
362
|
+
const readTarget = (target) => {
|
|
363
|
+
const absPath = resolve(targetAbs, target);
|
|
364
|
+
if (!existsSync(absPath))
|
|
365
|
+
return undefined;
|
|
366
|
+
return readFileSync(absPath, "utf8");
|
|
367
|
+
};
|
|
368
|
+
const computed = computeModulePatches(patches, { ...options, readTarget });
|
|
369
|
+
writeModulePatches(computed, targetAbs);
|
|
370
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceVersionMap } from "../substitute.js";
|
|
1
2
|
export interface ExtractModuleBundleOptions {
|
|
2
3
|
/** The fetched bundle bytes (a gzipped tar archive). */
|
|
3
4
|
bytes: Uint8Array;
|
|
@@ -5,12 +6,20 @@ export interface ExtractModuleBundleOptions {
|
|
|
5
6
|
targetDir: string;
|
|
6
7
|
/** The module name — used only for error messages and staging dir naming. */
|
|
7
8
|
moduleName: string;
|
|
9
|
+
/**
|
|
10
|
+
* Canonical `@working-theory/*` package versions (spec 5418 § 3 D2), for
|
|
11
|
+
* `package.json` patch targets' `workspace:*` substitution — the same map
|
|
12
|
+
* `copyTemplate` threads to `substituteWorkspaceVersions`. Defaults to `{}`
|
|
13
|
+
* (a bundle with no `patches`, or none using `workspace:*`, needs nothing).
|
|
14
|
+
*/
|
|
15
|
+
versions?: WorkspaceVersionMap;
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
18
|
* Extract a fetched module bundle into `targetDir`, merging its files onto
|
|
11
19
|
* the tree `copyTemplate` already wrote. Throws on any unsafe entry,
|
|
12
|
-
* decompress error,
|
|
13
|
-
* failure (
|
|
14
|
-
*
|
|
20
|
+
* decompress error, limit overrun, invalid `installMap`, or `patches`
|
|
21
|
+
* failure (D4) — `targetDir` is left untouched on failure in every one of
|
|
22
|
+
* those cases: validation AND patch computation both complete before any
|
|
23
|
+
* file is copied or written into place.
|
|
15
24
|
*/
|
|
16
25
|
export declare function extractModuleBundle(options: ExtractModuleBundleOptions): Promise<void>;
|