build: add Vite web production pipeline

This commit is contained in:
237899745
2026-07-27 13:07:10 +08:00
parent dce1e3622a
commit 6692736e88
16 changed files with 1564 additions and 22 deletions

View File

@@ -0,0 +1,34 @@
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const frontendRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputDirectory = path.join(frontendRoot, 'dist');
mkdirSync(outputDirectory, { recursive: true });
function copyEntry(source, destination) {
const entries = readdirSync(source, { withFileTypes: true });
mkdirSync(destination, { recursive: true });
for (const entry of entries) {
const sourcePath = path.join(source, entry.name);
const destinationPath = path.join(destination, entry.name);
if (entry.isDirectory()) {
copyEntry(sourcePath, destinationPath);
} else if (entry.isFile()) {
copyFileSync(sourcePath, destinationPath);
}
}
}
for (const entry of ['libs', 'favicon.ico']) {
const source = path.join(frontendRoot, entry);
if (existsSync(source)) {
const destination = path.join(outputDirectory, entry);
if (entry.includes('.')) {
copyFileSync(source, destination);
} else {
copyEntry(source, destination);
}
}
}

View File

@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const frontendRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const outputDirectory = path.join(frontendRoot, 'dist');
const htmlFiles = readdirSync(frontendRoot).filter((fileName) => fileName.endsWith('.html'));
assert.ok(existsSync(path.join(outputDirectory, 'index.html')), 'dist/index.html is missing');
assert.ok(existsSync(path.join(outputDirectory, 'favicon.ico')), 'dist/favicon.ico is missing');
assert.strictEqual(existsSync(path.join(outputDirectory, 'downloads')), false, 'runtime installers must stay outside dist');
for (const fileName of htmlFiles) {
const sourceHtml = readFileSync(path.join(frontendRoot, fileName), 'utf8');
const builtHtmlPath = path.join(outputDirectory, fileName);
assert.strictEqual(/\?v=\d+/.test(sourceHtml), false, `${fileName} still uses a manual cache version`);
assert.ok(existsSync(builtHtmlPath), `${fileName} was not emitted`);
const builtHtml = readFileSync(builtHtmlPath, 'utf8');
const assetUrls = [...builtHtml.matchAll(/(?:src|href)="(\/(?:assets|libs)\/[^"?#]+)"/g)]
.map((match) => match[1]);
for (const assetUrl of assetUrls) {
const assetPath = path.join(outputDirectory, ...assetUrl.split('/').filter(Boolean));
assert.ok(existsSync(assetPath), `${fileName} references missing asset ${assetUrl}`);
}
}
for (const fileName of ['app.html', 'share.html']) {
const html = readFileSync(path.join(outputDirectory, fileName), 'utf8');
assert.ok(/\/assets\/[^"']+\.js/.test(html), `${fileName} is not using a bundled module`);
assert.strictEqual(/vue\.global(?:\.prod)?\.js|axios\.min\.js/.test(html), false, `${fileName} still loads global Vue/Axios`);
}
console.log(`Verified ${htmlFiles.length} HTML entries and their emitted assets.`);