@controlfront/detect 0.0.1
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/bin/cfb.js +202 -0
- package/package.json +64 -0
- package/src/commands/baseline.js +198 -0
- package/src/commands/init.js +309 -0
- package/src/commands/login.js +71 -0
- package/src/commands/logout.js +44 -0
- package/src/commands/scan.js +1547 -0
- package/src/commands/snapshot.js +191 -0
- package/src/commands/sync.js +127 -0
- package/src/config/baseUrl.js +49 -0
- package/src/data/tailwind-core-spec.js +149 -0
- package/src/engine/runRules.js +210 -0
- package/src/lib/collectDeclaredTokensAuto.js +67 -0
- package/src/lib/collectTokenMatches.js +330 -0
- package/src/lib/collectTokenMatches.js.regex +252 -0
- package/src/lib/loadRules.js +73 -0
- package/src/rules/core/no-hardcoded-colors.js +28 -0
- package/src/rules/core/no-hardcoded-spacing.js +29 -0
- package/src/rules/core/no-inline-styles.js +28 -0
- package/src/utils/authorId.js +106 -0
- package/src/utils/buildAIContributions.js +224 -0
- package/src/utils/buildBlameData.js +388 -0
- package/src/utils/buildDeclaredCssVars.js +185 -0
- package/src/utils/buildDeclaredJson.js +214 -0
- package/src/utils/buildFileChanges.js +372 -0
- package/src/utils/buildRuntimeUsage.js +337 -0
- package/src/utils/detectDeclaredDrift.js +59 -0
- package/src/utils/extractImports.js +178 -0
- package/src/utils/fileExtensions.js +65 -0
- package/src/utils/generateInsights.js +332 -0
- package/src/utils/getAllFiles.js +63 -0
- package/src/utils/getCommitMetaData.js +102 -0
- package/src/utils/getLine.js +14 -0
- package/src/utils/resolveProjectForFolder/index.js +47 -0
- package/src/utils/twClassify.js +138 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snapshot.js
|
|
3
|
+
*
|
|
4
|
+
* Snapshot orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - determine which files to scan
|
|
8
|
+
* - collect raw per-file indicators
|
|
9
|
+
*
|
|
10
|
+
* This file does NOT:
|
|
11
|
+
* - compute baselines
|
|
12
|
+
* - compare snapshots
|
|
13
|
+
* - infer drift, deltas, volatility, or causality
|
|
14
|
+
* - compute per-file characteristics
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { collectFileIndicators } from "../fileCharacteristics/collectFileIndicators.js";
|
|
18
|
+
import getAllFiles from "../utils/getAllFiles.js";
|
|
19
|
+
import getCommitMetaData from "../utils/getCommitMetaData.js";
|
|
20
|
+
import { buildBlameData } from "../utils/buildBlameData.js";
|
|
21
|
+
import { buildFileChanges } from "../utils/buildFileChanges.js";
|
|
22
|
+
import { buildDependencyMap } from "../utils/extractImports.js";
|
|
23
|
+
import { generateAuthorId } from "../utils/authorId.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run a snapshot scan at the current commit.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ files?: string[] }} options
|
|
29
|
+
*/
|
|
30
|
+
export async function runSnapshot(options = {}) {
|
|
31
|
+
const repoRoot = process.cwd();
|
|
32
|
+
|
|
33
|
+
// 1. Resolve files to scan
|
|
34
|
+
const files =
|
|
35
|
+
options.files && options.files.length > 0
|
|
36
|
+
? options.files
|
|
37
|
+
: getAllFiles(repoRoot);
|
|
38
|
+
|
|
39
|
+
// 2. Collect raw indicators
|
|
40
|
+
const fileIndicators = await collectFileIndicators(files, { repoRoot });
|
|
41
|
+
|
|
42
|
+
// 2a. Build dependency map (imports + imported_by)
|
|
43
|
+
console.log("📦 Building dependency map...");
|
|
44
|
+
// Use the actual files from fileIndicators to ensure key matching
|
|
45
|
+
const indicatorFiles = Object.keys(fileIndicators);
|
|
46
|
+
const dependencyMap = buildDependencyMap(indicatorFiles, repoRoot);
|
|
47
|
+
|
|
48
|
+
console.log(`📦 Dependency map built for ${indicatorFiles.length} files`);
|
|
49
|
+
|
|
50
|
+
// Debug: find a JS/TS file sample
|
|
51
|
+
const jsTsFile = indicatorFiles.find(f =>
|
|
52
|
+
/\.(jsx?|tsx?|mjs|cjs)$/i.test(f)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (jsTsFile && dependencyMap[jsTsFile]) {
|
|
56
|
+
console.log(`📦 Sample JS/TS file: ${jsTsFile}`);
|
|
57
|
+
console.log(` imports: ${dependencyMap[jsTsFile].imports.length}`);
|
|
58
|
+
console.log(` imported_by: ${dependencyMap[jsTsFile].imported_by.length}`);
|
|
59
|
+
if (dependencyMap[jsTsFile].imports.length > 0) {
|
|
60
|
+
console.log(` first import: ${dependencyMap[jsTsFile].imports[0]}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Assemble snapshot per file with indicators only
|
|
65
|
+
const filesSnapshot = {};
|
|
66
|
+
|
|
67
|
+
for (const [file, { indicators }] of Object.entries(fileIndicators)) {
|
|
68
|
+
const deps = dependencyMap[file] || { imports: [], imported_by: [] };
|
|
69
|
+
|
|
70
|
+
filesSnapshot[file] = {
|
|
71
|
+
indicators: { ...indicators },
|
|
72
|
+
imports: deps.imports,
|
|
73
|
+
imported_by: deps.imported_by,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Attach commit metadata
|
|
78
|
+
const commit = getCommitMetaData();
|
|
79
|
+
|
|
80
|
+
// Generate consistent author ID
|
|
81
|
+
const commitAuthorId = generateAuthorId({
|
|
82
|
+
email: commit.author_email,
|
|
83
|
+
name: commit.author,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 4a. Build per-changed-file unified-0 diffs (+line ranges) and scoped blame stats (step 2)
|
|
87
|
+
// `buildBlameData` will derive the parent SHA (single-parent only) and skip merge/root commits.
|
|
88
|
+
const changedFilePaths = Array.isArray(commit.changed_files)
|
|
89
|
+
? commit.changed_files
|
|
90
|
+
.map((cf) =>
|
|
91
|
+
(cf && typeof cf.path === "string" && cf.path) ||
|
|
92
|
+
(cf && typeof cf.file === "string" && cf.file) ||
|
|
93
|
+
(cf && typeof cf.to === "string" && cf.to) ||
|
|
94
|
+
(cf && typeof cf.from === "string" && cf.from) ||
|
|
95
|
+
null
|
|
96
|
+
)
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
: [];
|
|
99
|
+
|
|
100
|
+
let diffData = { status: "skipped", files: {} };
|
|
101
|
+
try {
|
|
102
|
+
if (commit && typeof commit.sha === "string" && commit.sha && changedFilePaths.length > 0) {
|
|
103
|
+
diffData = buildBlameData({
|
|
104
|
+
repoRoot,
|
|
105
|
+
commitSha: commit.sha,
|
|
106
|
+
filePaths: changedFilePaths,
|
|
107
|
+
commitTimestampIso: commit.timestamp || null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (_err) {
|
|
111
|
+
diffData = { status: "error", files: {} };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Enrich changed_files with indicator context from the full snapshot.
|
|
115
|
+
// This keeps `files` as the full snapshot (baseline-safe) while making
|
|
116
|
+
// `changed_files` useful for downstream rollups and charts.
|
|
117
|
+
const enrichedChangedFiles = Array.isArray(commit.changed_files)
|
|
118
|
+
? commit.changed_files.map((cf) => {
|
|
119
|
+
const path =
|
|
120
|
+
(cf && typeof cf.path === "string" && cf.path) ||
|
|
121
|
+
(cf && typeof cf.file === "string" && cf.file) ||
|
|
122
|
+
(cf && typeof cf.to === "string" && cf.to) ||
|
|
123
|
+
(cf && typeof cf.from === "string" && cf.from) ||
|
|
124
|
+
null;
|
|
125
|
+
|
|
126
|
+
const indicators =
|
|
127
|
+
path && filesSnapshot[path] && filesSnapshot[path].indicators
|
|
128
|
+
? filesSnapshot[path].indicators
|
|
129
|
+
: null;
|
|
130
|
+
|
|
131
|
+
const imports =
|
|
132
|
+
path && filesSnapshot[path] && filesSnapshot[path].imports
|
|
133
|
+
? filesSnapshot[path].imports
|
|
134
|
+
: [];
|
|
135
|
+
|
|
136
|
+
const importedBy =
|
|
137
|
+
path && filesSnapshot[path] && filesSnapshot[path].imported_by
|
|
138
|
+
? filesSnapshot[path].imported_by
|
|
139
|
+
: [];
|
|
140
|
+
|
|
141
|
+
const diffForFile = path && diffData && diffData.files ? diffData.files[path] : null;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
...cf,
|
|
145
|
+
|
|
146
|
+
// Stamp current-commit context onto each changed file entry.
|
|
147
|
+
// This is required for accurate cross-commit file sequencing.
|
|
148
|
+
commit_sha: commit.sha || null,
|
|
149
|
+
commit_timestamp: commit.timestamp || null,
|
|
150
|
+
commit_author: commit.author || null,
|
|
151
|
+
commit_author_email: commit.author_email || null,
|
|
152
|
+
commit_author_id: commitAuthorId,
|
|
153
|
+
|
|
154
|
+
indicators,
|
|
155
|
+
imports,
|
|
156
|
+
imported_by: importedBy,
|
|
157
|
+
diff_unified0: diffForFile ? diffForFile.diff_unified0 : null,
|
|
158
|
+
diff_ranges: diffForFile ? diffForFile.ranges : [],
|
|
159
|
+
blame_stats: diffForFile ? diffForFile.blame_stats : null,
|
|
160
|
+
};
|
|
161
|
+
})
|
|
162
|
+
: [];
|
|
163
|
+
|
|
164
|
+
// 4b. Build per-commit file change events (file-centric rows) for downstream sequencing.
|
|
165
|
+
// NOTE: This is a pure transform; persistence happens via the existing snapshots API.
|
|
166
|
+
const fileChanges = buildFileChanges({
|
|
167
|
+
projectId: options.projectId || options.project_id || null,
|
|
168
|
+
commit,
|
|
169
|
+
changedFiles: enrichedChangedFiles,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// 5. Emit snapshot payload
|
|
173
|
+
return {
|
|
174
|
+
commit_sha: commit.sha,
|
|
175
|
+
commit_author: commit.author,
|
|
176
|
+
author_email: commit.author_email || null,
|
|
177
|
+
commit_author_email: commit.author_email || null,
|
|
178
|
+
commit_author_id: commitAuthorId,
|
|
179
|
+
commit_branch: commit.branch,
|
|
180
|
+
commit_timestamp: commit.timestamp,
|
|
181
|
+
commit_message: commit.message || "",
|
|
182
|
+
diff_status: diffData && diffData.status ? diffData.status : "skipped",
|
|
183
|
+
blame_line_coverage_pct:
|
|
184
|
+
diffData && typeof diffData.line_coverage_pct === "number" ? diffData.line_coverage_pct : null,
|
|
185
|
+
changed_files: enrichedChangedFiles,
|
|
186
|
+
file_changes: fileChanges,
|
|
187
|
+
files: filesSnapshot,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default runSnapshot;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { makeUrl, resolveBaseUrl } from "../config/baseUrl.js";
|
|
8
|
+
|
|
9
|
+
import { runInit } from "./init.js";
|
|
10
|
+
import { runLogin } from "./login.js";
|
|
11
|
+
|
|
12
|
+
export async function runSync({
|
|
13
|
+
openWeb = true,
|
|
14
|
+
projectInfo,
|
|
15
|
+
token,
|
|
16
|
+
changedFiles,
|
|
17
|
+
slug,
|
|
18
|
+
} = {}) {
|
|
19
|
+
const configPath = path.join(os.homedir(), ".cf", "config.json");
|
|
20
|
+
const reportPath = path.resolve(process.cwd(), "scan-report.json");
|
|
21
|
+
const insightsPath = path.resolve(process.cwd(), "insights.json");
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(configPath)) {
|
|
24
|
+
console.log("🔐 No config found. Launching login flow...");
|
|
25
|
+
await runLogin();
|
|
26
|
+
await runInit();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(reportPath)) {
|
|
30
|
+
console.error("❌ scan-report.json not found. Run `cf scan` first.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolvedWorkspace =
|
|
35
|
+
projectInfo?.workspace ||
|
|
36
|
+
(projectInfo?.project?.workspace_id
|
|
37
|
+
? { id: projectInfo.project.workspace_id, name: "Unknown" }
|
|
38
|
+
: undefined);
|
|
39
|
+
|
|
40
|
+
if (!token) {
|
|
41
|
+
console.error("❌ Missing token. Please run `cf login`.");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!resolvedWorkspace?.id) {
|
|
46
|
+
console.error("❌ Missing workspace info. Please run `cf init`.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!projectInfo?.project?.id) {
|
|
51
|
+
console.error("❌ Missing project info. Please run `cf init`.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const scan = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
56
|
+
|
|
57
|
+
// IMPORTANT: when syncing an updated scan over an existing drift row, we must
|
|
58
|
+
// not send an `id` field back to the server. If we do, an overwrite/upsert can
|
|
59
|
+
// update the primary key and break stable URLs.
|
|
60
|
+
//
|
|
61
|
+
// We keep the rest of the payload intact; the server should decide whether to
|
|
62
|
+
// insert or update based on its own keys.
|
|
63
|
+
const scanForSync = { ...scan };
|
|
64
|
+
delete scanForSync.id;
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`📡 Syncing scan to ${resolvedWorkspace.name} / ${projectInfo.project.name}...`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(makeUrl(`/api/scans`), {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
Authorization: `Bearer ${token}`,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
scan: {
|
|
79
|
+
...scanForSync,
|
|
80
|
+
workspace_id: resolvedWorkspace.id,
|
|
81
|
+
project_id: projectInfo.project.id,
|
|
82
|
+
...(changedFiles ? { changed_files: changedFiles } : {}),
|
|
83
|
+
slug: slug || "manual",
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(text);
|
|
91
|
+
} catch {
|
|
92
|
+
console.error(`❌ Server response is not valid JSON:\n${text}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
console.error(
|
|
97
|
+
`❌ Sync failed with status ${res.status}:\n${JSON.stringify(
|
|
98
|
+
data,
|
|
99
|
+
null,
|
|
100
|
+
2
|
|
101
|
+
)}`
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log("✅ Scan synced successfully to ControlFront.");
|
|
106
|
+
if (data?.id && data?.workspace_id && data?.project_id) {
|
|
107
|
+
const url = `${resolveBaseUrl()}/workspaces/${
|
|
108
|
+
data.workspace_id
|
|
109
|
+
}/projects/${data.project_id}/baseline`;
|
|
110
|
+
console.log(`👀 View scan report: ${url}`);
|
|
111
|
+
|
|
112
|
+
if (openWeb) {
|
|
113
|
+
try {
|
|
114
|
+
await open(url);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error("❌ Failed to open browser:", err.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!openWeb) {
|
|
122
|
+
console.log("🚫 Web app not opened (per --no-web-open flag).");
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error("❌ Sync failed:", err.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Load .env from CLI root (one directory up from /src/config/)
|
|
11
|
+
dotenv.config({
|
|
12
|
+
path: path.join(__dirname, "..", "..", ".env"),
|
|
13
|
+
override: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function getStoredEnvironment() {
|
|
17
|
+
try {
|
|
18
|
+
const configPath = path.join(os.homedir(), ".cf", "config.json");
|
|
19
|
+
if (fs.existsSync(configPath)) {
|
|
20
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
21
|
+
return config.environment || "dev";
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// ignore errors
|
|
25
|
+
}
|
|
26
|
+
return "dev";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveBaseUrlForEnv(env) {
|
|
30
|
+
const url = env === "prod"
|
|
31
|
+
? process.env.CF_APP_URL_PROD
|
|
32
|
+
: process.env.CF_APP_URL_DEV;
|
|
33
|
+
|
|
34
|
+
if (url && url.trim()) {
|
|
35
|
+
return url.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback defaults
|
|
39
|
+
return env === "prod" ? "https://app.controlfront.io" : "http://localhost:3000";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveBaseUrl() {
|
|
43
|
+
const env = getStoredEnvironment();
|
|
44
|
+
return resolveBaseUrlForEnv(env);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function makeUrl(pathAndQuery = "") {
|
|
48
|
+
return new URL(pathAndQuery, resolveBaseUrl() + "/").toString();
|
|
49
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Tailwind v3.x+ core patterns (no plugins). Keep this in JS so we can use regexes & helpers.
|
|
2
|
+
// Covers: variants, breakpoints, colors/shades, sizing scale, radii, borders, typography,
|
|
3
|
+
// layout, effects, transforms, interactivity, tables, accessibility, etc.
|
|
4
|
+
|
|
5
|
+
const BREAKPOINTS = ["sm","md","lg","xl","2xl"];
|
|
6
|
+
const STATE_VARIANTS = [
|
|
7
|
+
"hover","focus","focus-visible","active","disabled","visited","first","last","odd","even",
|
|
8
|
+
"checked","indeterminate","focus-within","required","valid","invalid","read-only","empty",
|
|
9
|
+
"placeholder","placeholder-shown","autofill","selection","target","open"
|
|
10
|
+
];
|
|
11
|
+
const GROUP_VARIANTS = [
|
|
12
|
+
"group-hover","group-focus","group-active","group-disabled","group-aria-[\\w:-]+","peer-hover",
|
|
13
|
+
"peer-focus","peer-active","peer-disabled","peer-checked","peer-invalid","peer-aria-[\\w:-]+"
|
|
14
|
+
];
|
|
15
|
+
// Tailwind also supports arbitrary variants in brackets like [@supports(display:grid)]:, etc.
|
|
16
|
+
// We’ll just strip any leading "{variant}:" chunks before matching a base utility.
|
|
17
|
+
|
|
18
|
+
export const VARIANT_PREFIX_RE = new RegExp(
|
|
19
|
+
// responsive + state + dark + rtl/ltr + print + motion + forced-colors + data-* + aria-* + arbitrary [...]
|
|
20
|
+
`^(?:(${[
|
|
21
|
+
...BREAKPOINTS,
|
|
22
|
+
"dark","rtl","ltr","motion-safe","motion-reduce","portrait","landscape",
|
|
23
|
+
"print","contrast-more","contrast-less","forced-colors",
|
|
24
|
+
...STATE_VARIANTS,
|
|
25
|
+
].join("|")})|(?:data-[\\w-]+)|(?:aria-[\\w-]+)|(?:group\\/(?:[\\w-]+))|(?:group)|(?:peer)|(?:\$begin:math:display$.+?\\$end:math:display$)):`,
|
|
26
|
+
"g"
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Colors from the default palette (Tailwind v3)
|
|
30
|
+
const COLORS = [
|
|
31
|
+
"inherit","current","transparent","black","white",
|
|
32
|
+
"slate","gray","zinc","neutral","stone",
|
|
33
|
+
"red","orange","amber","yellow","lime","green","emerald","teal","cyan","sky","blue",
|
|
34
|
+
"indigo","violet","purple","fuchsia","pink","rose"
|
|
35
|
+
];
|
|
36
|
+
const SHADES = ["50","100","200","300","400","500","600","700","800","900","950"];
|
|
37
|
+
|
|
38
|
+
// Spacing scale (subset but representative of Tailwind defaults, expanded to cover common sizes)
|
|
39
|
+
const SPACING_KEYS = [
|
|
40
|
+
"px","0","0\\.5","1","1\\.5","2","2\\.5","3","3\\.5","4","5","6","7","8","9","10",
|
|
41
|
+
"11","12","14","16","20","24","28","32","36","40","44","48","52","56","60","64","72","80","96"
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Fractions for width/height
|
|
45
|
+
const FRACTIONS = ["1\\/2","1\\/3","2\\/3","1\\/4","2\\/4","3\\/4","1\\/5","2\\/5","3\\/5","4\\/5","1\\/6","2\\/6","3\\/6","4\\/6","5\\/6","full","screen","min","max","fit","auto"];
|
|
46
|
+
|
|
47
|
+
// Font sizes
|
|
48
|
+
const FONT_SIZES = ["xs","sm","base","lg","xl","2xl","3xl","4xl","5xl","6xl","7xl","8xl","9xl"];
|
|
49
|
+
|
|
50
|
+
// Border radii
|
|
51
|
+
const RADII = ["none","sm","", "md","lg","xl","2xl","3xl","full"]; // "" is default ("rounded")
|
|
52
|
+
|
|
53
|
+
// Border widths
|
|
54
|
+
const BORDER_W = ["0","2","4","8",""];
|
|
55
|
+
|
|
56
|
+
// Z-index keywords
|
|
57
|
+
const ZINDEX = ["0","10","20","30","40","50","auto"];
|
|
58
|
+
|
|
59
|
+
// Opacity
|
|
60
|
+
const OPACITY = ["0","5","10","20","25","30","40","50","60","70","75","80","90","95","100"];
|
|
61
|
+
|
|
62
|
+
// Object fit/position, overflow, display, position, etc. are constant words, we’ll regex by namespace.
|
|
63
|
+
|
|
64
|
+
const COLOR_CLASS = (ns) =>
|
|
65
|
+
new RegExp(`^${ns}-(?:${COLORS.join("|")})(?:-(?:${SHADES.join("|")}))?$`);
|
|
66
|
+
|
|
67
|
+
const SCALE_CLASS = (ns) =>
|
|
68
|
+
new RegExp(`^${ns}-(?:${SPACING_KEYS.join("|")})$`);
|
|
69
|
+
|
|
70
|
+
const FRACTION_CLASS = (ns) =>
|
|
71
|
+
new RegExp(`^${ns}-(?:${FRACTIONS.join("|")})$`);
|
|
72
|
+
|
|
73
|
+
const SIMPLE_SET = (ns, set) =>
|
|
74
|
+
new RegExp(`^${ns}-(?:${set.join("|")})$`);
|
|
75
|
+
|
|
76
|
+
const EXACT = (...names) => new RegExp(`^(?:${names.map(n=>n.replace(/[.*+?^${}()|[\\]\\\\]/g,"\\$&")).join("|")})$`);
|
|
77
|
+
|
|
78
|
+
export const TAILWIND_PATTERNS = [
|
|
79
|
+
// Display & Position
|
|
80
|
+
EXACT("block","inline-block","inline","flex","inline-flex","grid","inline-grid","contents","hidden"),
|
|
81
|
+
EXACT("static","fixed","absolute","relative","sticky"),
|
|
82
|
+
// Flex & Grid
|
|
83
|
+
EXACT("flex-row","flex-row-reverse","flex-col","flex-col-reverse","flex-wrap","flex-nowrap","flex-wrap-reverse"),
|
|
84
|
+
EXACT("items-start","items-center","items-end","items-baseline","items-stretch"),
|
|
85
|
+
EXACT("justify-start","justify-center","justify-end","justify-between","justify-around","justify-evenly","content-center","content-between","content-around","content-evenly"),
|
|
86
|
+
EXACT("self-auto","self-start","self-center","self-end","self-stretch","place-content-center","place-items-center","place-self-center"),
|
|
87
|
+
// Spacing
|
|
88
|
+
SCALE_CLASS("p"), SCALE_CLASS("px"), SCALE_CLASS("py"), SCALE_CLASS("pt"), SCALE_CLASS("pr"), SCALE_CLASS("pb"), SCALE_CLASS("pl"),
|
|
89
|
+
SCALE_CLASS("m"), SCALE_CLASS("mx"), SCALE_CLASS("my"), SCALE_CLASS("mt"), SCALE_CLASS("mr"), SCALE_CLASS("mb"), SCALE_CLASS("ml"),
|
|
90
|
+
SCALE_CLASS("gap"), SCALE_CLASS("space-x"), SCALE_CLASS("space-y"),
|
|
91
|
+
// Sizing
|
|
92
|
+
SCALE_CLASS("h"), SCALE_CLASS("w"), FRACTION_CLASS("h"), FRACTION_CLASS("w"),
|
|
93
|
+
// Extended sizing utilities
|
|
94
|
+
SCALE_CLASS("min-h"), SCALE_CLASS("min-w"),
|
|
95
|
+
SCALE_CLASS("max-h"), SCALE_CLASS("max-w"),
|
|
96
|
+
SIMPLE_SET("max-w", [
|
|
97
|
+
"0","none","xs","sm","md","lg","xl","2xl","3xl","4xl","5xl","6xl","7xl",
|
|
98
|
+
"full","min","max","fit","prose",
|
|
99
|
+
"screen-sm","screen-md","screen-lg","screen-xl","screen-2xl"
|
|
100
|
+
]),
|
|
101
|
+
SIMPLE_SET("min-h", ["0","full","screen","min","max","fit"]),
|
|
102
|
+
SIMPLE_SET("max-h", ["0","full","screen","min","max","fit"]),
|
|
103
|
+
SIMPLE_SET("min-w", ["0","full","min","max","fit"]),
|
|
104
|
+
// Typography
|
|
105
|
+
SIMPLE_SET("text", FONT_SIZES),
|
|
106
|
+
// Recognize all standard Tailwind font-size utilities (including text-md, text-base, etc.)
|
|
107
|
+
/^text-(?:xs|sm|base|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
|
|
108
|
+
EXACT("antialiased","subpixel-antialiased"),
|
|
109
|
+
EXACT("font-thin","font-extralight","font-light","font-normal","font-medium","font-semibold","font-bold","font-extrabold","font-black"),
|
|
110
|
+
EXACT("italic","not-italic"),
|
|
111
|
+
EXACT("uppercase","lowercase","capitalize","normal-case","truncate","break-words","break-normal"),
|
|
112
|
+
// Colors
|
|
113
|
+
COLOR_CLASS("text"), COLOR_CLASS("bg"), COLOR_CLASS("border"), COLOR_CLASS("divide"),
|
|
114
|
+
// Border radius & width & style
|
|
115
|
+
new RegExp(`^rounded(?:-(?:${RADII.join("|")}))?$`),
|
|
116
|
+
SIMPLE_SET("border", BORDER_W), EXACT("border","border-x","border-y","border-t","border-r","border-b","border-l"),
|
|
117
|
+
EXACT("border-solid","border-dashed","border-dotted","border-double","border-none"),
|
|
118
|
+
// Effects
|
|
119
|
+
EXACT("shadow","shadow-sm","shadow-md","shadow-lg","shadow-xl","shadow-2xl","shadow-inner","shadow-none"),
|
|
120
|
+
SIMPLE_SET("opacity", OPACITY),
|
|
121
|
+
// Transforms & Transitions
|
|
122
|
+
EXACT("transform","transform-gpu","transform-none"),
|
|
123
|
+
EXACT("transition","transition-none","transition-all","transition-colors","transition-opacity","transition-shadow","transition-transform"),
|
|
124
|
+
SIMPLE_SET("duration", ["75","100","150","200","300","500","700","1000"]),
|
|
125
|
+
SIMPLE_SET("delay", ["75","100","150","200","300","500","700","1000"]),
|
|
126
|
+
SIMPLE_SET("ease", ["linear","in","out","in-out"]),
|
|
127
|
+
// Layout helpers
|
|
128
|
+
EXACT("container","sr-only","not-sr-only","overflow-hidden","overflow-auto","overflow-scroll","overflow-x-hidden","overflow-y-hidden","overflow-x-auto","overflow-y-auto"),
|
|
129
|
+
// Z-index
|
|
130
|
+
SIMPLE_SET("z", ZINDEX),
|
|
131
|
+
// Cursors & pointer events
|
|
132
|
+
EXACT("pointer-events-none","pointer-events-auto","cursor-default","cursor-pointer","cursor-not-allowed"),
|
|
133
|
+
// Tables
|
|
134
|
+
EXACT("table-auto","table-fixed","border-collapse","border-separate","caption-top","caption-bottom")
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// Arbitrary value syntax (always Tailwind, but not default-scale):
|
|
138
|
+
export const ARBITRARY_RE = /-\\[(?:.|\\n)+?\\]$/;
|
|
139
|
+
|
|
140
|
+
// Known base namespaces (for quick prefix sniffing)
|
|
141
|
+
export const KNOWN_PREFIXES = [
|
|
142
|
+
"p","px","py","pt","pr","pb","pl",
|
|
143
|
+
"m","mx","my","mt","mr","mb","ml",
|
|
144
|
+
"text","bg","border","divide","ring","outline",
|
|
145
|
+
"rounded","grid","col","row","gap","justify","items","content","place",
|
|
146
|
+
"h","w","min-h","min-w","max-h","max-w",
|
|
147
|
+
"shadow","opacity","z","duration","delay","ease","transition",
|
|
148
|
+
"overflow","cursor","pointer-events","table","caption","container",
|
|
149
|
+
];
|