@birdycom/omniacss-migrate 0.12.1 → 0.12.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/README.md +2 -2
- package/package.json +2 -2
- package/src/cli.mjs +3 -2
- package/src/codemod.mjs +14 -2
- package/src/core.mjs +43 -6
- package/src/postcss.mjs +48 -7
- package/src/resolve.mjs +7 -2
- package/src/vite.mjs +22 -0
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ export default { plugins: [omniaMigrate({ content: ["resources/views/**/*.blade.
|
|
|
51
51
|
- Replaces `@import "@birdycom/omniacss-migrate"` / `@import "tailwindcss"` / `@tailwind base|components|utilities` with Omnia core (tokens + the Omnia classes you use), a small base reset and the compat CSS for the classes it finds in your content.
|
|
52
52
|
- Expands `@apply` (with variants, `!important`, and your own classes), `theme(colors.red.500)`, `--spacing(4)`, `@theme`, `@custom-variant`, `@variant`, `@source`, `@source inline("…")`; ignores `@plugin` and `@config`.
|
|
53
53
|
- Reads `tailwind.config.{js,cjs,mjs}` (`darkMode`, `theme.colors`/`extend.colors`, `fontFamily`, `borderRadius`, `spacing`, `boxShadow`, `screens`, `container`, `prefix`, `important`) and your `@theme` blocks.
|
|
54
|
-
- Output is wrapped in cascade layers `omnia, theme, base, components, utilities
|
|
54
|
+
- Output is wrapped in cascade layers `omnia, theme, base, components, utilities`: Omnia's reset and tokens lowest; the Omnia classes (for example what the codemod wrote) and the Tailwind-style names together in `utilities`, sorted the Tailwind way (variants after the base class, shorthands before longhands, the class with more properties first). So an Omnia class beats every reset and your own `@layer components`, and wins or loses against a Tailwind-style class in the same `class` exactly as before the codemod.
|
|
55
55
|
|
|
56
56
|
Class coverage: layout, flex/grid, spacing, sizing, typography, colors (text, bg, border, ring, outline, fill, stroke, divide, accent, caret, decoration, gradients `from-`/`via-`/`to-`), borders, radius, shadow, ring, opacity, filters and backdrop filters, transitions, animations, transforms, display, position, z-index, overflow, cursor, scroll snap, `tailwindcss-animate` classes, arbitrary values (`w-[37px]`, `bg-(--brand)`, `[mask-type:luminance]`), opacity modifiers (`bg-red-500/50`), negatives (`-mt-2`), `!important`, and variants `hover: focus: active: disabled: first: odd: before: after: placeholder: file: marker: selection:`, `sm: md: lg: xl: 2xl: max-md: min-[…]:`, `@md:` container queries, `dark:`, `group-*`, `peer-*`, `data-[…]:`, `aria-*:`, `has-[…]:`, `not-*`, `in-*`, `supports-[…]:`, `*:`, arbitrary variants `[&>svg]:`, `motion-safe:`, `print:`, `rtl:`.
|
|
57
57
|
|
|
@@ -82,7 +82,7 @@ omniacss-migrate css "md:hover:bg-red-500/50" print the CSS for one class
|
|
|
82
82
|
|
|
83
83
|
## Codemod (optional)
|
|
84
84
|
|
|
85
|
-
`codemod` rewrites `class`/`className`/`cn()`/`cva()` strings to **native** OmniaCSS names when an Omnia class has exactly the same declarations (for example `items-center` becomes `align-items-center`, `p-4` becomes `p-1`). Everything else is left alone and listed. After `--write` the file `omniacss-migrate.manifest.json` tells the compat layer which names are now native, so `p-1` (Omnia, `1rem`) is never confused with the old `p-1` (`0.25rem`). Run it over the whole project, not a single folder.
|
|
85
|
+
`codemod` rewrites `class`/`className`/`cn()`/`cva()` strings to **native** OmniaCSS names when an Omnia class has exactly the same declarations (for example `items-center` becomes `align-items-center`, `p-4` becomes `p-1`). Everything else is left alone and listed. After `--write` the file `omniacss-migrate.manifest.json` tells the compat layer which names are now native, so `p-1` (Omnia, `1rem`) is never confused with the old `p-1` (`0.25rem`). Run it over the whole project, not a single folder. Tests and scripts (`tests/`, `scripts/`, `*.test.*`, `*.spec.*` …) are skipped and listed, because they often assert class names as data; add `--include-tests` to rewrite them too.
|
|
86
86
|
|
|
87
87
|
## From Bootstrap
|
|
88
88
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@birdycom/omniacss-migrate",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Switch a Laravel / Vue / React project from Tailwind CSS to OmniaCSS and keep your class names (PostCSS + Vite plugin, scan, setup, codemod), or convert a Bootstrap 3/4/5 site to OmniaCSS + OmniaUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:colors": "node scripts/build-color-map.mjs"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@birdycom/omniacss": "^0.12.
|
|
57
|
+
"@birdycom/omniacss": "^0.12.2",
|
|
58
58
|
"postcss": "^8.5.3"
|
|
59
59
|
},
|
|
60
60
|
"publishConfig": {
|
package/src/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ Bootstrap 3/4/5 -> OmniaCSS + OmniaUI
|
|
|
26
26
|
|
|
27
27
|
Options
|
|
28
28
|
--write apply changes (setup, codemod)
|
|
29
|
+
--include-tests (codemod) also rewrite tests/, scripts/, *.test.* and *.spec.* files (skipped by default)
|
|
29
30
|
--top <n> rows in the unsupported list (scan, codemod; default 40)
|
|
30
31
|
--json machine readable output (scan)
|
|
31
32
|
--breakpoints <omnia|tailwind> sm/md/lg values (default omnia)
|
|
@@ -44,7 +45,7 @@ function parse(args) {
|
|
|
44
45
|
options: {
|
|
45
46
|
write: { type: "boolean" }, top: { type: "string" }, json: { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
46
47
|
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
|
+
content: { type: "string", short: "c", multiple: true }, output: { type: "string", short: "o" }, backup: { type: "boolean" }, "include-tests": { type: "boolean" },
|
|
48
49
|
bs: { type: "string" }, compat: { type: "string" }, "omnia-href": { type: "string" }, "omniaui-href": { type: "string" }, "compat-href": { type: "string" },
|
|
49
50
|
},
|
|
50
51
|
});
|
|
@@ -102,7 +103,7 @@ export async function run(args = process.argv.slice(2)) {
|
|
|
102
103
|
|
|
103
104
|
if (command === "codemod") {
|
|
104
105
|
const { ctx } = await loadProject(dir, { ...ctxOptions(), breakpoints: values.breakpoints || "omnia" });
|
|
105
|
-
const report = await runCodemod(dir, { ctx, write: !!values.write, cwd: dir });
|
|
106
|
+
const report = await runCodemod(dir, { ctx, write: !!values.write, cwd: dir, includeTests: !!values["include-tests"] });
|
|
106
107
|
console.log(formatCodemod(report, { top }));
|
|
107
108
|
return;
|
|
108
109
|
}
|
package/src/codemod.mjs
CHANGED
|
@@ -11,6 +11,11 @@ import { loadOmniaIndex, nativeFor } from "./native.mjs";
|
|
|
11
11
|
|
|
12
12
|
export const MANIFEST = "omniacss-migrate.manifest.json";
|
|
13
13
|
|
|
14
|
+
// Tests and scripts often assert or build class names as data: rewriting them breaks the tests. Skipped unless asked.
|
|
15
|
+
const TEST_DIRS = /[\\/](tests?|__tests__|specs?|e2e|cypress|playwright|scripts)[\\/]/i;
|
|
16
|
+
const TEST_FILES = /\.(test|spec|cy)\.[cm]?[jt]sx?$|Test\.php$/;
|
|
17
|
+
const isTestOrScript = (file, dir) => TEST_DIRS.test(path.sep + path.relative(dir, file)) || TEST_FILES.test(file);
|
|
18
|
+
|
|
14
19
|
/**
|
|
15
20
|
* @param {string} dir
|
|
16
21
|
* @param {{ ctx: object, write?: boolean, extensions?: string[], cwd?: string }} o
|
|
@@ -21,8 +26,10 @@ export async function runCodemod(dir, o) {
|
|
|
21
26
|
const kind = makeKind(ctx);
|
|
22
27
|
const memo = new Map();
|
|
23
28
|
const map = (t) => { if (!memo.has(t)) memo.set(t, nativeFor(t, ctx, index)); return memo.get(t); };
|
|
24
|
-
const
|
|
25
|
-
const
|
|
29
|
+
const all = await walk(dir, { extensions: o.extensions || DEFAULT_EXTENSIONS });
|
|
30
|
+
const skipped = o.includeTests ? [] : all.filter((f) => isTestOrScript(f, dir));
|
|
31
|
+
const files = o.includeTests ? all : all.filter((f) => !isTestOrScript(f, dir));
|
|
32
|
+
const report = { dir, write, files: [], skipped, total: 0, rewritten: 0, unchanged: 0, leftovers: new Map(), collisions: new Map(), mapping: new Map() };
|
|
26
33
|
for (const file of files) {
|
|
27
34
|
let text;
|
|
28
35
|
try { text = await fs.readFile(file, "utf8"); } catch { continue; }
|
|
@@ -71,6 +78,11 @@ export function formatCodemod(r, { top = 25 } = {}) {
|
|
|
71
78
|
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
79
|
for (const f of r.files.slice(0, 15)) lines.push(` ${f.file} (${f.changes}) e.g. ${f.sample.slice(0, 3).join(", ")}`);
|
|
73
80
|
if (r.files.length > 15) lines.push(` ... and ${r.files.length - 15} more files`);
|
|
81
|
+
if (r.skipped?.length) {
|
|
82
|
+
lines.push("", `Skipped ${r.skipped.length} test and script files (tests/, scripts/, *.test.*, *.spec.* …); add --include-tests to rewrite them too:`);
|
|
83
|
+
for (const f of r.skipped.slice(0, 8)) lines.push(` ${path.relative(r.dir, f)}`);
|
|
84
|
+
if (r.skipped.length > 8) lines.push(` ... and ${r.skipped.length - 8} more`);
|
|
85
|
+
}
|
|
74
86
|
const left = [...r.leftovers.entries()].sort((a, b) => b[1] - a[1]);
|
|
75
87
|
lines.push("", `Left as they are (no 1:1 native class): ${left.reduce((n, [, c]) => n + c, 0)} occurrences, ${left.length} unique`);
|
|
76
88
|
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)" : ""}`);
|
package/src/core.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import fs from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import postcss from "postcss";
|
|
9
10
|
|
|
10
11
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
let enginePromise;
|
|
@@ -42,9 +43,16 @@ export function omniaClassNames(full) {
|
|
|
42
43
|
* Omnia core CSS for a project: tokens (every var(--color-*) the compat output uses) plus the Omnia-native
|
|
43
44
|
* classes found in the content. Classes the compat layer resolves are excluded, so a Tailwind-style `p-4`
|
|
44
45
|
* never collides with Omnia's own `p-4`.
|
|
45
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* The result comes in two parts, so each can sit in its own cascade layer:
|
|
48
|
+
* - `base`: what styles elements, not classes (Omnia's reset, tokens, @keyframes, @property, @font-face);
|
|
49
|
+
* - `classes`: the rules of the Omnia classes. These are utilities: after the codemod they replace Tailwind-style
|
|
50
|
+
* classes, so they sort in with those (see nativeRules in postcss.mjs) and beat every reset, as those classes did.
|
|
51
|
+
* `used` is the set of Omnia class names that were kept.
|
|
52
|
+
* @param {{ candidates: Set<string>, compatCss: string, exclude: Set<string>, cwd?: string, mode?: true|"tokens" }} o
|
|
53
|
+
* @returns {Promise<{ base: string, classes: string, used: Set<string> }>}
|
|
46
54
|
*/
|
|
47
|
-
export async function buildCoreCss({ candidates, compatCss, exclude, cwd,
|
|
55
|
+
export async function buildCoreCss({ candidates, compatCss, exclude, cwd, mode = true }) {
|
|
48
56
|
const engine = await loadEngine(cwd);
|
|
49
57
|
const full = mode === true ? await engine.loadFullCss() : await fs.readFile(path.join(engine.DIST, "oc-colors.css"), "utf8");
|
|
50
58
|
const set = new Set();
|
|
@@ -52,12 +60,41 @@ export async function buildCoreCss({ candidates, compatCss, exclude, cwd, layer
|
|
|
52
60
|
const known = omniaClassNames(full);
|
|
53
61
|
for (const c of candidates) if (known.has(c) && !exclude.has(c)) set.add(c);
|
|
54
62
|
}
|
|
55
|
-
const key = `${
|
|
63
|
+
const key = `${mode}|${hash(compatCss)}|${[...set].sort().join(" ")}`;
|
|
56
64
|
if (cache.has(key)) return cache.get(key);
|
|
57
|
-
const
|
|
65
|
+
const parts = { ...splitByClass(engine.generate(full, set, { layer: false, referencedBy: [compatCss] })), used: set };
|
|
58
66
|
cache.clear();
|
|
59
|
-
cache.set(key,
|
|
60
|
-
return
|
|
67
|
+
cache.set(key, parts);
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A selector that targets a class (`.p-1`, `a.link`, `:where(.dark) .x`); attribute values do not count. */
|
|
72
|
+
const HAS_CLASS = (selector) => /\.(?:[A-Za-z_-]|\\)/.test(selector.replace(/\[[^\]]*\]/g, "").replace(/"[^"]*"|'[^']*'/g, ""));
|
|
73
|
+
|
|
74
|
+
/** Splits generated CSS into the element/global part and the class part, keeping the at-rules around each rule. */
|
|
75
|
+
function splitByClass(css) {
|
|
76
|
+
const root = postcss.parse(css);
|
|
77
|
+
const base = root.clone();
|
|
78
|
+
const classes = root.clone();
|
|
79
|
+
base.walkRules((rule) => { if (!inKeyframes(rule) && HAS_CLASS(rule.selector)) rule.remove(); });
|
|
80
|
+
classes.walkRules((rule) => { if (inKeyframes(rule) || !HAS_CLASS(rule.selector)) rule.remove(); });
|
|
81
|
+
// Only element-free at-rules (@keyframes, @property, @font-face …) belong to the base part.
|
|
82
|
+
classes.walkAtRules((at) => { if (/^(keyframes|-\w+-keyframes|property|font-face|font-feature-values|counter-style|import|charset)$/.test(at.name)) at.remove(); });
|
|
83
|
+
for (const part of [base, classes]) pruneEmpty(part);
|
|
84
|
+
return { base: base.toString().trim(), classes: classes.toString().trim() };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const inKeyframes = (node) => { for (let p = node.parent; p; p = p.parent) if (p.type === "atrule" && /keyframes$/.test(p.name)) return true; return false; };
|
|
88
|
+
|
|
89
|
+
/** Removes at-rules left without rules (innermost first), keeping statement at-rules such as `@layer a, b;`. */
|
|
90
|
+
function pruneEmpty(root) {
|
|
91
|
+
let again = true;
|
|
92
|
+
while (again) {
|
|
93
|
+
again = false;
|
|
94
|
+
root.walkAtRules((at) => {
|
|
95
|
+
if (at.nodes && !at.nodes.some((n) => n.type !== "comment")) { at.remove(); again = true; }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
61
98
|
}
|
|
62
99
|
|
|
63
100
|
function hash(s) {
|
package/src/postcss.mjs
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import postcss from "postcss";
|
|
15
|
-
import { createContext, resolveToken, serialize } from "./resolve.mjs";
|
|
15
|
+
import { createContext, resolveToken, serialize, propRank } from "./resolve.mjs";
|
|
16
16
|
import { KEYFRAMES } from "./utilities.mjs";
|
|
17
17
|
import { extractTokens } from "./extract.mjs";
|
|
18
18
|
import { loadProject, parseThemeBlocks } from "./project.mjs";
|
|
19
19
|
import { preflight } from "./preflight.mjs";
|
|
20
20
|
import { buildCoreCss } from "./core.mjs";
|
|
21
21
|
import { COLOR_MAP } from "./values.mjs";
|
|
22
|
-
import { applyVariant } from "./variants.mjs";
|
|
22
|
+
import { applyVariant, splitTop } from "./variants.mjs";
|
|
23
23
|
|
|
24
24
|
// `@import "tailwindcss"`, or the same entry under our own name, so a project can drop every Tailwind mention.
|
|
25
25
|
const TW_IMPORT = /^["'](?:tailwindcss|@birdycom\/omniacss-migrate)(?:\/(theme|preflight|utilities|base|components)(?:\.css)?)?["']/;
|
|
@@ -107,7 +107,8 @@ function withTheme(ctx, extra, patch = {}) {
|
|
|
107
107
|
* @param {string} [options.cwd]
|
|
108
108
|
* @param {true|"tokens"|false} [options.core=true] true: Omnia core on demand; "tokens": only color tokens + base; false: you load Omnia yourself
|
|
109
109
|
* @param {boolean} [options.preflight=true] include the base reset
|
|
110
|
-
* @param {boolean} [options.layers=true] wrap output in cascade layers (omnia, theme, base, components, utilities)
|
|
110
|
+
* @param {boolean} [options.layers=true] wrap output in cascade layers (omnia, theme, base, components, utilities):
|
|
111
|
+
* Omnia's reset and tokens lowest; the Omnia classes and the Tailwind-style names together in utilities, sorted the Tailwind way
|
|
111
112
|
* @param {3|4} [options.tailwind] naming flavor (default: detected from package.json / CSS)
|
|
112
113
|
* @param {"class"|"media"} [options.darkMode]
|
|
113
114
|
* @param {"omnia"|"tailwind"} [options.breakpoints="omnia"]
|
|
@@ -157,7 +158,7 @@ export default function omniaMigrate(options = {}) {
|
|
|
157
158
|
const ctx = withTheme(baseCtx, themeDecls.map(([k]) => k), { ...patch, customVariants, ...(options.important ? { important: true } : {}) });
|
|
158
159
|
|
|
159
160
|
// --- content ---------------------------------------------------------------------------------------------
|
|
160
|
-
let compatCss = "", exclude = new Set(), candidates = new Set(), files = [];
|
|
161
|
+
let compatCss = "", exclude = new Set(), candidates = new Set(), files = [], resolved = [];
|
|
161
162
|
if (hasEntry) {
|
|
162
163
|
const globs = [];
|
|
163
164
|
for (const at of sources) {
|
|
@@ -176,7 +177,6 @@ export default function omniaMigrate(options = {}) {
|
|
|
176
177
|
for (const t of collected.tokens) candidates.add(t);
|
|
177
178
|
for (const t of options.safelist || []) candidates.add(t);
|
|
178
179
|
|
|
179
|
-
const resolved = [];
|
|
180
180
|
const manifest = options.manifest === false ? {} : JSON.parse(await fs.readFile(path.join(cwd, "omniacss-migrate.manifest.json"), "utf8").catch(() => "{}"));
|
|
181
181
|
const native = new Set(manifest.native || []);
|
|
182
182
|
for (const t of candidates) {
|
|
@@ -191,12 +191,16 @@ export default function omniaMigrate(options = {}) {
|
|
|
191
191
|
const layers = options.layers !== false;
|
|
192
192
|
const wrap = (name, css) => (layers && css.trim() ? `@layer ${name} {\n${css}\n}` : css);
|
|
193
193
|
const pieces = {};
|
|
194
|
+
// omnia (Omnia's reset + tokens) < theme < base (compat preflight) < components < utilities. The Omnia classes
|
|
195
|
+
// (e.g. what the codemod wrote) share `utilities` with the Tailwind-style names, sorted together the Tailwind way,
|
|
196
|
+
// so an Omnia class beats every reset and wins or loses against a compat class exactly as before the codemod.
|
|
194
197
|
pieces.order = layers ? "@layer omnia, theme, base, components, utilities;" : "";
|
|
195
198
|
const themeBody = themeDecls.map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
196
199
|
pieces.theme = wrap("theme", (themeBody ? `:root, :host {\n${themeBody}\n}\n` : "") + keyframes.join("\n"));
|
|
197
200
|
pieces.preflight = options.preflight === false ? "" : wrap("base", preflight({ borderColor: ctx.v4 ? "currentColor" : `var(${COLOR_MAP["gray-200"]})` }));
|
|
198
|
-
|
|
199
|
-
pieces.
|
|
201
|
+
const core = options.core === false ? { base: "", classes: "", used: new Set() } : await buildCoreCss({ candidates, compatCss, exclude, cwd, mode: options.core === "tokens" ? "tokens" : true });
|
|
202
|
+
pieces.core = wrap("omnia", core.base);
|
|
203
|
+
pieces.utilities = wrap("utilities", core.classes ? serialize([...resolved, ...nativeRules(core.classes, core.used, ctx)]) : compatCss);
|
|
200
204
|
const all = [pieces.order, pieces.theme, pieces.preflight, pieces.core, pieces.utilities];
|
|
201
205
|
// `from`: the stylesheet being built, so Vite resolves url() in the generated CSS against it (no "did not pass from" warning).
|
|
202
206
|
const mk = (css) => postcss.parse(css, { from: result.opts.from }).nodes;
|
|
@@ -246,11 +250,48 @@ export default function omniaMigrate(options = {}) {
|
|
|
246
250
|
root.walkDecls((decl) => { const v = replaceThemeFunctions(decl.value, ctx); if (v !== decl.value) decl.value = v; });
|
|
247
251
|
|
|
248
252
|
for (const file of files) result.messages.push({ type: "dependency", plugin: "omniacss-migrate", file: path.resolve(file), parent: result.opts.from });
|
|
253
|
+
|
|
254
|
+
// Anything still here was not understood: the page would silently miss its tokens and classes.
|
|
255
|
+
root.walkAtRules((at) => {
|
|
256
|
+
if ((at.name === "import" && TW_IMPORT.test(at.params.trim())) || at.name === "theme" || at.name === "tailwind") {
|
|
257
|
+
at.warn(result, `omniacss-migrate left "@${at.name} ${at.params}" unprocessed. If you just updated @birdycom/omniacss-migrate, restart the dev server.`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
249
260
|
},
|
|
250
261
|
};
|
|
251
262
|
}
|
|
252
263
|
omniaMigrate.postcss = true;
|
|
253
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The Omnia class rules as resolved tokens, so serialize() sorts them in with the compat classes: the variant rank
|
|
267
|
+
* comes from the class's own prefixes (`hover:`, `md:` …), the property rank from its first declaration.
|
|
268
|
+
* @param {string} css the class part of the Omnia core output
|
|
269
|
+
* @param {Set<string>} used Omnia class names in it (to find a rule's own class among the ones in its selector)
|
|
270
|
+
*/
|
|
271
|
+
function nativeRules(css, used, ctx) {
|
|
272
|
+
const out = [];
|
|
273
|
+
postcss.parse(css).walkRules((rule) => {
|
|
274
|
+
const decls = [];
|
|
275
|
+
rule.each((n) => { if (n.type === "decl") decls.push([n.prop, n.important ? `${n.value} !important` : n.value]); });
|
|
276
|
+
if (!decls.length) return;
|
|
277
|
+
const media = [];
|
|
278
|
+
for (let p = rule.parent; p && p.type === "atrule"; p = p.parent) media.unshift(`@${p.name} ${p.params}`.trim());
|
|
279
|
+
const classes = [...rule.selector.matchAll(/\.((?:\\.|[A-Za-z0-9_-])+)/g)].map((m) => m[1].replace(/\\(.)/g, "$1"));
|
|
280
|
+
const token = classes.find((c) => used.has(c)) || classes.at(-1) || rule.selector;
|
|
281
|
+
out.push({ token, rules: [{ selectors: rule.selectors, media, decls, rank: variantRank(token, ctx), urank: propRank(decls[0][0]) }], keyframes: [] });
|
|
282
|
+
});
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Cascade rank of a class's variant prefixes, as the compat layer ranks them (unknown Omnia-only prefixes go late). */
|
|
287
|
+
function variantRank(token, ctx) {
|
|
288
|
+
const parts = splitTop(token, ":");
|
|
289
|
+
parts.pop();
|
|
290
|
+
const st = { sels: ["&"], media: [], prelude: [], rank: 0 };
|
|
291
|
+
for (let i = parts.length - 1; i >= 0; i--) if (!applyVariant(parts[i], st, ctx)) st.rank += 15000;
|
|
292
|
+
return st.rank;
|
|
293
|
+
}
|
|
294
|
+
|
|
254
295
|
function expandUtility(at) {
|
|
255
296
|
if (/--value|--modifier/.test(at.toString())) { at.remove(); return; }
|
|
256
297
|
const rule = postcss.rule({ selector: `.${at.params.trim()}` });
|
package/src/resolve.mjs
CHANGED
|
@@ -123,7 +123,7 @@ function containerRule(ctx) {
|
|
|
123
123
|
|
|
124
124
|
// property rank so shorthands sort before longhands (`p-4` before `px-2` before `pl-1`)
|
|
125
125
|
const SHORTHANDS = new Set(["margin", "padding", "inset", "gap", "border-width", "border-color", "border-radius", "border", "overflow", "scroll-margin", "scroll-padding", "transition-property", "flex", "background", "font", "outline", "translate", "rotate", "scale", "size"]);
|
|
126
|
-
function propRank(prop) {
|
|
126
|
+
export function propRank(prop) {
|
|
127
127
|
if (SHORTHANDS.has(prop)) return 0;
|
|
128
128
|
if (/-(top|right|bottom|left|start|end)\b|^(top|right|bottom|left)$|-(x|y)$/.test(prop)) return 2;
|
|
129
129
|
return 1;
|
|
@@ -178,7 +178,9 @@ export function serialize(resolved) {
|
|
|
178
178
|
r.keyframes.forEach((k) => frames.add(k));
|
|
179
179
|
for (const rule of r.rules) rules.push({ ...rule, token: r.token });
|
|
180
180
|
}
|
|
181
|
-
|
|
181
|
+
// Like Tailwind: variants after the base class, shorthands before longhands, then the class that sets more
|
|
182
|
+
// properties first (so `leading-*` overrides the line-height of `text-sm`), then by name.
|
|
183
|
+
rules.sort((a, b) => a.rank - b.rank || a.urank - b.urank || propCount(b) - propCount(a) || (a.token < b.token ? -1 : a.token > b.token ? 1 : 0));
|
|
182
184
|
const renderRule = (r, ind = "") => `${r.selectors.map((x) => ind + x).join(",\n")} {\n${fmtDecls(r.decls, ind + " ")}\n${ind}}`;
|
|
183
185
|
const out = [];
|
|
184
186
|
for (const k of frames) if (KEYFRAMES[k]) out.push(KEYFRAMES[k]);
|
|
@@ -203,6 +205,9 @@ export function serialize(resolved) {
|
|
|
203
205
|
return out.join("\n");
|
|
204
206
|
}
|
|
205
207
|
|
|
208
|
+
/** Number of real (not custom) properties a rule sets. */
|
|
209
|
+
const propCount = (rule) => new Set(rule.decls.map(([k]) => k).filter((k) => !k.startsWith("--"))).size;
|
|
210
|
+
|
|
206
211
|
/** Resolve many tokens and report what could not be resolved. */
|
|
207
212
|
export function compileTokens(tokens, ctx) {
|
|
208
213
|
const resolved = [], unsupported = [];
|
package/src/vite.mjs
CHANGED
|
@@ -49,9 +49,24 @@ async function loadExistingPlugins(root) {
|
|
|
49
49
|
return out;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// The version this process loaded. A running dev server keeps it after `npm install` brings a newer one.
|
|
53
|
+
const OWN_MANIFEST = new URL("../package.json", import.meta.url);
|
|
54
|
+
const LOADED_VERSION = JSON.parse(fs.readFileSync(OWN_MANIFEST, "utf8")).version;
|
|
55
|
+
|
|
56
|
+
/** The version now on disk when it differs from the loaded one (read again only when package.json changed). */
|
|
57
|
+
let seen = { mtime: 0, version: LOADED_VERSION };
|
|
58
|
+
function updatedVersion() {
|
|
59
|
+
try {
|
|
60
|
+
const mtime = fs.statSync(OWN_MANIFEST).mtimeMs;
|
|
61
|
+
if (mtime !== seen.mtime) seen = { mtime, version: JSON.parse(fs.readFileSync(OWN_MANIFEST, "utf8")).version };
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
return seen.version !== LOADED_VERSION ? seen.version : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
/** @param {Parameters<typeof omniaMigratePostcss>[0]} [options] */
|
|
53
67
|
export default function omniaMigrate(options = {}) {
|
|
54
68
|
let root = process.cwd();
|
|
69
|
+
let warnedStale = false;
|
|
55
70
|
return {
|
|
56
71
|
name: "omniacss-migrate",
|
|
57
72
|
enforce: "pre",
|
|
@@ -61,8 +76,15 @@ export default function omniaMigrate(options = {}) {
|
|
|
61
76
|
const file = id.split("?")[0];
|
|
62
77
|
if (!/\.(css|pcss|postcss)$/.test(file)) return null;
|
|
63
78
|
if (!/@import\s+["'](?:tailwindcss|@birdycom\/omniacss-migrate)|@tailwind\s+\w+|@apply\s|@theme\b/.test(code)) return null;
|
|
79
|
+
// Vite does not reload a plugin whose package changed underneath it; the old one may not understand the CSS.
|
|
80
|
+
const newer = !warnedStale && updatedVersion();
|
|
81
|
+
if (newer) {
|
|
82
|
+
warnedStale = true;
|
|
83
|
+
this.warn(`@birdycom/omniacss-migrate was updated from ${LOADED_VERSION} to ${newer} while this dev server runs. Restart the dev server: it still uses ${LOADED_VERSION}.`);
|
|
84
|
+
}
|
|
64
85
|
const result = await postcss([omniaMigratePostcss({ cwd: root, ...options })]).process(code, { from: file, map: false });
|
|
65
86
|
for (const m of result.messages) if (m.type === "dependency" && m.file) this.addWatchFile(m.file);
|
|
87
|
+
for (const w of result.warnings()) this.warn(w.text);
|
|
66
88
|
return { code: result.css, map: null };
|
|
67
89
|
},
|
|
68
90
|
configResolved(config) {
|