@weatherboard/gyde-design 0.3.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/LICENSE +21 -0
- package/README.md +264 -0
- package/adoption.mjs +138 -0
- package/agentdocs.mjs +246 -0
- package/boundaries.mjs +350 -0
- package/catalogue.mjs +506 -0
- package/cli.mjs +723 -0
- package/clientboundary.mjs +399 -0
- package/compound.mjs +123 -0
- package/docdrift.mjs +439 -0
- package/emit.mjs +862 -0
- package/enforcement.mjs +100 -0
- package/index.mjs +40 -0
- package/markup.mjs +177 -0
- package/migration.mjs +148 -0
- package/normalise.mjs +416 -0
- package/package.json +59 -0
- package/props.mjs +255 -0
- package/ratchet.mjs +290 -0
- package/rules.mjs +258 -0
- package/scan.mjs +291 -0
- package/stylex.mjs +178 -0
- package/tailwind.mjs +238 -0
- package/tokens.mjs +398 -0
- package/upgrade.mjs +344 -0
- package/usage.mjs +245 -0
- package/wiring.mjs +297 -0
- package/workflow.mjs +221 -0
- package/workspace.mjs +318 -0
package/stylex.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-100 — StyleX is the mandated styling layer.
|
|
3
|
+
*
|
|
4
|
+
* Completes what G-98 began. G-98 banned Tailwind because a utility framework
|
|
5
|
+
* is an uncontrolled escape hatch out of the token layer; that was a statement
|
|
6
|
+
* about Tailwind and it left the positive question open. This answers it: there
|
|
7
|
+
* is one styling implementation, and it is StyleX.
|
|
8
|
+
*
|
|
9
|
+
* WHAT WAS ARGUED AGAINST THIS, AND WHAT REMAINS TRUE.
|
|
10
|
+
*
|
|
11
|
+
* G-58 Decision 3 rejected the mandate on two measured grounds, and honesty
|
|
12
|
+
* about them is the point of writing this here rather than only in the ADR:
|
|
13
|
+
*
|
|
14
|
+
* 1. **System A reached 98% tokenisation without StyleX** — the
|
|
15
|
+
* highest of the three consumers — with a typed dictionary compiled to CSS
|
|
16
|
+
* custom properties and no build plugin anywhere. That is still true, and
|
|
17
|
+
* this mandate costs them a migration that fixes no measured defect. It is
|
|
18
|
+
* the price of one styling layer instead of two, and it was paid
|
|
19
|
+
* deliberately.
|
|
20
|
+
*
|
|
21
|
+
* 2. **The two-file problem.** `tokens.stylex.ts` cannot import `index.ts`,
|
|
22
|
+
* because `defineVars` must resolve statically, so a consumer maintained the
|
|
23
|
+
* same values by hand in two files. This one is **answered rather than
|
|
24
|
+
* accepted**: `generateStyleX` emits the StyleX module from the same
|
|
25
|
+
* dictionary as the CSS, in the same run, so there is no second copy for
|
|
26
|
+
* anybody to keep in step. G-58's own words — "the problem not existing
|
|
27
|
+
* rather than being solved".
|
|
28
|
+
*
|
|
29
|
+
* The first cost stands. A mandate whose costs are only recorded in the
|
|
30
|
+
* document nobody reads at 2am is a mandate that will be litigated again in six
|
|
31
|
+
* months by someone who thinks it was never considered.
|
|
32
|
+
*
|
|
33
|
+
* THE WIRING IS CHECKED, NOT ASSUMED — AND IT IS THE PART THAT ACTUALLY BREAKS.
|
|
34
|
+
*
|
|
35
|
+
* StyleX is a compiler. A project that declares the dependency and imports the
|
|
36
|
+
* API but never wires the plugin gets **no styles at all**: `stylex.create`
|
|
37
|
+
* returns objects, `stylex.props` returns class names, nothing throws, the
|
|
38
|
+
* build passes, and the page renders unstyled. It is the same failure shape as
|
|
39
|
+
* G-98's dependency removal and as `wiring.mjs`'s missing stylesheet — the
|
|
40
|
+
* third time this class of defect has appeared in this codebase, which is why
|
|
41
|
+
* it is checked rather than trusted.
|
|
42
|
+
*
|
|
43
|
+
* @gyde-emits-source-for-another-repo — the fixtures beside this file are
|
|
44
|
+
* package manifests and configuration written as data for another repository.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
|
|
50
|
+
export const RULE = "styling-layer";
|
|
51
|
+
|
|
52
|
+
const STYLEX_RUNTIME = /^@stylexjs\/stylex$/;
|
|
53
|
+
const STYLEX_TOOLING = /^@stylexjs\/(babel-plugin|postcss-plugin|webpack-plugin|rollup-plugin|nextjs-plugin|esbuild-plugin|open-props)$/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Styling layers that compete with StyleX for the same job.
|
|
57
|
+
*
|
|
58
|
+
* CSS Modules and plain stylesheets are deliberately absent: they carry the
|
|
59
|
+
* generated custom properties and are how the token layer reaches the page. The
|
|
60
|
+
* ban is on a SECOND way to express a component's styles, not on CSS.
|
|
61
|
+
*/
|
|
62
|
+
const COMPETING = {
|
|
63
|
+
"styled-components": "runtime CSS-in-JS",
|
|
64
|
+
"@emotion/react": "runtime CSS-in-JS",
|
|
65
|
+
"@emotion/styled": "runtime CSS-in-JS",
|
|
66
|
+
"@vanilla-extract/css": "a second compile-time CSS-in-JS",
|
|
67
|
+
"@stitches/react": "runtime CSS-in-JS",
|
|
68
|
+
"linaria": "a second compile-time CSS-in-JS",
|
|
69
|
+
"@linaria/core": "a second compile-time CSS-in-JS",
|
|
70
|
+
"jss": "runtime CSS-in-JS",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const CONFIG_HINT = /stylex/i;
|
|
74
|
+
const CONFIG_FILES = /^(next\.config\.(js|mjs|ts)|vite\.config\.(js|mjs|ts)|babel\.config\.(js|json|cjs)|\.babelrc(\.js|\.json)?|postcss\.config\.(js|cjs|mjs))$/;
|
|
75
|
+
|
|
76
|
+
function manifestOf(root, pkgPath) {
|
|
77
|
+
const p = join(root, pkgPath === "." ? "" : pkgPath, "package.json");
|
|
78
|
+
if (!existsSync(p)) return null;
|
|
79
|
+
try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const allDeps = (json) => ({
|
|
83
|
+
...(json.dependencies || {}), ...(json.devDependencies || {}), ...(json.peerDependencies || {}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Is the styling layer StyleX, is it wired, and is anything competing with it?
|
|
88
|
+
*
|
|
89
|
+
* `renders` narrows the check to packages that actually render UI — the same
|
|
90
|
+
* set `wiring.mjs` uses. A utility package with no components does not need a
|
|
91
|
+
* styling layer, and demanding one there is how a mandate gets a reputation for
|
|
92
|
+
* being about compliance rather than about the product.
|
|
93
|
+
*/
|
|
94
|
+
export function checkStyleX(root, { packages = [], renders = null } = {}) {
|
|
95
|
+
const scope = renders ? packages.filter((p) => renders.includes(p.path)) : packages;
|
|
96
|
+
|
|
97
|
+
if (scope.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
unknown: true,
|
|
100
|
+
why: renders
|
|
101
|
+
? "no package in this workspace renders UI, so there is no styling layer to mandate"
|
|
102
|
+
: "which packages render UI is not known, so the mandate cannot be scoped",
|
|
103
|
+
missing: [], unwired: [], competing: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const missing = [];
|
|
108
|
+
const unwired = [];
|
|
109
|
+
const competing = [];
|
|
110
|
+
|
|
111
|
+
for (const pkg of scope) {
|
|
112
|
+
const json = manifestOf(root, pkg.path);
|
|
113
|
+
if (!json) continue;
|
|
114
|
+
const deps = allDeps(json);
|
|
115
|
+
|
|
116
|
+
const hasRuntime = Object.keys(deps).some((n) => STYLEX_RUNTIME.test(n));
|
|
117
|
+
const hasTooling = Object.keys(deps).some((n) => STYLEX_TOOLING.test(n));
|
|
118
|
+
|
|
119
|
+
for (const [name, why] of Object.entries(COMPETING)) {
|
|
120
|
+
if (deps[name]) competing.push({ package: pkg.path, name, version: deps[name], why });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!hasRuntime) { missing.push({ package: pkg.path }); continue; }
|
|
124
|
+
|
|
125
|
+
// A compiler that is installed and not wired produces no styles and no
|
|
126
|
+
// error. Checked here rather than trusted, because this is the third time
|
|
127
|
+
// this codebase has met the same shape (G-98, wiring.mjs, and now this).
|
|
128
|
+
if (!hasTooling && !configMentionsStyleX(root, pkg.path)) {
|
|
129
|
+
unwired.push({ package: pkg.path });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
unknown: false,
|
|
135
|
+
missing, unwired, competing,
|
|
136
|
+
packagesChecked: scope.length,
|
|
137
|
+
ok: missing.length === 0 && unwired.length === 0 && competing.length === 0,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function configMentionsStyleX(root, pkgPath) {
|
|
142
|
+
const dir = join(root, pkgPath === "." ? "" : pkgPath);
|
|
143
|
+
let entries; try { entries = readdirSync(dir); } catch { return false; }
|
|
144
|
+
for (const name of entries) {
|
|
145
|
+
if (!CONFIG_FILES.test(name)) continue;
|
|
146
|
+
try { if (CONFIG_HINT.test(readFileSync(join(dir, name), "utf8"))) return true; } catch { /* unreadable */ }
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function stylexFindings(result) {
|
|
152
|
+
if (result.unknown) return [];
|
|
153
|
+
const at = (p) => `${p === "." ? "" : p + "/"}package.json`;
|
|
154
|
+
return [
|
|
155
|
+
...result.missing.map((m) => ({ file: at(m.package), rule: RULE, line: null, raw: "@stylexjs/stylex", kind: "missing" })),
|
|
156
|
+
...result.unwired.map((u) => ({ file: at(u.package), rule: RULE, line: null, raw: "stylex-plugin", kind: "unwired" })),
|
|
157
|
+
...result.competing.map((c) => ({ file: at(c.package), rule: RULE, line: null, raw: c.name, kind: "competing" })),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function formatStyleX(result) {
|
|
162
|
+
if (result.unknown) return `styling layer could not tell — ${result.why}`;
|
|
163
|
+
if (result.ok) return `styling layer StyleX, wired, in all ${result.packagesChecked} package(s) that render UI`;
|
|
164
|
+
|
|
165
|
+
const L = [`styling layer ${result.missing.length + result.unwired.length + result.competing.length} problem(s) across ${result.packagesChecked} UI package(s)`];
|
|
166
|
+
for (const m of result.missing) L.push(` ${m.package} no @stylexjs/stylex — StyleX is the mandated styling layer (G-100)`);
|
|
167
|
+
for (const u of result.unwired) L.push(` ${u.package} StyleX installed but no plugin and no config mentioning it`);
|
|
168
|
+
for (const c of result.competing) L.push(` ${c.package} ${c.name}@${c.version} — ${c.why}, competing with StyleX`);
|
|
169
|
+
|
|
170
|
+
if (result.unwired.length) {
|
|
171
|
+
L.push("");
|
|
172
|
+
L.push(" StyleX is a compiler. Installed and unwired it produces NO styles and no");
|
|
173
|
+
L.push(" error: create returns objects, props returns class names, the build passes,");
|
|
174
|
+
L.push(" and the page renders unstyled. That is the same failure as removing a");
|
|
175
|
+
L.push(" Tailwind dependency with classes still in source.");
|
|
176
|
+
}
|
|
177
|
+
return L.join("\n");
|
|
178
|
+
}
|
package/tailwind.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-98 — Tailwind is not a permitted styling layer.
|
|
3
|
+
*
|
|
4
|
+
* Reverses G-58 Decision 3, which left the styling layer unmandated. That
|
|
5
|
+
* decision was correct on the evidence it had and this one is a product
|
|
6
|
+
* decision rather than a measurement: Gyde must own the token layer completely,
|
|
7
|
+
* and a utility framework is a standing escape hatch out of it. `p-[13px]`
|
|
8
|
+
* needs no approval, leaves no trace in a dictionary, and is one keystroke away
|
|
9
|
+
* from every call site.
|
|
10
|
+
*
|
|
11
|
+
* THE ORDERING IS THE WHOLE DESIGN, AND IT IS NOT OBVIOUS.
|
|
12
|
+
*
|
|
13
|
+
* Tailwind exists in a repository at four levels, and they are removed in the
|
|
14
|
+
* opposite order to the one people reach for:
|
|
15
|
+
*
|
|
16
|
+
* 1. the DEPENDENCY `tailwindcss` in a package.json
|
|
17
|
+
* 2. the CONFIG tailwind.config.*, or an `@import "tailwindcss"`
|
|
18
|
+
* 3. the CLASSES `p-4`, `rounded-lg`, `bg-muted` in source
|
|
19
|
+
* 4. the STYLESHEETS `@apply p-4`, `theme(spacing.4)` in CSS
|
|
20
|
+
*
|
|
21
|
+
* The fourth was missed on the first pass and is the one that makes the ban a
|
|
22
|
+
* fiction if left out: a repository can hold nothing in any manifest, no
|
|
23
|
+
* config, and not one utility class in any component, and still be written in
|
|
24
|
+
* Tailwind — `.card { @apply p-4 rounded-lg bg-muted; }`.
|
|
25
|
+
*
|
|
26
|
+
* The obvious first move is to delete the dependency, because it is one line
|
|
27
|
+
* and it makes the ban pass. It is also the single most destructive thing
|
|
28
|
+
* anybody could do here. Utility classes are inert strings in a `className`
|
|
29
|
+
* attribute: with the dependency gone nothing throws, no build fails, no type
|
|
30
|
+
* errors — every one of those classes silently resolves to no style at all and
|
|
31
|
+
* the application renders unstyled. One consumer has 4,842 of them.
|
|
32
|
+
*
|
|
33
|
+
* So this check refuses to be satisfied in the wrong order. It reports the
|
|
34
|
+
* dependency as removable ONLY when the classes are gone, and until then it
|
|
35
|
+
* says so in the finding itself. A gate that can be passed by an action that
|
|
36
|
+
* breaks production is not a gate, it is a trap — and CHARTER §5's rule applies
|
|
37
|
+
* here in an unusual direction: "the ban is satisfied" and "the styling has
|
|
38
|
+
* been removed" must never render the same.
|
|
39
|
+
*
|
|
40
|
+
* WHAT THIS DELIBERATELY DOES NOT DO.
|
|
41
|
+
*
|
|
42
|
+
* It does not migrate anything, and it does not count utility classes as a new
|
|
43
|
+
* finding class. They are already counted — `normalise.mjs` reduces them to
|
|
44
|
+
* declarations and the existing rules judge them. Adding a second count of the
|
|
45
|
+
* same code would double the debt and move the ratchet for a reason that is not
|
|
46
|
+
* a change in the code.
|
|
47
|
+
*
|
|
48
|
+
* @gyde-emits-source-for-another-repo — the fixtures beside this file are
|
|
49
|
+
* package manifests and CSS written as data for a repository that is not this one.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
53
|
+
import { join } from "node:path";
|
|
54
|
+
|
|
55
|
+
export const RULE = "tailwind-present";
|
|
56
|
+
|
|
57
|
+
/** Package names that ARE Tailwind, rather than merely mentioning it. */
|
|
58
|
+
const TAILWIND_PACKAGE = /^(tailwindcss|@tailwindcss\/.+)$/;
|
|
59
|
+
|
|
60
|
+
/** A plugin is only Tailwind's problem while Tailwind is there; it is not itself the ban. */
|
|
61
|
+
const TAILWIND_PLUGIN = /^(prettier-plugin-tailwindcss|tailwind-merge|tailwind-variants|clsx)$/;
|
|
62
|
+
|
|
63
|
+
const CONFIG_NAMES = /^tailwind\.config\.(js|cjs|mjs|ts)$/;
|
|
64
|
+
|
|
65
|
+
/** `@import "tailwindcss"` / `@tailwind base` — the v4 and v3 spellings. */
|
|
66
|
+
const CSS_ENTRY = /@import\s+["']tailwindcss["']|@tailwind\s+(base|components|utilities)\b/;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Where Tailwind is declared, configured, and used.
|
|
70
|
+
*
|
|
71
|
+
* `utilityClasses` is passed in rather than counted here. `normalise.mjs`
|
|
72
|
+
* already reduces utilities to declarations and is the one place that knows
|
|
73
|
+
* what a Tailwind class is; counting them again here would be a second
|
|
74
|
+
* implementation of the same question, free to disagree with the first.
|
|
75
|
+
*/
|
|
76
|
+
export function detectTailwind(root, { packages = [], utilityClasses = null, cssDirectives = null } = {}) {
|
|
77
|
+
const dependencies = [];
|
|
78
|
+
const plugins = [];
|
|
79
|
+
|
|
80
|
+
for (const pkg of packages) {
|
|
81
|
+
const manifest = join(root, pkg.path === "." ? "" : pkg.path, "package.json");
|
|
82
|
+
if (!existsSync(manifest)) continue;
|
|
83
|
+
let json; try { json = JSON.parse(readFileSync(manifest, "utf8")); } catch { continue; }
|
|
84
|
+
|
|
85
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
86
|
+
for (const [name, version] of Object.entries(json[field] || {})) {
|
|
87
|
+
if (TAILWIND_PACKAGE.test(name)) dependencies.push({ package: pkg.path, name, version, field });
|
|
88
|
+
else if (TAILWIND_PLUGIN.test(name)) plugins.push({ package: pkg.path, name, version, field });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const configs = [];
|
|
94
|
+
for (const pkg of packages) {
|
|
95
|
+
const dir = join(root, pkg.path === "." ? "" : pkg.path);
|
|
96
|
+
let entries; try { entries = readdirSync(dir); } catch { continue; }
|
|
97
|
+
for (const name of entries) if (CONFIG_NAMES.test(name)) configs.push(`${pkg.path === "." ? "" : pkg.path + "/"}${name}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { dependencies, plugins, configs, utilityClasses, cssDirectives, ...verdict({ dependencies, configs, utilityClasses, cssDirectives }) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Does a stylesheet pull Tailwind in? Exported so a caller can walk CSS without this module walking twice. */
|
|
104
|
+
export const importsTailwind = (css) => CSS_ENTRY.test(css);
|
|
105
|
+
|
|
106
|
+
function verdict({ dependencies, configs, utilityClasses, cssDirectives }) {
|
|
107
|
+
const present = dependencies.length > 0 || configs.length > 0;
|
|
108
|
+
if (!present) return { present: false, safeToRemove: null, why: "Tailwind is not installed" };
|
|
109
|
+
|
|
110
|
+
// The load-bearing branch. Not knowing how many classes remain is not the
|
|
111
|
+
// same as knowing there are none, and only one of those is safe to act on.
|
|
112
|
+
// A stylesheet directive breaks on removal exactly as a class does, so
|
|
113
|
+
// "no classes left" is not the same as "safe". Counting only one of the two
|
|
114
|
+
// is how the ordering guard would have been satisfied by a repository that
|
|
115
|
+
// still stops rendering the moment the dependency goes.
|
|
116
|
+
if (utilityClasses === null || cssDirectives === null) {
|
|
117
|
+
return {
|
|
118
|
+
present: true, safeToRemove: null,
|
|
119
|
+
why: "Tailwind is installed, and how much still resolves through it is unknown — " +
|
|
120
|
+
"removing it now could leave every class and directive resolving to nothing",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (utilityClasses > 0 || cssDirectives > 0) {
|
|
124
|
+
const parts = [];
|
|
125
|
+
if (utilityClasses > 0) parts.push(`${utilityClasses} utility class(es)`);
|
|
126
|
+
if (cssDirectives > 0) parts.push(`${cssDirectives} stylesheet directive(s)`);
|
|
127
|
+
return {
|
|
128
|
+
present: true, safeToRemove: false,
|
|
129
|
+
why: `Tailwind is installed and ${parts.join(" and ")} still resolve through it. ` +
|
|
130
|
+
"Remove them first: they fail silently, so nothing throws when the dependency goes",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
present: true, safeToRemove: true,
|
|
135
|
+
why: "nothing resolves through Tailwind any more, so the dependency and config can go",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The finding, shaped like every other so it enters the ledger unchanged. */
|
|
140
|
+
export function tailwindFindings(result) {
|
|
141
|
+
if (!result.present) return [];
|
|
142
|
+
return [
|
|
143
|
+
...result.dependencies.map((d) => ({
|
|
144
|
+
file: `${d.package === "." ? "" : d.package + "/"}package.json`,
|
|
145
|
+
rule: RULE, line: null, raw: d.name, kind: "dependency", detail: `${d.name}@${d.version} in ${d.field}`,
|
|
146
|
+
})),
|
|
147
|
+
...result.configs.map((c) => ({ file: c, rule: RULE, line: null, raw: c, kind: "config", detail: "Tailwind configuration" })),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatTailwind(result) {
|
|
152
|
+
if (!result.present) return "tailwind not installed";
|
|
153
|
+
|
|
154
|
+
const L = [`tailwind PRESENT — ${result.dependencies.length} declaration(s), ${result.configs.length} config file(s)`];
|
|
155
|
+
for (const d of result.dependencies) L.push(` ${d.package} ${d.name}@${d.version} (${d.field})`);
|
|
156
|
+
for (const c of result.configs) L.push(` ${c}`);
|
|
157
|
+
if (result.plugins.length) {
|
|
158
|
+
L.push(` ${result.plugins.length} companion package(s): ${result.plugins.map((p) => p.name).join(", ")}`);
|
|
159
|
+
L.push(" not the ban themselves — they stop mattering when Tailwind goes");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
L.push("");
|
|
163
|
+
if (result.safeToRemove === true) {
|
|
164
|
+
L.push(" SAFE TO REMOVE. No utility class resolves through Tailwind any more.");
|
|
165
|
+
} else if (result.safeToRemove === false) {
|
|
166
|
+
L.push(" DO NOT REMOVE THE DEPENDENCY YET.");
|
|
167
|
+
if (result.utilityClasses > 0) L.push(` ${result.utilityClasses} utility class(es) — inert strings in a className attribute`);
|
|
168
|
+
if (result.cssDirectives > 0) L.push(` ${result.cssDirectives} stylesheet directive(s) — @apply and friends, which stop compiling`);
|
|
169
|
+
L.push(" Deleting the dependency throws nothing, fails no build, and renders the");
|
|
170
|
+
L.push(" application unstyled. Remove these first, then the config, then the dependency.");
|
|
171
|
+
} else {
|
|
172
|
+
L.push(` ${result.why}.`);
|
|
173
|
+
}
|
|
174
|
+
return L.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const CSS_RULE = "tailwind-in-css";
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Tailwind's vocabulary, carried in a stylesheet.
|
|
181
|
+
*
|
|
182
|
+
* The hole G-98 and G-100 both recorded and neither closed. A repository can
|
|
183
|
+
* satisfy the dependency ban completely — nothing in any manifest, no config,
|
|
184
|
+
* not one utility class in any JSX file — and still be written in Tailwind:
|
|
185
|
+
*
|
|
186
|
+
* .card { @apply p-4 rounded-lg bg-muted; }
|
|
187
|
+
*
|
|
188
|
+
* That is the ban being satisfiable without the thing it bans being gone, which
|
|
189
|
+
* is the same trap the removal-ordering guard above exists to prevent. A gate
|
|
190
|
+
* with a documented way through it is a gate that teaches people the way
|
|
191
|
+
* through it.
|
|
192
|
+
*
|
|
193
|
+
* WHAT IS AND IS NOT TAILWIND HERE.
|
|
194
|
+
*
|
|
195
|
+
* `@layer` is NOT included, and that is the one to get right: it is standard
|
|
196
|
+
* CSS cascade layers, it appears in stylesheets that have never seen Tailwind,
|
|
197
|
+
* and flagging it would make this rule fire on the CSS the mandate is asking
|
|
198
|
+
* people to write. The directives below have no meaning outside Tailwind's
|
|
199
|
+
* compiler, so their presence is not a guess.
|
|
200
|
+
*
|
|
201
|
+
* `theme()` is included with the same reasoning: it is a compile-time lookup
|
|
202
|
+
* into Tailwind's config, so a stylesheet using it cannot be read without
|
|
203
|
+
* Tailwind, whatever the manifest says.
|
|
204
|
+
*/
|
|
205
|
+
const CSS_DIRECTIVE = /(^|[\s;{}])@(apply|tailwind|screen|variants|responsive|config)\b|(^|[^\w-])theme\s*\(/;
|
|
206
|
+
|
|
207
|
+
/** Strip comments, preserving line count so a finding points at the right line. */
|
|
208
|
+
const stripCss = (text) => text.replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, " "));
|
|
209
|
+
|
|
210
|
+
export function tailwindInCss(text, { filename = "" } = {}) {
|
|
211
|
+
const found = [];
|
|
212
|
+
stripCss(text).split("\n").forEach((line, i) => {
|
|
213
|
+
const m = line.match(CSS_DIRECTIVE);
|
|
214
|
+
if (!m) return;
|
|
215
|
+
const directive = (m[2] ? "@" + m[2] : "theme()");
|
|
216
|
+
found.push({
|
|
217
|
+
file: filename, rule: CSS_RULE, line: i + 1,
|
|
218
|
+
raw: directive, directive, source: line.trim(),
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
return found;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function formatTailwindCss(findings) {
|
|
225
|
+
if (findings.length === 0) return "no stylesheet carries Tailwind's vocabulary";
|
|
226
|
+
const byFile = {};
|
|
227
|
+
for (const f of findings) (byFile[f.file] ??= []).push(f);
|
|
228
|
+
const L = [`${findings.length} Tailwind directive(s) in ${Object.keys(byFile).length} stylesheet(s):`];
|
|
229
|
+
for (const [file, fs] of Object.entries(byFile)) {
|
|
230
|
+
L.push(` ${file} ${[...new Set(fs.map((f) => f.directive))].join(", ")} (${fs.length})`);
|
|
231
|
+
}
|
|
232
|
+
L.push("");
|
|
233
|
+
L.push(" A repository can pass the dependency ban with no manifest entry, no config and");
|
|
234
|
+
L.push(" no utility class in any component, and still be written in Tailwind. These");
|
|
235
|
+
L.push(" directives have no meaning outside its compiler, so the stylesheet stops");
|
|
236
|
+
L.push(" working the moment the dependency goes — silently, like the classes.");
|
|
237
|
+
return L.join("\n");
|
|
238
|
+
}
|