diff --git a/.gitignore b/.gitignore index 3150f45..ade87d4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ node_modules/ # Documentation site build artifacts docs-site/.astro/ docs-site/dist/ +docs-site/public/demo/runtime/ docs-site/src/content/docs/ # WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build) @@ -34,6 +35,11 @@ bot/app/web/templates/subscription_webapp_admin.min.*.js.br bot/app/web/templates/subscription_webapp_admin.min.*.js.gz bot/app/web/templates/subscription_webapp_admin.*.css.br bot/app/web/templates/subscription_webapp_admin.*.css.gz +bot/app/web/templates/subscription_webapp_docs_demo.css +bot/app/web/templates/subscription_webapp_docs_demo.js +bot/app/web/templates/subscription_webapp_docs_demo.*.css +bot/app/web/templates/subscription_webapp_docs_demo.*.css.br +bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz backend/bot/app/web/templates/subscription_webapp.css backend/bot/app/web/templates/subscription_webapp.js backend/bot/app/web/templates/subscription_webapp.min.*.js @@ -50,6 +56,11 @@ backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.br backend/bot/app/web/templates/subscription_webapp_admin.min.*.js.gz backend/bot/app/web/templates/subscription_webapp_admin.*.css.br backend/bot/app/web/templates/subscription_webapp_admin.*.css.gz +backend/bot/app/web/templates/subscription_webapp_docs_demo.css +backend/bot/app/web/templates/subscription_webapp_docs_demo.js +backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css +backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.br +backend/bot/app/web/templates/subscription_webapp_docs_demo.*.css.gz tmp .claude diff --git a/docs-site/package.json b/docs-site/package.json index 72fc667..f1be9ed 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -3,8 +3,9 @@ "type": "module", "scripts": { "sync:docs": "node ./scripts/sync-docs.mjs", - "dev": "npm run sync:docs && astro dev", - "build": "npm run sync:docs && astro build", + "build:demo": "node ./scripts/build-demo-runtime.mjs", + "dev": "npm run sync:docs && npm run build:demo && astro dev", + "build": "npm run sync:docs && npm run build:demo && astro build", "preview": "astro preview" }, "dependencies": { diff --git a/docs-site/scripts/build-demo-runtime.mjs b/docs-site/scripts/build-demo-runtime.mjs new file mode 100644 index 0000000..e8fac26 --- /dev/null +++ b/docs-site/scripts/build-demo-runtime.mjs @@ -0,0 +1,149 @@ +import { spawn } from 'node:child_process'; +import { access, copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); +const repoRoot = path.resolve(siteRoot, '..'); +const frontendRoot = path.join(repoRoot, 'frontend'); +const runtimeDir = path.join(siteRoot, 'public', 'demo', 'runtime'); +const templatesDir = path.join(repoRoot, 'backend', 'bot', 'app', 'web', 'templates'); +const themesDir = path.join(repoRoot, 'backend', 'bot', 'app', 'web', 'themes'); +const localesDir = path.join(repoRoot, 'locales'); +const runtimeBase = '/demo/runtime'; +const isWindows = process.platform === 'win32'; +const npmExecPath = process.env.npm_execpath || ''; + +function run(command, args, options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: repoRoot, + stdio: 'inherit', + shell: false, + ...options, + }); + child.on('error', reject); + child.on('exit', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`)); + }); + }); +} + +function runNpm(args) { + if (npmExecPath) { + return run(process.execPath, [npmExecPath, ...args]); + } + return run(isWindows ? 'npm.cmd' : 'npm', args, { shell: isWindows }); +} + +async function pathExists(targetPath) { + try { + await access(targetPath); + return true; + } catch (_error) { + return false; + } +} + +async function ensureFrontendDependencies() { + const viteBin = isWindows + ? path.join(frontendRoot, 'node_modules', '.bin', 'vite.cmd') + : path.join(frontendRoot, 'node_modules', '.bin', 'vite'); + if (await pathExists(viteBin)) return; + await runNpm(['--prefix', frontendRoot, 'ci']); +} + +async function copyDirectory(sourceDir, targetDir, transform = null) { + await mkdir(targetDir, { recursive: true }); + const entries = await readdir(sourceDir, { withFileTypes: true }); + for (const entry of entries) { + const sourcePath = path.join(sourceDir, entry.name); + const targetPath = path.join(targetDir, entry.name); + if (entry.isDirectory()) { + await copyDirectory(sourcePath, targetPath, transform); + continue; + } + if (!entry.isFile()) continue; + if (transform) { + const handled = await transform(sourcePath, targetPath); + if (handled) continue; + } + await mkdir(path.dirname(targetPath), { recursive: true }); + await copyFile(sourcePath, targetPath); + } +} + +async function copyThemeFile(sourcePath, targetPath) { + if (path.extname(sourcePath).toLowerCase() !== '.css') return false; + const css = await readFile(sourcePath, 'utf8'); + const rewritten = css.replace(/\/webapp-theme-assets\//g, `${runtimeBase}/themes/`); + await mkdir(path.dirname(targetPath), { recursive: true }); + await writeFile(targetPath, rewritten, 'utf8'); + return true; +} + +async function copyRuntimeAsset(name) { + await copyFile(path.join(templatesDir, name), path.join(runtimeDir, name)); +} + +function jsonScriptPayload(value) { + return JSON.stringify(value).replace(/ + +
+ + + + +