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>
This commit is contained in:
1 parent
50bf5bda8e
commit
e27385e0f0
26 files changed
+1349
-1
No files matched your search
@@ -0,0 +1,70 @@
|
||||
// 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>`)
|
||||
Reference in new issue
Block a user