@mediaradar/design-components 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -3
- package/dist/index.cjs +397 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +260 -6
- package/dist/index.d.ts +260 -6
- package/dist/index.js +384 -13
- package/dist/index.js.map +1 -1
- package/package.json +18 -6
- package/scripts/check-tailwind-coverage.mjs +51 -0
- package/scripts/tailwind-coverage.mjs +96 -0
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediaradar/design-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"scripts/tailwind-coverage.mjs",
|
|
11
|
+
"scripts/check-tailwind-coverage.mjs"
|
|
10
12
|
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"design-components-check-coverage": "./scripts/check-tailwind-coverage.mjs"
|
|
15
|
+
},
|
|
11
16
|
"exports": {
|
|
12
17
|
".": {
|
|
13
18
|
"types": "./dist/index.d.ts",
|
|
@@ -21,12 +26,13 @@
|
|
|
21
26
|
},
|
|
22
27
|
"scripts": {
|
|
23
28
|
"build": "tsup",
|
|
24
|
-
"typecheck": "tsc --noEmit"
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"check:token-compat": "node scripts/check-token-compat.mjs"
|
|
25
31
|
},
|
|
26
32
|
"peerDependencies": {
|
|
33
|
+
"@mediaradar/design-tokens": ">=2.2.0",
|
|
27
34
|
"react": "^18.0.0 || ^19.0.0",
|
|
28
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
29
|
-
"@mediaradar/design-tokens": ">=2.0.0"
|
|
35
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
30
36
|
},
|
|
31
37
|
"peerDependenciesMeta": {
|
|
32
38
|
"@mediaradar/design-tokens": {
|
|
@@ -34,13 +40,19 @@
|
|
|
34
40
|
}
|
|
35
41
|
},
|
|
36
42
|
"dependencies": {
|
|
37
|
-
"
|
|
43
|
+
"@radix-ui/react-slot": "^1.3.3",
|
|
44
|
+
"class-variance-authority": "^0.7.1",
|
|
45
|
+
"clsx": "^2.1.1",
|
|
46
|
+
"lucide-react": "^1.17.0",
|
|
47
|
+
"tailwind-merge": "^3.7.0"
|
|
38
48
|
},
|
|
39
49
|
"devDependencies": {
|
|
50
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
40
51
|
"@types/react": "^19.2.6",
|
|
41
52
|
"@types/react-dom": "^19.2.6",
|
|
42
53
|
"react": "^19.2.6",
|
|
43
54
|
"react-dom": "^19.2.6",
|
|
55
|
+
"tailwindcss": "^4.3.3",
|
|
44
56
|
"tsup": "^8.3.5",
|
|
45
57
|
"typescript": "^5.7.2"
|
|
46
58
|
},
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run from a consuming app's root, AFTER its production build:
|
|
3
|
+
//
|
|
4
|
+
// npx design-components-check-coverage --css-dir dist (Vite)
|
|
5
|
+
// npx design-components-check-coverage --css-dir .next/static (Next.js)
|
|
6
|
+
//
|
|
7
|
+
// Fails if any Tailwind class used inside the app's installed copy of
|
|
8
|
+
// @mediaradar/design-components is missing from the app's compiled CSS.
|
|
9
|
+
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { parseArgs } from "util";
|
|
13
|
+
import { createRequire } from "module";
|
|
14
|
+
import { extractCandidateClasses, findMissingClasses, readCssFiles, MISSING_CLASS_CAUSES } from "./tailwind-coverage.mjs";
|
|
15
|
+
|
|
16
|
+
const PACKAGE = "@mediaradar/design-components";
|
|
17
|
+
|
|
18
|
+
const { values: args } = parseArgs({
|
|
19
|
+
options: { "css-dir": { type: "string", default: "dist" } },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Resolve from the app, not from this file, so the check reads the copy the
|
|
23
|
+
// app actually installed and bundles.
|
|
24
|
+
const appRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
25
|
+
let version;
|
|
26
|
+
let bundleText;
|
|
27
|
+
try {
|
|
28
|
+
version = appRequire(`${PACKAGE}/package.json`).version;
|
|
29
|
+
bundleText = fs.readFileSync(appRequire.resolve(PACKAGE), "utf8");
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`::error::Could not read ${PACKAGE} from this app's node_modules (${err.message}). Run npm ci first.`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const candidates = extractCandidateClasses(bundleText);
|
|
36
|
+
const cssDir = path.resolve(args["css-dir"]);
|
|
37
|
+
const { files, text } = readCssFiles(cssDir);
|
|
38
|
+
if (files.length === 0) {
|
|
39
|
+
console.error(`::error::No .css files under ${cssDir}. Run the app's production build first -- this check reads compiled CSS, not source.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const missing = findMissingClasses(candidates, text);
|
|
44
|
+
console.log(`Checked ${candidates.size} class(es) from ${PACKAGE}@${version} against ${files.length} compiled CSS file(s) under ${args["css-dir"]}.`);
|
|
45
|
+
if (missing.length) {
|
|
46
|
+
console.error(`\n::error::${missing.length} class(es) used by ${PACKAGE} are missing from this app's compiled CSS:`);
|
|
47
|
+
for (const c of missing) console.error(` - ${c}`);
|
|
48
|
+
console.error(`\n${MISSING_CLASS_CAUSES}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.log("All classes found in compiled CSS.");
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Shared core of the Tailwind coverage check. Used by the consumer-facing
|
|
2
|
+
// CLI (check-tailwind-coverage.mjs, shipped as this package's bin) and by
|
|
3
|
+
// this package's own CI (check-token-compat.mjs). Heuristic: it extracts
|
|
4
|
+
// every quoted string literal from the compiled bundle and keeps tokens
|
|
5
|
+
// shaped like a Tailwind utility -- simpler and more robust against a
|
|
6
|
+
// minified bundle than isolating specific call sites.
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
const STRING_LITERAL_RE = /(["'])((?:\\.|(?!\1)[^\\\n])*)\1/g;
|
|
12
|
+
|
|
13
|
+
// Dash-containing utilities only, optionally with variant prefixes and an
|
|
14
|
+
// arbitrary [value] or (--css-var). Bare keywords (flex, hidden) are left
|
|
15
|
+
// out: every consumer uses those in its own source, so they're not where a
|
|
16
|
+
// missing-class bug actually bites, and including them only adds noise.
|
|
17
|
+
const CANDIDATE_RE = /^(?:[a-z][a-zA-Z0-9]*:)*[a-z][a-zA-Z0-9]*-(?:\[[^\]\s"'<>]+\]|\([^)\s"'<>]+\)|[a-zA-Z0-9][a-zA-Z0-9.%/-]*)$/;
|
|
18
|
+
|
|
19
|
+
// Class-shaped strings in the bundle confirmed by real runs not to be
|
|
20
|
+
// classes: HTML attribute names, popper placement values, a package name,
|
|
21
|
+
// a locale tag, an SVG attribute value, an event name.
|
|
22
|
+
const KNOWN_NON_CLASS_RE = /^(aria|data)-/;
|
|
23
|
+
const KNOWN_NON_CLASS_EXACT = new Set([
|
|
24
|
+
"lucide-react",
|
|
25
|
+
"class-variance-authority",
|
|
26
|
+
"tailwind-merge",
|
|
27
|
+
"account-list-name",
|
|
28
|
+
"react-dom",
|
|
29
|
+
"en-US",
|
|
30
|
+
"non-scaling-stroke",
|
|
31
|
+
"report-ready",
|
|
32
|
+
"top-start",
|
|
33
|
+
"top-end",
|
|
34
|
+
"bottom-start",
|
|
35
|
+
"bottom-end",
|
|
36
|
+
"left-start",
|
|
37
|
+
"left-end",
|
|
38
|
+
"right-start",
|
|
39
|
+
"right-end",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export function extractCandidateClasses(bundleText) {
|
|
43
|
+
const candidates = new Set();
|
|
44
|
+
for (const match of bundleText.matchAll(STRING_LITERAL_RE)) {
|
|
45
|
+
const literal = match[2];
|
|
46
|
+
if (!/[\s-]/.test(literal)) continue;
|
|
47
|
+
for (const token of literal.split(/\s+/)) {
|
|
48
|
+
if (!CANDIDATE_RE.test(token)) continue;
|
|
49
|
+
if (KNOWN_NON_CLASS_RE.test(token) || KNOWN_NON_CLASS_EXACT.has(token)) continue;
|
|
50
|
+
candidates.add(token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return candidates;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Two separate escaping passes: Tailwind writes `border-x/40` as the
|
|
57
|
+
// selector text `.border-x\/40`, and a regex searching for that literal
|
|
58
|
+
// backslash must escape it again. Doing only one pass silently misses
|
|
59
|
+
// every class containing a special character.
|
|
60
|
+
function cssSelectorEscape(className) {
|
|
61
|
+
return className.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
|
|
62
|
+
}
|
|
63
|
+
function escapeRegex(text) {
|
|
64
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function findMissingClasses(candidates, cssText) {
|
|
68
|
+
const missing = [];
|
|
69
|
+
for (const className of [...candidates].sort()) {
|
|
70
|
+
const re = new RegExp(`\\.${escapeRegex(cssSelectorEscape(className))}(?![a-zA-Z0-9_-])`);
|
|
71
|
+
if (!re.test(cssText)) missing.push(className);
|
|
72
|
+
}
|
|
73
|
+
return missing;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function readCssFiles(dir) {
|
|
77
|
+
const files = [];
|
|
78
|
+
const walk = (d) => {
|
|
79
|
+
if (!fs.existsSync(d)) return;
|
|
80
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
81
|
+
const full = path.join(d, entry.name);
|
|
82
|
+
if (entry.isDirectory()) walk(full);
|
|
83
|
+
else if (entry.name.endsWith(".css")) files.push(full);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
walk(dir);
|
|
87
|
+
return { files, text: files.map((f) => fs.readFileSync(f, "utf8")).join("\n") };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const MISSING_CLASS_CAUSES = `Each missing class compiles to no CSS at all, with no build error. Usual causes:
|
|
91
|
+
1. The app's Tailwind setup doesn't scan this package's compiled output. Tailwind v4 skips node_modules:
|
|
92
|
+
add @source '<path-to>/node_modules/@mediaradar/design-components/dist/index.js'; after the last @import
|
|
93
|
+
(Tailwind v3 / @config apps: add that path to the content array instead).
|
|
94
|
+
2. The app's @mediaradar/design-tokens is older than this package's peerDependencies range, so the token
|
|
95
|
+
behind the class doesn't exist in its theme.css yet.
|
|
96
|
+
3. The class relies on an app-local alias that isn't in @mediaradar/design-tokens -- a bug in this package.`;
|