// Isolated local visual harness: compiles the real AppIcon/BottomNav components. // No login, production API, remote assets, or user data is used. import { createServer } from 'node:http'; import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { parse, compileScript } from 'vue/compiler-sfc'; import ts from 'typescript'; const require = createRequire(import.meta.url); const root = new URL('../', import.meta.url); const read = (path) => readFileSync(new URL(path, root), 'utf8'); const transpile = (content) => ts.transpileModule(content, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ES2020 }, }).outputText; const descriptors = new Map(['AppIcon', 'BottomNav'].map((name) => [ name, parse(read(`src/components/${name}.vue`), { filename: `${name}.vue` }).descriptor, ])); const componentModule = (name) => transpile(compileScript(descriptors.get(name), { id: `preview-${name}`, inlineTemplate: true, templateOptions: { compilerOptions: { isCustomElement: (tag) => ['view', 'text'].includes(tag) } }, }).content).replace(/from ['"]vue['"]/g, 'from "/vue.js"') .replace(/from ['"]\.\.\/utils\/icon-mask['"]/g, 'from "/icon-mask.js"') .replace(/from ['"]\.\/AppIcon.vue['"]/g, 'from "/AppIcon.js"') .replace(/from ['"]\.\.\/utils\/app-state['"]/g, 'from "/state.js"'); const css = [...descriptors.values()].flatMap((item) => item.styles.map((style) => style.content)).join('\n') .replace(/(-?\d+(?:\.\d+)?)rpx/g, 'calc(var(--preview-rpx) * $1)'); // Include the actual App-vue button defaults: auto margins/overflow:hidden are // essential to reproduce badge clipping (plain HTML buttons hide this bug). const nativeCSS = read('node_modules/@dcloudio/uni-app-plus/dist/style.css'); const nativeButtonRule = [...nativeCSS.matchAll(/uni-button\{([^{}]+)\}/g)] .find((match) => match[1].includes('margin-left:auto'))?.[1]; if (!nativeButtonRule) throw new Error('Native button defaults not found'); const html = `App 图标回归预览
`; const server = createServer((req, res) => { const path = new URL(req.url, 'http://localhost').pathname; const modules = { '/vue.js': () => readFileSync(require.resolve('vue/dist/vue.runtime.esm-browser.js'), 'utf8'), '/icon-mask.js': () => transpile(read('src/utils/icon-mask.ts')), '/AppIcon.js': () => componentModule('AppIcon'), '/BottomNav.js': () => componentModule('BottomNav'), '/state.js': () => 'import {reactive} from "/vue.js";export const clientState = reactive({ unreadMessages: 2 });', }; res.setHeader('Cache-Control', 'no-store'); if (path === '/') { res.setHeader('Content-Type', 'text/html;charset=utf-8'); res.end(html); } else if (modules[path]) { res.setHeader('Content-Type', 'application/javascript;charset=utf-8'); res.end(modules[path]()); } else { res.statusCode = 404; res.end('Not found'); } }); server.listen(Number(process.env.ICON_PREVIEW_PORT || 5197), '127.0.0.1', () => { console.log(`Icon preview: http://127.0.0.1:${server.address().port}/`); });