@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,366 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { MigrationContext } from "../types";
|
|
4
|
+
import type { ExtractedTheme } from "../analyzers/theme-extractor";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find the original site's custom CSS file.
|
|
8
|
+
* Deco sites typically have their custom CSS in:
|
|
9
|
+
* - tailwind.css (root level, combined directives + custom)
|
|
10
|
+
* - static/tailwind.css (compiled output — skip this)
|
|
11
|
+
* - static-{brand}/tailwind.css (compiled output — skip this)
|
|
12
|
+
* - styles/*.css
|
|
13
|
+
*/
|
|
14
|
+
function findOriginalCss(ctx: MigrationContext): string | null {
|
|
15
|
+
// Prefer root tailwind.css (has custom CSS + directives)
|
|
16
|
+
const rootCss = path.join(ctx.sourceDir, "tailwind.css");
|
|
17
|
+
if (fs.existsSync(rootCss)) {
|
|
18
|
+
return fs.readFileSync(rootCss, "utf-8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check for styles/ directory
|
|
22
|
+
const stylesDir = path.join(ctx.sourceDir, "styles");
|
|
23
|
+
if (fs.existsSync(stylesDir)) {
|
|
24
|
+
for (const file of fs.readdirSync(stylesDir)) {
|
|
25
|
+
if (file.endsWith(".css") && file !== "tailwind.css") {
|
|
26
|
+
return fs.readFileSync(path.join(stylesDir, file), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract custom CSS from the original site's CSS file.
|
|
36
|
+
* Strips TW3 directives (@tailwind base/components/utilities) and
|
|
37
|
+
* returns only the custom CSS (component overrides, @layer, @font-face, etc.)
|
|
38
|
+
*/
|
|
39
|
+
function extractCustomCss(rawCss: string): string {
|
|
40
|
+
return rawCss
|
|
41
|
+
// Remove Tailwind v3 directives (replaced by @import "tailwindcss")
|
|
42
|
+
.replace(/^@tailwind\s+(?:base|components|utilities)\s*;\s*$/gm, "")
|
|
43
|
+
// Remove empty lines left over
|
|
44
|
+
.replace(/^\s*\n/gm, "")
|
|
45
|
+
.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Transform @apply directives for TW3→TW4 compatibility.
|
|
50
|
+
* The tailwind.ts transform handles className= attributes in JSX,
|
|
51
|
+
* but @apply inside CSS also needs class renames.
|
|
52
|
+
*
|
|
53
|
+
* Also converts @apply with custom brand/theme colors to native CSS
|
|
54
|
+
* properties when the utility class might not be registered in TW4.
|
|
55
|
+
*/
|
|
56
|
+
function transformApplyDirectives(css: string): string {
|
|
57
|
+
return css
|
|
58
|
+
.replace(/@apply\s+([^;]+);/g, (_match, classes: string) => {
|
|
59
|
+
let fixed = classes;
|
|
60
|
+
// flex-grow-0 → grow-0
|
|
61
|
+
fixed = fixed.replace(/\bflex-grow-0\b/g, "grow-0");
|
|
62
|
+
fixed = fixed.replace(/\bflex-grow\b/g, "grow");
|
|
63
|
+
fixed = fixed.replace(/\bflex-shrink-0\b/g, "shrink-0");
|
|
64
|
+
fixed = fixed.replace(/\bflex-shrink\b/g, "shrink");
|
|
65
|
+
// transform → removed (auto in v4)
|
|
66
|
+
fixed = fixed.replace(/\btransform\b(?!-none)/g, "");
|
|
67
|
+
fixed = fixed.replace(/\bfilter\b/g, "");
|
|
68
|
+
// ring → ring-3
|
|
69
|
+
fixed = fixed.replace(/\bring\b(?!-)/g, "ring-3");
|
|
70
|
+
// Clean up multiple spaces
|
|
71
|
+
fixed = fixed.replace(/\s{2,}/g, " ").trim();
|
|
72
|
+
return `@apply ${fixed};`;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Test if a value looks like oklch coordinates (space-separated numbers,
|
|
78
|
+
* possibly with `/` for alpha). Examples: "0.5 0.2 30", "0.8 0.15 120 / 0.5"
|
|
79
|
+
* This distinguishes oklch coordinate values from hex colors.
|
|
80
|
+
*/
|
|
81
|
+
function isOklchCoordinates(val: string): boolean {
|
|
82
|
+
const trimmed = val.trim();
|
|
83
|
+
// oklch coordinates: 2-3 space-separated numbers, possibly with / alpha
|
|
84
|
+
// e.g. "0.5 0.2 30" or "0.85 0.15 120 / 0.5"
|
|
85
|
+
return /^[\d.]+\s+[\d.]+\s+[\d.]+(\s*\/\s*[\d.]+)?$/.test(trimmed);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extract the primary font family from @font-face declarations in CSS.
|
|
90
|
+
* Returns the first font-family name found, or null.
|
|
91
|
+
*/
|
|
92
|
+
function extractPrimaryFontFromCss(css: string): string | null {
|
|
93
|
+
const fontFaceRe = /@font-face\s*\{[^}]*font-family:\s*["']?([^"';]+)["']?\s*;/g;
|
|
94
|
+
const families = new Set<string>();
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = fontFaceRe.exec(css)) !== null) {
|
|
97
|
+
families.add(match[1].trim());
|
|
98
|
+
}
|
|
99
|
+
if (families.size === 0) return null;
|
|
100
|
+
// Prefer non-icon fonts
|
|
101
|
+
const nonIcon = [...families].filter(
|
|
102
|
+
(f) => !/icon|awesome|material/i.test(f),
|
|
103
|
+
);
|
|
104
|
+
return nonIcon[0] || [...families][0];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function generateAppCss(ctx: MigrationContext, theme?: ExtractedTheme): string {
|
|
108
|
+
const sections: string[] = [];
|
|
109
|
+
|
|
110
|
+
// ── DaisyUI theme plugin ──────────────────────────────────────────
|
|
111
|
+
const daisyColors = theme?.daisyUiColors ?? {};
|
|
112
|
+
const c = ctx.themeColors;
|
|
113
|
+
|
|
114
|
+
const semanticColors: Record<string, string> = {
|
|
115
|
+
"--color-primary": daisyColors["--color-primary"] || c["primary"] || "#B10200",
|
|
116
|
+
"--color-secondary": daisyColors["--color-secondary"] || c["secondary"] || "#141414",
|
|
117
|
+
"--color-accent": daisyColors["--color-accent"] || c["tertiary"] || "#FFF100",
|
|
118
|
+
"--color-neutral": daisyColors["--color-neutral"] || c["neutral"] || "#393939",
|
|
119
|
+
"--color-base-100": daisyColors["--color-base-100"] || c["base-100"] || "#FFFFFF",
|
|
120
|
+
"--color-base-200": daisyColors["--color-base-200"] || c["base-200"] || "#F3F3F3",
|
|
121
|
+
"--color-base-300": daisyColors["--color-base-300"] || c["base-300"] || "#868686",
|
|
122
|
+
"--color-info": daisyColors["--color-info"] || c["info"] || "#006CA1",
|
|
123
|
+
"--color-success": daisyColors["--color-success"] || c["success"] || "#007552",
|
|
124
|
+
"--color-warning": daisyColors["--color-warning"] || c["warning"] || "#F8D13A",
|
|
125
|
+
"--color-error": daisyColors["--color-error"] || c["error"] || "#CF040A",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const colorLines = Object.entries(semanticColors)
|
|
129
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
130
|
+
.join("\n");
|
|
131
|
+
|
|
132
|
+
sections.push(`@import "tailwindcss";
|
|
133
|
+
@plugin "daisyui";
|
|
134
|
+
@plugin "daisyui/theme" {
|
|
135
|
+
name: "light";
|
|
136
|
+
default: true;
|
|
137
|
+
color-scheme: light;
|
|
138
|
+
${colorLines}
|
|
139
|
+
}`);
|
|
140
|
+
|
|
141
|
+
// ── @theme block: Tailwind v3->v4 color migration ─────────────────
|
|
142
|
+
// Determine font family from theme, context, or original CSS @font-face
|
|
143
|
+
let fontFamily = theme?.fontFamily || ctx.fontFamily;
|
|
144
|
+
if (!fontFamily) {
|
|
145
|
+
const originalCssForFont = findOriginalCss(ctx);
|
|
146
|
+
if (originalCssForFont) {
|
|
147
|
+
const extracted = extractPrimaryFontFromCss(originalCssForFont);
|
|
148
|
+
if (extracted) fontFamily = extracted;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let fontLine = "";
|
|
152
|
+
if (fontFamily) {
|
|
153
|
+
const firstFont = fontFamily.split(",")[0].trim().replace(/['"]/g, "");
|
|
154
|
+
fontLine = `\n --font-sans: "${firstFont}", ui-sans-serif, system-ui, sans-serif;`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let themeBlock = `/* Tailwind v4: reset default palette (old sites replaced it entirely via theme.colors)
|
|
158
|
+
then re-add only the colors used by this site. */
|
|
159
|
+
@theme {
|
|
160
|
+
--color-*: initial;
|
|
161
|
+
|
|
162
|
+
--color-white: #fff;
|
|
163
|
+
--color-black: #000;
|
|
164
|
+
--color-transparent: transparent;
|
|
165
|
+
--color-current: currentColor;
|
|
166
|
+
--color-inherit: inherit;${fontLine}`;
|
|
167
|
+
|
|
168
|
+
// Add extracted theme variables to @theme
|
|
169
|
+
// Theme variables come from the CMS Theme section (.deco/blocks/Theme-*.json).
|
|
170
|
+
// The CMS sets CSS custom properties on :root at runtime, so @theme entries
|
|
171
|
+
// should reference var(--x) instead of hardcoding values. For oklch-formatted
|
|
172
|
+
// values (space-separated numbers like "0.5 0.2 30"), wrap with oklch().
|
|
173
|
+
const vars = theme?.variables ?? {};
|
|
174
|
+
if (Object.keys(vars).length > 0) {
|
|
175
|
+
themeBlock += `\n`;
|
|
176
|
+
const grouped: Record<string, Array<[string, string]>> = {};
|
|
177
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
178
|
+
const prefix = k.replace(/^--/, "").split("-").slice(0, 2).join("-");
|
|
179
|
+
if (!grouped[prefix]) grouped[prefix] = [];
|
|
180
|
+
grouped[prefix].push([k, v]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const [prefix, entries] of Object.entries(grouped)) {
|
|
184
|
+
themeBlock += `\n /* ${prefix} */`;
|
|
185
|
+
for (const [k, v] of entries) {
|
|
186
|
+
if (!k.startsWith("--color-") && !k.startsWith("--font-") && !k.startsWith("--breakpoint-")) {
|
|
187
|
+
const val = v.trim();
|
|
188
|
+
const isColor = /^#[0-9a-fA-F]{3,8}$/.test(val) ||
|
|
189
|
+
/^(rgb|hsl|oklch|oklab)\(/.test(val) ||
|
|
190
|
+
/^(transparent|currentColor|inherit)$/.test(val) ||
|
|
191
|
+
isOklchCoordinates(val);
|
|
192
|
+
if (isColor) {
|
|
193
|
+
const varName = k.replace(/^--/, "");
|
|
194
|
+
const colorKey = `--color-${varName}`;
|
|
195
|
+
if (isOklchCoordinates(val)) {
|
|
196
|
+
themeBlock += `\n ${colorKey}: oklch(var(${k}));`;
|
|
197
|
+
} else {
|
|
198
|
+
themeBlock += `\n ${colorKey}: var(${k});`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Gray scale compat (Tailwind v3 had gray-50..gray-950 by default)
|
|
207
|
+
themeBlock += `
|
|
208
|
+
|
|
209
|
+
/* Gray scale (Tailwind v3 default, required for bg-gray-*, text-gray-*, etc.) */
|
|
210
|
+
--color-gray-50: #f9fafb;
|
|
211
|
+
--color-gray-100: #f3f4f6;
|
|
212
|
+
--color-gray-200: #e5e7eb;
|
|
213
|
+
--color-gray-300: #d1d5db;
|
|
214
|
+
--color-gray-400: #9ca3af;
|
|
215
|
+
--color-gray-500: #6b7280;
|
|
216
|
+
--color-gray-600: #4b5563;
|
|
217
|
+
--color-gray-700: #374151;
|
|
218
|
+
--color-gray-800: #1f2937;
|
|
219
|
+
--color-gray-900: #111827;
|
|
220
|
+
--color-gray-950: #030712;
|
|
221
|
+
}`;
|
|
222
|
+
sections.push(themeBlock);
|
|
223
|
+
|
|
224
|
+
// ── DaisyUI v5 compat ─────────────────────────────────────────────
|
|
225
|
+
sections.push(`/* DaisyUI v5: flatten depth/noise to match v4 look */
|
|
226
|
+
:root {
|
|
227
|
+
--depth: 0;
|
|
228
|
+
--noise: 0;
|
|
229
|
+
}`);
|
|
230
|
+
|
|
231
|
+
// ── :root theme variables ─────────────────────────────────────────
|
|
232
|
+
if (Object.keys(vars).length > 0) {
|
|
233
|
+
let rootBlock = `:root {\n`;
|
|
234
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
235
|
+
rootBlock += ` ${k}: ${v};\n`;
|
|
236
|
+
}
|
|
237
|
+
if (fontFamily) {
|
|
238
|
+
const firstFont = fontFamily.split(",")[0].trim().replace(/['"]/g, "");
|
|
239
|
+
rootBlock += ` --font-family: ${firstFont}, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;\n`;
|
|
240
|
+
}
|
|
241
|
+
rootBlock += `}`;
|
|
242
|
+
sections.push(rootBlock);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── DaisyUI v5 carousel overflow fix ──────────────────────────────
|
|
246
|
+
sections.push(`/* DaisyUI v5 removed carousel overflow — re-add for horizontal scroll */
|
|
247
|
+
.carousel {
|
|
248
|
+
-webkit-overflow-scrolling: touch;
|
|
249
|
+
overflow-x: auto;
|
|
250
|
+
scroll-snap-type: x mandatory;
|
|
251
|
+
scroll-behavior: smooth;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.carousel > * {
|
|
255
|
+
scroll-snap-align: start;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
ul.carousel,
|
|
259
|
+
ol.carousel {
|
|
260
|
+
list-style: none;
|
|
261
|
+
padding: 0;
|
|
262
|
+
}`);
|
|
263
|
+
|
|
264
|
+
// ── Container utility (v3 -> v4 migration) ────────────────────────
|
|
265
|
+
sections.push(`/* Container: replaces Tailwind v3 container plugin config */
|
|
266
|
+
@utility container {
|
|
267
|
+
margin-inline: auto;
|
|
268
|
+
padding-inline: 1rem;
|
|
269
|
+
width: 100%;
|
|
270
|
+
|
|
271
|
+
@media (width >= 640px) { max-width: 640px; }
|
|
272
|
+
@media (width >= 768px) { max-width: 768px; }
|
|
273
|
+
@media (width >= 1024px) { max-width: 1024px; }
|
|
274
|
+
@media (width >= 1280px) { max-width: 1280px; }
|
|
275
|
+
@media (width >= 1536px) { max-width: 1536px; }
|
|
276
|
+
}`);
|
|
277
|
+
|
|
278
|
+
// ── Deferred section visibility ───────────────────────────────────
|
|
279
|
+
sections.push(`/* Deferred section visibility — reduces layout shift while loading */
|
|
280
|
+
section[data-deferred="true"] {
|
|
281
|
+
content-visibility: auto;
|
|
282
|
+
contain-intrinsic-size: auto 300px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.deferred-section {
|
|
286
|
+
content-visibility: auto;
|
|
287
|
+
contain-intrinsic-size: auto 300px;
|
|
288
|
+
}`);
|
|
289
|
+
|
|
290
|
+
// ── View transitions ──────────────────────────────────────────────
|
|
291
|
+
sections.push(`@view-transition {
|
|
292
|
+
navigation: auto;
|
|
293
|
+
}`);
|
|
294
|
+
|
|
295
|
+
// ── Base layer resets ─────────────────────────────────────────────
|
|
296
|
+
sections.push(`@layer base {
|
|
297
|
+
*,
|
|
298
|
+
*::before,
|
|
299
|
+
*::after {
|
|
300
|
+
box-sizing: border-box;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
body {
|
|
304
|
+
-webkit-font-smoothing: antialiased;
|
|
305
|
+
-moz-osx-font-smoothing: grayscale;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
img,
|
|
309
|
+
picture,
|
|
310
|
+
video,
|
|
311
|
+
canvas,
|
|
312
|
+
svg {
|
|
313
|
+
display: block;
|
|
314
|
+
max-width: 100%;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Drawer / modal scroll lock */
|
|
318
|
+
body:has(dialog[open]),
|
|
319
|
+
body:has(.drawer-toggle:checked) {
|
|
320
|
+
overflow: hidden;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Tailwind v3 → v4 compat: restore the default cursor:pointer on interactive
|
|
324
|
+
elements. Tailwind v4 dropped this from Preflight, leaving Fresh-era sites
|
|
325
|
+
with hundreds of buttons looking unclickable. See:
|
|
326
|
+
https://tailwindcss.com/docs/upgrade-guide#default-button-cursor */
|
|
327
|
+
button:not(:disabled),
|
|
328
|
+
[role="button"]:not([aria-disabled="true"]),
|
|
329
|
+
label[for],
|
|
330
|
+
summary {
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
}
|
|
333
|
+
button:disabled,
|
|
334
|
+
[role="button"][aria-disabled="true"] {
|
|
335
|
+
cursor: not-allowed;
|
|
336
|
+
}
|
|
337
|
+
}`);
|
|
338
|
+
|
|
339
|
+
// ── Scrollbar utility ─────────────────────────────────────────────
|
|
340
|
+
sections.push(`.scrollbar-none {
|
|
341
|
+
scrollbar-width: none;
|
|
342
|
+
-ms-overflow-style: none;
|
|
343
|
+
}
|
|
344
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
345
|
+
display: none;
|
|
346
|
+
}`);
|
|
347
|
+
|
|
348
|
+
// ── Incorporate original site's custom CSS ────────────────────
|
|
349
|
+
// Instead of throwing away the site's CSS, we extract and append
|
|
350
|
+
// all custom rules (component overrides, @layer base, @font-face,
|
|
351
|
+
// typography utilities, feature-specific CSS, etc.)
|
|
352
|
+
const originalCss = findOriginalCss(ctx);
|
|
353
|
+
if (originalCss) {
|
|
354
|
+
const customCss = extractCustomCss(originalCss);
|
|
355
|
+
if (customCss) {
|
|
356
|
+
const transformed = transformApplyDirectives(customCss);
|
|
357
|
+
sections.push(`/* ═══════════════════════════════════════════════════════════════
|
|
358
|
+
Original site CSS (migrated from tailwind.css)
|
|
359
|
+
═══════════════════════════════════════════════════════════════ */
|
|
360
|
+
|
|
361
|
+
${transformed}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return sections.join("\n\n") + "\n";
|
|
366
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types";
|
|
2
|
+
|
|
3
|
+
export function generateCacheConfig(ctx: MigrationContext): string {
|
|
4
|
+
if (ctx.platform !== "vtex") {
|
|
5
|
+
return `/**
|
|
6
|
+
* Cache configuration — customize edge cache profiles per route type.
|
|
7
|
+
* See @decocms/start/sdk/cacheHeaders for available profiles.
|
|
8
|
+
*/
|
|
9
|
+
// import { setCacheProfile } from "@decocms/start/sdk/cacheHeaders";
|
|
10
|
+
// setCacheProfile("product", { sMaxAge: 300, staleWhileRevalidate: 600 });
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `/**
|
|
15
|
+
* Cache configuration for VTEX storefront.
|
|
16
|
+
* Overrides default cache profiles from @decocms/start/sdk/cacheHeaders.
|
|
17
|
+
*/
|
|
18
|
+
// import { setCacheProfile } from "@decocms/start/sdk/cacheHeaders";
|
|
19
|
+
|
|
20
|
+
// Uncomment and adjust as needed:
|
|
21
|
+
// setCacheProfile("product", { sMaxAge: 300, staleWhileRevalidate: 600 });
|
|
22
|
+
// setCacheProfile("listing", { sMaxAge: 120, staleWhileRevalidate: 300 });
|
|
23
|
+
// setCacheProfile("search", { sMaxAge: 60, staleWhileRevalidate: 120 });
|
|
24
|
+
// setCacheProfile("static", { sMaxAge: 86400, staleWhileRevalidate: 172800 });
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
function hasLoaderByName(ctx: MigrationContext, name: string): boolean {
|
|
6
|
+
return ctx.loaderInventory.some(
|
|
7
|
+
(l) => l.path.toLowerCase().includes(name.toLowerCase()),
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function fileExists(ctx: MigrationContext, relPath: string): boolean {
|
|
12
|
+
const full = path.join(ctx.sourceDir, relPath);
|
|
13
|
+
if (fs.existsSync(full)) return true;
|
|
14
|
+
const src = path.join(ctx.sourceDir, "src", relPath);
|
|
15
|
+
return fs.existsSync(src);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function generateCommerceLoaders(ctx: MigrationContext): string {
|
|
19
|
+
const lines: string[] = [];
|
|
20
|
+
const hasSecrets = fileExists(ctx, "utils/secrets.ts") || fileExists(ctx, "src/utils/secrets.ts");
|
|
21
|
+
const hasProductReviews = hasLoaderByName(ctx, "reviews/productReviews");
|
|
22
|
+
const hasBuyTogether = hasLoaderByName(ctx, "product/buyTogether");
|
|
23
|
+
const hasAutocomplete = hasLoaderByName(ctx, "intelligenseSearch") || hasLoaderByName(ctx, "intelligentSearch");
|
|
24
|
+
const hasVtexAuth = hasLoaderByName(ctx, "vtex-auth-loader");
|
|
25
|
+
const hasCollectionPLP = hasLoaderByName(ctx, "productListPageCollection");
|
|
26
|
+
const hasStores = hasLoaderByName(ctx, "stores");
|
|
27
|
+
const hasProductCard = hasLoaderByName(ctx, "Layouts/ProductCard");
|
|
28
|
+
const hasSitename = fileExists(ctx, "utils/sitename.ts") || fileExists(ctx, "src/utils/sitename.ts");
|
|
29
|
+
|
|
30
|
+
lines.push(`/**`);
|
|
31
|
+
lines.push(` * Commerce Loaders — data fetchers registered for CMS block resolution.`);
|
|
32
|
+
lines.push(` *`);
|
|
33
|
+
lines.push(` * Standard VTEX loaders come from createVtexCommerceLoaders().`);
|
|
34
|
+
lines.push(` * Auto-generated pass-throughs come from loaders.gen.ts.`);
|
|
35
|
+
lines.push(` * This file only contains entries that need custom wiring`);
|
|
36
|
+
lines.push(` * (cookie injection, secrets, serialization, etc.).`);
|
|
37
|
+
lines.push(` */`);
|
|
38
|
+
|
|
39
|
+
if (ctx.platform === "vtex") {
|
|
40
|
+
lines.push(`import { getVtexConfig } from "@decocms/apps/vtex";`);
|
|
41
|
+
lines.push(`import { createVtexCommerceLoaders, createCachedPDPLoader } from "@decocms/apps/vtex/commerceLoaders";`);
|
|
42
|
+
if (hasAutocomplete) {
|
|
43
|
+
lines.push(`import { autocompleteSearch } from "@decocms/apps/vtex/loaders/autocomplete";`);
|
|
44
|
+
}
|
|
45
|
+
lines.push(`import { getAddressByPostalCode } from "@decocms/apps/vtex/loaders/address";`);
|
|
46
|
+
lines.push(`import { createAddressFromRequest, updateAddressFromRequest, deleteAddressFromRequest } from "@decocms/apps/vtex/actions/address";`);
|
|
47
|
+
lines.push(`import { updateProfileFromRequest, newsletterProfileFromRequest, deletePaymentFromRequest, getPasswordLastUpdate } from "@decocms/apps/vtex/actions/profile";`);
|
|
48
|
+
lines.push(`import { createCachedLoader } from "@decocms/start/sdk/cachedLoader";`);
|
|
49
|
+
|
|
50
|
+
if (hasVtexAuth) {
|
|
51
|
+
lines.push(`import vtexAuthLoader from "../loaders/vtex-auth-loader";`);
|
|
52
|
+
}
|
|
53
|
+
if (hasProductReviews) {
|
|
54
|
+
lines.push(`import productReviewsLoader from "../loaders/reviews/productReviews";`);
|
|
55
|
+
}
|
|
56
|
+
if (hasBuyTogether) {
|
|
57
|
+
lines.push(`import buyTogetherLoader from "../loaders/product/buyTogether";`);
|
|
58
|
+
}
|
|
59
|
+
if (hasSecrets) {
|
|
60
|
+
lines.push(`import { secrets } from "../utils/secrets";`);
|
|
61
|
+
}
|
|
62
|
+
if (hasSitename && hasCollectionPLP) {
|
|
63
|
+
lines.push(`import { useAccount } from "../utils/sitename";`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Always import siteLoaders — the generate:loaders script creates this file
|
|
67
|
+
lines.push(`import { siteLoaders } from "../server/cms/loaders.gen";`);
|
|
68
|
+
lines.push(``);
|
|
69
|
+
|
|
70
|
+
lines.push(`const DOMAIN_RE = /;\\s*domain=[^;]*/gi;`);
|
|
71
|
+
lines.push(``);
|
|
72
|
+
lines.push(`export const vtexLoaders = createVtexCommerceLoaders();`);
|
|
73
|
+
lines.push(`export const cachedPDP = createCachedPDPLoader();`);
|
|
74
|
+
|
|
75
|
+
if (hasCollectionPLP) {
|
|
76
|
+
lines.push(`const cachedPLP = vtexLoaders["vtex/loaders/intelligentSearch/productListingPage.ts"];`);
|
|
77
|
+
}
|
|
78
|
+
if (hasAutocomplete) {
|
|
79
|
+
lines.push(`const cachedAutocomplete = createCachedLoader("vtex/autocomplete", autocompleteSearch, "search");`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push(``);
|
|
83
|
+
lines.push(``);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push(`export const COMMERCE_LOADERS: Record<string, (props: any, request?: Request) => Promise<any>> = {`);
|
|
87
|
+
|
|
88
|
+
if (ctx.platform === "vtex") {
|
|
89
|
+
lines.push(` ...vtexLoaders,`);
|
|
90
|
+
lines.push(` ...siteLoaders,`);
|
|
91
|
+
lines.push(``);
|
|
92
|
+
|
|
93
|
+
// Autocomplete aliases
|
|
94
|
+
if (hasAutocomplete) {
|
|
95
|
+
lines.push(` // Autocomplete search — from @decocms/apps`);
|
|
96
|
+
lines.push(` "site/loaders/search/intelligenseSearch.ts": cachedAutocomplete,`);
|
|
97
|
+
lines.push(` "site/loaders/search/intelligenseSearch": cachedAutocomplete,`);
|
|
98
|
+
lines.push(``);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Stores pass-through
|
|
102
|
+
if (hasStores) {
|
|
103
|
+
lines.push(` // Stores pass-through`);
|
|
104
|
+
lines.push(` "site/loaders/stores.ts": async (props: any) => {`);
|
|
105
|
+
lines.push(` const result = props.stores ?? props ?? [];`);
|
|
106
|
+
lines.push(` return Array.isArray(result) ? result : [];`);
|
|
107
|
+
lines.push(` },`);
|
|
108
|
+
lines.push(``);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// VTEX address CRUD
|
|
112
|
+
lines.push(` // VTEX address CRUD — request-aware wrappers from @decocms/apps`);
|
|
113
|
+
lines.push(` "vtex/actions/address/createAddress": createAddressFromRequest as any,`);
|
|
114
|
+
lines.push(` "vtex/actions/address/updateAddress": updateAddressFromRequest as any,`);
|
|
115
|
+
lines.push(` "vtex/actions/address/deleteAddress": deleteAddressFromRequest as any,`);
|
|
116
|
+
lines.push(` "vtex/loaders/address/getAddressByZIP": async (props: any) => {`);
|
|
117
|
+
lines.push(` return getAddressByPostalCode(props.countryCode, props.postalCode);`);
|
|
118
|
+
lines.push(` },`);
|
|
119
|
+
lines.push(``);
|
|
120
|
+
|
|
121
|
+
// Auth cookie stripping
|
|
122
|
+
if (hasVtexAuth) {
|
|
123
|
+
lines.push(` // VTEX auth (Set-Cookie forwarding)`);
|
|
124
|
+
lines.push(` "site/loaders/vtex-auth-loader": async (props: any) => {`);
|
|
125
|
+
lines.push(` const result = await vtexAuthLoader(props);`);
|
|
126
|
+
lines.push(` if (result instanceof Response) {`);
|
|
127
|
+
lines.push(` const setCookies = result.headers.getSetCookie?.() ?? [];`);
|
|
128
|
+
lines.push(` const strippedCookies = setCookies.map((c) => c.replace(DOMAIN_RE, ""));`);
|
|
129
|
+
lines.push(` const body = await result.text();`);
|
|
130
|
+
lines.push(` const headers = new Headers({ "Content-Type": "application/json" });`);
|
|
131
|
+
lines.push(` for (const cookie of strippedCookies) {`);
|
|
132
|
+
lines.push(` headers.append("Set-Cookie", cookie);`);
|
|
133
|
+
lines.push(` }`);
|
|
134
|
+
lines.push(` return new Response(body, { status: result.status, headers });`);
|
|
135
|
+
lines.push(` }`);
|
|
136
|
+
lines.push(` return result;`);
|
|
137
|
+
lines.push(` },`);
|
|
138
|
+
lines.push(``);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ProductCard dynamic import
|
|
142
|
+
if (hasProductCard) {
|
|
143
|
+
lines.push(` // CMS-referenced site loaders`);
|
|
144
|
+
lines.push(` "site/loaders/Layouts/ProductCard.tsx": async (props: any) => {`);
|
|
145
|
+
lines.push(` const mod = await import("../loaders/Layouts/ProductCard");`);
|
|
146
|
+
lines.push(` return mod.default(props);`);
|
|
147
|
+
lines.push(` },`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Reviews with secrets
|
|
151
|
+
if (hasProductReviews) {
|
|
152
|
+
lines.push(` "site/loaders/reviews/productReviews.ts": async (props: any) => {`);
|
|
153
|
+
lines.push(` const { account } = getVtexConfig();`);
|
|
154
|
+
lines.push(` const result = await productReviewsLoader(props, null as any, { account${hasSecrets ? ", ...secrets" : ""} } as any);`);
|
|
155
|
+
lines.push(` if (!result) return result;`);
|
|
156
|
+
lines.push(` const { getProductReview: _r, reviewLikeAction: _l, reviewVote: _v, getProductsListReviews: _p, ...serializable } = result as any;`);
|
|
157
|
+
lines.push(` return serializable;`);
|
|
158
|
+
lines.push(` },`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// BuyTogether with secrets
|
|
162
|
+
if (hasBuyTogether) {
|
|
163
|
+
lines.push(` "site/loaders/product/buyTogether.ts": async (props: any) => {`);
|
|
164
|
+
lines.push(` const { account } = getVtexConfig();`);
|
|
165
|
+
lines.push(` return buyTogetherLoader(props, null as any, { account${hasSecrets ? ", ...secrets" : ""} } as any);`);
|
|
166
|
+
lines.push(` },`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Collection PLP
|
|
170
|
+
if (hasCollectionPLP && hasSitename) {
|
|
171
|
+
lines.push(``);
|
|
172
|
+
lines.push(` // Collection PLP`);
|
|
173
|
+
lines.push(` "site/loaders/search/productListPageCollection.ts": async (props: any) => {`);
|
|
174
|
+
lines.push(` const url = new URL(props.__pageUrl || props.__pagePath || "/", "https://localhost");`);
|
|
175
|
+
lines.push(` const { search_collection_urls_cvlb } = await import("../utils/search-collection-url-cvlb");`);
|
|
176
|
+
lines.push(` const { createBreadcrumbFromPath } = await import("../utils/plpHelpers/plpCollection");`);
|
|
177
|
+
lines.push(` const { an: accountName } = useAccount();`);
|
|
178
|
+
lines.push(` const collections = search_collection_urls_cvlb[accountName] ?? [];`);
|
|
179
|
+
lines.push(``);
|
|
180
|
+
lines.push(` const normalize = (t: string) =>`);
|
|
181
|
+
lines.push(` t.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase()`);
|
|
182
|
+
lines.push(` .replace(/[^a-z0-9\\s-]/g, "").replace(/\\s+/g, "-").replace(/-+/g, "-");`);
|
|
183
|
+
lines.push(``);
|
|
184
|
+
lines.push(` const slug = decodeURIComponent(url.pathname.split("/").pop() ?? "").replace(/-/g, " ");`);
|
|
185
|
+
lines.push(` const now = Date.now();`);
|
|
186
|
+
lines.push(` const collection = collections.find((c: any) =>`);
|
|
187
|
+
lines.push(` normalize(c.name) === normalize(slug) &&`);
|
|
188
|
+
lines.push(` new Date(c.dateFrom).getTime() <= now &&`);
|
|
189
|
+
lines.push(` now <= new Date(c.dateTo).getTime()`);
|
|
190
|
+
lines.push(` );`);
|
|
191
|
+
lines.push(` if (!collection) return null;`);
|
|
192
|
+
lines.push(``);
|
|
193
|
+
lines.push(` const collectionId = String(collection.id);`);
|
|
194
|
+
lines.push(` const response = await cachedPLP({`);
|
|
195
|
+
lines.push(` ...props,`);
|
|
196
|
+
lines.push(` selectedFacets: [{ key: "productClusterIds", value: collectionId }],`);
|
|
197
|
+
lines.push(` __pageUrl: url.toString(),`);
|
|
198
|
+
lines.push(` __pagePath: url.pathname,`);
|
|
199
|
+
lines.push(` });`);
|
|
200
|
+
lines.push(` if (!response) return null;`);
|
|
201
|
+
lines.push(``);
|
|
202
|
+
lines.push(` return {`);
|
|
203
|
+
lines.push(` ...response,`);
|
|
204
|
+
lines.push(` breadcrumb: createBreadcrumbFromPath(url.pathname, url, collection.name) ?? {},`);
|
|
205
|
+
lines.push(` seo: {`);
|
|
206
|
+
lines.push(` title: collection.name,`);
|
|
207
|
+
lines.push(` // MIGRATION TODO: replace with site-specific category description`);
|
|
208
|
+
lines.push(` description: collection.name,`);
|
|
209
|
+
lines.push(` noIndexing: false,`);
|
|
210
|
+
lines.push(` canonical: url.toString(),`);
|
|
211
|
+
lines.push(` },`);
|
|
212
|
+
lines.push(` };`);
|
|
213
|
+
lines.push(` },`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
lines.push(``);
|
|
217
|
+
// Profile actions
|
|
218
|
+
lines.push(` // Profile actions — request-aware wrappers from @decocms/apps`);
|
|
219
|
+
lines.push(` "vtex/actions/profile/updateProfile": updateProfileFromRequest as any,`);
|
|
220
|
+
lines.push(` "vtex/actions/profile/updateProfile.ts": updateProfileFromRequest as any,`);
|
|
221
|
+
lines.push(` "vtex/actions/profile/newsletterProfile": newsletterProfileFromRequest as any,`);
|
|
222
|
+
lines.push(` "vtex/actions/profile/newsletterProfile.ts": newsletterProfileFromRequest as any,`);
|
|
223
|
+
lines.push(` "vtex/actions/payments/delete": deletePaymentFromRequest as any,`);
|
|
224
|
+
lines.push(` "vtex/loaders/profile/passwordLastUpdate": getPasswordLastUpdate as any,`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lines.push(`};`);
|
|
228
|
+
|
|
229
|
+
return lines.join("\n") + "\n";
|
|
230
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateMigrationPolicyPointerRule } from "./cursor-rules";
|
|
3
|
+
|
|
4
|
+
describe("generateMigrationPolicyPointerRule", () => {
|
|
5
|
+
const body = generateMigrationPolicyPointerRule("acme");
|
|
6
|
+
|
|
7
|
+
it("emits a Cursor MDC rule with alwaysApply: true frontmatter", () => {
|
|
8
|
+
expect(body.startsWith("---\n")).toBe(true);
|
|
9
|
+
expect(body).toContain("alwaysApply: true");
|
|
10
|
+
expect(body).toContain("description:");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("interpolates the site name into the body", () => {
|
|
14
|
+
expect(body).toContain("`acme`");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("links to the canonical rule and plan in decocms/deco-start", () => {
|
|
18
|
+
expect(body).toContain(
|
|
19
|
+
"https://github.com/decocms/deco-start/blob/main/.cursor/rules/migration-tooling-policy.mdc",
|
|
20
|
+
);
|
|
21
|
+
expect(body).toContain(
|
|
22
|
+
"https://github.com/decocms/deco-start/blob/main/MIGRATION_TOOLING_PLAN.md",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("documents D1–D5 by ID, not just by name", () => {
|
|
27
|
+
expect(body).toContain("**D1**");
|
|
28
|
+
expect(body).toContain("**D2**");
|
|
29
|
+
expect(body).toContain("**D3**");
|
|
30
|
+
expect(body).toContain("**D4**");
|
|
31
|
+
expect(body).toContain("**D5**");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("points at the post-cleanup --fix command rather than restating policy", () => {
|
|
35
|
+
expect(body).toContain("deco-post-cleanup --fix");
|
|
36
|
+
expect(body).toContain("deco-post-cleanup --strict");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does NOT restate the canonical rule body verbatim (pointer, not a copy)", () => {
|
|
40
|
+
// Length budget: pointer must stay short to discourage drift.
|
|
41
|
+
// The canonical rule in decocms/deco-start is ~110 lines / 4–5 KB;
|
|
42
|
+
// the pointer must be substantially smaller than a copy.
|
|
43
|
+
expect(body.length).toBeLessThan(3000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is deterministic — same site name, same output", () => {
|
|
47
|
+
const a = generateMigrationPolicyPointerRule("foo");
|
|
48
|
+
const b = generateMigrationPolicyPointerRule("foo");
|
|
49
|
+
expect(a).toBe(b);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("escapes nothing weird from siteName — siteName is used as a label only", () => {
|
|
53
|
+
// We don't sanitise; we trust the migration script to pass a real
|
|
54
|
+
// package name. But verify nothing surprising happens with hyphens
|
|
55
|
+
// (a common shape, e.g. "casaevideo-storefront").
|
|
56
|
+
const out = generateMigrationPolicyPointerRule("casaevideo-storefront");
|
|
57
|
+
expect(out).toContain("`casaevideo-storefront`");
|
|
58
|
+
});
|
|
59
|
+
});
|