Files
HMDesign/packages/sw/tests/ai-template-consumer.mjs
T
shkim4andClaude Opus 5.5 e27385e0f0 feat(sw): prepare reusable AI-friendly component package
- Ship an AI guide in the package (ai/: usage rules, component catalog,
  design rules, composition patterns) via package.json files.
- Add examples A-D that import only public exports: default header,
  viewer header, feature header, and a consumer-side search extension
  that reuses SWHeaderInput and the public SW tokens.
- Add SW-only checks: docs/API consistency, tarball consumer project
  (React 19 + TS + Vite typecheck/build) and a browser check (27/27).
- No runtime source or public API change; README gains a pointer section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:22:07 +09:00

71 lines
4.2 KiB
JavaScript

// Independent consumer check for @hm-design/sw (AI template, 2026-10-19).
// Usage: node packages/sw/tests/ai-template-consumer.mjs <empty-dir-outside-the-repo>
// Packs the built package (npm pack), creates a React 19 + TypeScript + Vite project in <dir>, installs the
// tarball, copies the shipped ai/examples from node_modules (not from the repo) and runs tsc + vite build.
// Build the package first: npm run build -w @hm-design/sw
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 out = path.resolve(process.argv[2] ?? "")
if (!process.argv[2] || out.startsWith(path.resolve(packageRoot, "../.."))) 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)
const tarball = (await readdir(out)).find(name => name.endsWith(".tgz"))
const files = {
"package.json": JSON.stringify({
name: "sw-ai-template-consumer", private: true, type: "module",
scripts: { typecheck: "tsc -p tsconfig.json", build: "vite build" },
}, 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": `import { defineConfig } from "vite"\nimport react from "@vitejs/plugin-react"\nexport default defineConfig({ plugins: [react()] })\n`,
"index.html": `<!doctype html><html lang="ko"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>SW consumer</title><link rel="icon" href="data:," /><style>body { margin: 0; }</style></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>\n`,
"src/main.tsx": `import { StrictMode, useState } from "react"
import { createRoot } from "react-dom/client"
import "@hm-design/sw/styles.css"
import { ExampleADefaultHeader } from "./examples/ExampleA-DefaultHeader"
import { ExampleBViewerHeader } from "./examples/ExampleB-ViewerHeader"
import { ExampleCFeatureHeader } from "./examples/ExampleC-FeatureHeader"
import { ExampleDSearchExtension } from "./examples/ExampleD-SearchExtension"
const examples = { a: ExampleADefaultHeader, b: ExampleBViewerHeader, c: ExampleCFeatureHeader, d: ExampleDSearchExtension }
function App() {
const [events, setEvents] = useState<string[]>([])
const onEvent = (name: string) => setEvents(list => [...list, name])
return (
<>
{Object.entries(examples).map(([id, Example]) => (
<section key={id} data-example={id} style={{ marginBottom: 360 }}><Example onEvent={onEvent} /></section>
))}
<pre id="events">{events.join("\\n")}</pre>
</>
)
}
createRoot(document.getElementById("root")!).render(<StrictMode><App /></StrictMode>)
`,
}
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", `./${tarball}`, "react@19.3.0", "react-dom@19.3.0"], out)
npm(["install", "--no-audit", "--no-fund", "-D", "typescript@7.0.2", "vite@8.3.0", "@vitejs/plugin-react@6.1.1", "@types/react@19.3.0", "@types/react-dom@19.3.0"], out)
// The examples are taken from the installed package: what an AI tool in another project would read.
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} (dist/ built). Next: node packages/sw/tests/ai-template-consumer.browser.mjs ${out} <evidence-dir>`)