This commit is contained in:
73
scripts/admin-web-build.mjs
Normal file
73
scripts/admin-web-build.mjs
Normal file
@@ -0,0 +1,73 @@
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import {dirname, resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(scriptDir, '..');
|
||||
const adminWebDir = resolve(repoRoot, 'apps/admin-web');
|
||||
const adminTsconfigPath = resolve(adminWebDir, 'tsconfig.json');
|
||||
const adminViteConfigPath = resolve(adminWebDir, 'vite.config.ts');
|
||||
const tscBinPath = resolve(repoRoot, 'node_modules/typescript/bin/tsc');
|
||||
const viteCliPath = resolve(scriptDir, 'vite-cli.mjs');
|
||||
|
||||
const command = process.argv[2] ?? 'build';
|
||||
const extraArgs = process.argv.slice(3);
|
||||
|
||||
function usage() {
|
||||
console.error('用法: node scripts/admin-web-build.mjs <typecheck|build> [vite-build-args...]');
|
||||
}
|
||||
|
||||
function runNodeScript(label, args) {
|
||||
console.log(`[admin-web] ${label}`);
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
if (result.stdout) {
|
||||
process.stdout.write(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
console.error(`[admin-web] ${label} failed to start: ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.signal) {
|
||||
console.error(`[admin-web] ${label} was terminated by signal ${result.signal}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if ((result.status ?? 0) !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function runTypecheck() {
|
||||
runNodeScript('typecheck', [
|
||||
tscBinPath,
|
||||
'--noEmit',
|
||||
'-p',
|
||||
adminTsconfigPath,
|
||||
]);
|
||||
}
|
||||
|
||||
if (command === 'typecheck') {
|
||||
runTypecheck();
|
||||
} else if (command === 'build') {
|
||||
runTypecheck();
|
||||
runNodeScript('vite build', [
|
||||
viteCliPath,
|
||||
'build',
|
||||
'--config',
|
||||
adminViteConfigPath,
|
||||
...extraArgs,
|
||||
]);
|
||||
} else {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -2,39 +2,60 @@ import {spawnSync} from 'node:child_process';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url));
|
||||
const args = [viteCliPath, 'build', ...process.argv.slice(2)];
|
||||
const adminWebBuildPath = fileURLToPath(new URL('./admin-web-build.mjs', import.meta.url));
|
||||
const forwardedArgs = process.argv.slice(2);
|
||||
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
if (result.stdout) {
|
||||
process.stdout.write(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
|
||||
if ((result.status ?? 0) !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
const warningPattern = /\bwarn(?:ing)?\b/i;
|
||||
const ignoredWarningPatterns = [
|
||||
/ExperimentalWarning/u,
|
||||
const results = [
|
||||
runBuildStep('web', [viteCliPath, 'build', ...forwardedArgs]),
|
||||
runBuildStep('admin-web', [adminWebBuildPath, 'build']),
|
||||
];
|
||||
|
||||
const warningLines = `${result.stdout ?? ''}\n${result.stderr ?? ''}`
|
||||
.split(/\r?\n/u)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.filter(line => warningPattern.test(line))
|
||||
.filter(line => !ignoredWarningPatterns.some(pattern => pattern.test(line)));
|
||||
const failedResult = results.find(result => result.error || result.signal || (result.status ?? 0) !== 0);
|
||||
if (failedResult) {
|
||||
if (failedResult.error) {
|
||||
console.error(`Build gate failed to start a build step: ${failedResult.error.message}`);
|
||||
} else if (failedResult.signal) {
|
||||
console.error(`Build gate step was terminated by signal ${failedResult.signal}`);
|
||||
}
|
||||
process.exit(failedResult.status ?? 1);
|
||||
}
|
||||
|
||||
const warningLines = results.flatMap((result) => collectWarningLines(result));
|
||||
|
||||
if (warningLines.length > 0) {
|
||||
console.error('Build gate failed because warnings were emitted:');
|
||||
[...new Set(warningLines)].forEach(line => console.error(`- ${line}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function runBuildStep(label, args) {
|
||||
console.log(`[build-gate] ${label}`);
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
if (result.stdout) {
|
||||
process.stdout.write(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectWarningLines(result) {
|
||||
const warningPattern = /\bwarn(?:ing)?\b/i;
|
||||
const ignoredWarningPatterns = [
|
||||
/ExperimentalWarning/u,
|
||||
];
|
||||
|
||||
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`
|
||||
.split(/\r?\n/u)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
.filter(line => warningPattern.test(line))
|
||||
.filter(line => !ignoredWarningPatterns.some(pattern => pattern.test(line)));
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@ fi
|
||||
|
||||
TARGET_DIR="${BUILD_ROOT}/${BUILD_NAME}"
|
||||
WEB_DIR="${TARGET_DIR}/web"
|
||||
ADMIN_WEB_DIR="${WEB_DIR}/admin"
|
||||
API_BINARY_SOURCE="${SERVER_RS_DIR}/target/x86_64-unknown-linux-gnu/release/api-server"
|
||||
WASM_SOURCE="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm"
|
||||
|
||||
@@ -364,6 +365,12 @@ if [[ "${SKIP_WEB_BUILD}" -ne 1 ]]; then
|
||||
cd "${REPO_ROOT}"
|
||||
node scripts/vite-cli.mjs build --outDir "${WEB_DIR}" --emptyOutDir
|
||||
)
|
||||
|
||||
echo "[deploy:rust] 构建后台 Vite release -> ${ADMIN_WEB_DIR}"
|
||||
(
|
||||
cd "${REPO_ROOT}"
|
||||
MSYS2_ARG_CONV_EXCL="--base=" node scripts/admin-web-build.mjs build --base=/admin/ --outDir "${ADMIN_WEB_DIR}" --emptyOutDir
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_API_BUILD}" -ne 1 ]]; then
|
||||
@@ -421,11 +428,14 @@ import {fileURLToPath} from 'node:url';
|
||||
|
||||
const releaseDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const webRoot = path.join(releaseDir, 'web');
|
||||
const adminWebRoot = path.join(webRoot, 'admin');
|
||||
const webHost = process.env.GENARRATIVE_WEB_HOST || '127.0.0.1';
|
||||
const webPort = Number(process.env.GENARRATIVE_WEB_PORT || '3000');
|
||||
const apiTarget = new URL(process.env.GENARRATIVE_API_TARGET || 'http://127.0.0.1:8082');
|
||||
const indexPath = path.join(webRoot, 'index.html');
|
||||
const adminIndexPath = path.join(adminWebRoot, 'index.html');
|
||||
const proxyPrefixes = [
|
||||
'/admin/api',
|
||||
'/api/',
|
||||
'/api',
|
||||
'/generated-character-drafts',
|
||||
@@ -466,11 +476,11 @@ function sendFile(response, filePath) {
|
||||
.pipe(response);
|
||||
}
|
||||
|
||||
function serveStatic(request, response, pathname) {
|
||||
function serveStaticFromRoot(response, pathname, rootDir, fallbackIndexPath) {
|
||||
const decodedPath = decodeURIComponent(pathname);
|
||||
const relativePath = decodedPath === '/' ? '/index.html' : decodedPath;
|
||||
const filePath = path.normalize(path.join(webRoot, relativePath));
|
||||
const safeRelativePath = path.relative(webRoot, filePath);
|
||||
const filePath = path.normalize(path.join(rootDir, relativePath));
|
||||
const safeRelativePath = path.relative(rootDir, filePath);
|
||||
|
||||
if (safeRelativePath.startsWith('..') || path.isAbsolute(safeRelativePath)) {
|
||||
response.writeHead(403, {'content-type': 'text/plain; charset=utf-8'});
|
||||
@@ -480,12 +490,21 @@ function serveStatic(request, response, pathname) {
|
||||
|
||||
const resolvedFilePath = fs.existsSync(filePath) && fs.statSync(filePath).isFile()
|
||||
? filePath
|
||||
: indexPath;
|
||||
: fallbackIndexPath;
|
||||
|
||||
response.writeHead(200, {'content-type': contentTypeFor(resolvedFilePath)});
|
||||
sendFile(response, resolvedFilePath);
|
||||
}
|
||||
|
||||
function serveStatic(request, response, pathname) {
|
||||
serveStaticFromRoot(response, pathname, webRoot, indexPath);
|
||||
}
|
||||
|
||||
function serveAdminStatic(response, pathname) {
|
||||
const adminPath = pathname === '/admin/' ? '/' : pathname.replace(/^\/admin/u, '');
|
||||
serveStaticFromRoot(response, adminPath, adminWebRoot, adminIndexPath);
|
||||
}
|
||||
|
||||
function proxyToApi(request, response) {
|
||||
const targetUrl = new URL(request.url || '/', apiTarget);
|
||||
const proxyRequest = http.request(
|
||||
@@ -522,6 +541,17 @@ const server = http.createServer((request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/admin') {
|
||||
response.writeHead(301, {location: '/admin/'});
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/admin/' || url.pathname.startsWith('/admin/')) {
|
||||
serveAdminStatic(response, url.pathname);
|
||||
return;
|
||||
}
|
||||
|
||||
serveStatic(request, response, url.pathname);
|
||||
});
|
||||
|
||||
@@ -1189,7 +1219,7 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
|
||||
## 内容
|
||||
|
||||
- \`.env\` / \`.env.local\`:从仓库根目录复制的环境文件,同时各保留一份到 \`web/\`
|
||||
- \`web/\`:Vite release 静态资源
|
||||
- \`web/\`:主前端 Vite release 静态资源,\`web/admin/\` 为后台管理前端静态资源
|
||||
- \`api-server\`:x86_64-unknown-linux-gnu release 可执行文件
|
||||
- \`spacetime_module.wasm\`:wasm32-unknown-unknown release 模块
|
||||
- \`migration-bootstrap-secret.txt\`:本发布包 wasm 编译时注入的迁移引导密钥;服务器 \`start.sh\` 发布时会显示,迁移授权完成后可删除
|
||||
@@ -1211,6 +1241,11 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
|
||||
|
||||
默认启动会先尝试无清库发布;如果 SpacetimeDB 返回 schema 冲突,\`start.sh\` 会把旧库导出到 \`database-migrations/<database>/\`,随后清库发布新 wasm,并用 \`--replace-existing\` 导入回灌。
|
||||
|
||||
## 入口
|
||||
|
||||
- 主站:\`http://<web-host>:<web-port>/\`
|
||||
- 后台:\`http://<web-host>:<web-port>/admin/\`
|
||||
|
||||
## 环境变量
|
||||
|
||||
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
|
||||
|
||||
@@ -10,7 +10,7 @@ usage() {
|
||||
说明:
|
||||
1. 如果部署目录已有旧版本且存在 stop.sh,则先执行旧版本 stop.sh。
|
||||
2. 仅删除并替换发布产物文件或目录,保留部署目录中的运行数据目录。
|
||||
3. 把指定发布目录中的白名单产物复制覆盖到部署目录。
|
||||
3. 把指定发布目录中的白名单产物复制覆盖到部署目录,后台前端随 web/admin/ 一并覆盖。
|
||||
4. 如指定 --clear-database,则以清库模式执行新版本 start.sh。
|
||||
5. 默认允许新版本 start.sh 在 schema 冲突时自动导出、清库发布、导入回灌。
|
||||
6. 最后执行新版本 start.sh。
|
||||
|
||||
Reference in New Issue
Block a user