@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.
- package/package.json +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- 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
|
+
}
|