@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/extract.mjs CHANGED
@@ -1,91 +1,91 @@
1
- // File: packages/migrate/src/extract.mjs
2
- // Extract class-like tokens from source text (Blade, Vue, JSX/TSX, JS, HTML …), keeping arbitrary values
3
- // such as `w-[37px]`, `[&_svg:not([class*='size-'])]:size-4` and `bg-(--brand)` intact.
4
- // Author: Serge Cools | Framework: OmniaCSS
5
-
6
- import fs from "node:fs/promises";
7
- import path from "node:path";
8
-
9
- export const DEFAULT_EXTENSIONS = ["blade.php", "php", "vue", "jsx", "tsx", "js", "ts", "mjs", "html", "svelte", "astro", "mdx", "twig", "erb"];
10
- const IGNORE_DIRS = new Set(["node_modules", "vendor", ".git", "storage", "dist", "build", "public", ".next", ".nuxt", ".output", "bootstrap", ".cache", ".turbo", ".astro"]);
11
-
12
- const DELIM = /[\s"'`{},;<>=|\\$+?^~]/;
13
-
14
- /**
15
- * Call `cb(token, start, end)` for every class-like token in `text`.
16
- * Arbitrary values such as `w-[37px]` stay in one piece.
17
- */
18
- export function scanTokens(text, cb) {
19
- let cur = "", depth = 0, mark = 0, start = 0;
20
- const flush = (end) => {
21
- if (cur) {
22
- const lead = cur.length - cur.replace(/^[.:]+/, "").length;
23
- const t = cur.replace(/^[.:]+/, "").replace(/[.:,]+$/, "");
24
- if (t && t.length <= 250 && /^[A-Za-z0-9!@\[*(-]/.test(t)) cb(t, start + lead, start + lead + t.length);
25
- }
26
- cur = "";
27
- };
28
- for (let i = 0; i < text.length; i++) {
29
- const ch = text[i];
30
- if (depth > 0) {
31
- if (/\s/.test(ch) || cur.length > 250) { i = mark; cur = ""; depth = 0; continue; } // not an arbitrary value after all
32
- cur += ch;
33
- if (ch === "[" || ch === "(") depth++;
34
- else if (ch === "]" || ch === ")") depth--;
35
- continue;
36
- }
37
- if (ch === "[" || ch === "(") {
38
- const prev = cur.slice(-1);
39
- const next = text[i + 1];
40
- const attach = ch === "[" ? (cur === "" ? !!next && !/[\s"'`\]]/.test(next) : /[-:!]/.test(prev)) : /[-:!]/.test(prev);
41
- if (attach) { if (!cur) start = i; mark = i; depth = 1; cur += ch; continue; }
42
- flush(i); continue;
43
- }
44
- if (DELIM.test(ch) || ch === ")" || ch === "]" || ch === "&" || (ch === "#" && cur === "")) { flush(i); continue; }
45
- if (!cur) start = i;
46
- cur += ch;
47
- }
48
- flush(text.length);
49
- }
50
-
51
- /** Class-like tokens in a text (duplicates are kept, so callers can count occurrences). */
52
- export function extractTokens(text) {
53
- const out = [];
54
- scanTokens(text, (t) => out.push(t));
55
- return out;
56
- }
57
-
58
- /** Walk `dir` and return files with one of the given extensions. */
59
- export async function walk(dir, { extensions = DEFAULT_EXTENSIONS, ignore = IGNORE_DIRS } = {}) {
60
- const files = [];
61
- async function rec(d) {
62
- let entries;
63
- try { entries = await fs.readdir(d, { withFileTypes: true }); } catch { return; }
64
- for (const e of entries) {
65
- if (e.isDirectory()) { if (!ignore.has(e.name) && !e.name.startsWith(".")) await rec(path.join(d, e.name)); continue; }
66
- if (extensions.some((x) => e.name.endsWith("." + x))) files.push(path.join(d, e.name));
67
- }
68
- }
69
- await rec(dir);
70
- return files;
71
- }
72
-
73
- const STRING_RE = /"(?:[^"\\\n]|\\.){0,3000}"|'(?:[^'\\\n]|\\.){0,3000}'|`(?:[^`\\]|\\.){0,4000}`/g;
74
- const CLASS_CONTEXT = /(class(Name)?|:class|\bcn|clsx|cva|twMerge|classNames|tv|@class)\W{0,6}$/;
75
-
76
- /**
77
- * Candidate class strings: quoted strings (and attribute values) with their preceding context.
78
- * @returns {Array<{ words: string[], spans: Array<{token:string,start:number,end:number}>, hinted: boolean }>}
79
- */
80
- export function classStrings(text) {
81
- const out = [];
82
- for (const m of text.matchAll(STRING_RE)) {
83
- const inner = m[0].slice(1, -1);
84
- if (!inner.trim()) continue;
85
- const before = text.slice(Math.max(0, m.index - 24), m.index);
86
- const tokens = [];
87
- scanTokens(inner, (t, s, e) => tokens.push({ token: t, start: m.index + 1 + s, end: m.index + 1 + e }));
88
- out.push({ words: tokens.map((t) => t.token), spans: tokens, hinted: CLASS_CONTEXT.test(before) });
89
- }
90
- return out;
91
- }
1
+ // File: packages/migrate/src/extract.mjs
2
+ // Extract class-like tokens from source text (Blade, Vue, JSX/TSX, JS, HTML …), keeping arbitrary values
3
+ // such as `w-[37px]`, `[&_svg:not([class*='size-'])]:size-4` and `bg-(--brand)` intact.
4
+ // Author: Serge Cools | Framework: OmniaCSS
5
+
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+
9
+ export const DEFAULT_EXTENSIONS = ["blade.php", "php", "vue", "jsx", "tsx", "js", "ts", "mjs", "html", "svelte", "astro", "mdx", "twig", "erb"];
10
+ const IGNORE_DIRS = new Set(["node_modules", "vendor", ".git", "storage", "dist", "build", "public", ".next", ".nuxt", ".output", "bootstrap", ".cache", ".turbo", ".astro"]);
11
+
12
+ const DELIM = /[\s"'`{},;<>=|\\$+?^~]/;
13
+
14
+ /**
15
+ * Call `cb(token, start, end)` for every class-like token in `text`.
16
+ * Arbitrary values such as `w-[37px]` stay in one piece.
17
+ */
18
+ export function scanTokens(text, cb) {
19
+ let cur = "", depth = 0, mark = 0, start = 0;
20
+ const flush = (end) => {
21
+ if (cur) {
22
+ const lead = cur.length - cur.replace(/^[.:]+/, "").length;
23
+ const t = cur.replace(/^[.:]+/, "").replace(/[.:,]+$/, "");
24
+ if (t && t.length <= 250 && /^[A-Za-z0-9!@\[*(-]/.test(t)) cb(t, start + lead, start + lead + t.length);
25
+ }
26
+ cur = "";
27
+ };
28
+ for (let i = 0; i < text.length; i++) {
29
+ const ch = text[i];
30
+ if (depth > 0) {
31
+ if (/\s/.test(ch) || cur.length > 250) { i = mark; cur = ""; depth = 0; continue; } // not an arbitrary value after all
32
+ cur += ch;
33
+ if (ch === "[" || ch === "(") depth++;
34
+ else if (ch === "]" || ch === ")") depth--;
35
+ continue;
36
+ }
37
+ if (ch === "[" || ch === "(") {
38
+ const prev = cur.slice(-1);
39
+ const next = text[i + 1];
40
+ const attach = ch === "[" ? (cur === "" ? !!next && !/[\s"'`\]]/.test(next) : /[-:!]/.test(prev)) : /[-:!]/.test(prev);
41
+ if (attach) { if (!cur) start = i; mark = i; depth = 1; cur += ch; continue; }
42
+ flush(i); continue;
43
+ }
44
+ if (DELIM.test(ch) || ch === ")" || ch === "]" || ch === "&" || (ch === "#" && cur === "")) { flush(i); continue; }
45
+ if (!cur) start = i;
46
+ cur += ch;
47
+ }
48
+ flush(text.length);
49
+ }
50
+
51
+ /** Class-like tokens in a text (duplicates are kept, so callers can count occurrences). */
52
+ export function extractTokens(text) {
53
+ const out = [];
54
+ scanTokens(text, (t) => out.push(t));
55
+ return out;
56
+ }
57
+
58
+ /** Walk `dir` and return files with one of the given extensions. */
59
+ export async function walk(dir, { extensions = DEFAULT_EXTENSIONS, ignore = IGNORE_DIRS } = {}) {
60
+ const files = [];
61
+ async function rec(d) {
62
+ let entries;
63
+ try { entries = await fs.readdir(d, { withFileTypes: true }); } catch { return; }
64
+ for (const e of entries) {
65
+ if (e.isDirectory()) { if (!ignore.has(e.name) && !e.name.startsWith(".")) await rec(path.join(d, e.name)); continue; }
66
+ if (extensions.some((x) => e.name.endsWith("." + x))) files.push(path.join(d, e.name));
67
+ }
68
+ }
69
+ await rec(dir);
70
+ return files;
71
+ }
72
+
73
+ const STRING_RE = /"(?:[^"\\\n]|\\.){0,3000}"|'(?:[^'\\\n]|\\.){0,3000}'|`(?:[^`\\]|\\.){0,4000}`/g;
74
+ const CLASS_CONTEXT = /(class(Name)?|:class|\bcn|clsx|cva|twMerge|classNames|tv|@class)\W{0,6}$/;
75
+
76
+ /**
77
+ * Candidate class strings: quoted strings (and attribute values) with their preceding context.
78
+ * @returns {Array<{ words: string[], spans: Array<{token:string,start:number,end:number}>, hinted: boolean }>}
79
+ */
80
+ export function classStrings(text) {
81
+ const out = [];
82
+ for (const m of text.matchAll(STRING_RE)) {
83
+ const inner = m[0].slice(1, -1);
84
+ if (!inner.trim()) continue;
85
+ const before = text.slice(Math.max(0, m.index - 24), m.index);
86
+ const tokens = [];
87
+ scanTokens(inner, (t, s, e) => tokens.push({ token: t, start: m.index + 1 + s, end: m.index + 1 + e }));
88
+ out.push({ words: tokens.map((t) => t.token), spans: tokens, hinted: CLASS_CONTEXT.test(before) });
89
+ }
90
+ return out;
91
+ }
package/src/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
- // File: packages/migrate/src/index.mjs
2
- // Public API of @birdycom/omniacss-migrate.
3
- // Author: Serge Cools | Framework: OmniaCSS
4
- export { createContext, resolveToken, resolveUtility, parseToken, compileToken, compileTokens, serialize, SCREENS } from "./resolve.mjs";
5
- export { extractTokens, classStrings } from "./extract.mjs";
6
- export { scanDir, formatScan } from "./scan.mjs";
7
- export { runCodemod, formatCodemod } from "./codemod.mjs";
8
- export { planSetup, applySetup, formatPlan } from "./setup.mjs";
9
- export { loadProject, themeFromConfig } from "./project.mjs";
10
- export { COLOR_MAP } from "./values.mjs";
11
- export { default as postcss } from "./postcss.mjs";
12
- export { default as vite } from "./vite.mjs";
1
+ // File: packages/migrate/src/index.mjs
2
+ // Public API of @birdycom/omniacss-migrate.
3
+ // Author: Serge Cools | Framework: OmniaCSS
4
+ export { createContext, resolveToken, resolveUtility, parseToken, compileToken, compileTokens, serialize, SCREENS } from "./resolve.mjs";
5
+ export { extractTokens, classStrings } from "./extract.mjs";
6
+ export { scanDir, formatScan } from "./scan.mjs";
7
+ export { runCodemod, formatCodemod } from "./codemod.mjs";
8
+ export { planSetup, applySetup, formatPlan } from "./setup.mjs";
9
+ export { loadProject, themeFromConfig } from "./project.mjs";
10
+ export { COLOR_MAP } from "./values.mjs";
11
+ export { default as postcss } from "./postcss.mjs";
12
+ export { default as vite } from "./vite.mjs";
package/src/native.mjs CHANGED
@@ -1,72 +1,72 @@
1
- // File: packages/migrate/src/native.mjs
2
- // Codemod brain: find the native OmniaCSS class that has exactly the same declarations as a Tailwind-style class.
3
- // The index is read from the shipped @birdycom/omniacss CSS, so it always matches the installed version.
4
- // Author: Serge Cools | Framework: OmniaCSS
5
-
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- import { createRequire } from "node:module";
9
- import { fileURLToPath } from "node:url";
10
- import { parseToken, resolveToken } from "./resolve.mjs";
11
- import { esc } from "./variants.mjs";
12
-
13
- const here = path.dirname(fileURLToPath(import.meta.url));
14
-
15
- function findOmniaCss(cwd) {
16
- const tries = [
17
- () => createRequire(path.join(cwd, "package.json")).resolve("@birdycom/omniacss/dist/omnia.css"),
18
- () => createRequire(import.meta.url).resolve("@birdycom/omniacss/dist/omnia.css"),
19
- () => path.resolve(here, "../../core/dist/omnia.css"),
20
- ];
21
- for (const t of tries) { try { const p = t(); if (fs.existsSync(p)) return p; } catch { /* next */ } }
22
- throw new Error("@birdycom/omniacss-migrate: cannot find @birdycom/omniacss/dist/omnia.css (npm install @birdycom/omniacss)");
23
- }
24
-
25
- const normDecls = (body) =>
26
- body.replace(/\/\*[\s\S]*?\*\//g, "").split(";").map((s) => s.trim().replace(/\s+/g, " ")).filter(Boolean).map((s) => s.replace(/\s*:\s*/, ":")).join(";");
27
-
28
- let cached;
29
-
30
- /** Index of Omnia's own classes: declarations -> class names, plus the set of every class name. */
31
- export function loadOmniaIndex(cwd = process.cwd()) {
32
- const file = findOmniaCss(cwd);
33
- if (cached?.file === file) return cached;
34
- const css = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
35
- const names = new Set();
36
- for (const m of css.matchAll(/\.((?:\\.|[A-Za-z0-9_-])+)/g)) names.add(m[1].replace(/\\(.)/g, "$1"));
37
- const byDecls = new Map();
38
- for (const m of css.matchAll(/^\.((?:\\.|[A-Za-z0-9_-])+) \{\n([^}]*)\}/gm)) {
39
- const name = m[1].replace(/\\(.)/g, "$1");
40
- const key = normDecls(m[2]);
41
- if (!key) continue;
42
- const list = byDecls.get(key) || [];
43
- if (!list.includes(name)) list.push(name);
44
- byDecls.set(key, list);
45
- }
46
- cached = { file, names, byDecls };
47
- return cached;
48
- }
49
-
50
- const SAFE_VARIANTS = new Set(["sm", "md", "lg", "dark", "hover", "focus", "active", "focus-visible", "focus-within", "disabled", "group-hover", "first", "last", "odd", "even"]);
51
-
52
- /**
53
- * Native Omnia class for a Tailwind-style token, or null.
54
- * @returns {{ native: string } | { collision: true } | null}
55
- */
56
- export function nativeFor(token, ctx, index) {
57
- const p = parseToken(token, ctx);
58
- if (p.foreign || p.important || !p.body) return null;
59
- if (!p.variants.every((v) => SAFE_VARIANTS.has(v))) return null;
60
- const r = resolveToken(p.neg ? "-" + p.utility : p.utility, ctx);
61
- if (!r || r.marker) return r?.marker ? { native: token } : null;
62
- if (r.rules.length !== 1 || r.rules[0].media.length || r.rules[0].selectors.length !== 1) return null;
63
- const rule = r.rules[0];
64
- if (rule.selectors[0] !== `.${esc(p.neg ? "-" + p.utility : p.utility)}`) return null;
65
- const key = rule.decls.map(([k, v]) => `${k}:${v.replace(/\s+/g, " ")}`).join(";");
66
- const list = index.byDecls.get(normDecls(key));
67
- if (!list) return index.names.has(p.utility) ? { collision: true } : null;
68
- const base = list.includes(p.utility) ? p.utility : [...list].sort((a, b) => a.length - b.length)[0];
69
- const native = p.variants.length ? `${p.variants.join(":")}:${base}` : base;
70
- if (p.variants.length && !index.names.has(native)) return null;
71
- return { native };
72
- }
1
+ // File: packages/migrate/src/native.mjs
2
+ // Codemod brain: find the native OmniaCSS class that has exactly the same declarations as a Tailwind-style class.
3
+ // The index is read from the shipped @birdycom/omniacss CSS, so it always matches the installed version.
4
+ // Author: Serge Cools | Framework: OmniaCSS
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { createRequire } from "node:module";
9
+ import { fileURLToPath } from "node:url";
10
+ import { parseToken, resolveToken } from "./resolve.mjs";
11
+ import { esc } from "./variants.mjs";
12
+
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ function findOmniaCss(cwd) {
16
+ const tries = [
17
+ () => createRequire(path.join(cwd, "package.json")).resolve("@birdycom/omniacss/dist/omnia.css"),
18
+ () => createRequire(import.meta.url).resolve("@birdycom/omniacss/dist/omnia.css"),
19
+ () => path.resolve(here, "../../core/dist/omnia.css"),
20
+ ];
21
+ for (const t of tries) { try { const p = t(); if (fs.existsSync(p)) return p; } catch { /* next */ } }
22
+ throw new Error("@birdycom/omniacss-migrate: cannot find @birdycom/omniacss/dist/omnia.css (npm install @birdycom/omniacss)");
23
+ }
24
+
25
+ const normDecls = (body) =>
26
+ body.replace(/\/\*[\s\S]*?\*\//g, "").split(";").map((s) => s.trim().replace(/\s+/g, " ")).filter(Boolean).map((s) => s.replace(/\s*:\s*/, ":")).join(";");
27
+
28
+ let cached;
29
+
30
+ /** Index of Omnia's own classes: declarations -> class names, plus the set of every class name. */
31
+ export function loadOmniaIndex(cwd = process.cwd()) {
32
+ const file = findOmniaCss(cwd);
33
+ if (cached?.file === file) return cached;
34
+ const css = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
35
+ const names = new Set();
36
+ for (const m of css.matchAll(/\.((?:\\.|[A-Za-z0-9_-])+)/g)) names.add(m[1].replace(/\\(.)/g, "$1"));
37
+ const byDecls = new Map();
38
+ for (const m of css.matchAll(/^\.((?:\\.|[A-Za-z0-9_-])+) \{\n([^}]*)\}/gm)) {
39
+ const name = m[1].replace(/\\(.)/g, "$1");
40
+ const key = normDecls(m[2]);
41
+ if (!key) continue;
42
+ const list = byDecls.get(key) || [];
43
+ if (!list.includes(name)) list.push(name);
44
+ byDecls.set(key, list);
45
+ }
46
+ cached = { file, names, byDecls };
47
+ return cached;
48
+ }
49
+
50
+ const SAFE_VARIANTS = new Set(["sm", "md", "lg", "dark", "hover", "focus", "active", "focus-visible", "focus-within", "disabled", "group-hover", "first", "last", "odd", "even"]);
51
+
52
+ /**
53
+ * Native Omnia class for a Tailwind-style token, or null.
54
+ * @returns {{ native: string } | { collision: true } | null}
55
+ */
56
+ export function nativeFor(token, ctx, index) {
57
+ const p = parseToken(token, ctx);
58
+ if (p.foreign || p.important || !p.body) return null;
59
+ if (!p.variants.every((v) => SAFE_VARIANTS.has(v))) return null;
60
+ const r = resolveToken(p.neg ? "-" + p.utility : p.utility, ctx);
61
+ if (!r || r.marker) return r?.marker ? { native: token } : null;
62
+ if (r.rules.length !== 1 || r.rules[0].media.length || r.rules[0].selectors.length !== 1) return null;
63
+ const rule = r.rules[0];
64
+ if (rule.selectors[0] !== `.${esc(p.neg ? "-" + p.utility : p.utility)}`) return null;
65
+ const key = rule.decls.map(([k, v]) => `${k}:${v.replace(/\s+/g, " ")}`).join(";");
66
+ const list = index.byDecls.get(normDecls(key));
67
+ if (!list) return index.names.has(p.utility) ? { collision: true } : null;
68
+ const base = list.includes(p.utility) ? p.utility : [...list].sort((a, b) => a.length - b.length)[0];
69
+ const native = p.variants.length ? `${p.variants.join(":")}:${base}` : base;
70
+ if (p.variants.length && !index.names.has(native)) return null;
71
+ return { native };
72
+ }