@decocms/blocks-cli 7.21.0 → 7.22.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 +4 -3
- package/scripts/lib/upgrade-6-to-7.test.ts +145 -0
- package/scripts/lib/upgrade-6-to-7.ts +157 -0
- package/scripts/migrate/analyzers/tailwind-config.test.ts +149 -0
- package/scripts/migrate/analyzers/tailwind-config.ts +283 -0
- package/scripts/migrate/css-compile-check.test.ts +115 -0
- package/scripts/migrate/css-compile-check.ts +137 -0
- package/scripts/migrate/phase-analyze.ts +13 -0
- package/scripts/migrate/phase-cleanup.test.ts +1 -0
- package/scripts/migrate/phase-compile.test.ts +48 -0
- package/scripts/migrate/phase-compile.ts +38 -0
- package/scripts/migrate/phase-report.test.ts +103 -0
- package/scripts/migrate/phase-report.ts +64 -4
- package/scripts/migrate/phase-verify-legacy-specifiers.test.ts +134 -0
- package/scripts/migrate/phase-verify.test.ts +1 -0
- package/scripts/migrate/phase-verify.ts +24 -0
- package/scripts/migrate/post-cleanup/migration-artifacts.test.ts +262 -0
- package/scripts/migrate/post-cleanup/rules.ts +321 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +2 -0
- package/scripts/migrate/templates/app-css.test.ts +150 -0
- package/scripts/migrate/templates/app-css.ts +142 -52
- package/scripts/migrate/templates/hooks.test.ts +1 -0
- package/scripts/migrate/templates/package-json.ts +5 -0
- package/scripts/migrate/templates/server-entry-vtex-invoke.test.ts +51 -0
- package/scripts/migrate/templates/server-entry.ts +53 -1
- package/scripts/migrate/transforms/color-oklch.test.ts +83 -0
- package/scripts/migrate/transforms/color-oklch.ts +122 -0
- package/scripts/migrate/transforms/css.test.ts +109 -0
- package/scripts/migrate/transforms/css.ts +186 -0
- package/scripts/migrate/transforms/tailwind-renames.test.ts +37 -0
- package/scripts/migrate/transforms/tailwind-renames.ts +224 -0
- package/scripts/migrate/transforms/tailwind.test.ts +74 -0
- package/scripts/migrate/transforms/tailwind.ts +27 -123
- package/scripts/migrate/types.ts +18 -0
- package/scripts/tailwind-lint.ts +9 -54
- package/scripts/upgrade-6-to-7.test.ts +80 -0
- package/scripts/upgrade-6-to-7.ts +169 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"deco-cf-observability": "./scripts/migrate-to-cf-observability.ts",
|
|
17
17
|
"deco-audit-observability": "./scripts/audit-observability-config.ts",
|
|
18
18
|
"deco-migrate-blocks-to-kv": "./scripts/migrate-blocks-to-kv.ts",
|
|
19
|
-
"deco-sync-blocks-to-kv": "./scripts/sync-blocks-to-kv.ts"
|
|
19
|
+
"deco-sync-blocks-to-kv": "./scripts/sync-blocks-to-kv.ts",
|
|
20
|
+
"deco-upgrade-6-to-7": "./scripts/upgrade-6-to-7.ts"
|
|
20
21
|
},
|
|
21
22
|
"//exports": "Deliberately narrow. ./generate is the ONE public module entry (unified orchestrator); ./generate-blocks stays only because @decocms/tanstack's vite plugin tsImports it (readBlockDelta + programmatic generateBlocks) — each surviving entry must name its consumer here. The other scripts remain shipped FILES (the orchestrator spawns them; sites' existing `tsx node_modules/@decocms/blocks-cli/scripts/generate-*.ts` invocations keep working) but are internal implementation details of ./generate, not module subpaths. CLIs are exposed via bin, not exports.",
|
|
22
23
|
"exports": {
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"lint:unused": "knip"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@decocms/blocks": "7.
|
|
34
|
+
"@decocms/blocks": "7.22.0",
|
|
34
35
|
"ts-morph": "^27.0.0",
|
|
35
36
|
"tsx": "^4.22.5"
|
|
36
37
|
},
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { upgradeFileToV7 } from "./upgrade-6-to-7";
|
|
3
|
+
|
|
4
|
+
describe("upgradeFileToV7 (#367) — simple 1:1 specifier rewrites", () => {
|
|
5
|
+
it("rewrites @decocms/start/vite", () => {
|
|
6
|
+
const r = upgradeFileToV7(`import { decoVitePlugin } from "@decocms/start/vite";`);
|
|
7
|
+
expect(r.changed).toBe(true);
|
|
8
|
+
expect(r.content).toBe(`import { decoVitePlugin } from "@decocms/tanstack/vite";`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rewrites @decocms/start/sdk/cookiePassthrough", () => {
|
|
12
|
+
const r = upgradeFileToV7(
|
|
13
|
+
`import { getRequestCookieHeader } from "@decocms/start/sdk/cookiePassthrough";`,
|
|
14
|
+
);
|
|
15
|
+
expect(r.content).toBe(
|
|
16
|
+
`import { getRequestCookieHeader } from "@decocms/tanstack/sdk/cookiePassthrough";`,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("rewrites @decocms/start/sdk/workerEntry and sdk/router to @decocms/tanstack root", () => {
|
|
21
|
+
const r = upgradeFileToV7(
|
|
22
|
+
[
|
|
23
|
+
`import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";`,
|
|
24
|
+
`import { createDecoRouter } from "@decocms/start/sdk/router";`,
|
|
25
|
+
].join("\n"),
|
|
26
|
+
);
|
|
27
|
+
expect(r.content).toBe(
|
|
28
|
+
[
|
|
29
|
+
`import { createDecoWorkerEntry } from "@decocms/tanstack";`,
|
|
30
|
+
`import { createDecoRouter } from "@decocms/tanstack";`,
|
|
31
|
+
].join("\n"),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("rewrites generic @decocms/start/sdk/* to @decocms/blocks/sdk/*", () => {
|
|
36
|
+
const r = upgradeFileToV7(
|
|
37
|
+
[
|
|
38
|
+
`import { useScript } from "@decocms/start/sdk/useScript";`,
|
|
39
|
+
`import { useDevice } from "@decocms/start/sdk/useDevice";`,
|
|
40
|
+
`import { clx } from "@decocms/start/sdk/clx";`,
|
|
41
|
+
].join("\n"),
|
|
42
|
+
);
|
|
43
|
+
expect(r.content).toBe(
|
|
44
|
+
[
|
|
45
|
+
`import { useScript } from "@decocms/blocks/sdk/useScript";`,
|
|
46
|
+
`import { useDevice } from "@decocms/blocks/sdk/useDevice";`,
|
|
47
|
+
`import { clx } from "@decocms/blocks/sdk/clx";`,
|
|
48
|
+
].join("\n"),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rewrites @decocms/start/cms, /setup, /types/widgets, /admin", () => {
|
|
53
|
+
const r = upgradeFileToV7(
|
|
54
|
+
[
|
|
55
|
+
`import { resolveDecoPage } from "@decocms/start/cms";`,
|
|
56
|
+
`import { createSiteSetup } from "@decocms/start/setup";`,
|
|
57
|
+
`import type { ImageWidget } from "@decocms/start/types/widgets";`,
|
|
58
|
+
`import { handleMeta } from "@decocms/start/admin";`,
|
|
59
|
+
].join("\n"),
|
|
60
|
+
);
|
|
61
|
+
expect(r.content).toContain(`from "@decocms/blocks/cms"`);
|
|
62
|
+
expect(r.content).toContain(`from "@decocms/blocks/setup"`);
|
|
63
|
+
expect(r.content).toContain(`from "@decocms/blocks/types/widgets"`);
|
|
64
|
+
expect(r.content).toContain(`from "@decocms/blocks-admin"`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rewrites @decocms/apps/commerce/components/Image to @decocms/blocks/hooks", () => {
|
|
68
|
+
const r = upgradeFileToV7(`import { Image } from "@decocms/apps/commerce/components/Image";`);
|
|
69
|
+
expect(r.content).toBe(`import { Image } from "@decocms/blocks/hooks";`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rewrites @decocms/apps/commerce/* to @decocms/apps-commerce/*", () => {
|
|
73
|
+
const r = upgradeFileToV7(`import type { Product } from "@decocms/apps/commerce/types";`);
|
|
74
|
+
expect(r.content).toBe(`import type { Product } from "@decocms/apps-commerce/types";`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rewrites @decocms/apps/vtex/* to @decocms/apps-vtex/*", () => {
|
|
78
|
+
const r = upgradeFileToV7(`import { getUser } from "@decocms/apps/vtex/loaders/user";`);
|
|
79
|
+
expect(r.content).toBe(`import { getUser } from "@decocms/apps-vtex/loaders/user";`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("is a no-op for a file already on the 7.x split packages", () => {
|
|
83
|
+
const src = `import { createDecoWorkerEntry } from "@decocms/tanstack";\nimport { resolveDecoPage } from "@decocms/blocks/cms";\n`;
|
|
84
|
+
const r = upgradeFileToV7(src);
|
|
85
|
+
expect(r.changed).toBe(false);
|
|
86
|
+
expect(r.content).toBe(src);
|
|
87
|
+
expect(r.notes).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("upgradeFileToV7 (#367) — @decocms/start/routes fan-out", () => {
|
|
92
|
+
it("moves cmsRouteConfig/cmsHomeRouteConfig to @decocms/tanstack root", () => {
|
|
93
|
+
const r = upgradeFileToV7(
|
|
94
|
+
`import { cmsRouteConfig, cmsHomeRouteConfig } from "@decocms/start/routes";`,
|
|
95
|
+
);
|
|
96
|
+
expect(r.content).toBe(
|
|
97
|
+
`import { cmsRouteConfig, cmsHomeRouteConfig } from "@decocms/tanstack";`,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("splits deferredSectionLoader into its own subpath import", () => {
|
|
102
|
+
const r = upgradeFileToV7(
|
|
103
|
+
`import { cmsRouteConfig, deferredSectionLoader } from "@decocms/start/routes";`,
|
|
104
|
+
);
|
|
105
|
+
expect(r.content).toContain(`import { cmsRouteConfig } from "@decocms/tanstack";`);
|
|
106
|
+
expect(r.content).toContain(
|
|
107
|
+
`import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader";`,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("renames decoMetaRoute/decoRenderRoute/decoInvokeRoute to their *RouteConfig factory names and flags a MANUAL call-site note", () => {
|
|
112
|
+
const src = [
|
|
113
|
+
`import { decoMetaRoute, decoRenderRoute, decoInvokeRoute } from "@decocms/start/routes";`,
|
|
114
|
+
`export const Route = createFileRoute("/deco/meta")({ ...decoMetaRoute });`,
|
|
115
|
+
].join("\n");
|
|
116
|
+
const r = upgradeFileToV7(src);
|
|
117
|
+
expect(r.content).toContain(
|
|
118
|
+
`import { decoMetaRouteConfig, decoRenderRouteConfig, decoInvokeRouteConfig } from "@decocms/tanstack";`,
|
|
119
|
+
);
|
|
120
|
+
expect(r.content).toContain("...decoMetaRouteConfig }");
|
|
121
|
+
expect(r.notes.some((n) => n.includes("decoMetaRoute") && n.includes("factory"))).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("upgradeFileToV7 (#367) — @decocms/start/hooks fan-out", () => {
|
|
126
|
+
it("moves DecoPageRenderer/DecoRootLayout/PreviewProviders to @decocms/tanstack root", () => {
|
|
127
|
+
const r = upgradeFileToV7(
|
|
128
|
+
`import { DecoPageRenderer, DecoRootLayout, PreviewProviders } from "@decocms/start/hooks";`,
|
|
129
|
+
);
|
|
130
|
+
expect(r.content).toBe(
|
|
131
|
+
`import { DecoPageRenderer, DecoRootLayout, PreviewProviders } from "@decocms/tanstack";`,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("renames RenderSection to SectionRenderer at the import and every usage", () => {
|
|
136
|
+
const src = [
|
|
137
|
+
`import { RenderSection } from "@decocms/start/hooks";`,
|
|
138
|
+
`export const x = <RenderSection section={s} />;`,
|
|
139
|
+
].join("\n");
|
|
140
|
+
const r = upgradeFileToV7(src);
|
|
141
|
+
expect(r.content).toContain(`import { SectionRenderer } from "@decocms/tanstack";`);
|
|
142
|
+
expect(r.content).toContain("<SectionRenderer section={s} />");
|
|
143
|
+
expect(r.content).not.toContain("RenderSection");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codemod: upgrade a site off the frozen pre-split `@decocms/start@6.30.0` /
|
|
3
|
+
* `@decocms/apps@5.4.0` packages onto the current 7.x split
|
|
4
|
+
* (`@decocms/tanstack` + `@decocms/blocks` + `@decocms/blocks-admin` +
|
|
5
|
+
* `@decocms/apps-*`). See #367 — this is a DIFFERENT migration than
|
|
6
|
+
* Fresh→TanStack (`deco-migrate`): the site is already on TanStack, it's
|
|
7
|
+
* only the package dependency that needs to move.
|
|
8
|
+
*
|
|
9
|
+
* Mapping derived by hand against a real production site
|
|
10
|
+
* (montecarlo-tanstack) — see the issue for the full worked table. Symbol
|
|
11
|
+
* renames and relocations mean a pure specifier-string rewrite is not
|
|
12
|
+
* enough for every case; those are called out below.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface UpgradeResult {
|
|
16
|
+
content: string;
|
|
17
|
+
changed: boolean;
|
|
18
|
+
notes: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Straightforward 1:1 specifier rewrites — no symbol splitting needed. */
|
|
22
|
+
const SIMPLE_SPECIFIER_MAP: Array<{ re: RegExp; to: string }> = [
|
|
23
|
+
{ re: /@decocms\/start\/vite\b/g, to: "@decocms/tanstack/vite" },
|
|
24
|
+
{ re: /@decocms\/start\/sdk\/cookiePassthrough\b/g, to: "@decocms/tanstack/sdk/cookiePassthrough" },
|
|
25
|
+
{ re: /@decocms\/start\/sdk\/workerEntry\b/g, to: "@decocms/tanstack" },
|
|
26
|
+
{ re: /@decocms\/start\/sdk\/router\b/g, to: "@decocms/tanstack" },
|
|
27
|
+
{ re: /@decocms\/start\/cms\b/g, to: "@decocms/blocks/cms" },
|
|
28
|
+
{ re: /@decocms\/start\/setup\b/g, to: "@decocms/blocks/setup" },
|
|
29
|
+
{ re: /@decocms\/start\/types\/widgets\b/g, to: "@decocms/blocks/types/widgets" },
|
|
30
|
+
{ re: /@decocms\/start\/admin\b/g, to: "@decocms/blocks-admin" },
|
|
31
|
+
{ re: /@decocms\/apps\/commerce\/components\/Image\b/g, to: "@decocms/blocks/hooks" },
|
|
32
|
+
{ re: /@decocms\/apps\/commerce\//g, to: "@decocms/apps-commerce/" },
|
|
33
|
+
{ re: /@decocms\/apps\/vtex\//g, to: "@decocms/apps-vtex/" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `@decocms/start/sdk/*` (useScript, useDevice, clx, signal, ...) → the
|
|
38
|
+
* matching `@decocms/blocks/sdk/*` subpath. Runs AFTER the more specific
|
|
39
|
+
* sdk/workerEntry, sdk/router, sdk/cookiePassthrough rewrites above so
|
|
40
|
+
* those aren't double-handled by this catch-all.
|
|
41
|
+
*/
|
|
42
|
+
const GENERIC_SDK_RE = /@decocms\/start\/sdk\/([\w-]+)/g;
|
|
43
|
+
|
|
44
|
+
/** Named-import line: `import { A, B as C } from "spec";` (captures the clause + spec). */
|
|
45
|
+
const NAMED_IMPORT_RE = /import\s+\{([^}]*)\}\s+from\s+["']([^"']+)["'];?/g;
|
|
46
|
+
|
|
47
|
+
/** Symbols from `@decocms/start/routes` that move to `@decocms/tanstack/sdk/deferredSectionLoader`. */
|
|
48
|
+
const DEFERRED_LOADER_SYMBOL = "deferredSectionLoader";
|
|
49
|
+
|
|
50
|
+
/** Symbols renamed on the way from `@decocms/start/{routes,hooks}` to `@decocms/tanstack`. */
|
|
51
|
+
const ROUTE_FACTORY_RENAMES: Record<string, string> = {
|
|
52
|
+
decoMetaRoute: "decoMetaRouteConfig",
|
|
53
|
+
decoRenderRoute: "decoRenderRouteConfig",
|
|
54
|
+
decoInvokeRoute: "decoInvokeRouteConfig",
|
|
55
|
+
};
|
|
56
|
+
const HOOKS_RENAMES: Record<string, string> = {
|
|
57
|
+
RenderSection: "SectionRenderer",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function splitNamedClause(clause: string): string[] {
|
|
61
|
+
return clause
|
|
62
|
+
.split(",")
|
|
63
|
+
.map((s) => s.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Handles the `@decocms/start/routes` and `@decocms/start/hooks` imports, which
|
|
68
|
+
* fan out to `@decocms/tanstack` (root) plus, for routes, a dedicated subpath
|
|
69
|
+
* for `deferredSectionLoader`. Symbol renames are applied to both the import
|
|
70
|
+
* clause and every usage in the file. */
|
|
71
|
+
function rewriteRoutesAndHooksImports(
|
|
72
|
+
content: string,
|
|
73
|
+
notes: string[],
|
|
74
|
+
): { content: string; changed: boolean } {
|
|
75
|
+
let changed = false;
|
|
76
|
+
// Collected here during the import-line pass, applied to the WHOLE file
|
|
77
|
+
// afterward — must not mutate `content`/the replace target mid-scan.
|
|
78
|
+
const pendingRenames: Array<{ from: string; to: string }> = [];
|
|
79
|
+
|
|
80
|
+
let result = content.replace(NAMED_IMPORT_RE, (line, clauseRaw: string, spec: string) => {
|
|
81
|
+
if (spec !== "@decocms/start/routes" && spec !== "@decocms/start/hooks") return line;
|
|
82
|
+
|
|
83
|
+
const isRoutes = spec === "@decocms/start/routes";
|
|
84
|
+
const renames = isRoutes ? ROUTE_FACTORY_RENAMES : HOOKS_RENAMES;
|
|
85
|
+
const names = splitNamedClause(clauseRaw);
|
|
86
|
+
|
|
87
|
+
const deferred: string[] = [];
|
|
88
|
+
const rootImports: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
if (isRoutes && name === DEFERRED_LOADER_SYMBOL) {
|
|
92
|
+
deferred.push(name);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const rename = renames[name];
|
|
96
|
+
if (rename) {
|
|
97
|
+
pendingRenames.push({ from: name, to: rename });
|
|
98
|
+
rootImports.push(rename);
|
|
99
|
+
if (isRoutes) {
|
|
100
|
+
notes.push(
|
|
101
|
+
`MANUAL: \`${name}\` is now the factory \`${rename}()\` in @decocms/tanstack — ` +
|
|
102
|
+
`add the call at every usage site (e.g. \`...${rename}()\` instead of \`...${rename}\`).`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
rootImports.push(name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
changed = true;
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
if (rootImports.length > 0) {
|
|
113
|
+
lines.push(`import { ${rootImports.join(", ")} } from "@decocms/tanstack";`);
|
|
114
|
+
}
|
|
115
|
+
if (deferred.length > 0) {
|
|
116
|
+
lines.push(`import { ${deferred.join(", ")} } from "@decocms/tanstack/sdk/deferredSectionLoader";`);
|
|
117
|
+
}
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const { from, to } of pendingRenames) {
|
|
122
|
+
result = result.replace(new RegExp(`\\b${from}\\b`, "g"), to);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { content: result, changed };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Upgrade a single file's source from `@decocms/start` / `@decocms/apps`
|
|
130
|
+
* (pre-7.x-split) specifiers to the current split packages. Pure and
|
|
131
|
+
* side-effect-free — the CLI wrapper handles file I/O and package.json.
|
|
132
|
+
*/
|
|
133
|
+
export function upgradeFileToV7(content: string): UpgradeResult {
|
|
134
|
+
const notes: string[] = [];
|
|
135
|
+
let result = content;
|
|
136
|
+
let changed = false;
|
|
137
|
+
|
|
138
|
+
if (result.includes("@decocms/start/routes") || result.includes("@decocms/start/hooks")) {
|
|
139
|
+
const r = rewriteRoutesAndHooksImports(result, notes);
|
|
140
|
+
result = r.content;
|
|
141
|
+
changed = changed || r.changed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const { re, to } of SIMPLE_SPECIFIER_MAP) {
|
|
145
|
+
const before = result;
|
|
146
|
+
result = result.replace(re, to);
|
|
147
|
+
if (result !== before) changed = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
const before = result;
|
|
152
|
+
result = result.replace(GENERIC_SDK_RE, "@decocms/blocks/sdk/$1");
|
|
153
|
+
if (result !== before) changed = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { content: result, changed, notes };
|
|
157
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { extractTailwindConfig } from "./tailwind-config";
|
|
6
|
+
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tw-config-test-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function writeConfig(content: string) {
|
|
18
|
+
fs.writeFileSync(path.join(tmpDir, "tailwind.config.ts"), content, "utf-8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("extractTailwindConfig", () => {
|
|
22
|
+
it("returns an empty extract when no config file exists", () => {
|
|
23
|
+
const result = extractTailwindConfig(tmpDir);
|
|
24
|
+
expect(result.colors).toEqual({});
|
|
25
|
+
expect(result.reviewItems).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("maps a DEFAULT sub-key to the bare parent color, not a literal -DEFAULT suffix", () => {
|
|
29
|
+
writeConfig(`
|
|
30
|
+
export default {
|
|
31
|
+
theme: {
|
|
32
|
+
extend: {
|
|
33
|
+
colors: {
|
|
34
|
+
primary: {
|
|
35
|
+
DEFAULT: "#112233",
|
|
36
|
+
500: "#445566",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
const result = extractTailwindConfig(tmpDir);
|
|
45
|
+
expect(result.colors).toEqual({
|
|
46
|
+
primary: "#112233",
|
|
47
|
+
"primary-500": "#445566",
|
|
48
|
+
});
|
|
49
|
+
expect(result.colors["primary-DEFAULT"]).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("flattens nested theme.extend.colors into dash-joined tokens", () => {
|
|
53
|
+
writeConfig(`
|
|
54
|
+
export default {
|
|
55
|
+
theme: {
|
|
56
|
+
extend: {
|
|
57
|
+
colors: {
|
|
58
|
+
perola: "#F5F0E8",
|
|
59
|
+
brand: {
|
|
60
|
+
100: "#FFEFEF",
|
|
61
|
+
500: "#B10200",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
const result = extractTailwindConfig(tmpDir);
|
|
70
|
+
expect(result.colors).toEqual({
|
|
71
|
+
perola: "#F5F0E8",
|
|
72
|
+
"brand-100": "#FFEFEF",
|
|
73
|
+
"brand-500": "#B10200",
|
|
74
|
+
});
|
|
75
|
+
expect(result.reviewItems).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("joins fontFamily array literals with a comma", () => {
|
|
79
|
+
writeConfig(`
|
|
80
|
+
export default {
|
|
81
|
+
theme: {
|
|
82
|
+
extend: {
|
|
83
|
+
fontFamily: {
|
|
84
|
+
sans: ["Bebas Neue", "sans-serif"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
const result = extractTailwindConfig(tmpDir);
|
|
92
|
+
expect(result.fontFamily.sans).toBe("Bebas Neue, sans-serif");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("splits safelist into literal strings and regex patterns", () => {
|
|
96
|
+
writeConfig(`
|
|
97
|
+
export default {
|
|
98
|
+
safelist: ["bg-red-500", /^grid-cols-/],
|
|
99
|
+
};
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
const result = extractTailwindConfig(tmpDir);
|
|
103
|
+
expect(result.safelist).toEqual(["bg-red-500"]);
|
|
104
|
+
expect(result.safelistPatterns).toEqual(["/^grid-cols-/"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("flags non-literal color entries as review items instead of dropping silently", () => {
|
|
108
|
+
writeConfig(`
|
|
109
|
+
const shared = { accent: "#000" };
|
|
110
|
+
export default {
|
|
111
|
+
theme: {
|
|
112
|
+
extend: {
|
|
113
|
+
colors: {
|
|
114
|
+
...shared,
|
|
115
|
+
primary: someFn(),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
const result = extractTailwindConfig(tmpDir);
|
|
123
|
+
expect(result.colors.primary).toBeUndefined();
|
|
124
|
+
expect(result.reviewItems.length).toBeGreaterThan(0);
|
|
125
|
+
expect(result.reviewItems[0].severity).toBe("warning");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("supports module.exports = {...} form", () => {
|
|
129
|
+
writeConfig(`
|
|
130
|
+
module.exports = {
|
|
131
|
+
theme: { extend: { colors: { primary: "#111111" } } },
|
|
132
|
+
};
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
const result = extractTailwindConfig(tmpDir);
|
|
136
|
+
expect(result.colors).toEqual({ primary: "#111111" });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("reports an error review item when the export shape is unrecognized", () => {
|
|
140
|
+
writeConfig(`
|
|
141
|
+
const config = buildConfig();
|
|
142
|
+
export default config;
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
const result = extractTailwindConfig(tmpDir);
|
|
146
|
+
expect(result.colors).toEqual({});
|
|
147
|
+
expect(result.reviewItems.some((r) => r.severity === "error")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|