Files
wordpress-theme-aksrt/tools/build-public-assets.mjs
T

205 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(path.join(repoRoot, 'assets/admin/package.json'));
const { transform } = require('esbuild');
const cssBuildDir = path.join(repoRoot, 'assets/css/build');
const jsBuildDir = path.join(repoRoot, 'assets/js/build');
const cssBundles = {
// bootstrap / fontawesome5 / main.min.css 由 PHP 单独入队,不在此重复打包。
'public.css': [
'style.css',
'assets/css/core/tokens.css',
'assets/css/core/reset.css',
'assets/css/core/typography.css',
'assets/css/core/buttons.css',
'assets/css/core/forms.css',
'assets/css/core/alerts.css',
'assets/css/core/animations.css',
'assets/css/core/components-ext.css',
'assets/css/core/color-utilities.css',
'assets/css/frontend/base.css',
'assets/css/frontend/header-userbox.css',
'assets/css/frontend/card-primitives.css',
'assets/css/frontend/overlay-primitives.css',
'assets/css/frontend/layout.css',
'assets/css/frontend/footer.css',
'assets/css/frontend/floating-buttons.css',
'assets/css/frontend/enhancements.css',
'assets/css/frontend/empty-state.css',
'assets/css/frontend/pagination.css',
'assets/css/frontend/modal-runtime.css',
'assets/css/frontend/notyf.css',
'assets/css/shared/captcha.css',
'assets/css/frontend/single-post/rewards.css',
],
'homepage.css': [
'assets/css/frontend/post-card.css',
'assets/css/frontend/sidebar.css',
'assets/css/frontend/breadcrumb.css',
'assets/css/frontend/homepage-fixed/base.css',
],
'single.css': [
'assets/css/frontend/legacy-utilities.css',
'assets/css/frontend/single-post/shell.css',
'assets/css/frontend/single-post/content.css',
'assets/css/frontend/single-post/author-actions.css',
],
'comments.css': [
'assets/css/frontend/comments/layout.css',
'assets/css/frontend/comments/shell.css',
'assets/css/frontend/comments/mobile-item.css',
],
};
const runtimePreludeScripts = [
'assets/js/notyf.js',
'assets/js/core/modal-telemetry.js',
'assets/js/core/request.js',
'assets/js/core/modal-cache.js',
'assets/js/core/modal-state.js',
'assets/js/core/modal-runtime.js',
'assets/js/core/modal-triggers.js',
'assets/js/core/article.js',
'assets/js/core/comments.js',
'assets/js/core/comment-fixed-input.js',
'assets/js/core/comment-move-form.js',
'assets/js/core/ajax-list.js',
'assets/js/core/pagination.js',
'assets/js/core/archive.js',
'assets/js/core/footer-tabbar.js',
'assets/js/core/misc.js',
'assets/js/core/theme.js',
'assets/js/follow.js',
'assets/js/enhancements.js',
'assets/js/enhancements2.js',
'assets/js/color-extract.js',
'assets/js/effects.js',
'assets/js/captcha-front.js',
'assets/js/instant-page.js',
];
const imgboxScripts = [
'assets/js/runtime/imgbox.js',
];
function normalizePath(filePath) {
return filePath.split(path.sep).join('/');
}
function rewriteCssUrls(css, sourcePath, outputPath) {
const outputDir = path.dirname(outputPath);
return css.replace(/url\(\s*(['"]?)([^)'"\s]+)\1\s*\)/gi, (match, quote, rawUrl) => {
if (/^(?:data:|https?:|\/\/|\/|#|var\()/i.test(rawUrl)) {
return match;
}
const suffixIndex = rawUrl.search(/[?#]/);
const pathname = suffixIndex === -1 ? rawUrl : rawUrl.slice(0, suffixIndex);
const suffix = suffixIndex === -1 ? '' : rawUrl.slice(suffixIndex);
const absoluteAsset = path.resolve(path.dirname(sourcePath), pathname);
let relativeAsset = normalizePath(path.relative(outputDir, absoluteAsset));
if (!relativeAsset.startsWith('.')) {
relativeAsset = `./${relativeAsset}`;
}
const wrapper = quote || '"';
return `url(${wrapper}${relativeAsset}${suffix}${wrapper})`;
});
}
async function expandCss(sourcePath, outputPath, stack = []) {
const absoluteSource = path.resolve(sourcePath);
if (stack.includes(absoluteSource)) {
throw new Error(`Circular CSS import: ${normalizePath(path.relative(repoRoot, absoluteSource))}`);
}
const source = await readFile(absoluteSource, 'utf8');
const importPattern = /@import\s+(?:url\(\s*)?['"]([^'"]+\.css)['"]\s*\)?\s*;/gi;
let expanded = '';
let cursor = 0;
let match;
while ((match = importPattern.exec(source)) !== null) {
expanded += rewriteCssUrls(source.slice(cursor, match.index), absoluteSource, outputPath);
const importedPath = path.resolve(path.dirname(absoluteSource), match[1]);
expanded += await expandCss(importedPath, outputPath, [...stack, absoluteSource]);
cursor = importPattern.lastIndex;
}
expanded += rewriteCssUrls(source.slice(cursor), absoluteSource, outputPath);
return expanded.trimEnd();
}
async function buildCssBundle(outputName, sources) {
const outputPath = path.join(cssBuildDir, outputName);
const sections = [];
for (const source of sources) {
const sourcePath = path.join(repoRoot, source);
sections.push(`/* source: ${source} */\n${await expandCss(sourcePath, outputPath)}`);
}
const bundle = `${sections.join('\n\n')}\n`;
const minified = await transform(bundle, {
charset: 'utf8',
legalComments: 'inline',
loader: 'css',
minify: true,
sourcefile: outputName,
});
const minifiedPath = outputPath.replace(/\.css$/, '.min.css');
await Promise.all([
writeFile(outputPath, bundle, 'utf8'),
writeFile(minifiedPath, minified.code, 'utf8'),
]);
}
async function buildScriptBundle(sources, outputName) {
const sections = [];
for (const source of sources) {
const content = (await readFile(path.join(repoRoot, source), 'utf8')).trimEnd();
sections.push(`/* source: ${source} */\n${content}\n;`);
}
const bundle = `${sections.join('\n\n')}\n`;
const minified = await transform(bundle, {
charset: 'utf8',
legalComments: 'inline',
loader: 'js',
// 保留原始标识符:压缩产生的 `$` 局部变量会破坏「禁用美元别名」契约。
minifyIdentifiers: false,
minifySyntax: true,
minifyWhitespace: true,
sourcefile: outputName,
});
await writeFile(path.join(repoRoot, outputName), minified.code, 'utf8');
}
// 评论区硬切换后 main.min.css 由 main.css 重新生成,保持两者同步。
async function buildMainMin() {
const sourcePath = path.join(repoRoot, 'assets/css/main.css');
const outputPath = path.join(repoRoot, 'assets/css/main.min.css');
const source = await readFile(sourcePath, 'utf8');
const minified = await transform(source, {
charset: 'utf8',
legalComments: 'inline',
loader: 'css',
minify: true,
sourcefile: 'main.css',
});
await writeFile(outputPath, minified.code, 'utf8');
}
await mkdir(cssBuildDir, { recursive: true });
await mkdir(jsBuildDir, { recursive: true });
// 先刷新 main.min.css,public.css 打包时才能读到最新内容。
await buildMainMin();
for (const [outputName, sources] of Object.entries(cssBundles)) {
await buildCssBundle(outputName, sources);
}
await buildScriptBundle([...runtimePreludeScripts, 'assets/js/runtime/main.js'], 'assets/js/runtime/main.min.js');
await buildScriptBundle(imgboxScripts, 'assets/js/runtime/imgbox.min.js');
console.log('Built public CSS and runtime JS bundles.');