@decocms/blocks-cli 7.0.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.
Files changed (93) hide show
  1. package/package.json +44 -0
  2. package/scripts/analyze-traces.mjs +1117 -0
  3. package/scripts/audit-observability-config.test.ts +446 -0
  4. package/scripts/audit-observability-config.ts +511 -0
  5. package/scripts/deco-migrate-cli.ts +444 -0
  6. package/scripts/fast-deploy-kv.test.ts +131 -0
  7. package/scripts/generate-blocks.test.ts +94 -0
  8. package/scripts/generate-blocks.ts +274 -0
  9. package/scripts/generate-invoke.test.ts +195 -0
  10. package/scripts/generate-invoke.ts +469 -0
  11. package/scripts/generate-loaders.ts +217 -0
  12. package/scripts/generate-schema.ts +1287 -0
  13. package/scripts/generate-sections.ts +237 -0
  14. package/scripts/htmx-analyze.ts +226 -0
  15. package/scripts/lib/blocks-dedupe.test.ts +179 -0
  16. package/scripts/lib/blocks-dedupe.ts +142 -0
  17. package/scripts/lib/cf-kv-rest.ts +78 -0
  18. package/scripts/lib/jsonc.ts +122 -0
  19. package/scripts/lib/kv-snapshot.ts +51 -0
  20. package/scripts/lib/read-decofile.ts +70 -0
  21. package/scripts/lib/sync-helpers.ts +44 -0
  22. package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
  23. package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
  24. package/scripts/migrate/analyzers/island-classifier.ts +96 -0
  25. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  26. package/scripts/migrate/analyzers/section-metadata.ts +147 -0
  27. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  28. package/scripts/migrate/colors.ts +46 -0
  29. package/scripts/migrate/config.test.ts +202 -0
  30. package/scripts/migrate/config.ts +186 -0
  31. package/scripts/migrate/phase-analyze.test.ts +63 -0
  32. package/scripts/migrate/phase-analyze.ts +782 -0
  33. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  34. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  35. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  36. package/scripts/migrate/phase-cleanup.ts +1588 -0
  37. package/scripts/migrate/phase-compile.test.ts +193 -0
  38. package/scripts/migrate/phase-compile.ts +177 -0
  39. package/scripts/migrate/phase-report.ts +243 -0
  40. package/scripts/migrate/phase-scaffold.ts +593 -0
  41. package/scripts/migrate/phase-transform.ts +310 -0
  42. package/scripts/migrate/phase-verify.test.ts +127 -0
  43. package/scripts/migrate/phase-verify.ts +572 -0
  44. package/scripts/migrate/post-cleanup/rules.ts +1708 -0
  45. package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
  46. package/scripts/migrate/post-cleanup/runner.ts +137 -0
  47. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  48. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  49. package/scripts/migrate/post-cleanup/types.ts +106 -0
  50. package/scripts/migrate/source-layout.test.ts +111 -0
  51. package/scripts/migrate/source-layout.ts +103 -0
  52. package/scripts/migrate/templates/app-css.ts +366 -0
  53. package/scripts/migrate/templates/cache-config.ts +26 -0
  54. package/scripts/migrate/templates/commerce-loaders.ts +230 -0
  55. package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
  56. package/scripts/migrate/templates/cursor-rules.ts +70 -0
  57. package/scripts/migrate/templates/hooks.test.ts +141 -0
  58. package/scripts/migrate/templates/hooks.ts +152 -0
  59. package/scripts/migrate/templates/knip-config.ts +27 -0
  60. package/scripts/migrate/templates/lib-utils.test.ts +139 -0
  61. package/scripts/migrate/templates/lib-utils.ts +326 -0
  62. package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
  63. package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
  64. package/scripts/migrate/templates/package-json.ts +177 -0
  65. package/scripts/migrate/templates/routes.ts +237 -0
  66. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  67. package/scripts/migrate/templates/section-loaders.ts +456 -0
  68. package/scripts/migrate/templates/server-entry.ts +561 -0
  69. package/scripts/migrate/templates/setup.ts +148 -0
  70. package/scripts/migrate/templates/tsconfig.ts +21 -0
  71. package/scripts/migrate/templates/types-gen.ts +174 -0
  72. package/scripts/migrate/templates/ui-components.ts +144 -0
  73. package/scripts/migrate/templates/vite-config.ts +101 -0
  74. package/scripts/migrate/transforms/dead-code.ts +455 -0
  75. package/scripts/migrate/transforms/deno-isms.ts +85 -0
  76. package/scripts/migrate/transforms/fresh-apis.ts +223 -0
  77. package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
  78. package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
  79. package/scripts/migrate/transforms/imports.ts +385 -0
  80. package/scripts/migrate/transforms/jsx.ts +317 -0
  81. package/scripts/migrate/transforms/section-conventions.ts +210 -0
  82. package/scripts/migrate/transforms/tailwind.ts +739 -0
  83. package/scripts/migrate/types.ts +244 -0
  84. package/scripts/migrate-blocks-to-kv.ts +104 -0
  85. package/scripts/migrate-post-cleanup.ts +191 -0
  86. package/scripts/migrate-to-cf-observability.test.ts +215 -0
  87. package/scripts/migrate-to-cf-observability.ts +699 -0
  88. package/scripts/migrate.ts +282 -0
  89. package/scripts/smoke-otlp-errorlog.ts +46 -0
  90. package/scripts/smoke-otlp-meter.ts +48 -0
  91. package/scripts/sync-blocks-to-kv.ts +153 -0
  92. package/scripts/tailwind-lint.ts +518 -0
  93. package/tsconfig.json +7 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Templates for src/lib/ utility wrappers that bridge signature gaps
3
+ * between deco-cx/apps (old stack) and @decocms/apps-start (new stack).
4
+ *
5
+ * These are written *lazily*: only shims that are actually imported by
6
+ * the migrated codebase get generated. See `selectImportedLibTemplates`.
7
+ *
8
+ * Lazy generation matters because:
9
+ * - Most sites end up importing zero of these (apps-start exports
10
+ * direct equivalents for most VTEX utilities — see migrate#107).
11
+ * - Eager generation creates dead `src/lib/*.ts` files that every site
12
+ * then has to clean up by hand (see baggagio-tanstack#7, ~235 LOC).
13
+ *
14
+ * Registry shape: `"src/lib/<name>.ts"` → file contents.
15
+ */
16
+ export const LIB_TEMPLATES: Record<string, string> = {
17
+ // Filled in below after the const declarations to keep the registry
18
+ // and template literals colocated. See trailing assignment.
19
+ };
20
+
21
+ /**
22
+ * Given the set of `~/lib/X` imports actually present in the migrated
23
+ * codebase, return the subset of templates to write.
24
+ *
25
+ * `importedSpecifiers` are the `X` parts (without `~/lib/` prefix and
26
+ * without `.ts` extension), e.g. `"vtex-transform"`, `"http-utils"`.
27
+ */
28
+ export function selectImportedLibTemplates(
29
+ importedSpecifiers: Set<string>,
30
+ ): Record<string, string> {
31
+ const out: Record<string, string> = {};
32
+ for (const spec of importedSpecifiers) {
33
+ const key = `src/lib/${spec}.ts`;
34
+ if (key in LIB_TEMPLATES) {
35
+ out[key] = LIB_TEMPLATES[key];
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ // Per the migration tooling policy (D3 — Throwing stubs):
42
+ // generated stubs MUST throw at runtime so the first call surfaces the
43
+ // gap loudly. Silent identity-cast `toProduct` was the bug behind
44
+ // baggagio-tanstack#10 (PDP product data was being dropped on the floor
45
+ // for weeks before anyone noticed).
46
+ //
47
+ // Each thrown message points at the canonical replacement so the fix
48
+ // is mechanical. `deco-post-cleanup --fix` automates the swap.
49
+ const LIB_VTEX_TRANSFORM = `import type { Product } from "@decocms/apps/commerce/types";
50
+
51
+ const STUB =
52
+ "[deco-migrate] \`~/lib/vtex-transform.toProduct\` is a generated stub. " +
53
+ "Replace with: import { toProduct } from '@decocms/apps/vtex/utils/transform' " +
54
+ "(canonical signature: \`toProduct(product, sku, level, options)\`). " +
55
+ "Run \`deco-post-cleanup --fix\` or see the deco-to-tanstack-migration skill " +
56
+ "(post-migration-cleanup § 5).";
57
+
58
+ export function toProduct(_vtexProduct: any, ..._rest: any[]): Product {
59
+ throw new Error(STUB);
60
+ }
61
+ `;
62
+
63
+ const LIB_VTEX_INTELLIGENT_SEARCH = `// Per the migration tooling policy (D3): \`getISCookiesFromBag\` cannot
64
+ // be implemented on TanStack Start because the bag-based lookup
65
+ // mechanism does not exist. Sites must read cookies directly from the
66
+ // request — see the \`vtex-shim-regression\` audit rule for guidance.
67
+ const STUB_GET_IS_COOKIES =
68
+ "[deco-migrate] \`~/lib/vtex-intelligent-search.getISCookiesFromBag\` is a " +
69
+ "generated stub. Refactor: extract IS cookies from " +
70
+ "\`request.headers.get('cookie')\` directly. The bag-based lookup mechanism " +
71
+ "does not exist on TanStack Start. See the deco-to-tanstack-migration " +
72
+ "skill (post-migration-cleanup § 5).";
73
+
74
+ export function getISCookiesFromBag(_req?: any): Record<string, string> {
75
+ throw new Error(STUB_GET_IS_COOKIES);
76
+ }
77
+
78
+ export function isFilterParam(key: string): boolean {
79
+ return key.startsWith("filter.");
80
+ }
81
+
82
+ export function toPath(facets: { key: string; value: string }[]): string {
83
+ return facets.map((f) => \`\${f.key}/\${f.value}\`).join("/");
84
+ }
85
+
86
+ export function withDefaultFacets(
87
+ facets: { key: string; value: string }[],
88
+ defaults?: any,
89
+ ): { key: string; value: string }[] {
90
+ if (Array.isArray(defaults)) {
91
+ return [...defaults, ...facets];
92
+ }
93
+ return [...facets];
94
+ }
95
+
96
+ export function withDefaultParams(
97
+ params: any,
98
+ defaults?: Record<string, string>,
99
+ ): any {
100
+ if (params instanceof URLSearchParams) {
101
+ if (defaults) {
102
+ for (const [key, value] of Object.entries(defaults)) {
103
+ if (!params.has(key)) {
104
+ params.set(key, value);
105
+ }
106
+ }
107
+ }
108
+ return params;
109
+ }
110
+ return { ...params, ...defaults };
111
+ }
112
+ `;
113
+
114
+ const LIB_VTEX_SEGMENT = `// Per the migration tooling policy (D3): both these stubs throw at
115
+ // runtime to force the call site to be fixed. Silent fallbacks here
116
+ // mean the storefront silently fails to forward VTEX segment data
117
+ // (sales channel, regionId, currency, etc.) and pricing/inventory
118
+ // quietly diverge from what the user should see.
119
+ const STUB_GET_SEGMENT_FROM_BAG =
120
+ "[deco-migrate] \`~/lib/vtex-segment.getSegmentFromBag\` is a generated " +
121
+ "stub. Refactor: read cookies via \`request.headers.get('cookie')\` then " +
122
+ "call \`buildSegmentFromCookies()\` from '@decocms/apps/vtex/utils/segment'. " +
123
+ "The bag-based lookup mechanism does not exist on TanStack Start.";
124
+
125
+ const STUB_WITH_SEGMENT_COOKIE =
126
+ "[deco-migrate] \`~/lib/vtex-segment.withSegmentCookie\` is a generated " +
127
+ "stub. Replace with: import { withSegmentCookie } from " +
128
+ "'@decocms/apps/vtex/utils/segment' (canonical signature: " +
129
+ "\`withSegmentCookie(segment, headers?)\`). Run \`deco-post-cleanup --fix\` " +
130
+ "or see the deco-to-tanstack-migration skill.";
131
+
132
+ export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
133
+ throw new Error(STUB_GET_SEGMENT_FROM_BAG);
134
+ }
135
+
136
+ export function withSegmentCookie(..._args: any[]): any {
137
+ throw new Error(STUB_WITH_SEGMENT_COOKIE);
138
+ }
139
+ `;
140
+
141
+ const LIB_HTTP_UTILS = `/**
142
+ * Drop-in replacement for the typed HTTP client from deco-cx/apps.
143
+ * Supports both simple \`.get(path)\` / \`.post(path, body)\` calls AND
144
+ * the indexed pattern \`client["GET /api/path"]({params}, {init})\`
145
+ * used by legacy loaders.
146
+ */
147
+ export function createHttpClient<_T = any>(options: {
148
+ base: string;
149
+ headers?: Record<string, string> | Headers;
150
+ fetcher?: typeof fetch;
151
+ }) {
152
+ const base = options.base.replace(/\\/$/, "");
153
+ const defaultHeaders: Record<string, string> =
154
+ options.headers instanceof Headers
155
+ ? Object.fromEntries(options.headers.entries())
156
+ : (options.headers || {});
157
+
158
+ const handler: ProxyHandler<Record<string, unknown>> = {
159
+ get(_target, prop) {
160
+ if (prop === "get") {
161
+ return async <R = any>(path: string, init?: RequestInit): Promise<R> => {
162
+ const res = await fetch(\`\${base}\${path}\`, {
163
+ ...init,
164
+ headers: { ...defaultHeaders, ...(init?.headers as Record<string, string>) },
165
+ });
166
+ return res.json();
167
+ };
168
+ }
169
+ if (prop === "post") {
170
+ return async <R = any>(path: string, body: unknown, init?: RequestInit): Promise<R> => {
171
+ const res = await fetch(\`\${base}\${path}\`, {
172
+ method: "POST",
173
+ ...init,
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ ...defaultHeaders,
177
+ ...(init?.headers as Record<string, string>),
178
+ },
179
+ body: JSON.stringify(body),
180
+ });
181
+ return res.json();
182
+ };
183
+ }
184
+ if (typeof prop === "string" && /^(GET|POST|PUT|PATCH|DELETE)\\s+/.test(prop)) {
185
+ const spaceIdx = prop.indexOf(" ");
186
+ const method = prop.slice(0, spaceIdx);
187
+ let apiPath = prop.slice(spaceIdx + 1);
188
+
189
+ return async (params: Record<string, any> = {}, init?: RequestInit) => {
190
+ const cleanParams = { ...params };
191
+
192
+ const starMatch = apiPath.match(/\\*(\\w+)/);
193
+ if (starMatch) {
194
+ const paramName = starMatch[1];
195
+ if (cleanParams[paramName] != null) {
196
+ apiPath = apiPath.replace(\`*\${paramName}\`, String(cleanParams[paramName]));
197
+ delete cleanParams[paramName];
198
+ } else {
199
+ apiPath = apiPath.replace(/\\/\\*\\w+/, "");
200
+ }
201
+ }
202
+
203
+ let url = \`\${base}\${apiPath}\`;
204
+
205
+ if (method === "GET") {
206
+ const sp = new URLSearchParams();
207
+ for (const [k, v] of Object.entries(cleanParams)) {
208
+ if (v !== undefined && v !== null) sp.set(k, String(v));
209
+ }
210
+ const qs = sp.toString();
211
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
212
+ }
213
+
214
+ const fetchInit: RequestInit = {
215
+ method,
216
+ ...init,
217
+ headers: {
218
+ ...defaultHeaders,
219
+ ...(init?.headers instanceof Headers
220
+ ? Object.fromEntries(init.headers.entries())
221
+ : (init?.headers as Record<string, string>)),
222
+ },
223
+ ...(method !== "GET" && Object.keys(cleanParams).length > 0
224
+ ? { body: JSON.stringify(cleanParams) }
225
+ : {}),
226
+ };
227
+
228
+ const res = await fetch(url, fetchInit);
229
+ return { json: () => res.json(), ok: res.ok, status: res.status, headers: res.headers };
230
+ };
231
+ }
232
+ return undefined;
233
+ },
234
+ };
235
+
236
+ return new Proxy({} as Record<string, unknown>, handler) as any;
237
+ }
238
+ `;
239
+
240
+ const LIB_VTEX_CLIENT = `export interface VTEXCommerceStable {
241
+ account: string;
242
+ environment?: string;
243
+ }
244
+ `;
245
+
246
+ const LIB_FETCH_UTILS = `export const STALE = {
247
+ "Cache-Control": "public, max-age=120, stale-while-revalidate=600",
248
+ };
249
+ `;
250
+
251
+ const LIB_VTEX_FETCH = `export async function fetchSafe(
252
+ input: string | URL | Request,
253
+ init?: RequestInit,
254
+ ): Promise<Response> {
255
+ const response = await fetch(input, init);
256
+ if (!response.ok) {
257
+ console.error(\`VTEX fetch failed: \${response.status} \${response.statusText}\`);
258
+ }
259
+ return response;
260
+ }
261
+ `;
262
+
263
+ const LIB_VTEX_ID = `export function parseCookie(cookieStr?: string | null): Record<string, string> {
264
+ if (!cookieStr) return {};
265
+ return Object.fromEntries(
266
+ cookieStr.split(";").map((c) => {
267
+ const [key, ...rest] = c.trim().split("=");
268
+ return [key, rest.join("=")];
269
+ }),
270
+ );
271
+ }
272
+ `;
273
+
274
+ const LIB_GRAPHQL_UTILS = `export function createGraphqlClient(options: {
275
+ endpoint: string;
276
+ headers?: Record<string, string>;
277
+ }) {
278
+ return {
279
+ async query<T = any>(query: string, variables?: Record<string, unknown>): Promise<T> {
280
+ const res = await fetch(options.endpoint, {
281
+ method: "POST",
282
+ headers: {
283
+ "Content-Type": "application/json",
284
+ ...options.headers,
285
+ },
286
+ body: JSON.stringify({ query, variables }),
287
+ });
288
+ const json = await res.json();
289
+ return json.data;
290
+ },
291
+ };
292
+ }
293
+ `;
294
+
295
+ const LIB_FILTER_NAVIGATE = `/**
296
+ * Converts a VTEX filter URL string (e.g. "?filter.brand=x&filter.price=10:50")
297
+ * into a clean search string without internal params like \`payload\`.
298
+ * Returns "" or "?filter.brand=x&..." ready to append to pathname.
299
+ */
300
+ export function toFilterSearchString(filterUrl: string): string {
301
+ const str = filterUrl.startsWith("?") ? filterUrl.slice(1) : filterUrl;
302
+ if (!str) return "";
303
+
304
+ const params = new URLSearchParams(str);
305
+ params.delete("payload");
306
+
307
+ const clean = params.toString();
308
+ return clean ? \`?\${clean}\` : "";
309
+ }
310
+ `;
311
+
312
+ // Populate the registry now that all template literals are declared.
313
+ // Keys must match the relative path the migration script writes; values
314
+ // are the file contents.
315
+ Object.assign(LIB_TEMPLATES, {
316
+ "src/lib/vtex-transform.ts": LIB_VTEX_TRANSFORM,
317
+ "src/lib/vtex-intelligent-search.ts": LIB_VTEX_INTELLIGENT_SEARCH,
318
+ "src/lib/vtex-segment.ts": LIB_VTEX_SEGMENT,
319
+ "src/lib/http-utils.ts": LIB_HTTP_UTILS,
320
+ "src/lib/vtex-client.ts": LIB_VTEX_CLIENT,
321
+ "src/lib/fetch-utils.ts": LIB_FETCH_UTILS,
322
+ "src/lib/vtex-fetch.ts": LIB_VTEX_FETCH,
323
+ "src/lib/vtex-id.ts": LIB_VTEX_ID,
324
+ "src/lib/graphql-utils.ts": LIB_GRAPHQL_UTILS,
325
+ "src/lib/filter-navigate.ts": LIB_FILTER_NAVIGATE,
326
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateLockfileCheckYml } from "./lockfile-check-yml";
3
+
4
+ describe("generateLockfileCheckYml", () => {
5
+ it("emits a valid GitHub Actions workflow with the bun version pinned", () => {
6
+ const yaml = generateLockfileCheckYml("1.3.5");
7
+ expect(yaml).toContain("name: lockfile-check");
8
+ expect(yaml).toContain("on:");
9
+ expect(yaml).toContain("pull_request:");
10
+ expect(yaml).toContain("bun-version: 1.3.5");
11
+ expect(yaml).toContain("bun install --frozen-lockfile");
12
+ });
13
+
14
+ it("strips an accidental `bun@` prefix from the version input", () => {
15
+ const yaml = generateLockfileCheckYml("bun@1.3.5");
16
+ expect(yaml).toContain("bun-version: 1.3.5");
17
+ expect(yaml).not.toMatch(/bun-version:\s*bun@/);
18
+ });
19
+
20
+ it("scopes the trigger to package.json + bun.lock + the workflow itself", () => {
21
+ const yaml = generateLockfileCheckYml("1.3.5");
22
+ expect(yaml).toContain('"package.json"');
23
+ expect(yaml).toContain('"bun.lock"');
24
+ expect(yaml).toContain('".github/workflows/lockfile-check.yml"');
25
+ });
26
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Generates `.github/workflows/lockfile-check.yml`, the PR-time
3
+ * guardrail that fails any pull request which would have failed
4
+ * Cloudflare Workers Builds with `lockfile had changes, but lockfile
5
+ * is frozen`. This catches drift before it reaches main, instead of
6
+ * only after deploy attempts.
7
+ *
8
+ * Lives in the site repo (per-site, not centralised) because per
9
+ * D6.3 we are NOT scaffolding caller stubs that pull in
10
+ * `decocms/deco-start@vN` reusable workflows. The check is small
11
+ * enough that copy-paste-per-site is the right tradeoff.
12
+ *
13
+ * Bun version pinning matches the `packageManager` field in the
14
+ * scaffolded `package.json` (see `templates/package-json.ts`'s
15
+ * `CANONICAL_BUN_VERSION`). Kept in lockstep manually for now;
16
+ * future iterations may consolidate both into a shared constant.
17
+ */
18
+ export function generateLockfileCheckYml(bunVersion: string): string {
19
+ const v = stripBunPrefix(bunVersion);
20
+ return `name: lockfile-check
21
+
22
+ # PR-time guardrail: re-runs the same install Cloudflare Workers Builds
23
+ # runs on deploy (\`bun install --frozen-lockfile\`). Fails the PR if
24
+ # bun.lock would have rejected the install — closes the loop so drift
25
+ # can never reach main, only to be caught by Workers Builds at deploy
26
+ # time.
27
+ #
28
+ # Pairs with:
29
+ # - "packageManager": "bun@${v}" in package.json
30
+ # - .gitignore bans on package-lock.json / yarn.lock / pnpm-lock.yaml
31
+ # - the deco-post-cleanup audit's lockfile-* rules
32
+ #
33
+ # Adjust \`bun-version\` here in lockstep with the package.json
34
+ # \`packageManager\` field.
35
+
36
+ on:
37
+ pull_request:
38
+ paths:
39
+ - "package.json"
40
+ - "bun.lock"
41
+ - ".github/workflows/lockfile-check.yml"
42
+
43
+ permissions:
44
+ contents: read
45
+
46
+ jobs:
47
+ frozen-install:
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: oven-sh/setup-bun@v2
52
+ with:
53
+ bun-version: ${v}
54
+ - name: bun install --frozen-lockfile
55
+ run: bun install --frozen-lockfile
56
+ `;
57
+ }
58
+
59
+ /**
60
+ * Strip an accidental `bun@` prefix from the version string so the
61
+ * YAML's `bun-version` input (which expects a bare semver) doesn't
62
+ * receive `bun@1.3.5`.
63
+ */
64
+ function stripBunPrefix(input: string): string {
65
+ return input.replace(/^bun@/, "");
66
+ }
@@ -0,0 +1,177 @@
1
+ import { execSync } from "node:child_process";
2
+ import type { MigrationContext } from "../types";
3
+
4
+ /**
5
+ * Fleet-wide canonical bun version. Bumped here propagates to all newly
6
+ * migrated sites via the `packageManager` field AND the
7
+ * `lockfile-check.yml` workflow's `bun-version` input. See
8
+ * MIGRATION_TOOLING_PLAN.md for the bun-canonical decision.
9
+ *
10
+ * Exported because the lockfile-check workflow template reads it
11
+ * directly to keep both files in lockstep.
12
+ */
13
+ export const CANONICAL_BUN_VERSION = "1.3.5";
14
+
15
+ /**
16
+ * Get the latest published version of an npm package.
17
+ * Falls back to the provided default if the lookup fails.
18
+ */
19
+ function getLatestVersion(pkg: string, fallback: string): string {
20
+ try {
21
+ const version = execSync(`npm view ${pkg} version`, {
22
+ encoding: "utf-8",
23
+ timeout: 10000,
24
+ stdio: ["pipe", "pipe", "pipe"],
25
+ }).trim();
26
+ return version || fallback;
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Extract npm dependencies from deno.json import map.
34
+ * Entries like `"fuse.js": "npm:fuse.js@7.0.0"` become `"fuse.js": "^7.0.0"`.
35
+ */
36
+ function extractNpmDeps(importMap: Record<string, string>): Record<string, string> {
37
+ const deps: Record<string, string> = {};
38
+ const SKIP_KEYS = new Set([
39
+ "daisyui", "preact-render-to-string", "simple-git", "fast-json-patch",
40
+ "postcss", "cssnano", "partytown",
41
+ ]);
42
+ for (const [key, value] of Object.entries(importMap)) {
43
+ // Skip framework deps we handle ourselves
44
+ if (key.startsWith("preact") || key.startsWith("@preact/")) continue;
45
+ if (key.startsWith("@deco/")) continue;
46
+ if (key.startsWith("@biomejs/")) continue;
47
+ if (key.startsWith("firebase/")) continue;
48
+ if (SKIP_KEYS.has(key)) continue;
49
+
50
+ // npm: protocol — direct npm import
51
+ if (value.startsWith("npm:")) {
52
+ const raw = value.slice(4);
53
+ const atIdx = raw.lastIndexOf("@");
54
+ if (atIdx <= 0) {
55
+ deps[raw] = "*";
56
+ } else {
57
+ const name = raw.slice(0, atIdx);
58
+ let version = raw.slice(atIdx + 1);
59
+ if (/^[~^>=<]/.test(version)) {
60
+ deps[name] = version;
61
+ } else {
62
+ deps[name] = `^${version}`;
63
+ }
64
+ }
65
+ continue;
66
+ }
67
+
68
+ // esm.sh URLs — extract package name and version
69
+ const esmMatch = value.match(/esm\.sh\/(@?[^@?]+)@([^?/]+)/);
70
+ if (esmMatch) {
71
+ const [, name, version] = esmMatch;
72
+ if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
73
+ deps[name] = `^${version}`;
74
+ continue;
75
+ }
76
+ }
77
+ return deps;
78
+ }
79
+
80
+ export function generatePackageJson(ctx: MigrationContext): string {
81
+ const extractedDeps = {
82
+ ...extractNpmDeps(ctx.importMap),
83
+ ...ctx.discoveredNpmDeps,
84
+ };
85
+
86
+ // Consolidate firebase/* split imports into single package
87
+ const hasFirebase = Object.keys(ctx.importMap).some((k) => k.startsWith("firebase"));
88
+ if (hasFirebase && !extractedDeps["firebase"]) {
89
+ extractedDeps["firebase"] = "^12.10.0";
90
+ }
91
+ // Remove wildcard versions
92
+ for (const [k, v] of Object.entries(extractedDeps)) {
93
+ if (v === "^*" || v === "*") extractedDeps[k] = "latest";
94
+ }
95
+
96
+ const siteDeps = extractedDeps;
97
+
98
+ // Fetch latest versions from npm registry
99
+ const startVersion = getLatestVersion("@decocms/start", "0.34.0");
100
+ const appsVersion = getLatestVersion("@decocms/apps", "0.27.0");
101
+
102
+ const pkg = {
103
+ name: ctx.siteName,
104
+ version: "0.1.0",
105
+ type: "module",
106
+ description: `${ctx.siteName} storefront powered by TanStack Start`,
107
+ scripts: {
108
+ dev: "vite dev",
109
+ "dev:clean":
110
+ "rm -rf node_modules/.vite .wrangler/state .tanstack && vite dev",
111
+ "generate:blocks":
112
+ "tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
113
+ "generate:routes": "tsr generate",
114
+ "generate:schema": `tsx node_modules/@decocms/start/scripts/generate-schema.ts --site ${ctx.siteName}`,
115
+ "generate:invoke":
116
+ "tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
117
+ "generate:sections":
118
+ "tsx node_modules/@decocms/start/scripts/generate-sections.ts",
119
+ "generate:loaders": `tsx node_modules/@decocms/start/scripts/generate-loaders.ts --exclude vtex/loaders,vtex/actions,loaders/vtex-auth-loader,loaders/reviews/productReviews,loaders/product/buyTogether,loaders/search/productListPageCollection,loaders/search/intelligenseSearch,loaders/Layouts/ProductCard`,
120
+ build:
121
+ "npm run generate:blocks && npm run generate:sections && npm run generate:loaders && npm run generate:schema && npm run generate:invoke && tsr generate && vite build",
122
+ preview: "vite preview",
123
+ // Deploy is owned by Cloudflare Workers Builds (D6.3); the
124
+ // repo<->worker connection is configured per-worker in the CF
125
+ // dashboard. Local devs use plain `wrangler` against the
126
+ // committed wrangler.jsonc.
127
+ deploy: "wrangler deploy",
128
+ types: "wrangler types",
129
+ typecheck: "tsc --noEmit",
130
+ format: 'prettier --write "src/**/*.{ts,tsx}"',
131
+ "format:check": 'prettier --check "src/**/*.{ts,tsx}"',
132
+ knip: "knip",
133
+ clean:
134
+ "rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && bun install",
135
+ "tailwind:lint":
136
+ "tsx scripts/tailwind-lint.ts",
137
+ "tailwind:fix":
138
+ "tsx scripts/tailwind-lint.ts --fix",
139
+ },
140
+ author: "deco.cx",
141
+ license: "MIT",
142
+ packageManager: `bun@${CANONICAL_BUN_VERSION}`,
143
+ dependencies: {
144
+ "@decocms/apps": `^${appsVersion}`,
145
+ "@decocms/start": `^${startVersion}`,
146
+ "@tanstack/react-query": "5.90.21",
147
+ "@tanstack/react-router": "1.166.7",
148
+ "@tanstack/react-start": "1.166.8",
149
+ "@tanstack/react-store": "0.9.2",
150
+ "@tanstack/store": "0.9.2",
151
+ "colorjs.io": "^0.6.1",
152
+ react: "^19.2.4",
153
+ "react-dom": "^19.2.4",
154
+ ...siteDeps,
155
+ },
156
+ devDependencies: {
157
+ "@cloudflare/vite-plugin": "^1.27.0",
158
+ "@tailwindcss/vite": "^4.2.1",
159
+ "@tanstack/router-cli": "1.166.7",
160
+ "@types/react": "^19.2.14",
161
+ "@types/react-dom": "^19.2.3",
162
+ "@vitejs/plugin-react": "^5.1.4",
163
+ "babel-plugin-react-compiler": "^1.0.0",
164
+ "daisyui": "^5.5.19",
165
+ knip: "^5.61.2",
166
+ prettier: "^3.5.3",
167
+ tailwindcss: "^4.2.1",
168
+ "ts-morph": "^27.0.2",
169
+ tsx: "^4.19.4",
170
+ typescript: "^5.9.3",
171
+ vite: "^7.3.1",
172
+ wrangler: "^4.72.0",
173
+ },
174
+ };
175
+
176
+ return JSON.stringify(pkg, null, 2) + "\n";
177
+ }