Skip to content

Commit 8dde756

Browse files
committed
fix: support tailwind source exclusions
1 parent 6501b37 commit 8dde756

18 files changed

Lines changed: 2249 additions & 177 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"weapp-tailwindcss": patch
3+
---
4+
5+
修复生成模式的额外源码候选扫描绕过 Tailwind 扫描排除规则的问题,确保 Tailwind CSS v3 `content` 中的 `!` 排除以及 Tailwind CSS v4 `@source not` 不会被 Vite/PostCSS 补扫重新引入。
6+
7+
新增 e2e-watch HMR 速度报告产物,CI 每次 watch 回归都会输出 hot update 的 avg/p50/p95/max、按项目和按场景拆分的耗时摘要,并随 artifact 上传。
8+
9+
补齐 Tailwind CSS v4 `@source inline(...)``@source not inline(...)` 在 Vite/PostCSS 生成模式下的候选收集支持,覆盖 brace expansion、换行参数、`source(none)`/全量排除以及内联排除文件候选等场景。

.github/scripts/e2e-watch-report.cjs

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const ROUND_NAME_RE = /round=([a-z0-9-]+)/g
3838
const ROOT_DIR = path.resolve(process.cwd(), 'e2e/benchmark/e2e-watch-hmr')
3939
const SNAPSHOTS_DIR = path.join(ROOT_DIR, 'snapshots')
4040
const FAILURES_DIR = path.join(ROOT_DIR, 'failures')
41+
const SPEED_REPORT_JSON = path.join(ROOT_DIR, 'hmr-speed-report.json')
42+
const SPEED_REPORT_MD = path.join(ROOT_DIR, 'hmr-speed-report.md')
4143

4244
function ensureFailuresDir() {
4345
fs.mkdirSync(FAILURES_DIR, { recursive: true })
@@ -229,6 +231,271 @@ function pickMetricFromReport(report, primary) {
229231
return undefined
230232
}
231233

234+
function toFiniteNumber(value) {
235+
return Number.isFinite(value) ? value : undefined
236+
}
237+
238+
function pushSpeedSample(samples, item) {
239+
const hotUpdateMs = toFiniteNumber(item.hotUpdateMs)
240+
const rollbackMs = toFiniteNumber(item.rollbackMs)
241+
if (hotUpdateMs == null) {
242+
return
243+
}
244+
samples.push({
245+
caseName: item.caseName || 'unknown',
246+
project: item.project || 'unknown',
247+
surface: item.surface || 'unknown',
248+
sourceFile: item.sourceFile || '',
249+
hotUpdateMs,
250+
rollbackMs,
251+
initialReadyMs: toFiniteNumber(item.initialReadyMs),
252+
reportFile: item.reportFile || '',
253+
})
254+
}
255+
256+
function percentile(sortedValues, ratio) {
257+
if (sortedValues.length === 0) {
258+
return 0
259+
}
260+
const index = Math.ceil(sortedValues.length * ratio) - 1
261+
return sortedValues[Math.min(Math.max(index, 0), sortedValues.length - 1)]
262+
}
263+
264+
function summarizeSpeedSamples(samples) {
265+
if (samples.length === 0) {
266+
return {
267+
count: 0,
268+
avgMs: 0,
269+
minMs: 0,
270+
maxMs: 0,
271+
p50Ms: 0,
272+
p95Ms: 0,
273+
}
274+
}
275+
const values = samples
276+
.map(item => item.hotUpdateMs)
277+
.filter(Number.isFinite)
278+
.sort((a, b) => a - b)
279+
const sum = values.reduce((total, value) => total + value, 0)
280+
return {
281+
count: values.length,
282+
avgMs: Math.round(sum / values.length),
283+
minMs: values[0],
284+
maxMs: values.at(-1),
285+
p50Ms: percentile(values, 0.5),
286+
p95Ms: percentile(values, 0.95),
287+
}
288+
}
289+
290+
function groupSpeedSamples(samples, key) {
291+
const grouped = new Map()
292+
for (const sample of samples) {
293+
const groupKey = sample[key] || 'unknown'
294+
const list = grouped.get(groupKey) || []
295+
list.push(sample)
296+
grouped.set(groupKey, list)
297+
}
298+
return Object.fromEntries(
299+
[...grouped.entries()]
300+
.sort(([a], [b]) => a.localeCompare(b))
301+
.map(([name, groupSamples]) => [name, summarizeSpeedSamples(groupSamples)]),
302+
)
303+
}
304+
305+
function collectSpeedSamplesFromReport(report, reportFile) {
306+
const samples = []
307+
for (const oneCase of report?.cases ?? []) {
308+
const caseName = oneCase.name || oneCase.label || oneCase.project || 'unknown'
309+
const project = oneCase.project || 'unknown'
310+
pushSpeedSample(samples, {
311+
caseName,
312+
project,
313+
surface: 'case-template-preferred',
314+
sourceFile: '',
315+
hotUpdateMs: oneCase.hotUpdateEffectiveMs,
316+
rollbackMs: oneCase.rollbackEffectiveMs,
317+
initialReadyMs: oneCase.initialReadyMs,
318+
reportFile,
319+
})
320+
321+
for (const mutation of oneCase.mutationMetrics ?? []) {
322+
const sourceFile = mutation.sourceFile || ''
323+
if (Array.isArray(mutation.rounds)) {
324+
for (const round of mutation.rounds) {
325+
pushSpeedSample(samples, {
326+
caseName,
327+
project,
328+
surface: `${mutation.mutationKind}:${round.roundName}`,
329+
sourceFile,
330+
hotUpdateMs: round.hotUpdateEffectiveMs,
331+
rollbackMs: round.rollbackEffectiveMs,
332+
initialReadyMs: oneCase.initialReadyMs,
333+
reportFile,
334+
})
335+
}
336+
}
337+
else {
338+
pushSpeedSample(samples, {
339+
caseName,
340+
project,
341+
surface: mutation.mutationKind,
342+
sourceFile,
343+
hotUpdateMs: mutation.hotUpdateEffectiveMs,
344+
rollbackMs: mutation.rollbackEffectiveMs,
345+
initialReadyMs: oneCase.initialReadyMs,
346+
reportFile,
347+
})
348+
}
349+
350+
for (const [extraKey, extraValue] of [
351+
['added-class', mutation.addedClassHmr],
352+
['same-class-literal', mutation.sameClassLiteralHmr],
353+
['comment-carrier', mutation.commentCarrierHmr],
354+
]) {
355+
if (!extraValue) {
356+
continue
357+
}
358+
pushSpeedSample(samples, {
359+
caseName,
360+
project,
361+
surface: `${mutation.mutationKind}:${extraKey}`,
362+
sourceFile,
363+
hotUpdateMs: extraValue.hotUpdateEffectiveMs,
364+
rollbackMs: extraValue.rollbackEffectiveMs,
365+
initialReadyMs: oneCase.initialReadyMs,
366+
reportFile,
367+
})
368+
}
369+
}
370+
371+
for (const subPackage of oneCase.subPackageMutationMetrics ?? []) {
372+
for (const [kind, mutation] of [
373+
['template', subPackage.template],
374+
['style', subPackage.style],
375+
]) {
376+
if (!mutation) {
377+
continue
378+
}
379+
pushSpeedSample(samples, {
380+
caseName,
381+
project,
382+
surface: `subpackage:${subPackage.root}:${kind}`,
383+
sourceFile: mutation.sourceFile || '',
384+
hotUpdateMs: mutation.hotUpdateEffectiveMs,
385+
rollbackMs: mutation.rollbackEffectiveMs,
386+
initialReadyMs: oneCase.initialReadyMs,
387+
reportFile,
388+
})
389+
}
390+
}
391+
}
392+
return samples
393+
}
394+
395+
function readWatchReports() {
396+
const reportFiles = listFilesSafe(ROOT_DIR, name =>
397+
name.endsWith('.json') && name !== path.basename(SPEED_REPORT_JSON))
398+
.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs)
399+
const reports = []
400+
for (const file of reportFiles) {
401+
try {
402+
reports.push({
403+
file,
404+
report: JSON.parse(readUtf8(file)),
405+
})
406+
}
407+
catch {}
408+
}
409+
return reports
410+
}
411+
412+
function buildSpeedReport() {
413+
const reports = readWatchReports()
414+
const samples = reports.flatMap(item =>
415+
collectSpeedSamplesFromReport(item.report, path.basename(item.file)))
416+
const initialReadyValues = samples
417+
.map(item => item.initialReadyMs)
418+
.filter(Number.isFinite)
419+
.sort((a, b) => a - b)
420+
const slowest = [...samples]
421+
.sort((a, b) => b.hotUpdateMs - a.hotUpdateMs)
422+
.slice(0, 10)
423+
424+
return {
425+
generatedAt: new Date().toISOString(),
426+
reportCount: reports.length,
427+
sampleCount: samples.length,
428+
summary: summarizeSpeedSamples(samples),
429+
initialReady: summarizeSpeedSamples(
430+
initialReadyValues.map(value => ({ hotUpdateMs: value })),
431+
),
432+
byProject: groupSpeedSamples(samples, 'project'),
433+
bySurface: groupSpeedSamples(samples, 'surface'),
434+
slowest,
435+
sourceReports: reports.map(item => path.basename(item.file)),
436+
}
437+
}
438+
439+
function renderSummaryTable(grouped) {
440+
const lines = [
441+
'| name | count | avg | p50 | p95 | min | max |',
442+
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
443+
]
444+
for (const [name, summary] of Object.entries(grouped)) {
445+
lines.push(
446+
`| ${name} | ${summary.count} | ${summary.avgMs}ms | ${summary.p50Ms}ms | ${summary.p95Ms}ms | ${summary.minMs}ms | ${summary.maxMs}ms |`,
447+
)
448+
}
449+
return lines
450+
}
451+
452+
function renderSpeedReportMarkdown(report) {
453+
const lines = []
454+
lines.push('# e2e-watch HMR 速度报告')
455+
lines.push('')
456+
lines.push(`- generated_at: ${report.generatedAt}`)
457+
lines.push(`- source reports: ${report.reportCount}`)
458+
lines.push(`- samples: ${report.sampleCount}`)
459+
lines.push(
460+
`- hot update: avg=${report.summary.avgMs}ms, p50=${report.summary.p50Ms}ms, p95=${report.summary.p95Ms}ms, max=${report.summary.maxMs}ms`,
461+
)
462+
lines.push(
463+
`- initial ready: avg=${report.initialReady.avgMs}ms, p50=${report.initialReady.p50Ms}ms, p95=${report.initialReady.p95Ms}ms, max=${report.initialReady.maxMs}ms`,
464+
)
465+
lines.push('')
466+
lines.push('## 按项目')
467+
lines.push(...renderSummaryTable(report.byProject))
468+
lines.push('')
469+
lines.push('## 按 HMR 场景')
470+
lines.push(...renderSummaryTable(report.bySurface))
471+
lines.push('')
472+
lines.push('## 最慢样本')
473+
if (report.slowest.length === 0) {
474+
lines.push('- no samples')
475+
}
476+
else {
477+
for (const sample of report.slowest) {
478+
lines.push(
479+
`- ${sample.hotUpdateMs}ms | ${sample.project} | ${sample.surface} | ${sample.sourceFile || 'n/a'} | ${sample.reportFile}`,
480+
)
481+
}
482+
}
483+
lines.push('')
484+
lines.push('## 实现依据')
485+
lines.push('- Tailwind v3/v4 官方 Vite/Webpack 插件在 watch 生命周期复用 compiler、scanner 与 candidates 集合。')
486+
lines.push('- 本仓库 HMR 回归沿用同一思路:启动一次开发 watcher,在同一 session 内连续验证 template/script/style/content/subpackage 变更,并记录增量输出与实际生效耗时。')
487+
return `${lines.join('\n')}\n`
488+
}
489+
490+
function generateSpeedReport() {
491+
fs.mkdirSync(ROOT_DIR, { recursive: true })
492+
const report = buildSpeedReport()
493+
fs.writeFileSync(SPEED_REPORT_JSON, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
494+
fs.writeFileSync(SPEED_REPORT_MD, renderSpeedReportMarkdown(report), 'utf8')
495+
process.stdout.write(`[e2e-watch] hmr speed report written: ${SPEED_REPORT_MD}\n`)
496+
return report
497+
}
498+
232499
function pickSnippet(source, probes) {
233500
if (!source) {
234501
return '(empty)'
@@ -632,6 +899,7 @@ function publishJobSummary() {
632899

633900
const title = process.env.SUMMARY_TITLE || 'e2e-watch'
634901
const rootCauseReportPath = path.join(FAILURES_DIR, 'root-cause-report.md')
902+
const speedReport = generateSpeedReport()
635903

636904
const reportFiles = listFilesSafe(ROOT_DIR, name => name.endsWith('.json'))
637905
reportFiles.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs)
@@ -674,6 +942,12 @@ function publishJobSummary() {
674942
lines.push(
675943
`- RCA report: ${fs.existsSync(rootCauseReportPath) ? '[root-cause-report.md](./e2e/benchmark/e2e-watch-hmr/failures/root-cause-report.md)' : 'not-found'}`,
676944
)
945+
lines.push(
946+
`- HMR speed: avg=${speedReport.summary.avgMs}ms, p50=${speedReport.summary.p50Ms}ms, p95=${speedReport.summary.p95Ms}ms, max=${speedReport.summary.maxMs}ms`,
947+
)
948+
lines.push(
949+
`- HMR speed report: ${fs.existsSync(SPEED_REPORT_MD) ? '[hmr-speed-report.md](./e2e/benchmark/e2e-watch-hmr/hmr-speed-report.md)' : 'not-found'}`,
950+
)
677951

678952
fs.appendFileSync(summaryPath, `${lines.join('\n')}\n`, 'utf8')
679953
}
@@ -692,6 +966,10 @@ function main() {
692966
publishJobSummary()
693967
return
694968
}
969+
if (command === 'speed-report') {
970+
generateSpeedReport()
971+
return
972+
}
695973
throw new Error(`unknown command: ${command || '(empty)'}`)
696974
}
697975

.github/workflows/e2e-watch.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@ jobs:
317317
uses: actions/upload-artifact@v4
318318
with:
319319
name: e2e-watch-hmr-pr-${{ matrix.runner_label }}-node${{ matrix['node-version'] || 22 }}-${{ matrix.watch_case }}-${{ matrix.round_profile }}-${{ github.run_id }}
320-
path: e2e/benchmark/e2e-watch-hmr/*.json
320+
path: |
321+
e2e/benchmark/e2e-watch-hmr/*.json
322+
e2e/benchmark/e2e-watch-hmr/hmr-speed-report.md
321323
if-no-files-found: ignore
322324
retention-days: 14
323325

@@ -518,7 +520,9 @@ jobs:
518520
uses: actions/upload-artifact@v4
519521
with:
520522
name: e2e-watch-hmr-nightly-${{ matrix.runner_label }}-node${{ matrix['node-version'] || 22 }}-${{ matrix.watch_case }}-${{ matrix.round_profile }}-${{ github.run_id }}
521-
path: e2e/benchmark/e2e-watch-hmr/*.json
523+
path: |
524+
e2e/benchmark/e2e-watch-hmr/*.json
525+
e2e/benchmark/e2e-watch-hmr/hmr-speed-report.md
522526
if-no-files-found: ignore
523527
retention-days: 14
524528

0 commit comments

Comments
 (0)