Files
CloudSSH/scripts/build-html.js
T
TanXinandTanXin db4bbcfc44 release: 发布 v1.7.0 (#73)
## 发布概述

发布 CloudSSH v1.7.0,增加可选的 GitHub 用户访问限制与私有实例模式,并包含同期的依赖、构建和类型安全加固。

## 主要变化

- 新增 `GITHUB_ALLOWED_USER_IDS`,按稳定的 GitHub 数字用户 ID 限制允许登录的账号。
- 新增 `REQUIRE_GITHUB_AUTH`,启用后禁止匿名 SSH;与白名单组合后仅允许指定 GitHub 用户使用实例。
- 在 OAuth 回调、既有 session、一次性连接令牌和 WebSocket 升级路径持续执行后端访问策略校验。
- 强制登录模式下校验一次性连接令牌与当前 GitHub 用户的归属关系。
- 未登录页面新增强制 GitHub 登录状态,并同步更新中英文部署与配置文档。
- DOMPurify 升级至 3.4.13,并完善前端构建产物校验、忽略规则和服务器保存响应类型。

## 版本与文档

- `package.json` 版本更新为 `1.7.0`。
- `CHANGELOG.md` 新增 `1.7.0` 发布记录(2026-08-11)。

## 验证

- Worker 与前端 TypeScript 类型检查通过
- 36 个测试文件、548 项单元与集成测试通过
- 32 项 Chromium E2E 与无障碍测试通过
- 生产前端构建与 Worker 内嵌生成通过
- test 分支 CI
与测试环境部署通过:https://github.com/newbietan/CloudSSH/actions/runs/31460428306
- `git diff --check` 通过

---------

Co-authored-by: TanXin <tanxin@example.com>
2026-08-11 13:19:41 +08:00

89 lines
3.5 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const rootDir = path.resolve(__dirname, '..');
const frontendDir = path.join(rootDir, 'frontend');
const distDir = path.join(frontendDir, 'dist');
const workerDir = path.join(rootDir, 'src', 'worker');
const htmlTsFile = path.join(workerDir, 'html.ts');
function inlineRequiredAsset(html, patterns, replacement, assetType) {
for (const pattern of patterns) {
if (pattern.test(html)) {
return html.replace(pattern, replacement);
}
}
throw new Error(`Unable to find the ${assetType} reference in the generated frontend HTML.`);
}
console.log('Building frontend...');
try {
execFileSync(process.execPath, ['scripts/sync-theme-editor.js'], { cwd: rootDir, stdio: 'inherit' });
// Dependencies are installed explicitly by developers/CI. Keeping installs out
// of the build makes production artifacts deterministic and offline-buildable.
execFileSync('pnpm', ['run', 'build'], { cwd: frontendDir, stdio: 'inherit' });
console.log('Inlining assets...');
// 2. Read dist/index.html
let html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8');
// 3. Find assets in dist/assets
const assetsDir = path.join(distDir, 'assets');
const files = fs.readdirSync(assetsDir).sort();
const jsFiles = files.filter((file) => file.endsWith('.js'));
const cssFiles = files.filter((file) => file.endsWith('.css'));
if (jsFiles.length !== 1 || cssFiles.length !== 1) {
throw new Error(
`Expected exactly one JS and one CSS bundle for Worker inlining, got ${jsFiles.length} JS and ${cssFiles.length} CSS.`
);
}
const jsContent = fs.readFileSync(path.join(assetsDir, jsFiles[0]), 'utf8');
const cssContent = fs.readFileSync(path.join(assetsDir, cssFiles[0]), 'utf8');
// 4. Inline favicon.svg as base64 data URL
const faviconPath = path.join(frontendDir, 'public', 'favicon.svg');
if (fs.existsSync(faviconPath)) {
const faviconContent = fs.readFileSync(faviconPath, 'utf8');
const faviconBase64 = Buffer.from(faviconContent).toString('base64');
html = html.replace(
/href=["']\/favicon\.svg["']/i,
`href="data:image/svg+xml;base64,${faviconBase64}"`
);
}
// 5. Inline CSS (replace <link rel="stylesheet" ...>)
html = inlineRequiredAsset(html, [
/<link[^>]*rel=["']stylesheet["'][^>]*href=["']\/assets\/[^"']+["'][^>]*>/i,
/<link[^>]*rel=["']stylesheet["'][^>]*href=["']\/src\/[^"']+["'][^>]*>/i,
], () => `<style>${cssContent}</style>`, 'stylesheet');
// 6. Inline JS (replace <script type="module" src="/src/main.ts"></script> or similar)
html = inlineRequiredAsset(html, [
/<script[^>]*type=["']module["'][^>]*src=["']\/assets\/[^"']+["'][^>]*><\/script>/i,
/<script[^>]*type=["']module["'][^>]*src=["']\/src\/[^"']+["'][^>]*><\/script>/i,
], () => `<script type="module">${jsContent}</script>`, 'module script');
// 7. Write to src/worker/html.ts
const escapedHtml = html
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\${/g, '\\${');
const htmlTsContent = `// Automatically generated by scripts/build-html.js. Do not edit directly.
export const HTML = \`${escapedHtml}\`;
`;
if (!fs.existsSync(workerDir)) {
fs.mkdirSync(workerDir, { recursive: true });
}
fs.writeFileSync(htmlTsFile, htmlTsContent, 'utf8');
console.log('Frontend built and inlined successfully to src/worker/html.ts!');
} catch (error) {
console.error('Failed to build and inline frontend:', error);
process.exit(1);
}