@birdycom/omniacss-migrate 0.11.0 → 0.11.2

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/src/cli.mjs CHANGED
@@ -1,139 +1,139 @@
1
- // File: packages/migrate/src/cli.mjs
2
- // OmniaCSS migrate CLI (used by bin/omniacss-migrate.mjs and `omniacss migrate`): scan | setup | codemod | build | css
3
- // Author: Serge Cools | Framework: OmniaCSS
4
-
5
- import path from "node:path";
6
- import fs from "node:fs/promises";
7
- import { parseArgs } from "node:util";
8
- import { loadProject } from "./project.mjs";
9
- import { scanDir, formatScan } from "./scan.mjs";
10
- import { runCodemod, formatCodemod } from "./codemod.mjs";
11
- import { planSetup, applySetup, formatPlan } from "./setup.mjs";
12
- import { compileToken } from "./resolve.mjs";
13
-
14
- export const HELP = `
15
- omniacss-migrate - switch from Tailwind CSS to OmniaCSS and keep your class names
16
-
17
- omniacss-migrate scan <dir> coverage report: how many classes the compat layer supports
18
- omniacss-migrate setup <project> [--write] plan (default) or apply the switch (deps, vite/postcss config, CSS)
19
- omniacss-migrate codemod <dir> [--write] rewrite classes to native OmniaCSS names where 1:1 (dry run by default)
20
- omniacss-migrate build [-c glob]... [-o file] write the compat + core CSS once (no Vite/PostCSS needed)
21
- omniacss-migrate css "<class>" print the CSS for one class
22
-
23
- Bootstrap 3/4/5 -> OmniaCSS + OmniaUI
24
- omniacss-migrate bootstrap scan <dir> report: what converts, what stays (grid), what needs a hand
25
- omniacss-migrate bootstrap codemod <dir> --write rewrite classes, swap the <link>, write bootstrap-compat.css
26
-
27
- Options
28
- --write apply changes (setup, codemod)
29
- --top <n> rows in the unsupported list (scan, codemod; default 40)
30
- --json machine readable output (scan)
31
- --breakpoints <omnia|tailwind> sm/md/lg values (default omnia)
32
- --dark <class|media> dark mode strategy (default: detected)
33
- --tailwind <3|4> naming flavor (default: detected)
34
- --keep-brand-colors keep literal brand colors from tailwind.config instead of mapping to Omnia tokens
35
- --bs <3|4|5> (bootstrap) Bootstrap version (default: detected)
36
- --breakpoints exact (bootstrap) keep Bootstrap's px breakpoints as min-[…]: variants instead of the nearest Omnia breakpoint
37
- --compat <file> (bootstrap) where to write the compat stylesheet (default bootstrap-compat.css)
38
- --omnia-href, --omniaui-href, --compat-href (bootstrap) stylesheet URLs that replace the Bootstrap <link>
39
- `;
40
-
41
- function parse(args) {
42
- return parseArgs({
43
- args, allowPositionals: true,
44
- options: {
45
- write: { type: "boolean" }, top: { type: "string" }, json: { type: "boolean" }, help: { type: "boolean", short: "h" },
46
- breakpoints: { type: "string" }, dark: { type: "string" }, tailwind: { type: "string" }, "keep-brand-colors": { type: "boolean" },
47
- content: { type: "string", short: "c", multiple: true }, output: { type: "string", short: "o" }, backup: { type: "boolean" },
48
- bs: { type: "string" }, compat: { type: "string" }, "omnia-href": { type: "string" }, "omniaui-href": { type: "string" }, "compat-href": { type: "string" },
49
- },
50
- });
51
- }
52
-
53
- let values, positionals;
54
-
55
- function ctxOptions() {
56
- return {
57
- breakpoints: values.breakpoints, darkMode: values.dark, v4: values.tailwind ? Number(values.tailwind) >= 4 : undefined,
58
- keepBrandColors: values["keep-brand-colors"],
59
- };
60
- }
61
-
62
- export async function run(args = process.argv.slice(2)) {
63
- ({ values, positionals } = parse(args));
64
- const [command, target] = positionals;
65
- if (!command || values.help || command === "help") { console.log(HELP); return; }
66
- const cwd = process.cwd();
67
- const dir = path.resolve(cwd, target || ".");
68
- const top = Number(values.top || 40);
69
-
70
- if (command === "bootstrap") {
71
- const [, sub, where] = positionals;
72
- if (!["scan", "codemod"].includes(sub)) { console.log(HELP); process.exitCode = 1; return; }
73
- const { migrateBootstrap, formatBootstrap } = await import("./bootstrap/migrate.mjs");
74
- const report = await migrateBootstrap(path.resolve(cwd, where || "."), {
75
- write: sub === "codemod" && !!values.write, version: values.bs ? Number(values.bs) : undefined,
76
- breakpoints: values.breakpoints === "exact" ? "exact" : "omnia", compatFile: values.compat,
77
- omniaHref: values["omnia-href"], omniauiHref: values["omniaui-href"], compatHref: values["compat-href"],
78
- });
79
- console.log(formatBootstrap(report, { top }));
80
- return;
81
- }
82
-
83
- if (command === "scan") {
84
- const { ctx, info } = await loadProject(dir, ctxOptions());
85
- const stats = await scanDir(dir, { ctx });
86
- if (values.json) {
87
- console.log(JSON.stringify({ dir, files: stats.files, occurrences: stats.occurrences, supported: stats.supported, coverage: +stats.coverage.toFixed(2),
88
- unsupported: Object.fromEntries([...stats.unsupported].sort((a, b) => b[1] - a[1]).slice(0, top)) }, null, 2));
89
- } else {
90
- console.log(`Tailwind ${info.tailwindVersion ?? "?"} project, dark mode: ${ctx.darkMode}, ${info.themeVars.size} custom theme variables\n`);
91
- console.log(formatScan(stats, { top }));
92
- }
93
- return;
94
- }
95
-
96
- if (command === "setup") {
97
- const plan = await planSetup(dir, { keepBrandColors: values["keep-brand-colors"] });
98
- if (values.write) await applySetup(plan, { backup: values.backup });
99
- console.log(formatPlan(plan, dir, { write: !!values.write }));
100
- return;
101
- }
102
-
103
- if (command === "codemod") {
104
- const { ctx } = await loadProject(dir, { ...ctxOptions(), breakpoints: values.breakpoints || "omnia" });
105
- const report = await runCodemod(dir, { ctx, write: !!values.write, cwd: dir });
106
- console.log(formatCodemod(report, { top }));
107
- return;
108
- }
109
-
110
- if (command === "build") {
111
- const postcss = (await import("postcss")).default;
112
- const plugin = (await import("./postcss.mjs")).default;
113
- const output = values.output || "omnia-compat.css";
114
- const content = values.content?.length ? values.content : undefined;
115
- const input = '@import "tailwindcss";\n';
116
- const result = await postcss([plugin({ cwd: dir, content, ...ctxOptionsForPlugin() })]).process(input, { from: path.join(dir, "entry.css") });
117
- await fs.writeFile(path.resolve(cwd, output), result.css);
118
- console.log(`Wrote ${output} (${(result.css.length / 1024).toFixed(1)} KB)`);
119
- return;
120
- }
121
-
122
- if (command === "css") {
123
- const { ctx } = await loadProject(cwd, ctxOptions());
124
- for (const token of positionals.slice(1).length ? positionals.slice(1) : [target]) {
125
- const css = compileToken(token, ctx);
126
- console.log(css ?? `/* ${token}: not supported */`);
127
- }
128
- return;
129
- }
130
-
131
- console.error(`Unknown command: ${command}`);
132
- console.log(HELP);
133
- process.exitCode = 1;
134
- }
135
-
136
- function ctxOptionsForPlugin() {
137
- return { breakpoints: values.breakpoints, darkMode: values.dark, tailwind: values.tailwind ? Number(values.tailwind) : undefined };
138
- }
139
-
1
+ // File: packages/migrate/src/cli.mjs
2
+ // OmniaCSS migrate CLI (used by bin/omniacss-migrate.mjs and `omniacss migrate`): scan | setup | codemod | build | css
3
+ // Author: Serge Cools | Framework: OmniaCSS
4
+
5
+ import path from "node:path";
6
+ import fs from "node:fs/promises";
7
+ import { parseArgs } from "node:util";
8
+ import { loadProject } from "./project.mjs";
9
+ import { scanDir, formatScan } from "./scan.mjs";
10
+ import { runCodemod, formatCodemod } from "./codemod.mjs";
11
+ import { planSetup, applySetup, formatPlan } from "./setup.mjs";
12
+ import { compileToken } from "./resolve.mjs";
13
+
14
+ export const HELP = `
15
+ omniacss-migrate - switch from Tailwind CSS to OmniaCSS and keep your class names
16
+
17
+ omniacss-migrate scan <dir> coverage report: how many classes the compat layer supports
18
+ omniacss-migrate setup <project> [--write] plan (default) or apply the switch (deps, vite/postcss config, CSS)
19
+ omniacss-migrate codemod <dir> [--write] rewrite classes to native OmniaCSS names where 1:1 (dry run by default)
20
+ omniacss-migrate build [-c glob]... [-o file] write the compat + core CSS once (no Vite/PostCSS needed)
21
+ omniacss-migrate css "<class>" print the CSS for one class
22
+
23
+ Bootstrap 3/4/5 -> OmniaCSS + OmniaUI
24
+ omniacss-migrate bootstrap scan <dir> report: what converts, what stays (grid), what needs a hand
25
+ omniacss-migrate bootstrap codemod <dir> --write rewrite classes, swap the <link>, write bootstrap-compat.css
26
+
27
+ Options
28
+ --write apply changes (setup, codemod)
29
+ --top <n> rows in the unsupported list (scan, codemod; default 40)
30
+ --json machine readable output (scan)
31
+ --breakpoints <omnia|tailwind> sm/md/lg values (default omnia)
32
+ --dark <class|media> dark mode strategy (default: detected)
33
+ --tailwind <3|4> naming flavor (default: detected)
34
+ --keep-brand-colors keep literal brand colors from tailwind.config instead of mapping to Omnia tokens
35
+ --bs <3|4|5> (bootstrap) Bootstrap version (default: detected)
36
+ --breakpoints exact (bootstrap) keep Bootstrap's px breakpoints as min-[…]: variants instead of the nearest Omnia breakpoint
37
+ --compat <file> (bootstrap) where to write the compat stylesheet (default bootstrap-compat.css)
38
+ --omnia-href, --omniaui-href, --compat-href (bootstrap) stylesheet URLs that replace the Bootstrap <link>
39
+ `;
40
+
41
+ function parse(args) {
42
+ return parseArgs({
43
+ args, allowPositionals: true,
44
+ options: {
45
+ write: { type: "boolean" }, top: { type: "string" }, json: { type: "boolean" }, help: { type: "boolean", short: "h" },
46
+ breakpoints: { type: "string" }, dark: { type: "string" }, tailwind: { type: "string" }, "keep-brand-colors": { type: "boolean" },
47
+ content: { type: "string", short: "c", multiple: true }, output: { type: "string", short: "o" }, backup: { type: "boolean" },
48
+ bs: { type: "string" }, compat: { type: "string" }, "omnia-href": { type: "string" }, "omniaui-href": { type: "string" }, "compat-href": { type: "string" },
49
+ },
50
+ });
51
+ }
52
+
53
+ let values, positionals;
54
+
55
+ function ctxOptions() {
56
+ return {
57
+ breakpoints: values.breakpoints, darkMode: values.dark, v4: values.tailwind ? Number(values.tailwind) >= 4 : undefined,
58
+ keepBrandColors: values["keep-brand-colors"],
59
+ };
60
+ }
61
+
62
+ export async function run(args = process.argv.slice(2)) {
63
+ ({ values, positionals } = parse(args));
64
+ const [command, target] = positionals;
65
+ if (!command || values.help || command === "help") { console.log(HELP); return; }
66
+ const cwd = process.cwd();
67
+ const dir = path.resolve(cwd, target || ".");
68
+ const top = Number(values.top || 40);
69
+
70
+ if (command === "bootstrap") {
71
+ const [, sub, where] = positionals;
72
+ if (!["scan", "codemod"].includes(sub)) { console.log(HELP); process.exitCode = 1; return; }
73
+ const { migrateBootstrap, formatBootstrap } = await import("./bootstrap/migrate.mjs");
74
+ const report = await migrateBootstrap(path.resolve(cwd, where || "."), {
75
+ write: sub === "codemod" && !!values.write, version: values.bs ? Number(values.bs) : undefined,
76
+ breakpoints: values.breakpoints === "exact" ? "exact" : "omnia", compatFile: values.compat,
77
+ omniaHref: values["omnia-href"], omniauiHref: values["omniaui-href"], compatHref: values["compat-href"],
78
+ });
79
+ console.log(formatBootstrap(report, { top }));
80
+ return;
81
+ }
82
+
83
+ if (command === "scan") {
84
+ const { ctx, info } = await loadProject(dir, ctxOptions());
85
+ const stats = await scanDir(dir, { ctx });
86
+ if (values.json) {
87
+ console.log(JSON.stringify({ dir, files: stats.files, occurrences: stats.occurrences, supported: stats.supported, coverage: +stats.coverage.toFixed(2),
88
+ unsupported: Object.fromEntries([...stats.unsupported].sort((a, b) => b[1] - a[1]).slice(0, top)) }, null, 2));
89
+ } else {
90
+ console.log(`Tailwind ${info.tailwindVersion ?? "?"} project, dark mode: ${ctx.darkMode}, ${info.themeVars.size} custom theme variables\n`);
91
+ console.log(formatScan(stats, { top }));
92
+ }
93
+ return;
94
+ }
95
+
96
+ if (command === "setup") {
97
+ const plan = await planSetup(dir, { keepBrandColors: values["keep-brand-colors"] });
98
+ if (values.write) await applySetup(plan, { backup: values.backup });
99
+ console.log(formatPlan(plan, dir, { write: !!values.write }));
100
+ return;
101
+ }
102
+
103
+ if (command === "codemod") {
104
+ const { ctx } = await loadProject(dir, { ...ctxOptions(), breakpoints: values.breakpoints || "omnia" });
105
+ const report = await runCodemod(dir, { ctx, write: !!values.write, cwd: dir });
106
+ console.log(formatCodemod(report, { top }));
107
+ return;
108
+ }
109
+
110
+ if (command === "build") {
111
+ const postcss = (await import("postcss")).default;
112
+ const plugin = (await import("./postcss.mjs")).default;
113
+ const output = values.output || "omnia-compat.css";
114
+ const content = values.content?.length ? values.content : undefined;
115
+ const input = '@import "tailwindcss";\n';
116
+ const result = await postcss([plugin({ cwd: dir, content, ...ctxOptionsForPlugin() })]).process(input, { from: path.join(dir, "entry.css") });
117
+ await fs.writeFile(path.resolve(cwd, output), result.css);
118
+ console.log(`Wrote ${output} (${(result.css.length / 1024).toFixed(1)} KB)`);
119
+ return;
120
+ }
121
+
122
+ if (command === "css") {
123
+ const { ctx } = await loadProject(cwd, ctxOptions());
124
+ for (const token of positionals.slice(1).length ? positionals.slice(1) : [target]) {
125
+ const css = compileToken(token, ctx);
126
+ console.log(css ?? `/* ${token}: not supported */`);
127
+ }
128
+ return;
129
+ }
130
+
131
+ console.error(`Unknown command: ${command}`);
132
+ console.log(HELP);
133
+ process.exitCode = 1;
134
+ }
135
+
136
+ function ctxOptionsForPlugin() {
137
+ return { breakpoints: values.breakpoints, darkMode: values.dark, tailwind: values.tailwind ? Number(values.tailwind) : undefined };
138
+ }
139
+
package/src/codemod.mjs CHANGED
@@ -1,82 +1,82 @@
1
- // File: packages/migrate/src/codemod.mjs
2
- // Codemod: rewrite Tailwind-style class names to native OmniaCSS classes where the declarations are identical.
3
- // Everything else is left untouched and reported. Dry-run unless `write` is true.
4
- // Author: Serge Cools | Framework: OmniaCSS
5
-
6
- import fs from "node:fs/promises";
7
- import path from "node:path";
8
- import { classStrings, walk, DEFAULT_EXTENSIONS } from "./extract.mjs";
9
- import { isClassString, makeKind } from "./scan.mjs";
10
- import { loadOmniaIndex, nativeFor } from "./native.mjs";
11
-
12
- export const MANIFEST = "omniacss-migrate.manifest.json";
13
-
14
- /**
15
- * @param {string} dir
16
- * @param {{ ctx: object, write?: boolean, extensions?: string[], cwd?: string }} o
17
- */
18
- export async function runCodemod(dir, o) {
19
- const { ctx, write = false } = o;
20
- const index = loadOmniaIndex(o.cwd || dir);
21
- const kind = makeKind(ctx);
22
- const memo = new Map();
23
- const map = (t) => { if (!memo.has(t)) memo.set(t, nativeFor(t, ctx, index)); return memo.get(t); };
24
- const files = await walk(dir, { extensions: o.extensions || DEFAULT_EXTENSIONS });
25
- const report = { dir, write, files: [], total: 0, rewritten: 0, unchanged: 0, leftovers: new Map(), collisions: new Map(), mapping: new Map() };
26
- for (const file of files) {
27
- let text;
28
- try { text = await fs.readFile(file, "utf8"); } catch { continue; }
29
- const edits = [];
30
- for (const s of classStrings(text)) {
31
- if (!s.words.length) continue;
32
- const kinds = s.words.map(kind);
33
- if (!isClassString(kinds, s.hinted)) continue;
34
- s.spans.forEach((sp, i) => {
35
- if (kinds[i] !== "supported") return;
36
- report.total++;
37
- const r = map(sp.token);
38
- if (r?.native && r.native !== sp.token) {
39
- edits.push({ ...sp, to: r.native });
40
- report.rewritten++;
41
- report.mapping.set(sp.token, r.native);
42
- } else if (r?.collision) {
43
- report.collisions.set(sp.token, (report.collisions.get(sp.token) || 0) + 1);
44
- report.leftovers.set(sp.token, (report.leftovers.get(sp.token) || 0) + 1);
45
- } else if (r?.native) report.unchanged++;
46
- else report.leftovers.set(sp.token, (report.leftovers.get(sp.token) || 0) + 1);
47
- });
48
- }
49
- if (!edits.length) continue;
50
- let out = text;
51
- for (const e of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.to + out.slice(e.end);
52
- report.files.push({ file, changes: edits.length, sample: edits.slice(0, 5).map((e) => `${e.token} -> ${e.to}`) });
53
- if (write) await fs.writeFile(file, out);
54
- }
55
- const native = new Set(report.mapping.values());
56
- const manifestFile = path.join(o.manifestDir || dir, MANIFEST);
57
- if (write && native.size) {
58
- const prev = JSON.parse(await fs.readFile(manifestFile, "utf8").catch(() => "{}"));
59
- for (const t of prev.native || []) native.add(t);
60
- const ambiguous = [...native].filter((t) => report.leftovers.has(t));
61
- await fs.writeFile(manifestFile, JSON.stringify({ note: "Written by omniacss-migrate codemod: native OmniaCSS classes the compat layer must not emit.", native: [...native].sort(), ambiguous }, null, 1) + "\n");
62
- report.manifestFile = manifestFile;
63
- }
64
- report.ambiguous = [...native].filter((t) => report.leftovers.has(t));
65
- return report;
66
- }
67
-
68
- export function formatCodemod(r, { top = 25 } = {}) {
69
- const pct = r.total ? ((r.rewritten / r.total) * 100).toFixed(1) : "0";
70
- const lines = [];
71
- lines.push(`${r.write ? "Rewrote" : "Dry run, would rewrite"} ${r.rewritten} of ${r.total} class occurrences (${pct}%) in ${r.files.length} files under ${r.dir}`);
72
- for (const f of r.files.slice(0, 15)) lines.push(` ${f.file} (${f.changes}) e.g. ${f.sample.slice(0, 3).join(", ")}`);
73
- if (r.files.length > 15) lines.push(` ... and ${r.files.length - 15} more files`);
74
- const left = [...r.leftovers.entries()].sort((a, b) => b[1] - a[1]);
75
- lines.push("", `Left as they are (no 1:1 native class): ${left.reduce((n, [, c]) => n + c, 0)} occurrences, ${left.length} unique`);
76
- for (const [t, n] of left.slice(0, top)) lines.push(` ${String(n).padStart(5)} ${t}${r.collisions.has(t) ? " (name exists in OmniaCSS with different meaning)" : ""}`);
77
- if (r.manifestFile) lines.push("", `Wrote ${r.manifestFile}: the compat layer skips these native classes so names never collide.`);
78
- if (r.ambiguous?.length) lines.push(`Ambiguous (both a native class and a Tailwind-style leftover): ${r.ambiguous.slice(0, 10).join(", ")}`);
79
- lines.push("", "Note: leftover classes keep working only while the compat layer (@birdycom/omniacss-migrate/postcss) stays enabled.");
80
- if (!r.write) lines.push("Run again with --write to apply.");
81
- return lines.join("\n");
82
- }
1
+ // File: packages/migrate/src/codemod.mjs
2
+ // Codemod: rewrite Tailwind-style class names to native OmniaCSS classes where the declarations are identical.
3
+ // Everything else is left untouched and reported. Dry-run unless `write` is true.
4
+ // Author: Serge Cools | Framework: OmniaCSS
5
+
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { classStrings, walk, DEFAULT_EXTENSIONS } from "./extract.mjs";
9
+ import { isClassString, makeKind } from "./scan.mjs";
10
+ import { loadOmniaIndex, nativeFor } from "./native.mjs";
11
+
12
+ export const MANIFEST = "omniacss-migrate.manifest.json";
13
+
14
+ /**
15
+ * @param {string} dir
16
+ * @param {{ ctx: object, write?: boolean, extensions?: string[], cwd?: string }} o
17
+ */
18
+ export async function runCodemod(dir, o) {
19
+ const { ctx, write = false } = o;
20
+ const index = loadOmniaIndex(o.cwd || dir);
21
+ const kind = makeKind(ctx);
22
+ const memo = new Map();
23
+ const map = (t) => { if (!memo.has(t)) memo.set(t, nativeFor(t, ctx, index)); return memo.get(t); };
24
+ const files = await walk(dir, { extensions: o.extensions || DEFAULT_EXTENSIONS });
25
+ const report = { dir, write, files: [], total: 0, rewritten: 0, unchanged: 0, leftovers: new Map(), collisions: new Map(), mapping: new Map() };
26
+ for (const file of files) {
27
+ let text;
28
+ try { text = await fs.readFile(file, "utf8"); } catch { continue; }
29
+ const edits = [];
30
+ for (const s of classStrings(text)) {
31
+ if (!s.words.length) continue;
32
+ const kinds = s.words.map(kind);
33
+ if (!isClassString(kinds, s.hinted)) continue;
34
+ s.spans.forEach((sp, i) => {
35
+ if (kinds[i] !== "supported") return;
36
+ report.total++;
37
+ const r = map(sp.token);
38
+ if (r?.native && r.native !== sp.token) {
39
+ edits.push({ ...sp, to: r.native });
40
+ report.rewritten++;
41
+ report.mapping.set(sp.token, r.native);
42
+ } else if (r?.collision) {
43
+ report.collisions.set(sp.token, (report.collisions.get(sp.token) || 0) + 1);
44
+ report.leftovers.set(sp.token, (report.leftovers.get(sp.token) || 0) + 1);
45
+ } else if (r?.native) report.unchanged++;
46
+ else report.leftovers.set(sp.token, (report.leftovers.get(sp.token) || 0) + 1);
47
+ });
48
+ }
49
+ if (!edits.length) continue;
50
+ let out = text;
51
+ for (const e of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.to + out.slice(e.end);
52
+ report.files.push({ file, changes: edits.length, sample: edits.slice(0, 5).map((e) => `${e.token} -> ${e.to}`) });
53
+ if (write) await fs.writeFile(file, out);
54
+ }
55
+ const native = new Set(report.mapping.values());
56
+ const manifestFile = path.join(o.manifestDir || dir, MANIFEST);
57
+ if (write && native.size) {
58
+ const prev = JSON.parse(await fs.readFile(manifestFile, "utf8").catch(() => "{}"));
59
+ for (const t of prev.native || []) native.add(t);
60
+ const ambiguous = [...native].filter((t) => report.leftovers.has(t));
61
+ await fs.writeFile(manifestFile, JSON.stringify({ note: "Written by omniacss-migrate codemod: native OmniaCSS classes the compat layer must not emit.", native: [...native].sort(), ambiguous }, null, 1) + "\n");
62
+ report.manifestFile = manifestFile;
63
+ }
64
+ report.ambiguous = [...native].filter((t) => report.leftovers.has(t));
65
+ return report;
66
+ }
67
+
68
+ export function formatCodemod(r, { top = 25 } = {}) {
69
+ const pct = r.total ? ((r.rewritten / r.total) * 100).toFixed(1) : "0";
70
+ const lines = [];
71
+ lines.push(`${r.write ? "Rewrote" : "Dry run, would rewrite"} ${r.rewritten} of ${r.total} class occurrences (${pct}%) in ${r.files.length} files under ${r.dir}`);
72
+ for (const f of r.files.slice(0, 15)) lines.push(` ${f.file} (${f.changes}) e.g. ${f.sample.slice(0, 3).join(", ")}`);
73
+ if (r.files.length > 15) lines.push(` ... and ${r.files.length - 15} more files`);
74
+ const left = [...r.leftovers.entries()].sort((a, b) => b[1] - a[1]);
75
+ lines.push("", `Left as they are (no 1:1 native class): ${left.reduce((n, [, c]) => n + c, 0)} occurrences, ${left.length} unique`);
76
+ for (const [t, n] of left.slice(0, top)) lines.push(` ${String(n).padStart(5)} ${t}${r.collisions.has(t) ? " (name exists in OmniaCSS with different meaning)" : ""}`);
77
+ if (r.manifestFile) lines.push("", `Wrote ${r.manifestFile}: the compat layer skips these native classes so names never collide.`);
78
+ if (r.ambiguous?.length) lines.push(`Ambiguous (both a native class and a Tailwind-style leftover): ${r.ambiguous.slice(0, 10).join(", ")}`);
79
+ lines.push("", "Note: leftover classes keep working only while the compat layer (@birdycom/omniacss-migrate/postcss) stays enabled.");
80
+ if (!r.write) lines.push("Run again with --write to apply.");
81
+ return lines.join("\n");
82
+ }
package/src/core.mjs CHANGED
@@ -1,67 +1,67 @@
1
- // File: packages/migrate/src/core.mjs
2
- // Bridge to @birdycom/omniacss: the on-demand engine that emits tokens + Omnia-native classes.
3
- // Author: Serge Cools | Framework: OmniaCSS
4
-
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
- import { createRequire } from "node:module";
8
- import { fileURLToPath, pathToFileURL } from "node:url";
9
-
10
- const here = path.dirname(fileURLToPath(import.meta.url));
11
- let enginePromise;
12
-
13
- /** Load @birdycom/omniacss/engine (installed package first, monorepo sibling as fallback). */
14
- export function loadEngine(cwd = process.cwd()) {
15
- enginePromise ||= (async () => {
16
- const tries = [
17
- () => createRequire(path.join(cwd, "package.json")).resolve("@birdycom/omniacss/engine"),
18
- () => createRequire(import.meta.url).resolve("@birdycom/omniacss/engine"),
19
- () => path.resolve(here, "../../core/src/engine.mjs"),
20
- ];
21
- for (const t of tries) {
22
- try { const p = t(); await fs.access(p); return await import(pathToFileURL(p).href); } catch { /* next */ }
23
- }
24
- throw new Error("@birdycom/omniacss-migrate: cannot find @birdycom/omniacss. Install it with: npm install @birdycom/omniacss");
25
- })();
26
- return enginePromise;
27
- }
28
-
29
- const cache = new Map();
30
- let classNames, classNamesFor;
31
-
32
- /** Every class name that exists in the full Omnia CSS (cached): lets us ignore unrelated words in the content. */
33
- export function omniaClassNames(full) {
34
- if (classNames && classNamesFor === full.length) return classNames;
35
- classNames = new Set();
36
- for (const m of full.matchAll(/\.((?:\\.|[A-Za-z0-9_-])+)/g)) classNames.add(m[1].replace(/\\(.)/g, "$1"));
37
- classNamesFor = full.length;
38
- return classNames;
39
- }
40
-
41
- /**
42
- * Omnia core CSS for a project: tokens (every var(--color-*) the compat output uses) plus the Omnia-native
43
- * classes found in the content. Classes the compat layer resolves are excluded, so a Tailwind-style `p-4`
44
- * never collides with Omnia's own `p-4`.
45
- * @param {{ candidates: Set<string>, compatCss: string, exclude: Set<string>, cwd?: string, layer?: string|false, mode?: true|"tokens" }} o
46
- */
47
- export async function buildCoreCss({ candidates, compatCss, exclude, cwd, layer = "omnia", mode = true }) {
48
- const engine = await loadEngine(cwd);
49
- const full = mode === true ? await engine.loadFullCss() : await fs.readFile(path.join(engine.DIST, "oc-colors.css"), "utf8");
50
- const set = new Set();
51
- if (mode === true) {
52
- const known = omniaClassNames(full);
53
- for (const c of candidates) if (known.has(c) && !exclude.has(c)) set.add(c);
54
- }
55
- const key = `${layer}|${mode}|${hash(compatCss)}|${[...set].sort().join(" ")}`;
56
- if (cache.has(key)) return cache.get(key);
57
- const css = engine.generate(full, set, { layer, referencedBy: [compatCss] });
58
- cache.clear();
59
- cache.set(key, css);
60
- return css;
61
- }
62
-
63
- function hash(s) {
64
- let h = 5381;
65
- for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
66
- return h;
67
- }
1
+ // File: packages/migrate/src/core.mjs
2
+ // Bridge to @birdycom/omniacss: the on-demand engine that emits tokens + Omnia-native classes.
3
+ // Author: Serge Cools | Framework: OmniaCSS
4
+
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { createRequire } from "node:module";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+
10
+ const here = path.dirname(fileURLToPath(import.meta.url));
11
+ let enginePromise;
12
+
13
+ /** Load @birdycom/omniacss/engine (installed package first, monorepo sibling as fallback). */
14
+ export function loadEngine(cwd = process.cwd()) {
15
+ enginePromise ||= (async () => {
16
+ const tries = [
17
+ () => createRequire(path.join(cwd, "package.json")).resolve("@birdycom/omniacss/engine"),
18
+ () => createRequire(import.meta.url).resolve("@birdycom/omniacss/engine"),
19
+ () => path.resolve(here, "../../core/src/engine.mjs"),
20
+ ];
21
+ for (const t of tries) {
22
+ try { const p = t(); await fs.access(p); return await import(pathToFileURL(p).href); } catch { /* next */ }
23
+ }
24
+ throw new Error("@birdycom/omniacss-migrate: cannot find @birdycom/omniacss. Install it with: npm install @birdycom/omniacss");
25
+ })();
26
+ return enginePromise;
27
+ }
28
+
29
+ const cache = new Map();
30
+ let classNames, classNamesFor;
31
+
32
+ /** Every class name that exists in the full Omnia CSS (cached): lets us ignore unrelated words in the content. */
33
+ export function omniaClassNames(full) {
34
+ if (classNames && classNamesFor === full.length) return classNames;
35
+ classNames = new Set();
36
+ for (const m of full.matchAll(/\.((?:\\.|[A-Za-z0-9_-])+)/g)) classNames.add(m[1].replace(/\\(.)/g, "$1"));
37
+ classNamesFor = full.length;
38
+ return classNames;
39
+ }
40
+
41
+ /**
42
+ * Omnia core CSS for a project: tokens (every var(--color-*) the compat output uses) plus the Omnia-native
43
+ * classes found in the content. Classes the compat layer resolves are excluded, so a Tailwind-style `p-4`
44
+ * never collides with Omnia's own `p-4`.
45
+ * @param {{ candidates: Set<string>, compatCss: string, exclude: Set<string>, cwd?: string, layer?: string|false, mode?: true|"tokens" }} o
46
+ */
47
+ export async function buildCoreCss({ candidates, compatCss, exclude, cwd, layer = "omnia", mode = true }) {
48
+ const engine = await loadEngine(cwd);
49
+ const full = mode === true ? await engine.loadFullCss() : await fs.readFile(path.join(engine.DIST, "oc-colors.css"), "utf8");
50
+ const set = new Set();
51
+ if (mode === true) {
52
+ const known = omniaClassNames(full);
53
+ for (const c of candidates) if (known.has(c) && !exclude.has(c)) set.add(c);
54
+ }
55
+ const key = `${layer}|${mode}|${hash(compatCss)}|${[...set].sort().join(" ")}`;
56
+ if (cache.has(key)) return cache.get(key);
57
+ const css = engine.generate(full, set, { layer, referencedBy: [compatCss] });
58
+ cache.clear();
59
+ cache.set(key, css);
60
+ return css;
61
+ }
62
+
63
+ function hash(s) {
64
+ let h = 5381;
65
+ for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
66
+ return h;
67
+ }