// Independent consumer of the 2026-10-19 SW integrated improvement: React 19 + TypeScript + Vite + Tailwind CSS 4 // with BOTH tarballs (@hm-design/sw and @hm-design/ui). Two builds load the same app with the stylesheets in both // orders (SW first / UI first) to show that they do not change each other. The shipped ai/examples (A–F) are // copied from node_modules, plus a popup holding HM UI components. // Usage: node packages/sw/tests/integrated-consumer.mjs // Build first: npm run build -w @hm-design/sw && npm run build -w @hm-design/ui import { execFileSync } from "node:child_process" import { cp, mkdir, readdir, writeFile } from "node:fs/promises" import path from "node:path" import { fileURLToPath } from "node:url" const packageRoot = fileURLToPath(new URL("../", import.meta.url)) const repoRoot = path.resolve(packageRoot, "../..") const out = path.resolve(process.argv[2] ?? "") const relative = path.relative(repoRoot, out) if (!process.argv[2] || !(relative.startsWith("..") || path.isAbsolute(relative))) throw new Error("Pass an empty directory outside the repository") await mkdir(out, { recursive: true }) if ((await readdir(out)).length) throw new Error(`${out} is not empty`) const npm = (args, cwd) => execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", args, { cwd, stdio: "inherit", shell: process.platform === "win32" }) npm(["pack", "--ignore-scripts", "--pack-destination", out], packageRoot) npm(["pack", "--ignore-scripts", "--pack-destination", out], path.join(repoRoot, "packages/ui")) const tarballs = (await readdir(out)).filter(name => name.endsWith(".tgz")) const app = `import { useState } from "react" import { SWHeaderInput, SWPopup, SWPopupRow, SWPopupSection, SWTheme } from "@hm-design/sw" import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider, Switch } from "@hm-design/ui" import { ExampleADefaultHeader } from "./examples/ExampleA-DefaultHeader" import { ExampleBViewerHeader } from "./examples/ExampleB-ViewerHeader" import { ExampleCFeatureHeader } from "./examples/ExampleC-FeatureHeader" import { ExampleDSearchExtension } from "./examples/ExampleD-SearchExtension" import { ExampleECustomPopup } from "./examples/ExampleE-CustomPopup" import { ExampleFViewerToolbar } from "./examples/ExampleF-ViewerToolbar" const examples = { a: ExampleADefaultHeader, b: ExampleBViewerHeader, c: ExampleCFeatureHeader, d: ExampleDSearchExtension, e: ExampleECustomPopup, f: ExampleFViewerToolbar } function UiPopup({ onEvent }: { onEvent: (name: string) => void }) { const [snap, setSnap] = useState(true) const [unit, setUnit] = useState("mm") const [opacity, setOpacity] = useState([60]) return ( onEvent("ui:close")}> onEvent("ui:memo:" + next)} /> { setSnap(next); onEvent("ui:snap:" + next) }} /> onEvent("ui:opacity:" + next[0])} /> ) } export function App() { const [events, setEvents] = useState([]) const onEvent = (name: string) => setEvents(list => [...list, name]) return ( <> {Object.entries(examples).map(([id, Example]) => (
))}
{events.join("\\n")}
) } ` // The bundler merges the CSS of pages that share modules, so the two stylesheet orders are two builds // (--mode sw-first / ui-first): "app-order.css" is aliased to the stylesheet entry of that order. const entry = `import { StrictMode } from "react" import { createRoot } from "react-dom/client" import "app-order.css" import { App } from "./App" createRoot(document.getElementById("root")!).render() ` const viteConfig = `import { fileURLToPath } from "node:url" import { defineConfig } from "vite" import react from "@vitejs/plugin-react" import tailwindcss from "@tailwindcss/vite" export default defineConfig(({ mode }) => ({ base: "./", plugins: [react(), tailwindcss()], resolve: { alias: { "app-order.css": fileURLToPath(new URL(mode === "ui-first" ? "./src/ui-first.css" : "./src/sw-first.css", import.meta.url)) } }, build: { outDir: mode === "ui-first" ? "dist/ui-first" : "dist/sw-first" }, })) ` const ui = `@import "tailwindcss";\n@import "@hm-design/ui/tailwind.css";\n@import "@hm-design/ui/theme.css";\n` const sw = `@import "@hm-design/sw/styles.css";\n` const html = `SW consumer
\n` const files = { "package.json": JSON.stringify({ name: "sw-integrated-consumer", private: true, type: "module", scripts: { typecheck: "tsc -p tsconfig.json", build: "vite build --mode sw-first && vite build --mode ui-first" } }, null, 2), "tsconfig.json": JSON.stringify({ compilerOptions: { target: "ES2022", lib: ["ES2022", "DOM", "DOM.Iterable"], module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", strict: true, noEmit: true, skipLibCheck: false, isolatedModules: true, noUnusedLocals: true, noUnusedParameters: true, types: ["vite/client"] }, include: ["src"] }, null, 2), "vite.config.ts": viteConfig, "index.html": html, "src/sw-first.css": sw + ui, "src/ui-first.css": ui + sw, "src/App.tsx": app, "src/main.tsx": entry, } for (const [name, text] of Object.entries(files)) { await mkdir(path.dirname(path.join(out, name)), { recursive: true }) await writeFile(path.join(out, name), text) } npm(["install", "--no-audit", "--no-fund", ...tarballs.map(name => `./${name}`), "react@19.3.0", "react-dom@19.3.0", "tailwindcss@4.3.3"], out) npm(["install", "--no-audit", "--no-fund", "-D", "typescript@7.0.2", "vite@8.3.0", "@vitejs/plugin-react@6.1.1", "@tailwindcss/vite@4.3.3", "@types/react@19.3.0", "@types/react-dom@19.3.0"], out) await cp(path.join(out, "node_modules/@hm-design/sw/ai/examples"), path.join(out, "src/examples"), { recursive: true }) npm(["run", "typecheck"], out) npm(["run", "build"], out) console.log(`Consumer ready: ${out}. Next: node packages/sw/tests/integrated-consumer.browser.mjs ${out} `)