@maizzle/framework 6.0.0-rc.11 → 6.0.0-rc.13
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/dist/build.mjs +4 -1
- package/dist/build.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +23 -11
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +54 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +890 -76
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/linter.d.mts +15 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +194 -43
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/sfc-utils.d.mts +18 -0
- package/dist/server/sfc-utils.d.mts.map +1 -0
- package/dist/server/sfc-utils.mjs +184 -0
- package/dist/server/sfc-utils.mjs.map +1 -0
- package/dist/server/ui/App.vue +4 -41
- package/dist/server/ui/components/SidebarClose.vue +12 -0
- package/dist/server/ui/components/ui/command/Command.vue +1 -0
- package/dist/server/ui/components/ui/input/Input.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +1 -1
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +1 -1
- package/dist/server/ui/pages/Preview.vue +194 -151
- package/dist/transformers/addAttributes.mjs +10 -6
- package/dist/transformers/addAttributes.mjs.map +1 -1
- package/dist/transformers/inlineCSS.mjs +2 -2
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/purgeCSS.mjs +1 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.mjs +2 -4
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +42 -2
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/package.json +1 -3
- package/dist/_virtual/_rolldown/runtime.mjs +0 -32
- package/dist/node_modules/picomatch/index.mjs +0 -13
- package/dist/node_modules/picomatch/index.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/constants.mjs +0 -174
- package/dist/node_modules/picomatch/lib/constants.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/parse.mjs +0 -1067
- package/dist/node_modules/picomatch/lib/parse.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/picomatch.mjs +0 -304
- package/dist/node_modules/picomatch/lib/picomatch.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/scan.mjs +0 -296
- package/dist/node_modules/picomatch/lib/scan.mjs.map +0 -1
- package/dist/node_modules/picomatch/lib/utils.mjs +0 -53
- package/dist/node_modules/picomatch/lib/utils.mjs.map +0 -1
|
@@ -1,91 +1,905 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tailwindcss } from "../transformers/tailwindcss.mjs";
|
|
2
|
+
import { buildComponentMap, findComponentTags, parseSfcBlocks } from "./sfc-utils.mjs";
|
|
3
|
+
import { scanLint } from "./linter.mjs";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { Parser } from "htmlparser2";
|
|
7
|
+
import { DomHandler } from "domhandler";
|
|
8
|
+
import safeParser from "postcss-safe-parser";
|
|
9
|
+
import valueParser from "postcss-value-parser";
|
|
2
10
|
|
|
3
11
|
//#region src/server/compatibility.ts
|
|
4
|
-
|
|
12
|
+
const API_URL = "https://www.caniemail.com/api/data.json";
|
|
13
|
+
const DEFAULT_CLIENTS = new Set([
|
|
14
|
+
"gmail",
|
|
15
|
+
"apple-mail",
|
|
16
|
+
"outlook",
|
|
17
|
+
"yahoo"
|
|
18
|
+
]);
|
|
19
|
+
let indexes = null;
|
|
20
|
+
let initPromise = null;
|
|
21
|
+
function mpush(m, k, v) {
|
|
22
|
+
const arr = m.get(k);
|
|
23
|
+
if (arr) arr.push(v);
|
|
24
|
+
else m.set(k, [v]);
|
|
25
|
+
}
|
|
26
|
+
function emptyIndexes(nicenames, familyNicenames) {
|
|
27
|
+
return {
|
|
28
|
+
nicenames,
|
|
29
|
+
familyNicenames,
|
|
30
|
+
cssProp: /* @__PURE__ */ new Map(),
|
|
31
|
+
cssPropValue: /* @__PURE__ */ new Map(),
|
|
32
|
+
cssAtRule: /* @__PURE__ */ new Map(),
|
|
33
|
+
cssMediaFeature: /* @__PURE__ */ new Map(),
|
|
34
|
+
cssPseudoClass: /* @__PURE__ */ new Map(),
|
|
35
|
+
cssPseudoElement: /* @__PURE__ */ new Map(),
|
|
36
|
+
cssFunction: /* @__PURE__ */ new Map(),
|
|
37
|
+
cssUnit: /* @__PURE__ */ new Map(),
|
|
38
|
+
htmlTag: /* @__PURE__ */ new Map(),
|
|
39
|
+
htmlAttr: /* @__PURE__ */ new Map(),
|
|
40
|
+
htmlInputType: /* @__PURE__ */ new Map(),
|
|
41
|
+
htmlButtonType: /* @__PURE__ */ new Map(),
|
|
42
|
+
imageExt: /* @__PURE__ */ new Map(),
|
|
43
|
+
bySlug: /* @__PURE__ */ new Map()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function hasAnyNonY(stats) {
|
|
47
|
+
if (!stats) return false;
|
|
48
|
+
for (const family in stats) for (const plat in stats[family]) for (const ver in stats[family][plat]) {
|
|
49
|
+
const v = stripNotes(String(stats[family][plat][ver]).trim());
|
|
50
|
+
if (v && v !== "y") return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/** Strip `#N` note markers — `"y #1"` → `"y"`. Notes document edge cases but
|
|
55
|
+
* don't change support semantics, so treat `y #1` as fully supported. */
|
|
56
|
+
function stripNotes(v) {
|
|
57
|
+
return v.split(/\s+/).filter((t) => t && !t.startsWith("#")).join(" ");
|
|
58
|
+
}
|
|
59
|
+
function computeSupport(stats, familyNicenames, allowedClients) {
|
|
60
|
+
let nY = 0, nN = 0, nU = 0, nPartial = 0, total = 0;
|
|
61
|
+
const affectedFamilies = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const family in stats) {
|
|
63
|
+
if (allowedClients !== "all" && !allowedClients.has(family)) continue;
|
|
64
|
+
let familyHasNonY = false;
|
|
65
|
+
for (const plat in stats[family]) {
|
|
66
|
+
const versions = Object.keys(stats[family][plat]).sort();
|
|
67
|
+
const latest = versions[versions.length - 1];
|
|
68
|
+
if (!latest) continue;
|
|
69
|
+
total++;
|
|
70
|
+
const v = stripNotes(String(stats[family][plat][latest]).trim());
|
|
71
|
+
if (v === "y") nY++;
|
|
72
|
+
else if (v === "n") {
|
|
73
|
+
nN++;
|
|
74
|
+
familyHasNonY = true;
|
|
75
|
+
} else if (v === "u") {
|
|
76
|
+
nU++;
|
|
77
|
+
familyHasNonY = true;
|
|
78
|
+
} else {
|
|
79
|
+
nPartial++;
|
|
80
|
+
familyHasNonY = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (familyHasNonY) affectedFamilies.add(family);
|
|
84
|
+
}
|
|
85
|
+
if (!total) return null;
|
|
86
|
+
if (nY === total) return null;
|
|
87
|
+
const affected = [...affectedFamilies].map((f) => familyNicenames[f] ?? f).sort();
|
|
88
|
+
if (nN === total) return {
|
|
89
|
+
level: "unsupported",
|
|
90
|
+
affected
|
|
91
|
+
};
|
|
92
|
+
if (nU === total) return {
|
|
93
|
+
level: "unknown",
|
|
94
|
+
affected
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
level: "mitigated",
|
|
98
|
+
affected
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Slugs we never report. Fundamental HTML (every email uses these) plus
|
|
103
|
+
* CSS noise that's not actionable (comments, !important usage).
|
|
104
|
+
*/
|
|
105
|
+
const IGNORED_SLUGS = new Set([
|
|
106
|
+
"html-doctype",
|
|
107
|
+
"html-comments",
|
|
108
|
+
"html-html",
|
|
109
|
+
"html-head",
|
|
110
|
+
"html-body",
|
|
111
|
+
"html-title",
|
|
112
|
+
"html-meta",
|
|
113
|
+
"html-meta-color-scheme",
|
|
114
|
+
"html-style",
|
|
115
|
+
"html-link",
|
|
116
|
+
"html-div",
|
|
117
|
+
"html-span",
|
|
118
|
+
"html-br",
|
|
119
|
+
"html-p",
|
|
120
|
+
"html-a",
|
|
121
|
+
"html-img",
|
|
122
|
+
"html-table",
|
|
123
|
+
"html-tr",
|
|
124
|
+
"html-td",
|
|
125
|
+
"html-th",
|
|
126
|
+
"html-thead",
|
|
127
|
+
"html-tbody",
|
|
128
|
+
"html-tfoot",
|
|
129
|
+
"html-h1-h6",
|
|
130
|
+
"html-lists",
|
|
131
|
+
"html-strong",
|
|
132
|
+
"html-em",
|
|
133
|
+
"html-b",
|
|
134
|
+
"html-i",
|
|
135
|
+
"html-u",
|
|
136
|
+
"html-semantics",
|
|
137
|
+
"html-role",
|
|
138
|
+
"html-hidden",
|
|
139
|
+
"html-width",
|
|
140
|
+
"html-height",
|
|
141
|
+
"css-comments",
|
|
142
|
+
"css-important",
|
|
143
|
+
"css-margin",
|
|
144
|
+
"css-padding",
|
|
145
|
+
"css-border",
|
|
146
|
+
"css-font-size",
|
|
147
|
+
"css-font-weight",
|
|
148
|
+
"css-font",
|
|
149
|
+
"css-font-family",
|
|
150
|
+
"css-line-height",
|
|
151
|
+
"css-letter-spacing",
|
|
152
|
+
"css-text-align",
|
|
153
|
+
"css-text-decoration",
|
|
154
|
+
"css-text-transform",
|
|
155
|
+
"css-color",
|
|
156
|
+
"css-background",
|
|
157
|
+
"css-background-color",
|
|
158
|
+
"css-width",
|
|
159
|
+
"css-height",
|
|
160
|
+
"css-display"
|
|
161
|
+
]);
|
|
162
|
+
function classify(f, idx) {
|
|
163
|
+
const slug = f.slug;
|
|
164
|
+
if (slug === "html-style") {
|
|
165
|
+
idx.htmlStyleInBody = {
|
|
166
|
+
...f,
|
|
167
|
+
title: `${f.title} in <body>`
|
|
168
|
+
};
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (IGNORED_SLUGS.has(slug)) return;
|
|
172
|
+
if (f.category === "css") return classifyCss(f, slug, idx);
|
|
173
|
+
if (f.category === "html") return classifyHtml(f, slug, idx);
|
|
174
|
+
if (f.category === "image") {
|
|
175
|
+
const ext = slug.slice(6);
|
|
176
|
+
if (ext === "base64") return;
|
|
177
|
+
mpush(idx.imageExt, ext, f);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function classifyCss(f, slug, idx) {
|
|
181
|
+
switch (slug) {
|
|
182
|
+
case "css-important":
|
|
183
|
+
idx.cssImportant = f;
|
|
184
|
+
return;
|
|
185
|
+
case "css-variables":
|
|
186
|
+
idx.cssVariables = f;
|
|
187
|
+
return;
|
|
188
|
+
case "css-nesting":
|
|
189
|
+
idx.cssNesting = f;
|
|
190
|
+
return;
|
|
191
|
+
case "css-comments":
|
|
192
|
+
idx.cssComments = f;
|
|
193
|
+
return;
|
|
194
|
+
case "css-modern-color":
|
|
195
|
+
idx.cssModernColor = f;
|
|
196
|
+
return;
|
|
197
|
+
case "css-display-flex":
|
|
198
|
+
mpushPropValue(idx, "display", "flex", f);
|
|
199
|
+
return;
|
|
200
|
+
case "css-display-grid":
|
|
201
|
+
mpushPropValue(idx, "display", "grid", f);
|
|
202
|
+
return;
|
|
203
|
+
case "css-display-none":
|
|
204
|
+
mpushPropValue(idx, "display", "none", f);
|
|
205
|
+
return;
|
|
206
|
+
case "css-rgb":
|
|
207
|
+
mpush(idx.cssFunction, "rgb", f);
|
|
208
|
+
return;
|
|
209
|
+
case "css-rgba":
|
|
210
|
+
mpush(idx.cssFunction, "rgba", f);
|
|
211
|
+
return;
|
|
212
|
+
case "css-linear-gradient":
|
|
213
|
+
mpush(idx.cssFunction, "linear-gradient", f);
|
|
214
|
+
return;
|
|
215
|
+
case "css-radial-gradient":
|
|
216
|
+
mpush(idx.cssFunction, "radial-gradient", f);
|
|
217
|
+
return;
|
|
218
|
+
case "css-conic-gradient":
|
|
219
|
+
mpush(idx.cssFunction, "conic-gradient", f);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (slug.startsWith("css-at-media-") && slug !== "css-at-media") {
|
|
223
|
+
mpush(idx.cssMediaFeature, slug.slice(13), f);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (slug.startsWith("css-at-")) {
|
|
227
|
+
mpush(idx.cssAtRule, slug.slice(7), f);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (slug.startsWith("css-pseudo-class-")) {
|
|
231
|
+
mpush(idx.cssPseudoClass, slug.slice(17), f);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (slug.startsWith("css-pseudo-element-")) {
|
|
235
|
+
mpush(idx.cssPseudoElement, slug.slice(19), f);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (slug.startsWith("css-unit-")) {
|
|
239
|
+
const u = slug.slice(9);
|
|
240
|
+
if (u === "calc") {
|
|
241
|
+
mpush(idx.cssFunction, "calc", f);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (u === "initial") return;
|
|
245
|
+
const unit = u === "percent" ? "%" : u;
|
|
246
|
+
mpush(idx.cssUnit, unit, f);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (slug.startsWith("css-function-")) {
|
|
250
|
+
mpush(idx.cssFunction, slug.slice(13), f);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (slug.startsWith("css-selector-")) return;
|
|
254
|
+
mpush(idx.cssProp, slug.slice(4), f);
|
|
255
|
+
}
|
|
256
|
+
function mpushPropValue(idx, prop, value, f) {
|
|
257
|
+
const arr = idx.cssPropValue.get(prop);
|
|
258
|
+
if (arr) arr.push({
|
|
259
|
+
value,
|
|
260
|
+
feature: f
|
|
261
|
+
});
|
|
262
|
+
else idx.cssPropValue.set(prop, [{
|
|
263
|
+
value,
|
|
264
|
+
feature: f
|
|
265
|
+
}]);
|
|
266
|
+
}
|
|
267
|
+
const HTML_ATTR_SLUGS = new Set([
|
|
268
|
+
"align",
|
|
269
|
+
"background",
|
|
270
|
+
"cellpadding",
|
|
271
|
+
"cellspacing",
|
|
272
|
+
"height",
|
|
273
|
+
"width",
|
|
274
|
+
"valign",
|
|
275
|
+
"target",
|
|
276
|
+
"srcset",
|
|
277
|
+
"lang",
|
|
278
|
+
"dir",
|
|
279
|
+
"role",
|
|
280
|
+
"required",
|
|
281
|
+
"hidden"
|
|
282
|
+
]);
|
|
283
|
+
function classifyHtml(f, slug, idx) {
|
|
284
|
+
switch (slug) {
|
|
285
|
+
case "html-doctype":
|
|
286
|
+
idx.htmlDoctype = f;
|
|
287
|
+
return;
|
|
288
|
+
case "html-comments":
|
|
289
|
+
idx.htmlComments = f;
|
|
290
|
+
return;
|
|
291
|
+
case "html-anchor-links":
|
|
292
|
+
idx.htmlAnchorLinks = f;
|
|
293
|
+
return;
|
|
294
|
+
case "html-mailto-links":
|
|
295
|
+
idx.htmlMailtoLinks = f;
|
|
296
|
+
return;
|
|
297
|
+
case "html-meta-color-scheme":
|
|
298
|
+
idx.htmlMetaColorScheme = f;
|
|
299
|
+
return;
|
|
300
|
+
case "html-semantics":
|
|
301
|
+
idx.htmlSemantics = f;
|
|
302
|
+
return;
|
|
303
|
+
case "html-loading-attribute":
|
|
304
|
+
mpush(idx.htmlAttr, "loading", f);
|
|
305
|
+
return;
|
|
306
|
+
case "html-image-maps":
|
|
307
|
+
mpush(idx.htmlTag, "map", f);
|
|
308
|
+
mpush(idx.htmlTag, "area", f);
|
|
309
|
+
mpush(idx.htmlAttr, "usemap", f);
|
|
310
|
+
return;
|
|
311
|
+
case "html-lists":
|
|
312
|
+
for (const t of [
|
|
313
|
+
"ul",
|
|
314
|
+
"ol",
|
|
315
|
+
"li",
|
|
316
|
+
"dl",
|
|
317
|
+
"dt",
|
|
318
|
+
"dd"
|
|
319
|
+
]) mpush(idx.htmlTag, t, f);
|
|
320
|
+
return;
|
|
321
|
+
case "html-h1-h6":
|
|
322
|
+
for (const t of [
|
|
323
|
+
"h1",
|
|
324
|
+
"h2",
|
|
325
|
+
"h3",
|
|
326
|
+
"h4",
|
|
327
|
+
"h5",
|
|
328
|
+
"h6"
|
|
329
|
+
]) mpush(idx.htmlTag, t, f);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (slug.startsWith("html-input-")) {
|
|
333
|
+
mpush(idx.htmlInputType, slug.slice(11), f);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (slug.startsWith("html-button-")) {
|
|
337
|
+
mpush(idx.htmlButtonType, slug.slice(12), f);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (slug.startsWith("html-aria-")) {
|
|
341
|
+
mpush(idx.htmlAttr, slug.slice(5), f);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const name = slug.slice(5);
|
|
345
|
+
if (HTML_ATTR_SLUGS.has(name)) {
|
|
346
|
+
mpush(idx.htmlAttr, name, f);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
mpush(idx.htmlTag, name, f);
|
|
350
|
+
}
|
|
351
|
+
async function initCompatibility() {
|
|
352
|
+
if (indexes) return indexes;
|
|
353
|
+
if (initPromise) return initPromise;
|
|
354
|
+
initPromise = (async () => {
|
|
355
|
+
try {
|
|
356
|
+
const res = await fetch(API_URL);
|
|
357
|
+
if (!res.ok) return null;
|
|
358
|
+
const data = await res.json();
|
|
359
|
+
const idx = emptyIndexes(data.nicenames?.support ?? {}, data.nicenames?.family ?? {});
|
|
360
|
+
for (const item of data.data ?? []) {
|
|
361
|
+
if (item.slug && item.url) idx.bySlug.set(item.slug, {
|
|
362
|
+
title: item.title,
|
|
363
|
+
url: item.url
|
|
364
|
+
});
|
|
365
|
+
if (!hasAnyNonY(item.stats)) continue;
|
|
366
|
+
classify({
|
|
367
|
+
slug: item.slug,
|
|
368
|
+
title: item.title,
|
|
369
|
+
url: item.url,
|
|
370
|
+
category: item.category,
|
|
371
|
+
stats: item.stats
|
|
372
|
+
}, idx);
|
|
373
|
+
}
|
|
374
|
+
indexes = idx;
|
|
375
|
+
return idx;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
})();
|
|
380
|
+
return initPromise;
|
|
381
|
+
}
|
|
382
|
+
function collectStreams(filePath, componentMap, visited, out) {
|
|
383
|
+
if (visited.has(filePath)) return;
|
|
384
|
+
visited.add(filePath);
|
|
385
|
+
let source;
|
|
5
386
|
try {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
387
|
+
source = readFileSync(filePath, "utf-8");
|
|
388
|
+
} catch {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const { template, styles } = parseSfcBlocks(source);
|
|
392
|
+
const classes = /* @__PURE__ */ new Set();
|
|
393
|
+
if (template) extractClasses(template.content, classes);
|
|
394
|
+
out.push({
|
|
395
|
+
path: filePath,
|
|
396
|
+
source,
|
|
397
|
+
template,
|
|
398
|
+
styles,
|
|
399
|
+
classes
|
|
400
|
+
});
|
|
401
|
+
if (template) for (const tag of findComponentTags(template.content)) {
|
|
402
|
+
const cp = componentMap.get(tag.toLowerCase());
|
|
403
|
+
if (cp) collectStreams(cp, componentMap, visited, out);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function extractClasses(html, out) {
|
|
407
|
+
const parser = new Parser({ onopentag(_tag, attrs) {
|
|
408
|
+
const c = attrs.class;
|
|
409
|
+
if (!c) return;
|
|
410
|
+
for (const t of c.split(/\s+/)) if (t) out.add(t);
|
|
411
|
+
} }, { decodeEntities: true });
|
|
412
|
+
parser.write(html);
|
|
413
|
+
parser.end();
|
|
414
|
+
}
|
|
415
|
+
function parseWithIndices(html) {
|
|
416
|
+
const handler = new DomHandler(void 0, { withStartIndices: true });
|
|
417
|
+
const parser = new Parser(handler);
|
|
418
|
+
parser.write(html);
|
|
419
|
+
parser.end();
|
|
420
|
+
return handler.dom;
|
|
421
|
+
}
|
|
422
|
+
function findStyleNodes(nodes, out = []) {
|
|
423
|
+
for (const n of nodes) {
|
|
424
|
+
const el = n;
|
|
425
|
+
if (el.name === "style") out.push(el);
|
|
426
|
+
if (el.children?.length) findStyleNodes(el.children, out);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Parse each file's template, collect every `<style>` node with its source
|
|
432
|
+
* line (via htmlparser2 start indices), then pass the combined DOM through
|
|
433
|
+
* the framework's real Tailwind pipeline. The pipeline resolves imports
|
|
434
|
+
* (@maizzle/tailwindcss), compiles utilities from class attrs, lowers modern
|
|
435
|
+
* CSS via lightningcss, and resolves static calc() — so what we walk matches
|
|
436
|
+
* what ships.
|
|
437
|
+
*/
|
|
438
|
+
async function compileViaPipeline(streams, config, rootFile) {
|
|
439
|
+
const all = [];
|
|
440
|
+
const tracked = [];
|
|
441
|
+
for (const s of streams) {
|
|
442
|
+
if (!s.template) continue;
|
|
443
|
+
const templateStart = s.source.indexOf(s.template.content);
|
|
444
|
+
const nodes = parseWithIndices(s.template.content);
|
|
445
|
+
for (const styleNode of findStyleNodes(nodes)) {
|
|
446
|
+
const startIdx = styleNode.startIndex ?? 0;
|
|
447
|
+
const line = offsetToLine(s.source, templateStart + startIdx);
|
|
448
|
+
tracked.push({
|
|
449
|
+
node: styleNode,
|
|
450
|
+
file: s.path,
|
|
451
|
+
line
|
|
10
452
|
});
|
|
11
|
-
req.on("end", () => resolve(body));
|
|
12
|
-
req.on("error", reject);
|
|
13
|
-
});
|
|
14
|
-
if (!html) {
|
|
15
|
-
res.setHeader("Content-Type", "application/json");
|
|
16
|
-
res.end(JSON.stringify([]));
|
|
17
|
-
return;
|
|
18
453
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
454
|
+
for (const n of nodes) all.push(n);
|
|
455
|
+
}
|
|
456
|
+
if (!tracked.length) return [];
|
|
457
|
+
try {
|
|
458
|
+
await tailwindcss(all, config, rootFile);
|
|
459
|
+
} catch {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
return tracked.map((t) => {
|
|
463
|
+
const txt = t.node.children?.find((c) => c.type === "text");
|
|
464
|
+
return txt?.data ? {
|
|
465
|
+
file: t.file,
|
|
466
|
+
css: txt.data,
|
|
467
|
+
line: t.line
|
|
468
|
+
} : null;
|
|
469
|
+
}).filter((x) => x !== null);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Walk CSS AST with detectors. Calls onHit per feature hit.
|
|
473
|
+
* `selector` is the containing rule's selector (undefined if no rule ancestor).
|
|
474
|
+
*/
|
|
475
|
+
function walkCss(css, idx, onHit) {
|
|
476
|
+
let root;
|
|
477
|
+
try {
|
|
478
|
+
root = safeParser(css);
|
|
479
|
+
} catch {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const containingSelector = (n) => {
|
|
483
|
+
let p = n?.parent;
|
|
484
|
+
while (p && p.type !== "root") {
|
|
485
|
+
if (p.type === "rule") return p.selector;
|
|
486
|
+
p = p.parent;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
if (idx.cssComments) root.walkComments((c) => {
|
|
490
|
+
onHit(idx.cssComments, {
|
|
491
|
+
line: c.source?.start?.line,
|
|
492
|
+
selector: containingSelector(c)
|
|
27
493
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
494
|
+
});
|
|
495
|
+
root.walkAtRules((atRule) => {
|
|
496
|
+
const line = atRule.source?.start?.line;
|
|
497
|
+
let sel = containingSelector(atRule);
|
|
498
|
+
if (atRule.name === "media" && !sel) {
|
|
499
|
+
const innerSelectors = [];
|
|
500
|
+
atRule.walkRules((r) => {
|
|
501
|
+
innerSelectors.push(r.selector);
|
|
502
|
+
});
|
|
503
|
+
if (innerSelectors.length) sel = innerSelectors.join(", ");
|
|
504
|
+
}
|
|
505
|
+
if (atRule.name === "media") {
|
|
506
|
+
const specific = [];
|
|
507
|
+
if (idx.cssMediaFeature.size) {
|
|
508
|
+
for (const [feat, fs2] of idx.cssMediaFeature) if (atRule.params.includes(`(${feat}`) || atRule.params.includes(feat)) specific.push(...fs2);
|
|
509
|
+
}
|
|
510
|
+
if (specific.length) for (const f of specific) onHit(f, {
|
|
511
|
+
line,
|
|
512
|
+
selector: sel
|
|
513
|
+
});
|
|
514
|
+
else {
|
|
515
|
+
const fs = idx.cssAtRule.get("media");
|
|
516
|
+
if (fs) for (const f of fs) onHit(f, {
|
|
517
|
+
line,
|
|
518
|
+
selector: sel
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
const fs = idx.cssAtRule.get(atRule.name);
|
|
523
|
+
if (fs) for (const f of fs) onHit(f, {
|
|
524
|
+
line,
|
|
525
|
+
selector: sel
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
root.walkRules((rule) => {
|
|
530
|
+
const line = rule.source?.start?.line;
|
|
531
|
+
const sel = rule.selector;
|
|
532
|
+
if (idx.cssPseudoClass.size) {
|
|
533
|
+
for (const [name, fs] of idx.cssPseudoClass) if (new RegExp(`(^|[^:]):${escapeRe(name)}(\\b|\\()`).test(sel)) for (const f of fs) onHit(f, {
|
|
534
|
+
line,
|
|
535
|
+
selector: sel
|
|
536
|
+
});
|
|
33
537
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
538
|
+
if (idx.cssPseudoElement.size) {
|
|
539
|
+
for (const [name, fs] of idx.cssPseudoElement) if (new RegExp(`::${escapeRe(name)}\\b`).test(sel)) for (const f of fs) onHit(f, {
|
|
540
|
+
line,
|
|
541
|
+
selector: sel
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
root.walkDecls((decl) => {
|
|
546
|
+
const line = decl.source?.start?.line;
|
|
547
|
+
const sel = containingSelector(decl);
|
|
548
|
+
const prop = decl.prop;
|
|
549
|
+
if (idx.cssImportant && decl.important) onHit(idx.cssImportant, {
|
|
550
|
+
line,
|
|
551
|
+
selector: sel
|
|
41
552
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
title: issue.title,
|
|
46
|
-
notes: issue.notes,
|
|
47
|
-
line: issue.position?.start.line
|
|
553
|
+
if (idx.cssVariables && prop.startsWith("--")) onHit(idx.cssVariables, {
|
|
554
|
+
line,
|
|
555
|
+
selector: sel
|
|
48
556
|
});
|
|
49
|
-
const
|
|
50
|
-
for (const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
notes: [...issue.notes]
|
|
61
|
-
});
|
|
62
|
-
} else grouped.set(key, {
|
|
63
|
-
type: issue.type,
|
|
64
|
-
title: issue.title,
|
|
65
|
-
category: categoryMap.get(issue.title) || "others",
|
|
66
|
-
clients: [{
|
|
67
|
-
name: clientName,
|
|
68
|
-
notes: [...issue.notes]
|
|
69
|
-
}],
|
|
70
|
-
url: urlMap.get(issue.title),
|
|
71
|
-
line: issue.line
|
|
557
|
+
const fs = idx.cssProp.get(prop);
|
|
558
|
+
if (fs) for (const f of fs) onHit(f, {
|
|
559
|
+
line,
|
|
560
|
+
selector: sel
|
|
561
|
+
});
|
|
562
|
+
const pvs = idx.cssPropValue.get(prop);
|
|
563
|
+
if (pvs) {
|
|
564
|
+
const v = decl.value.trim().toLowerCase();
|
|
565
|
+
for (const pv of pvs) if (v === pv.value) onHit(pv.feature, {
|
|
566
|
+
line,
|
|
567
|
+
selector: sel
|
|
72
568
|
});
|
|
73
569
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
570
|
+
if (idx.cssFunction.size || idx.cssUnit.size || idx.cssVariables || idx.cssModernColor) try {
|
|
571
|
+
valueParser(decl.value).walk((n) => {
|
|
572
|
+
if (n.type === "function") {
|
|
573
|
+
const fname = n.value.toLowerCase();
|
|
574
|
+
const fs2 = idx.cssFunction.get(fname);
|
|
575
|
+
if (fs2) for (const f of fs2) onHit(f, {
|
|
576
|
+
line,
|
|
577
|
+
selector: sel
|
|
578
|
+
});
|
|
579
|
+
if (idx.cssVariables && fname === "var") onHit(idx.cssVariables, {
|
|
580
|
+
line,
|
|
581
|
+
selector: sel
|
|
582
|
+
});
|
|
583
|
+
if (idx.cssModernColor && MODERN_COLOR_FNS.has(fname)) onHit(idx.cssModernColor, {
|
|
584
|
+
line,
|
|
585
|
+
selector: sel
|
|
586
|
+
});
|
|
587
|
+
} else if (n.type === "word") {
|
|
588
|
+
const m = /^-?\d*\.?\d+([a-z%]+)$/i.exec(n.value);
|
|
589
|
+
if (m) {
|
|
590
|
+
const unit = m[1].toLowerCase();
|
|
591
|
+
const fs2 = idx.cssUnit.get(unit);
|
|
592
|
+
if (fs2) for (const f of fs2) onHit(f, {
|
|
593
|
+
line,
|
|
594
|
+
selector: sel
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
} catch {}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const MODERN_COLOR_FNS = new Set([
|
|
603
|
+
"oklch",
|
|
604
|
+
"oklab",
|
|
605
|
+
"lch",
|
|
606
|
+
"lab",
|
|
607
|
+
"color",
|
|
608
|
+
"color-mix",
|
|
609
|
+
"hwb"
|
|
610
|
+
]);
|
|
611
|
+
function escapeRe(s) {
|
|
612
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
613
|
+
}
|
|
614
|
+
function offsetToLine(source, offset) {
|
|
615
|
+
let line = 1;
|
|
616
|
+
for (let i = 0; i < offset && i < source.length; i++) if (source.charCodeAt(i) === 10) line++;
|
|
617
|
+
return line;
|
|
618
|
+
}
|
|
619
|
+
function walkTemplate(html, idx, fileLineOffset, source, templateStartOffset, onHit) {
|
|
620
|
+
const semanticTags = new Set([
|
|
621
|
+
"article",
|
|
622
|
+
"aside",
|
|
623
|
+
"details",
|
|
624
|
+
"figcaption",
|
|
625
|
+
"figure",
|
|
626
|
+
"footer",
|
|
627
|
+
"header",
|
|
628
|
+
"main",
|
|
629
|
+
"mark",
|
|
630
|
+
"nav",
|
|
631
|
+
"section",
|
|
632
|
+
"time",
|
|
633
|
+
"summary"
|
|
634
|
+
]);
|
|
635
|
+
const bodyScopeStack = [];
|
|
636
|
+
const parser = new Parser({
|
|
637
|
+
onopentag(tag, attrs) {
|
|
638
|
+
const startIdx = parser.startIndex;
|
|
639
|
+
const line = offsetToLine(source, templateStartOffset + startIdx);
|
|
640
|
+
const tagFs = idx.htmlTag.get(tag);
|
|
641
|
+
if (tagFs) for (const f of tagFs) onHit(f, line);
|
|
642
|
+
if (idx.htmlSemantics && semanticTags.has(tag)) onHit(idx.htmlSemantics, line);
|
|
643
|
+
if (tag === "style" && bodyScopeStack.length > 0 && idx.htmlStyleInBody) onHit(idx.htmlStyleInBody, line);
|
|
644
|
+
if (tag === "body") bodyScopeStack.push(tag);
|
|
645
|
+
else if (tag === "teleport" && /body/i.test(attrs.to ?? "")) bodyScopeStack.push(tag);
|
|
646
|
+
for (const attr in attrs) {
|
|
647
|
+
const attrFs = idx.htmlAttr.get(attr);
|
|
648
|
+
if (attrFs) for (const f of attrFs) onHit(f, line);
|
|
649
|
+
}
|
|
650
|
+
if (tag === "input" && attrs.type) {
|
|
651
|
+
const fs = idx.htmlInputType.get(attrs.type.toLowerCase());
|
|
652
|
+
if (fs) for (const f of fs) onHit(f, line);
|
|
653
|
+
}
|
|
654
|
+
if (tag === "button" && attrs.type) {
|
|
655
|
+
const fs = idx.htmlButtonType.get(attrs.type.toLowerCase());
|
|
656
|
+
if (fs) for (const f of fs) onHit(f, line);
|
|
657
|
+
}
|
|
658
|
+
if (tag === "a" && attrs.href) {
|
|
659
|
+
const h = attrs.href.trim();
|
|
660
|
+
if (idx.htmlMailtoLinks && /^mailto:/i.test(h)) onHit(idx.htmlMailtoLinks, line);
|
|
661
|
+
else if (idx.htmlAnchorLinks && h.startsWith("#")) onHit(idx.htmlAnchorLinks, line);
|
|
662
|
+
}
|
|
663
|
+
if (tag === "meta" && idx.htmlMetaColorScheme && attrs.name?.toLowerCase() === "color-scheme") onHit(idx.htmlMetaColorScheme, line);
|
|
664
|
+
if (idx.imageExt.size && (attrs.src || attrs.srcset)) {
|
|
665
|
+
const urls = [];
|
|
666
|
+
if (attrs.src) urls.push(attrs.src);
|
|
667
|
+
if (attrs.srcset) for (const part of attrs.srcset.split(",")) urls.push(part.trim().split(/\s+/)[0]);
|
|
668
|
+
for (const url of urls) {
|
|
669
|
+
const m = /\.([a-z0-9]+)(?:\?|#|$)/i.exec(url);
|
|
670
|
+
if (!m) continue;
|
|
671
|
+
const fs = idx.imageExt.get(m[1].toLowerCase());
|
|
672
|
+
if (fs) for (const f of fs) onHit(f, line);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (attrs.style) scanInlineStyle(attrs.style, idx, line, onHit);
|
|
676
|
+
},
|
|
677
|
+
onclosetag(tag) {
|
|
678
|
+
if (bodyScopeStack[bodyScopeStack.length - 1] === tag) bodyScopeStack.pop();
|
|
679
|
+
},
|
|
680
|
+
onprocessinginstruction(name) {
|
|
681
|
+
if (idx.htmlDoctype && name.toLowerCase() === "!doctype") {
|
|
682
|
+
const startIdx = parser.startIndex;
|
|
683
|
+
onHit(idx.htmlDoctype, offsetToLine(source, templateStartOffset + startIdx));
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
oncomment() {
|
|
687
|
+
if (idx.htmlComments) {
|
|
688
|
+
const startIdx = parser.startIndex;
|
|
689
|
+
onHit(idx.htmlComments, offsetToLine(source, templateStartOffset + startIdx));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}, {
|
|
693
|
+
decodeEntities: false,
|
|
694
|
+
lowerCaseTags: true,
|
|
695
|
+
lowerCaseAttributeNames: true
|
|
696
|
+
});
|
|
697
|
+
parser.write(html);
|
|
698
|
+
parser.end();
|
|
699
|
+
}
|
|
700
|
+
function scanInlineStyle(style, idx, line, onHit) {
|
|
701
|
+
const wrapped = `*{${style}}`;
|
|
702
|
+
try {
|
|
703
|
+
safeParser(wrapped).walkDecls((decl) => {
|
|
704
|
+
if (idx.cssImportant && decl.important) onHit(idx.cssImportant, line);
|
|
705
|
+
const fs = idx.cssProp.get(decl.prop);
|
|
706
|
+
if (fs) for (const f of fs) onHit(f, line);
|
|
707
|
+
if (idx.cssVariables && decl.prop.startsWith("--")) onHit(idx.cssVariables, line);
|
|
708
|
+
const pvs = idx.cssPropValue.get(decl.prop);
|
|
709
|
+
if (pvs) {
|
|
710
|
+
const v = decl.value.trim().toLowerCase();
|
|
711
|
+
for (const pv of pvs) if (v === pv.value) onHit(pv.feature, line);
|
|
712
|
+
}
|
|
713
|
+
if (idx.cssFunction.size || idx.cssUnit.size || idx.cssVariables || idx.cssModernColor) try {
|
|
714
|
+
valueParser(decl.value).walk((n) => {
|
|
715
|
+
if (n.type === "function") {
|
|
716
|
+
const fname = n.value.toLowerCase();
|
|
717
|
+
const fs2 = idx.cssFunction.get(fname);
|
|
718
|
+
if (fs2) for (const f of fs2) onHit(f, line);
|
|
719
|
+
if (idx.cssVariables && fname === "var") onHit(idx.cssVariables, line);
|
|
720
|
+
if (idx.cssModernColor && MODERN_COLOR_FNS.has(fname)) onHit(idx.cssModernColor, line);
|
|
721
|
+
} else if (n.type === "word") {
|
|
722
|
+
const m = /^-?\d*\.?\d+([a-z%]+)$/i.exec(n.value);
|
|
723
|
+
if (m) {
|
|
724
|
+
const fs2 = idx.cssUnit.get(m[1].toLowerCase());
|
|
725
|
+
if (fs2) for (const f of fs2) onHit(f, line);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
} catch {}
|
|
730
|
+
});
|
|
731
|
+
} catch {}
|
|
732
|
+
}
|
|
733
|
+
function labelFor(idx, level) {
|
|
734
|
+
const n = idx.nicenames;
|
|
735
|
+
if (level === "unsupported") return n.unsupported ?? "Not supported";
|
|
736
|
+
if (level === "mitigated") return n.mitigated ?? "Partially supported";
|
|
737
|
+
return n.unknown ?? "Support unknown";
|
|
738
|
+
}
|
|
739
|
+
async function scan(rootFile, config, componentDirs, allowedClients) {
|
|
740
|
+
const idx = await initCompatibility();
|
|
741
|
+
if (!idx) return [];
|
|
742
|
+
const componentMap = await buildComponentMap(config.root ?? process.cwd(), componentDirs);
|
|
743
|
+
const streams = [];
|
|
744
|
+
collectStreams(rootFile, componentMap, /* @__PURE__ */ new Set(), streams);
|
|
745
|
+
const issues = [];
|
|
746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
747
|
+
const resolvedCache = /* @__PURE__ */ new Map();
|
|
748
|
+
const resolveSupport = (f) => {
|
|
749
|
+
let cached = resolvedCache.get(f.slug);
|
|
750
|
+
if (cached === void 0) {
|
|
751
|
+
cached = computeSupport(f.stats, idx.familyNicenames, allowedClients);
|
|
752
|
+
resolvedCache.set(f.slug, cached);
|
|
753
|
+
}
|
|
754
|
+
return cached;
|
|
755
|
+
};
|
|
756
|
+
const add = (f, file, line) => {
|
|
757
|
+
const key = `${f.slug}|${file}|${line ?? 0}`;
|
|
758
|
+
if (seen.has(key)) return;
|
|
759
|
+
const support = resolveSupport(f);
|
|
760
|
+
if (!support) return;
|
|
761
|
+
seen.add(key);
|
|
762
|
+
issues.push({
|
|
763
|
+
kind: "compat",
|
|
764
|
+
slug: f.slug,
|
|
765
|
+
title: f.title,
|
|
766
|
+
url: f.url,
|
|
767
|
+
category: f.category,
|
|
768
|
+
supportLevel: support.level,
|
|
769
|
+
supportLabel: labelFor(idx, support.level),
|
|
770
|
+
affectedClients: support.affected,
|
|
771
|
+
line,
|
|
772
|
+
file
|
|
86
773
|
});
|
|
774
|
+
};
|
|
775
|
+
const compiledBlocks = await compileViaPipeline(streams, config, rootFile);
|
|
776
|
+
for (const block of compiledBlocks) walkCss(block.css, idx, (feature, node) => {
|
|
777
|
+
const locations = classLocations(node.selector, streams);
|
|
778
|
+
if (!locations.length) {
|
|
779
|
+
add(feature, block.file, block.line);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (feature.slug.startsWith("css-at-media")) add(feature, locations[0].file, locations[0].line);
|
|
783
|
+
else for (const { file, line } of locations) add(feature, file, line);
|
|
784
|
+
});
|
|
785
|
+
for (const s of streams) {
|
|
786
|
+
if (!s.template) continue;
|
|
787
|
+
walkTemplate(s.template.content, idx, s.template.offset, s.source, s.source.indexOf(s.template.content), (feature, line) => add(feature, s.path, line));
|
|
788
|
+
}
|
|
789
|
+
return issues;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Return every (file, line) where any class from the selector appears in a
|
|
793
|
+
* template. Scans every stream so a shared utility class used in multiple
|
|
794
|
+
* components surfaces once per occurrence.
|
|
795
|
+
*/
|
|
796
|
+
function classLocations(selector, streams) {
|
|
797
|
+
if (!selector) return [];
|
|
798
|
+
const classNames = extractSelectorClasses(selector);
|
|
799
|
+
if (!classNames.length) return [];
|
|
800
|
+
const out = [];
|
|
801
|
+
const seen = /* @__PURE__ */ new Set();
|
|
802
|
+
for (const cn of classNames) for (const s of streams) {
|
|
803
|
+
if (!s.classes.has(cn) || !s.template) continue;
|
|
804
|
+
const tpl = s.template.content;
|
|
805
|
+
const tplStart = s.source.indexOf(tpl);
|
|
806
|
+
let pos = 0;
|
|
807
|
+
while (true) {
|
|
808
|
+
const i = tpl.indexOf(cn, pos);
|
|
809
|
+
if (i < 0) break;
|
|
810
|
+
pos = i + cn.length;
|
|
811
|
+
const before = i > 0 ? tpl[i - 1] : " ";
|
|
812
|
+
const after = i + cn.length < tpl.length ? tpl[i + cn.length] : " ";
|
|
813
|
+
if (!isClassBoundary(before) || !isClassBoundary(after)) continue;
|
|
814
|
+
const line = offsetToLine(s.source, tplStart + i);
|
|
815
|
+
const key = `${s.path}|${line}`;
|
|
816
|
+
if (seen.has(key)) continue;
|
|
817
|
+
seen.add(key);
|
|
818
|
+
out.push({
|
|
819
|
+
file: s.path,
|
|
820
|
+
line
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
function isClassBoundary(c) {
|
|
827
|
+
return c === " " || c === " " || c === "\n" || c === "\r" || c === "\"" || c === "'";
|
|
828
|
+
}
|
|
829
|
+
function extractSelectorClasses(selector) {
|
|
830
|
+
const out = [];
|
|
831
|
+
const re = /\.((?:\\.|[\w-])+)/g;
|
|
832
|
+
let m;
|
|
833
|
+
while ((m = re.exec(selector)) !== null) out.push(m[1].replace(/\\(.)/g, "$1"));
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
const CATEGORY_ORDER = [
|
|
837
|
+
"css",
|
|
838
|
+
"html",
|
|
839
|
+
"image",
|
|
840
|
+
"others"
|
|
841
|
+
];
|
|
842
|
+
const LEVEL_ORDER = {
|
|
843
|
+
error: 0,
|
|
844
|
+
unsupported: 1,
|
|
845
|
+
warning: 2,
|
|
846
|
+
mitigated: 3,
|
|
847
|
+
unknown: 4
|
|
848
|
+
};
|
|
849
|
+
function orderKey(i) {
|
|
850
|
+
if (i.kind === "lint") return LEVEL_ORDER[i.severity] ?? 99;
|
|
851
|
+
return LEVEL_ORDER[i.supportLevel] ?? 99;
|
|
852
|
+
}
|
|
853
|
+
function resolveChecksConfig(config) {
|
|
854
|
+
const raw = config.server?.checks;
|
|
855
|
+
if (raw === false) return null;
|
|
856
|
+
return {
|
|
857
|
+
clients: raw?.clients === "all" ? "all" : Array.isArray(raw?.clients) && raw.clients.length ? new Set(raw.clients) : DEFAULT_CLIENTS,
|
|
858
|
+
level: raw?.level ?? null
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function passesLevelFilter(issue, level) {
|
|
862
|
+
if (!level) return true;
|
|
863
|
+
if (level === "lint") return issue.kind === "lint";
|
|
864
|
+
if (issue.kind === "lint") return level === "error" ? issue.severity === "error" : issue.severity === "warning";
|
|
865
|
+
return level === "error" ? issue.supportLevel === "unsupported" : issue.supportLevel === "mitigated" || issue.supportLevel === "unknown";
|
|
866
|
+
}
|
|
867
|
+
async function serveCompatibility(url, res, config, componentDirs) {
|
|
868
|
+
const filePath = url.replace("/__maizzle/compatibility/", "").replace(/\?.*$/, "");
|
|
869
|
+
const checksCfg = resolveChecksConfig(config);
|
|
870
|
+
try {
|
|
87
871
|
res.setHeader("Content-Type", "application/json");
|
|
88
|
-
|
|
872
|
+
if (!checksCfg) {
|
|
873
|
+
res.end(JSON.stringify([]));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const absolutePath = resolve(filePath);
|
|
877
|
+
const [compatIssues, lintIssues] = await Promise.all([scan(absolutePath, config, componentDirs, checksCfg.clients), scanLint(absolutePath, config, componentDirs)]);
|
|
878
|
+
const idx = await initCompatibility();
|
|
879
|
+
const lintAsIssues = lintIssues.map((li) => {
|
|
880
|
+
const info = li.slug ? idx?.bySlug.get(li.slug) : void 0;
|
|
881
|
+
return {
|
|
882
|
+
kind: "lint",
|
|
883
|
+
slug: li.slug,
|
|
884
|
+
title: li.title,
|
|
885
|
+
url: info?.url,
|
|
886
|
+
category: li.category,
|
|
887
|
+
severity: li.type,
|
|
888
|
+
message: li.message,
|
|
889
|
+
line: li.line,
|
|
890
|
+
file: li.file
|
|
891
|
+
};
|
|
892
|
+
});
|
|
893
|
+
let issues = [...compatIssues, ...lintAsIssues];
|
|
894
|
+
if (checksCfg.level) issues = issues.filter((i) => passesLevelFilter(i, checksCfg.level));
|
|
895
|
+
issues.sort((a, b) => {
|
|
896
|
+
const c = CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
|
|
897
|
+
if (c) return c;
|
|
898
|
+
const l = orderKey(a) - orderKey(b);
|
|
899
|
+
if (l) return l;
|
|
900
|
+
return (a.slug ?? a.title).localeCompare(b.slug ?? b.title);
|
|
901
|
+
});
|
|
902
|
+
res.end(JSON.stringify(issues));
|
|
89
903
|
} catch (error) {
|
|
90
904
|
res.statusCode = 500;
|
|
91
905
|
res.end(JSON.stringify({ error: error.message }));
|
|
@@ -93,5 +907,5 @@ async function serveCompatibility(req, res) {
|
|
|
93
907
|
}
|
|
94
908
|
|
|
95
909
|
//#endregion
|
|
96
|
-
export { serveCompatibility };
|
|
910
|
+
export { initCompatibility, serveCompatibility };
|
|
97
911
|
//# sourceMappingURL=compatibility.mjs.map
|