Fix H5 all-in-one asset paths

This commit is contained in:
Chopper711
2026-06-17 16:45:36 +08:00
parent a19f039da4
commit e4e6bd272c
11 changed files with 185 additions and 17 deletions

View File

@@ -0,0 +1,143 @@
import assert from 'node:assert/strict';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import valueParser from 'postcss-value-parser';
import vueTemplateCompiler from 'vue-template-compiler';
const roots = ['pages', 'components'];
const extraFiles = ['App.vue', 'uni.scss'];
const styleExtensions = new Set(['.css', '.scss']);
function listFiles(dir) {
const result = [];
for (const entry of readdirSync(dir)) {
const fullPath = path.join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
result.push(...listFiles(fullPath));
} else {
result.push(fullPath);
}
}
return result;
}
function stripCssCommentsPreserveOffsets(source) {
let output = '';
let quote = null;
for (let i = 0; i < source.length; i += 1) {
const char = source[i];
const next = source[i + 1];
if (quote) {
output += char;
if (char === '\\') {
i += 1;
output += source[i] || '';
} else if (char === quote) {
quote = null;
}
continue;
}
if (char === '"' || char === "'") {
quote = char;
output += char;
continue;
}
if (char === '/' && next === '*') {
output += ' ';
i += 2;
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
output += source[i] === '\n' ? '\n' : ' ';
i += 1;
}
output += ' ';
i += 1;
continue;
}
if (char === '/' && next === '/') {
output += ' ';
i += 2;
while (i < source.length && source[i] !== '\n') {
output += ' ';
i += 1;
}
if (source[i] === '\n') {
output += '\n';
}
continue;
}
output += char;
}
return output;
}
function lineForOffset(source, offset) {
return source.slice(0, offset).split('\n').length;
}
function collectRootStaticStyleUrls(filePath, styleSource, sourceOffset = 0, originalSource = styleSource) {
const stripped = stripCssCommentsPreserveOffsets(styleSource);
const findings = [];
valueParser(stripped).walk((node) => {
if (node.type !== 'function' || node.value.toLowerCase() !== 'url') {
return;
}
const url = valueParser.stringify(node.nodes).trim().replace(/^['"]|['"]$/g, '');
if (!url.startsWith('/static/')) {
return;
}
findings.push({
filePath,
line: lineForOffset(originalSource, sourceOffset + node.sourceIndex),
url,
});
});
return findings;
}
function styleSourcesForFile(filePath) {
const source = readFileSync(filePath, 'utf8');
if (filePath.endsWith('.vue')) {
const descriptor = vueTemplateCompiler.parseComponent(source);
return descriptor.styles.map((style) => ({
source: style.content,
sourceOffset: style.start,
originalSource: source,
}));
}
return [{ source, sourceOffset: 0, originalSource: source }];
}
test('style url() references do not use H5-root /static paths', () => {
const files = [
...roots.flatMap((root) => listFiles(root)),
...extraFiles,
].filter((filePath) => filePath.endsWith('.vue') || styleExtensions.has(path.extname(filePath)));
const findings = files.flatMap((filePath) =>
styleSourcesForFile(filePath).flatMap(({ source, sourceOffset, originalSource }) =>
collectRootStaticStyleUrls(filePath, source, sourceOffset, originalSource)
)
);
assert.deepEqual(
findings,
[],
`style url() must use relative static paths so H5 /h5 deployments do not request site-root assets:\n${
findings.map((item) => `${item.filePath}:${item.line} ${item.url}`).join('\n')
}`
);
});