refactor: Extract web-templates package and unify build/pack workflow

Moves export-html and insight templates from cli/assets to a new
dedicated web-templates package. Updates Dockerfile and build scripts
to use consolidated bundle/prepare:package/pack workflow.

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
tanzhenxinandQwen-Coder committed 2026-02-26 21:02:46 +08:00
1 parent 5926b37f4d
commit 9e4c5ee891
44 files changed
+1153 -265

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
# Dependencies (npm ci installs fresh inside the container)
node_modules
**/node_modules
# Build artifacts (rebuilt from scratch inside the container)
dist
**/dist
# Version control
.git
+1
View File
@@ -47,6 +47,7 @@ packages/*/coverage/
# Generated files
packages/cli/src/generated/
packages/core/src/generated/
packages/web-templates/src/generated/
.integration-tests/
packages/vscode-ide-companion/*.vsix
+6 -7
View File
@@ -19,12 +19,12 @@ ENV PATH=$PATH:/usr/local/share/npm-global/bin
COPY . /home/node/app
WORKDIR /home/node/app
# Install dependencies and build packages
# Use scripts/build.js which handles workspace dependencies in correct order
# Install dependencies, build workspaces, bundle into a single distributable, and pack
RUN npm ci \
&& npm run build \
&& npm pack -w @qwen-code/qwen-code --pack-destination ./packages/cli/dist \
&& npm pack -w @qwen-code/qwen-code-core --pack-destination ./packages/core/dist
&& npm run bundle \
&& npm run prepare:package \
&& cd dist && npm pack
# Runtime stage
FROM docker.io/library/node:20-slim
@@ -61,9 +61,8 @@ RUN mkdir -p /usr/local/share/npm-global
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# Copy built packages from builder stage
COPY --from=builder /home/node/app/packages/cli/dist/*.tgz /tmp/
COPY --from=builder /home/node/app/packages/core/dist/*.tgz /tmp/
# Copy bundled package from builder stage
COPY --from=builder /home/node/app/dist/*.tgz /tmp/
# Install built packages globally
RUN npm install -g /tmp/*.tgz \
+3 -2
View File
@@ -254,9 +254,9 @@ export default tseslint.config(
'no-console': 'off',
},
},
// Settings for export-html assets
// Settings for web-templates assets
{
files: ['packages/cli/assets/export-html/**/*.{js,jsx,ts,tsx}'],
files: ['packages/web-templates/src/**/*.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
@@ -271,6 +271,7 @@ export default tseslint.config(
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'no-console': 'off',
},
},
// Prettier config must be last
+958 -7
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -25,7 +25,7 @@
"build:vscode": "node scripts/build_vscode_companion.js",
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"build:sandbox": "node scripts/build_sandbox.js -s",
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present --parallel",
"test:ci": "npm run test:ci --workspaces --if-present --parallel && npm run test:scripts",
@@ -50,7 +50,7 @@
"typecheck": "npm run typecheck --workspaces --if-present",
"check-i18n": "npm run check-i18n --workspace=packages/cli",
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
"prepare": "husky && npm run bundle",
"prepare": "husky && npm run build && npm run bundle",
"prepare:package": "node scripts/prepare-package.js",
"release:version": "node scripts/version.js",
"telemetry": "node scripts/telemetry.js",
-20
View File
@@ -1,20 +0,0 @@
{
"name": "@qwen-code/cli-insight",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "node build.mjs"
},
"dependencies": {},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^5.0.0"
}
}
-96
View File
@@ -1,96 +0,0 @@
import { access, readdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
const assetsDir = dirname(fileURLToPath(import.meta.url));
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const entries = await readdir(assetsDir, { withFileTypes: true });
const assetBuilds = [];
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const assetPath = join(assetsDir, entry.name);
const buildPath = join(assetPath, 'build.mjs');
const packageJsonPath = join(assetPath, 'package.json');
let hasBuild = false;
let hasPackageJson = false;
try {
await access(buildPath);
hasBuild = true;
} catch {
// ignore missing build.mjs
}
try {
await access(packageJsonPath);
hasPackageJson = true;
} catch {
// ignore missing package.json
}
if (hasBuild || hasPackageJson) {
assetBuilds.push({
name: entry.name,
assetPath,
buildPath,
useNpm: hasPackageJson,
});
}
}
if (assetBuilds.length === 0) {
process.exit(0);
}
const runCommand = ({ command, args, cwd, label }) =>
new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'inherit',
shell: process.platform === 'win32',
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${label} failed for ${cwd}.`));
}
});
});
const runBuild = async (asset) => {
if (asset.useNpm) {
await runCommand({
command: npmCommand,
args: ['install'],
cwd: asset.assetPath,
label: `npm install`,
});
await runCommand({
command: npmCommand,
args: ['run', 'build'],
cwd: asset.assetPath,
label: `npm run build`,
});
return;
}
await runCommand({
command: process.execPath,
args: [asset.buildPath],
cwd: asset.assetPath,
label: `Node build`,
});
};
await Promise.all(assetBuilds.map((asset) => runBuild(asset)));
+2 -2
View File
@@ -19,8 +19,7 @@
}
},
"scripts": {
"build:assets": "node ./assets/parallel-build.mjs",
"build": "npm run build:assets && node ../../scripts/build_package.js",
"build": "node ../../scripts/build_package.js",
"start": "node dist/index.js",
"debug": "node --inspect-brk dist/index.js",
"lint": "eslint . --ext .ts,.tsx",
@@ -41,6 +40,7 @@
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.25.1",
"@qwen-code/qwen-code-core": "file:../core",
"@qwen-code/web-templates": "file:../web-templates",
"@types/update-notifier": "^6.0.8",
"ansi-regex": "^6.2.2",
"command-exists": "^1.2.9",
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { INSIGHT_JS, INSIGHT_CSS } from '../templates/insightTemplate.js';
import { INSIGHT_JS, INSIGHT_CSS } from '@qwen-code/web-templates';
import type { InsightData } from '../types/StaticInsightTypes.js';
export class TemplateRenderer {
@@ -1,10 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* This file is code-generated; do not edit manually.
*/
export const INSIGHT_JS = "(function(e,z){\"use strict\";function j({data:t}){const{totalMessages:l=0,totalLinesAdded:n=0,totalLinesRemoved:r=0,totalFiles:o=0}=t,a=Object.keys(t.heatmap||{});let s=0;if(a.length>0){const d=a.map(u=>new Date(u)).map(u=>u.getTime()),c=new Date(Math.min(...d)),g=new Date(Math.max(...d)),f=Math.abs(g.getTime()-c.getTime());s=Math.ceil(f/(1e3*60*60*24))+1}const i=s>0?Math.round(l/s):0;return e.createElement(\"div\",{className:\"stats-row\"},e.createElement(\"div\",{className:\"stat\"},e.createElement(\"div\",{className:\"stat-value\"},l),e.createElement(\"div\",{className:\"stat-label\"},\"Messages\")),e.createElement(\"div\",{className:\"stat\"},e.createElement(\"div\",{className:\"stat-value\"},\"+\",n,\"/-\",r),e.createElement(\"div\",{className:\"stat-label\"},\"Lines\")),e.createElement(\"div\",{className:\"stat\"},e.createElement(\"div\",{className:\"stat-value\"},o),e.createElement(\"div\",{className:\"stat-label\"},\"Files\")),e.createElement(\"div\",{className:\"stat\"},e.createElement(\"div\",{className:\"stat-value\"},s),e.createElement(\"div\",{className:\"stat-label\"},\"Days\")),e.createElement(\"div\",{className:\"stat\"},e.createElement(\"div\",{className:\"stat-value\"},i),e.createElement(\"div\",{className:\"stat-label\"},\"Msgs/Day\")))}function B({insights:t}){return e.createElement(\"div\",{className:\"grid gap-4 md:grid-cols-2 md:gap-6\"},e.createElement(H,{activeHours:t.activeHours,cardClass:\"glass-card p-6\",sectionTitleClass:\"text-lg font-semibold tracking-tight text-slate-900\"}))}function H({activeHours:t,cardClass:l,sectionTitleClass:n}){const o=[{label:\"Morning\",time:\"06:00 - 12:00\",hours:[6,7,8,9,10,11],color:\"#fbbf24\"},{label:\"Afternoon\",time:\"12:00 - 18:00\",hours:[12,13,14,15,16,17],color:\"#0ea5e9\"},{label:\"Evening\",time:\"18:00 - 22:00\",hours:[18,19,20,21],color:\"#6366f1\"},{label:\"Night\",time:\"22:00 - 06:00\",hours:[22,23,0,1,2,3,4,5],color:\"#475569\"}].map(s=>{const i=s.hours.reduce((m,d)=>m+(t[d]||0),0);return{...s,total:i}}),a=Math.max(...o.map(s=>s.total));return e.createElement(\"div\",{className:`${l} h-full flex flex-col min-h-[320px]`},e.createElement(\"div\",{className:\"flex items-center justify-between mb-4\"},e.createElement(\"h3\",{className:n},\"Active Hours\")),e.createElement(\"div\",{className:\"flex-1 flex flex-col justify-center gap-4\"},o.map(s=>e.createElement(\"div\",{key:s.label,className:\"space-y-2\"},e.createElement(\"div\",{className:\"flex justify-between items-center text-sm\"},e.createElement(\"div\",{className:\"flex items-center gap-2\"},e.createElement(\"span\",{className:\"rounded-full\",style:{width:\"12px\",height:\"12px\",backgroundColor:s.color}}),e.createElement(\"span\",{className:\"font-medium text-slate-700\"},s.label),e.createElement(\"span\",{className:\"text-xs text-slate-400 hidden xl:inline\"},s.time)),e.createElement(\"span\",{className:\"font-semibold text-slate-900\"},s.total)),e.createElement(\"div\",{className:\"w-full rounded-full overflow-hidden\",style:{height:\"12px\",backgroundColor:\"#e2e8f0\"}},e.createElement(\"div\",{className:\"h-full rounded-full\",style:{width:`${a>0?s.total/a*100:0}%`,backgroundColor:s.color}}))))))}function O({heatmap:t}){return e.createElement(\"div\",{className:\"glass-card p-6 mt-4 md:mt-6\"},e.createElement(\"div\",{className:\"mb-3\"},e.createElement(\"h3\",{className:\"text-lg font-semibold tracking-tight text-slate-900\"},\"Activity Heatmap\"),e.createElement(\"p\",{className:\"text-xs text-slate-500\"},\"Showing past year of activity\")),e.createElement(\"div\",{className:\"heatmap-container\"},e.createElement(\"div\",{className:\"min-w-[720px] rounded-xl bg-white/70\"},e.createElement(q,{heatmapData:t}))),e.createElement(F,null))}function q({heatmapData:t}){const a=new Date,s=new Date(a);s.setFullYear(a.getFullYear()-1);const i=[],m=new Date(s);for(;m<=a;)i.push(new Date(m)),m.setDate(m.getDate()+1);const d=[0,2,4,10,20],c=[\"#ebedf0\",\"#9be9a8\",\"#40c463\",\"#30a14e\",\"#216e39\"];function g(h){if(h===0)return c[0];for(let E=d.length-1;E>=1;E--)if(h>=d[E])return c[E];return c[1]}const f=50,u=20,x=[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],S=s.getDay(),A=[];let D=-1,_=-100;return i.forEach((h,E)=>{const C=E+S,T=Math.floor(C/7),y=f+T*16,v=h.getMonth();v!==D&&(y-_>30&&(A.push({x:y,text:x[v]}),_=y),D=v)}),e.createElement(\"svg\",{className:\"heatmap-svg\",width:1e3,height:130,viewBox:\"0 0 1000 130\"},i.map((h,E)=>{const C=E+S,T=Math.floor(C/7),y=h.getDay(),v=f+T*16,I=u+y*16,b=h.toISOString().split(\"T\")[0],w=t[b]||0,oe=g(w);return e.createElement(\"rect\",{key:b,className:\"heatmap-day\",x:v,y:I,width:14,height:14,rx:\"2\",fill:oe,\"data-date\":b,\"data-count\":w},e.createElement(\"title\",null,b,\": \",w,\" activities\"))}),A.map((h,E)=>e.createElement(\"text\",{key:E,x:h.x,y:\"15\",fontSize:\"12\",fill:\"#64748b\"},h.text)))}function F(){const t=[\"#ebedf0\",\"#9be9a8\",\"#40cLine truncated
export const INSIGHT_CSS = "*,:before,:after,::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #3b82f680;--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content: \"\"}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",Segoe UI Symbol,\"Noto Color Emoji\";line-height:1.5}body{line-height:inherit;margin:0;background-image:linear-gradient(to bottom right,var(--tw-gradient-stops));--tw-gradient-from: #f8fafc var(--tw-gradient-from-position);--tw-gradient-to: #f1f5f9 var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #fff var(--tw-gradient-via-position), var(--tw-gradient-to);--tw-text-opacity: 1;min-height:100vh;color:rgb(15 23 42 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glass-card{--tw-border-opacity: 1;border-width:1px;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1));--tw-shadow: 0 10px 40px #0f172a14;--tw-shadow-colored: 0 10px 40px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);--tw-backdrop-blur: blur(8px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);background-color:#fff9;border-radius:1rem}.col-span-2{grid-column:span 2 / span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-56{height:14rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-full{width:100%}.min-w-\\[720px\\]{min-width:720px}.max-w-6xl{max-width:72rem}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-slate-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(226 232 240 / var(--tw-divide-opacity, 1))}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:9999px}.rounded-xl{border-radius:1.25rem}.border{border-width:1px}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.bg-emerald-50{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.bg-white\\/70{background-color:#ffffff73}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-slate-50{--tw-gradient-fromLine truncated
@@ -5,7 +5,7 @@
*/
import type { ExportSessionData } from '../types.js';
import { HTML_TEMPLATE } from './htmlTemplate.js';
import { EXPORT_HTML_TEMPLATE as HTML_TEMPLATE } from '@qwen-code/web-templates';
/**
* Escapes JSON for safe embedding in HTML.
@@ -1,10 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* This HTML template is code-generated; do not edit manually.
*/
export const HTML_TEMPLATE =
'<!doctype html>\n<html lang="en" class="dark">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <link\n rel="icon"\n type="image/svg+xml"\n href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20141.38%20140%22%3E%0A%20%20%3Cpath%0A%20%20%20%20fill%3D%22%23000000%22%0A%20%20%20%20d%3D%22m140.93%2085-16.35-28.33-1.93-3.34%208.66-15a3.323%203.323%200%200%200%200-3.34l-9.62-16.67c-.3-.51-.72-.93-1.22-1.22s-1.07-.45-1.67-.45H82.23l-8.66-15a3.33%203.33%200%200%200-2.89-1.67H51.43c-.59%200-1.17.16-1.66.45-.5.29-.92.71-1.22%201.22L32.19%2029.98l-1.92%203.33H12.96c-.59%200-1.17.16-1.66.45-.5.29-.93.71-1.22%201.22L.45%2051.66a3.323%203.323%200%200%200%200%203.34l18.28%2031.67-8.66%2015a3.32%203.32%200%200%200%200%203.34l9.62%2016.67c.3.51.72.93%201.22%201.22s1.07.45%201.67.45h36.56l8.66%2015a3.35%203.35%200%200%200%202.89%201.67h19.25a3.34%203.34%200%200%200%202.89-1.67l18.28-31.67h17.32c.6%200%201.17-.16%201.67-.45s.92-.71%201.22-1.22l9.62-16.67a3.323%203.323%200%200%200%200-3.34ZM51.44%203.33%2061.07%2020l-9.63%2016.66h76.98l-9.62%2016.66H45.67l-11.54-20zM57.21%20120H22.58l9.63-16.67h19.25l-38.5-66.67h19.25l9.62%2016.67L68.78%20100l-11.55%2020Zm61.59-33.34-9.62-16.67-38.49%2066.67-9.63-16.67%209.63-16.66%2026.94-46.67h23.1l17.32%2030z%22%0A%20%20%2F%3E%0A%3C%2Fsvg%3E"\n />\n <script\n crossorigin\n src="https://unpkg.com/[email protected]/umd/react.production.min.js"\n ></script>\n <script\n crossorigin\n src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"\n ></script>\n <script>\n const withKey = (props, key) =>\n key == null ? props : Object.assign({}, props, { key });\n const jsx = (type, props, key) =>\n React.createElement(type, withKey(props, key));\n const jsxRuntime = {\n Fragment: React.Fragment,\n jsx,\n jsxs: jsx,\n jsxDEV: jsx,\n };\n window.ReactJSXRuntime = jsxRuntime;\n window[\'react/jsx-runtime\'] = jsxRuntime;\n window[\'react/jsx-dev-runtime\'] = jsxRuntime;\n </script>\n <script src="https://unpkg.com/@qwen-code/webui@latest/dist/index.umd.js"></script>\n <link\n rel="stylesheet"\n href="https://unpkg.com/@qwen-code/webui@latest/dist/styles.css"\n />\n <title>Qwen Code Chat Export</title>\n <style>\n @font-face {\n font-family: "Press Start 2P";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url("data:font/woff2;base64,d09GMgABAAAAAALQAAwAAAAABfwAAAKBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHCgGYABUEQgKgkSCDAsOAAE2AiQDDgQgBYRKByAb4QTIBB76Oev9JKW02x5ClpELSxHm1M0JQ+OBJAtDpAAVUQ7i///V+A/YgLVswlGo/7a+lmW3sfnUvoBFcUKzUFEnKo08S0F2T8TiAAJQieDvSGUVdW3smBv2LuCGjSpG3jQTD/m9OI5lTDomdQG1hkoMkCJu1ZYsmL42dEu7W8q5gUan/J6yyO+FX5NO5qQPxaqGnEINZ2q7kDf0+i4UigAXdiQUNMppoJnOzcxnDMwX5lXzinnJPDtkzhQFwjTx0DGrx1zkoNhIYr5LTKHjclcs+z3Frrh2ue3Kvk6bxzoRJLsIQpWcQhW27bgJwoUHYXNi2WWnVRUWaVNS0XMJ1eIUHlSX0l3l7BZOOos1ySk7NUWl6+eobG3Nfpj96g8EvukB3Qjouo4+O6svG9/0b8ZyKmfoZj9gzBqq8I2MS7Pf2JquL9vZwHJ51djyD01vbVHRDxSslvFBZwMHjQvexMxIvz3oyftul2+k9yBS+xGM+l+d+Pvo/zH5fVXMRvsHhG4u9/dRsgQyF67l9+jYgZuy+Ath2O6EUzU7E6BxF41nQk2cgkN8IIlOPKecMgWccSrE4XdaOECf00oUaeTm34EpvHhZIodEEllllBWmWcJLyQmsPjNHAousMEkijVRQR5OgNc4qq7TiZZg89pJCE/G0MM4kPuYqqIOKrQw3WmQBjVQSSLLm1H1NiGmCN21uwK/JpZRuWignd/klmbWXFEtZZIn1o8cmkaDqX5JIJgWNNqYYR9s0GsRFZhhntKJSjI+KTaFDZ+MoRllkjHWK2ADCxilLQGTkJZo4NAJMDwhAiK0CJD+jjKFRwWBapYFh5rdzfogQC/m1XLplXiEFEwAA") format("woff2");\n }\n :root{--bg-primary: #18181b;--bg-secondary: #27272a;--text-primary: #f4f4f5;--text-secondary: #a1a1aa;--border-color: #3f3f46;--accent-color: #3b82f6}body{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;margin:0;padding:0;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;-webkit-font-smoothing:antialiased}.page-wrapper{min-height:100vh;display:flex;flex-direction:column;align-items:center}.header{width:100%;padding:16px 24px;border-bottom:1px solid var(--border-color);background-color:#18181bf2;backdrop-filter:blur(8px);position:sticky;top:0;z-index:100;display:flex;justify-content:space-between;align-items:center;box-sizing:border-box}.header-left{display:flex;align-items:center;gap:12px}.logo-icon{width:24px;height:24px;flex-shrink:0;display:flex;align-items:center;justify-content:center}.logo-icon svg{width:100%;height:100%}.logo{display:flex;flex-direction:column;line-height:1}.logo-text{font-family:"Press Start 2P",cursive;font-weight:400;font-size:24px;letter-spacing:-.05em;position:relative;color:#fff}.logo-text-inner{background:linear-gradient(to right,#60a5fa,#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent;position:relative;z-index:2}.logo-text:before,.logo-text:after{content:attr(data-text);position:absolute;top:0;left:0;width:1Line truncated
+54
View File
@@ -0,0 +1,54 @@
import { spawn } from 'node:child_process';
import { mkdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
const assetsDir = join(dirname(fileURLToPath(import.meta.url)), 'src');
await mkdir(join(assetsDir, 'generated'), { recursive: true });
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const assetBuilds = [
{
name: 'insight',
assetPath: join(assetsDir, 'insight'),
buildPath: join(assetsDir, 'insight', 'build.mjs'),
},
{
name: 'export-html',
assetPath: join(assetsDir, 'export-html'),
buildPath: join(assetsDir, 'export-html', 'build.mjs'),
},
];
const runCommand = ({ command, args, cwd, label }) =>
new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'inherit',
shell: process.platform === 'win32',
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${label} failed for ${cwd}.`));
}
});
});
const runBuild = async (asset) => {
await runCommand({
command: process.execPath,
args: [asset.buildPath],
cwd: asset.assetPath,
label: `Node build`,
});
};
console.log('Building web-templates...');
await Promise.all(assetBuilds.map((asset) => runBuild(asset)));
console.log('Successfully built all web-templates.');
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@qwen-code/web-templates",
"version": "0.10.0",
"description": "Web templates bundled as embeddable JS/CSS strings",
"repository": {
"type": "git",
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "node build.mjs && tsc --build --clean && tsc",
"build:templates": "node build.mjs"
},
"files": [
"dist"
],
"dependencies": {},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "^5.3.3",
"vite": "^5.0.0"
},
"engines": {
"node": ">=20"
}
}
@@ -8,32 +8,15 @@ import prettier from 'prettier';
const assetsDir = dirname(fileURLToPath(import.meta.url));
const srcDir = join(assetsDir, 'src');
const assetsDistDir = join(assetsDir, 'dist');
const packageDistDir = join(
assetsDir,
'..',
'..',
'dist',
'assets',
'export-html',
);
const templateModulePath = join(
assetsDir,
'..',
'..',
'src',
'ui',
'utils',
'export',
'formatters',
'htmlTemplate.ts',
);
const packageJsonPath = join(assetsDir, 'package.json');
const generatedDir = join(assetsDir, '..', 'generated');
await mkdir(generatedDir, { recursive: true });
await mkdir(assetsDistDir, { recursive: true });
await mkdir(packageDistDir, { recursive: true });
const templateModulePath = join(generatedDir, 'exportHtmlTemplate.ts');
const packageJsonPath = join(assetsDir, 'package.json');
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
const dependencyVersions = packageJson?.dependencies ?? {};
const getDependencyVersion = (name) => {
const version = dependencyVersions[name];
if (!version) {
@@ -101,5 +84,4 @@ const formattedTemplateModule = await prettier.format(templateModule, {
});
await writeFile(join(assetsDistDir, 'index.html'), htmlOutput);
await writeFile(join(packageDistDir, 'index.html'), htmlOutput);
await writeFile(templateModulePath, formattedTemplateModule);
File renamed without changes.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 933 B

+9
View File
@@ -0,0 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export { INSIGHT_JS, INSIGHT_CSS } from './generated/insightTemplate.js';
export { HTML_TEMPLATE as EXPORT_HTML_TEMPLATE } from './generated/exportHtmlTemplate.js';
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-undef */
import { writeFile, readFile } from 'node:fs/promises';
import { writeFile, readFile, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { build } from 'vite';
@@ -8,16 +8,10 @@ import { build } from 'vite';
const assetsDir = dirname(fileURLToPath(import.meta.url));
const distDir = join(assetsDir, 'dist');
const templateModulePath = join(
assetsDir,
'..',
'..',
'src',
'services',
'insight',
'templates',
'insightTemplate.ts',
);
const generatedDir = join(assetsDir, '..', 'generated');
await mkdir(generatedDir, { recursive: true });
const templateModulePath = join(generatedDir, 'insightTemplate.ts');
console.log('Building insight assets with Vite...');
await build();
@@ -0,0 +1,9 @@
{
"name": "@qwen-code/cli-insight",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "node build.mjs"
}
}
@@ -1,6 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};
@@ -14,7 +14,7 @@ import {
} from './Qualitative';
import { ShareCard, type Theme } from './ShareCard';
import './styles.css';
import { InsightData } from './types';
import type { InsightData } from './types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react';
@@ -1,4 +1,4 @@
import { InsightData } from './types';
import type { InsightData } from './types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react';
@@ -198,7 +198,7 @@ function ActivityHeatmap({
const startDayOfWeek = oneYearAgo.getDay();
// Generate month labels
const monthLabels: { x: number; text: string }[] = [];
const monthLabels: Array<{ x: number; text: string }> = [];
let lastMonth = -1;
let lastX = -100; // Initialize with a value far to the left
@@ -216,7 +216,7 @@ function ActivityHeatmap({
// Approximate width of a month label is about 25-30px
if (x - lastX > 30) {
monthLabels.push({
x: x,
x,
text: months[currentMonth],
});
lastX = x;
@@ -1,6 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react';
import { InsightData } from './types';
import type { InsightData } from './types';
// Header Component
export function Header({
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState } from 'react';
import { DashboardCards, HeatmapSection } from './Charts';
import { InsightData, QualitativeData } from './types';
import type { InsightData, QualitativeData } from './types';
import { CopyButton, MarkdownText } from './Components';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react';
@@ -72,7 +72,7 @@ export function ProjectAreas({
}: {
qualitative: QualitativeData;
topGoals?: Record<string, number>;
topTools?: Record<string, number> | [string, number][];
topTools?: Record<string, number> | Array<[string, number]>;
}) {
const { projectAreas } = qualitative;
@@ -1,6 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React from 'react';
import { InsightData } from './types';
import type { InsightData } from './types';
/**
* Theme configuration for the share card
@@ -494,7 +494,7 @@ function ActiveHoursChart({
function buildMiniHeatmap(
heatmap: Record<string, number>,
theme: ThemeConfig,
): { color: string }[] {
): Array<{ color: string }> {
const today = new Date();
const weeksToShow = 26;
const totalDays = weeksToShow * 7;
@@ -504,7 +504,7 @@ function buildMiniHeatmap(
// Align to the beginning of the week (Sunday)
startDate.setDate(startDate.getDate() - startDate.getDay());
const cells: { color: string }[] = [];
const cells: Array<{ color: string }> = [];
const endDate = new Date(today);
endDate.setDate(endDate.getDate() + (6 - endDate.getDay())); // end of this week
@@ -529,7 +529,7 @@ function heatColor(val: number, theme: ThemeConfig): string {
return theme.heatmapColors[3];
}
function MiniHeatmapGrid({ cells }: { cells: { color: string }[] }) {
function MiniHeatmapGrid({ cells }: { cells: Array<{ color: string }> }) {
const rows = 7;
const cols = Math.ceil(cells.length / rows);
const cellSize = 14;
@@ -2,7 +2,7 @@
* Dev entry point — injects mock data then mounts the app.
* Used by `vite` dev server via index.html.
*/
import { InsightData } from './types';
import type { InsightData } from './types';
const MOCK_DATA: InsightData = {
heatmap: {
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { InsightData } from '../../../src/services/insight/types/StaticInsightTypes';
import { QualitativeInsights as QualitativeData } from '../../../src/services/insight/types/QualitativeInsightTypes';
import type { InsightData } from '../../../src/services/insight/types/StaticInsightTypes';
import type { QualitativeInsights as QualitativeData } from '../../../src/services/insight/types/QualitativeInsightTypes';
declare global {
interface Window {
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ES2023"],
"types": ["node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist", "src/insight", "src/export-html"]
}
+6 -4
View File
@@ -36,13 +36,15 @@ execSync('npm run generate', { stdio: 'inherit', cwd: root });
// Build in dependency order:
// 1. test-utils (no internal dependencies)
// 2. core (foundation package)
// 3. cli (depends on core, test-utils)
// 4. webui (shared UI components - used by vscode companion)
// 5. sdk (no internal dependencies)
// 6. vscode-ide-companion (depends on webui)
// 3. web-templates (embeddable web templates - used by cli)
// 4. cli (depends on core, test-utils, web-templates)
// 5. webui (shared UI components - used by vscode companion)
// 6. sdk (no internal dependencies)
// 7. vscode-ide-companion (depends on webui)
const buildOrder = [
'packages/test-utils',
'packages/core',
'packages/web-templates',
'packages/cli',
'packages/webui',
'packages/sdk-typescript',
+16 -54
View File
@@ -19,9 +19,9 @@
import { execSync } from 'node:child_process';
import {
chmodSync,
existsSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs';
@@ -85,64 +85,26 @@ if (!image.length) {
);
}
// Build in dependency order to ensure packages are built before their dependents
// This is the same order as defined in build.js
const buildOrder = [
'packages/test-utils',
'packages/core',
'packages/cli',
'packages/webui',
'packages/sdk-typescript',
'packages/vscode-ide-companion',
];
if (!argv.s) {
execSync('npm install', { stdio: 'inherit' });
// Build in dependency order instead of using --workspaces
for (const workspace of buildOrder) {
execSync(`npm run build --workspace=${workspace}`, {
stdio: 'inherit',
});
execSync('npm run build', { stdio: 'inherit' });
console.log('bundling...');
execSync('npm run bundle', { stdio: 'inherit' });
console.log('preparing package...');
execSync('npm run prepare:package', { stdio: 'inherit' });
console.log('packing...');
const distDir = join(process.cwd(), 'dist');
for (const f of readdirSync(distDir)) {
if (f.endsWith('.tgz')) {
rmSync(join(distDir, f), { force: true });
}
}
execSync('npm pack', { stdio: 'ignore', cwd: distDir });
}
console.log('packing @qwen-code/qwen-code ...');
const cliPackageDir = join('packages', 'cli');
rmSync(join(cliPackageDir, 'dist', 'qwen-code-*.tgz'), { force: true });
execSync(
`npm pack -w @qwen-code/qwen-code --pack-destination ./packages/cli/dist`,
{
stdio: 'ignore',
},
);
console.log('packing @qwen-code/qwen-code-core ...');
const corePackageDir = join('packages', 'core');
rmSync(join(corePackageDir, 'dist', 'qwen-code-core-*.tgz'), {
force: true,
});
execSync(
`npm pack -w @qwen-code/qwen-code-core --pack-destination ./packages/core/dist`,
{ stdio: 'ignore' },
);
const packageVersion = JSON.parse(
readFileSync(join(process.cwd(), 'package.json'), 'utf-8'),
).version;
chmodSync(
join(cliPackageDir, 'dist', `qwen-code-qwen-code-${packageVersion}.tgz`),
0o755,
);
chmodSync(
join(
corePackageDir,
'dist',
`qwen-code-qwen-code-core-${packageVersion}.tgz`,
),
0o755,
);
const buildStdout = process.env.VERBOSE ? 'inherit' : 'ignore';
// Determine the appropriate shell based on OS