@birdycom/omniacss-migrate 0.12.0 → 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 +6 -5
- package/package.json +6 -3
- package/src/cli.mjs +3 -2
- package/src/codemod.mjs +14 -2
- package/src/core.mjs +43 -6
- package/src/entry.css +8 -0
- package/src/index.mjs +1 -0
- package/src/postcss.mjs +56 -12
- package/src/project.mjs +1 -1
- package/src/resolve.mjs +7 -2
- package/src/setup.mjs +21 -10
- package/src/utilities.mjs +2 -1
- package/src/vite.mjs +23 -1
package/README.md
CHANGED
|
@@ -31,9 +31,10 @@ export default defineConfig({ plugins: [laravel({ input: ["resources/css/app.css
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```css
|
|
34
|
-
/* resources/css/app.css:
|
|
34
|
+
/* resources/css/app.css: the entry, under our own name (setup --write renames @import "tailwindcss" to it): */
|
|
35
|
+
@import "@birdycom/omniacss-migrate";
|
|
36
|
+
/* the Tailwind forms keep working too: */
|
|
35
37
|
@import "tailwindcss";
|
|
36
|
-
/* or */
|
|
37
38
|
@tailwind base; @tailwind components; @tailwind utilities;
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -47,10 +48,10 @@ export default { plugins: [omniaMigrate({ content: ["resources/views/**/*.blade.
|
|
|
47
48
|
|
|
48
49
|
## What the plugin does
|
|
49
50
|
|
|
50
|
-
- Replaces `@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.
|
|
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.
|
|
51
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`.
|
|
52
53
|
- Reads `tailwind.config.{js,cjs,mjs}` (`darkMode`, `theme.colors`/`extend.colors`, `fontFamily`, `borderRadius`, `spacing`, `boxShadow`, `screens`, `container`, `prefix`, `important`) and your `@theme` blocks.
|
|
53
|
-
- 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.
|
|
54
55
|
|
|
55
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:`.
|
|
56
57
|
|
|
@@ -81,7 +82,7 @@ omniacss-migrate css "md:hover:bg-red-500/50" print the CSS for one class
|
|
|
81
82
|
|
|
82
83
|
## Codemod (optional)
|
|
83
84
|
|
|
84
|
-
`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.
|
|
85
86
|
|
|
86
87
|
## From Bootstrap
|
|
87
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",
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
"omniacss-migrate": "./bin/omniacss-migrate.mjs"
|
|
33
33
|
},
|
|
34
34
|
"exports": {
|
|
35
|
-
".":
|
|
35
|
+
".": {
|
|
36
|
+
"style": "./src/entry.css",
|
|
37
|
+
"default": "./src/index.mjs"
|
|
38
|
+
},
|
|
36
39
|
"./postcss": "./src/postcss.mjs",
|
|
37
40
|
"./vite": "./src/vite.mjs",
|
|
38
41
|
"./cli": "./src/cli.mjs",
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
"build:colors": "node scripts/build-color-map.mjs"
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|
|
54
|
-
"@birdycom/omniacss": "^0.12.
|
|
57
|
+
"@birdycom/omniacss": "^0.12.2",
|
|
55
58
|
"postcss": "^8.5.3"
|
|
56
59
|
},
|
|
57
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/entry.css
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* File: packages/migrate/src/entry.css
|
|
2
|
+
What `@import "@birdycom/omniacss-migrate";` resolves to when a bundler inlines the import itself (Vite with the
|
|
3
|
+
PostCSS plugin in postcss.config.js). The plugin replaces these directives with OmniaCSS core + compat CSS.
|
|
4
|
+
With the Vite plugin the import never gets this far: it is compiled before Vite looks at it.
|
|
5
|
+
Author: Serge Cools | Framework: OmniaCSS */
|
|
6
|
+
@tailwind base;
|
|
7
|
+
@tailwind components;
|
|
8
|
+
@tailwind utilities;
|
package/src/index.mjs
CHANGED
|
@@ -8,5 +8,6 @@ export { runCodemod, formatCodemod } from "./codemod.mjs";
|
|
|
8
8
|
export { planSetup, applySetup, formatPlan } from "./setup.mjs";
|
|
9
9
|
export { loadProject, themeFromConfig } from "./project.mjs";
|
|
10
10
|
export { COLOR_MAP } from "./values.mjs";
|
|
11
|
+
export { loadOmniaIndex, nativeFor } from "./native.mjs";
|
|
11
12
|
export { default as postcss } from "./postcss.mjs";
|
|
12
13
|
export { default as vite } from "./vite.mjs";
|
package/src/postcss.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// File: packages/migrate/src/postcss.mjs
|
|
2
2
|
// PostCSS plugin "keep your classes": replaces the framework directives in your CSS
|
|
3
|
-
// @import "tailwindcss"; / @tailwind base|components|utilities;
|
|
3
|
+
// @import "tailwindcss"; / @import "@birdycom/omniacss-migrate"; / @tailwind base|components|utilities;
|
|
4
4
|
// with OmniaCSS core + compat CSS for the classes found in your content, and expands @apply, theme() and @theme.
|
|
5
5
|
//
|
|
6
6
|
// // postcss.config.js
|
|
@@ -12,16 +12,17 @@
|
|
|
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
|
+
const TW_IMPORT = /^["'](?:tailwindcss|@birdycom\/omniacss-migrate)(?:\/(theme|preflight|utilities|base|components)(?:\.css)?)?["']/;
|
|
25
26
|
const fileTokenCache = new Map();
|
|
26
27
|
|
|
27
28
|
/** Default content globs when none are given. */
|
|
@@ -106,7 +107,8 @@ function withTheme(ctx, extra, patch = {}) {
|
|
|
106
107
|
* @param {string} [options.cwd]
|
|
107
108
|
* @param {true|"tokens"|false} [options.core=true] true: Omnia core on demand; "tokens": only color tokens + base; false: you load Omnia yourself
|
|
108
109
|
* @param {boolean} [options.preflight=true] include the base reset
|
|
109
|
-
* @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
|
|
110
112
|
* @param {3|4} [options.tailwind] naming flavor (default: detected from package.json / CSS)
|
|
111
113
|
* @param {"class"|"media"} [options.darkMode]
|
|
112
114
|
* @param {"omnia"|"tailwind"} [options.breakpoints="omnia"]
|
|
@@ -156,7 +158,7 @@ export default function omniaMigrate(options = {}) {
|
|
|
156
158
|
const ctx = withTheme(baseCtx, themeDecls.map(([k]) => k), { ...patch, customVariants, ...(options.important ? { important: true } : {}) });
|
|
157
159
|
|
|
158
160
|
// --- content ---------------------------------------------------------------------------------------------
|
|
159
|
-
let compatCss = "", exclude = new Set(), candidates = new Set(), files = [];
|
|
161
|
+
let compatCss = "", exclude = new Set(), candidates = new Set(), files = [], resolved = [];
|
|
160
162
|
if (hasEntry) {
|
|
161
163
|
const globs = [];
|
|
162
164
|
for (const at of sources) {
|
|
@@ -175,7 +177,6 @@ export default function omniaMigrate(options = {}) {
|
|
|
175
177
|
for (const t of collected.tokens) candidates.add(t);
|
|
176
178
|
for (const t of options.safelist || []) candidates.add(t);
|
|
177
179
|
|
|
178
|
-
const resolved = [];
|
|
179
180
|
const manifest = options.manifest === false ? {} : JSON.parse(await fs.readFile(path.join(cwd, "omniacss-migrate.manifest.json"), "utf8").catch(() => "{}"));
|
|
180
181
|
const native = new Set(manifest.native || []);
|
|
181
182
|
for (const t of candidates) {
|
|
@@ -190,14 +191,19 @@ export default function omniaMigrate(options = {}) {
|
|
|
190
191
|
const layers = options.layers !== false;
|
|
191
192
|
const wrap = (name, css) => (layers && css.trim() ? `@layer ${name} {\n${css}\n}` : css);
|
|
192
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.
|
|
193
197
|
pieces.order = layers ? "@layer omnia, theme, base, components, utilities;" : "";
|
|
194
198
|
const themeBody = themeDecls.map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
195
199
|
pieces.theme = wrap("theme", (themeBody ? `:root, :host {\n${themeBody}\n}\n` : "") + keyframes.join("\n"));
|
|
196
200
|
pieces.preflight = options.preflight === false ? "" : wrap("base", preflight({ borderColor: ctx.v4 ? "currentColor" : `var(${COLOR_MAP["gray-200"]})` }));
|
|
197
|
-
|
|
198
|
-
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);
|
|
199
204
|
const all = [pieces.order, pieces.theme, pieces.preflight, pieces.core, pieces.utilities];
|
|
200
|
-
|
|
205
|
+
// `from`: the stylesheet being built, so Vite resolves url() in the generated CSS against it (no "did not pass from" warning).
|
|
206
|
+
const mk = (css) => postcss.parse(css, { from: result.opts.from }).nodes;
|
|
201
207
|
const place = (at, css) => { const nodes = css ? mk(css) : []; if (nodes.length) at.replaceWith(nodes); else at.remove(); };
|
|
202
208
|
|
|
203
209
|
for (const at of twImports) {
|
|
@@ -244,11 +250,48 @@ export default function omniaMigrate(options = {}) {
|
|
|
244
250
|
root.walkDecls((decl) => { const v = replaceThemeFunctions(decl.value, ctx); if (v !== decl.value) decl.value = v; });
|
|
245
251
|
|
|
246
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
|
+
});
|
|
247
260
|
},
|
|
248
261
|
};
|
|
249
262
|
}
|
|
250
263
|
omniaMigrate.postcss = true;
|
|
251
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
|
+
|
|
252
295
|
function expandUtility(at) {
|
|
253
296
|
if (/--value|--modifier/.test(at.toString())) { at.remove(); return; }
|
|
254
297
|
const rule = postcss.rule({ selector: `.${at.params.trim()}` });
|
|
@@ -293,7 +336,8 @@ function expandApply(at, ctx, classIndex, usedKeyframes, result) {
|
|
|
293
336
|
else extras.push(rule);
|
|
294
337
|
}
|
|
295
338
|
}
|
|
296
|
-
|
|
339
|
+
// `source` of the @apply: Vite resolves url() in these declarations against the file they are written in.
|
|
340
|
+
const nodes = inline.map(([prop, value, imp]) => postcss.decl({ prop, value, important: !!imp, source: at.source }));
|
|
297
341
|
if (nodes.length) at.replaceWith(nodes); else at.remove();
|
|
298
342
|
if (!extras.length) return;
|
|
299
343
|
const flat = parent.parent && parent.parent.type !== "rule";
|
|
@@ -302,7 +346,7 @@ function expandApply(at, ctx, classIndex, usedKeyframes, result) {
|
|
|
302
346
|
const sels = flat ? x.selectors.flatMap((s) => parent.selectors.map((ps) => s.replace(/&/g, ps))) : x.selectors;
|
|
303
347
|
let css = `${sels.join(",\n")} {\n${x.decls.map(([p, v]) => ` ${p}: ${v};`).join("\n")}\n}`;
|
|
304
348
|
for (let i = x.media.length - 1; i >= 0; i--) css = `${x.media[i]} {\n${css}\n}`;
|
|
305
|
-
const parsed = postcss.parse(css, { from:
|
|
349
|
+
const parsed = postcss.parse(css, { from: at.source?.input.file ?? result.opts.from }).nodes;
|
|
306
350
|
if (flat) { for (const n of parsed) { anchor.after(n); anchor = n; } } else parent.append(parsed);
|
|
307
351
|
}
|
|
308
352
|
}
|
package/src/project.mjs
CHANGED
|
@@ -146,7 +146,7 @@ export async function loadProject(dir, overrides = {}) {
|
|
|
146
146
|
for (const f of info.cssFiles) {
|
|
147
147
|
const css = await fs.readFile(f, "utf8").catch(() => "");
|
|
148
148
|
for (const [k, v] of parseThemeBlocks(css)) info.themeVars.set(k, v);
|
|
149
|
-
if (/@import\s+["']tailwindcss/.test(css) || /@theme\b/.test(css)) info.tailwindVersion ||= 4;
|
|
149
|
+
if (/@import\s+["'](?:tailwindcss|@birdycom\/omniacss-migrate)/.test(css) || /@theme\b/.test(css)) info.tailwindVersion ||= 4;
|
|
150
150
|
if (/@tailwind\s+(base|utilities|components)/.test(css)) info.tailwindVersion ||= 3;
|
|
151
151
|
const cv = css.match(/@custom-variant\s+dark\s*\(([^;]*)\)\s*;/);
|
|
152
152
|
if (cv) {
|
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/setup.mjs
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
8
9
|
import { loadProject, loadTailwindConfig, themeFromConfig, findFiles } from "./project.mjs";
|
|
9
10
|
import { createContext } from "./resolve.mjs";
|
|
10
11
|
|
|
11
12
|
const TW_PKGS = ["tailwindcss", "@tailwindcss/vite", "@tailwindcss/postcss"];
|
|
12
13
|
const TW_NAME = "(?:@tailwindcss\\/vite|@tailwindcss\\/postcss|tailwindcss)";
|
|
13
|
-
|
|
14
|
+
// The range written into the project: this package's own version, so it never goes stale.
|
|
15
|
+
const VERSION = `^${createRequire(import.meta.url)("../package.json").version}`;
|
|
14
16
|
|
|
15
17
|
const read = (f) => fs.readFile(f, "utf8").catch(() => null);
|
|
16
18
|
|
|
@@ -147,10 +149,17 @@ export async function planSetup(dir, { keepBrandColors = false } = {}) {
|
|
|
147
149
|
let entryDone = false;
|
|
148
150
|
for (const f of cssFiles) {
|
|
149
151
|
const before = await read(f);
|
|
150
|
-
if (!/@import\s+["']tailwindcss|@tailwind\s+(base|utilities)/.test(before)) continue;
|
|
152
|
+
if (!/@import\s+["'](?:tailwindcss|@birdycom\/omniacss-migrate)|@tailwind\s+(base|utilities)/.test(before)) continue;
|
|
151
153
|
detected.css.push(path.relative(dir, f));
|
|
152
|
-
|
|
154
|
+
// `@import "tailwindcss"` -> `@import "@birdycom/omniacss-migrate"` (same entry, no Tailwind name left).
|
|
155
|
+
// The v3 `@tailwind …` directives stay: they may sit mid-file, where an @import is not allowed.
|
|
156
|
+
const renamed = before.replace(/(@import\s+)(["'])tailwindcss((?:\/[\w.-]+)?)\2/g, "$1$2@birdycom/omniacss-migrate$3$2");
|
|
157
|
+
const isEntry = !entryDone;
|
|
153
158
|
entryDone = true;
|
|
159
|
+
if (!isEntry) {
|
|
160
|
+
if (renamed !== before) changes.push({ file: f, before, after: renamed, note: 'rename @import "tailwindcss" to @import "@birdycom/omniacss-migrate"' });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
154
163
|
const add = [];
|
|
155
164
|
if (loaded?.config) {
|
|
156
165
|
const t = themeFromConfig(loaded.config, createContext({}), { keepColors: keepBrandColors });
|
|
@@ -165,15 +174,17 @@ export async function planSetup(dir, { keepBrandColors = false } = {}) {
|
|
|
165
174
|
if ((dm === "class" || dm === "selector") && !/@custom-variant\s+dark/.test(before)) add.push("@custom-variant dark (&:where(.dark, .dark *));");
|
|
166
175
|
}
|
|
167
176
|
if (add.length) {
|
|
168
|
-
const re = /^(@import\s+["']
|
|
169
|
-
const m =
|
|
170
|
-
const eol =
|
|
171
|
-
const after = m ?
|
|
172
|
-
changes.push({ file: f, before, after, note:
|
|
173
|
-
} else
|
|
177
|
+
const re = /^(@import\s+["']@birdycom\/omniacss-migrate["'][^;\n]*;|@tailwind\s+utilities;)[ \t]*\r?\n/m;
|
|
178
|
+
const m = renamed.match(re);
|
|
179
|
+
const eol = renamed.includes("\r\n") ? "\r\n" : "\n";
|
|
180
|
+
const after = m ? renamed.replace(re, `${m[0]}${eol}${add.join(eol)}${eol}`) : renamed + eol + add.join(eol) + eol;
|
|
181
|
+
changes.push({ file: f, before, after, note: `carry tailwind.config theme into CSS (@theme) and dark mode variant${renamed !== before ? '; rename @import "tailwindcss"' : ""}` });
|
|
182
|
+
} else if (renamed !== before) {
|
|
183
|
+
changes.push({ file: f, before, after: renamed, note: 'rename @import "tailwindcss" to @import "@birdycom/omniacss-migrate" (the plugin handles it)' });
|
|
184
|
+
} else notes.push(`${path.relative(dir, f)}: the directives (@import "@birdycom/omniacss-migrate" / @tailwind …) are handled by the plugin; no change needed.`);
|
|
174
185
|
if (/@plugin\s+['"]@iconify/.test(before)) notes.push("`@plugin '@iconify/...'` is ignored by the plugin (icon-[...] classes are not generated).");
|
|
175
186
|
}
|
|
176
|
-
if (!detected.css.length) notes.push("No CSS file with Tailwind directives found. The plugin works on `@import \"tailwindcss\"` or `@tailwind base/components/utilities`.");
|
|
187
|
+
if (!detected.css.length) notes.push("No CSS file with Tailwind directives found. The plugin works on `@import \"@birdycom/omniacss-migrate\"`, `@import \"tailwindcss\"` or `@tailwind base/components/utilities`.");
|
|
177
188
|
commands.push("npm uninstall " + (removed.join(" ") || "tailwindcss @tailwindcss/vite"));
|
|
178
189
|
commands.push("npm install -D @birdycom/omniacss-migrate");
|
|
179
190
|
return { changes, notes, commands, detected };
|
package/src/utilities.mjs
CHANGED
|
@@ -262,6 +262,7 @@ const SIDE = { t: "top", r: "right", b: "bottom", l: "left", s: "inline-start",
|
|
|
262
262
|
function boxHandler(prop) {
|
|
263
263
|
// m, mx, my, mt … (margin / padding / scroll-margin / scroll-padding)
|
|
264
264
|
return (u, root) => {
|
|
265
|
+
// The side after the root: "mt" -> "t", "scroll-mt" -> "t", "scroll-p" -> "".
|
|
265
266
|
const key = root.slice(prefixLen(prop));
|
|
266
267
|
const v = sp(u, u.value, { keywords: { auto: "auto" }, fractions: false });
|
|
267
268
|
if (v == null) return null;
|
|
@@ -275,7 +276,7 @@ function boxHandler(prop) {
|
|
|
275
276
|
return null;
|
|
276
277
|
};
|
|
277
278
|
}
|
|
278
|
-
const prefixLen = (prop) => ({ margin: 1, padding: 1, "scroll-margin":
|
|
279
|
+
const prefixLen = (prop) => ({ margin: 1, padding: 1, "scroll-margin": 8, "scroll-padding": 8 })[prop];
|
|
279
280
|
|
|
280
281
|
function insetHandler(prop) {
|
|
281
282
|
return (u) => {
|
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",
|
|
@@ -60,9 +75,16 @@ export default function omniaMigrate(options = {}) {
|
|
|
60
75
|
async transform(code, id) {
|
|
61
76
|
const file = id.split("?")[0];
|
|
62
77
|
if (!/\.(css|pcss|postcss)$/.test(file)) return null;
|
|
63
|
-
if (!/@import\s+["']tailwindcss|@tailwind\s+\w+|@apply\s|@theme\b/.test(code)) return null;
|
|
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) {
|