Files
wordpress-theme-aksrt/tools/build-public-assets.mjs
T
kate522andClaude Opus 5 fac4a714e0 feat(creator-studio): 前台 Gutenberg 创作工作台与受保护付费附件
前台投稿页改为沉浸式创作工作台:Gutenberg 负责区块解析与序列化,
React + TypeScript 负责工作台外壳与发布抽屉,Jio 服务端继续作为权限、
审核与付费的唯一权威。

- 新增 assets/creator-studio 独立构建(wp-scripts,锁定 WP 6.4 时代包),
  仅在投稿模板加载,构建缺失或游客访问时回退原有 TinyMCE 表单
- 自动保存(4s 防抖 + rev 冲突 409)、本地快照恢复、发布配置抽屉、
  移动端全屏单列与安全区适配
- 图片插入走现有 front-submit 上传管线,保留能力校验与待认领生命周期
- 已发布文章的改稿写入私有版本,审核通过前线上内容/链接/付费条款不变;
  原生后台「改稿审核」一次性应用或退回,并校验源文章并发指纹
- 私有附件新表 jio_private_asset + jio_dl_token.asset_id,schema 升到 3.4.0;
  文件存放 ABSPATH 之外,未配置私有存储时 fail closed
- 下载令牌绑定文章/附件/用户/IP/UA/次数,每次交付复核 Visibility 的
  订单、VIP 与配额判定;中断传输归还次数
- 付费字段改为写入前校验,非法价格不再留下半保存文章
- 新增 creator-studio 契约测试;更新受影响的 schema/下载/投稿契约

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 14:53:23 +08:00

206 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/wait-pay-float.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/footer-tabbar.js',
'assets/js/core/misc.js',
'assets/js/core/theme.js',
'assets/js/protected-download.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.');