feat(generators): ui byte-identical snapshot + dispatch entry + e2e test

- Adds ui.snapshot.json (28 entries) computed from packages/core-ui
- Wires CORE_PACKAGE_GENERATORS["ui"]: guard + emitTemplateTree +
  addToTranspilePackages + printUiNextSteps (web-tanstack/storybook
  wiring is printed, not generated — resists templating)
- Adds "ui" to the core-package generator choices list
- Adds core-package-ui.e2e.test.ts (byte-identical reconstruction)
  — passes in 14.6 s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 14:19:22 +02:00
parent d974bb6267
commit d42d0e5c5c
3 changed files with 210 additions and 1 deletions

View File

@@ -0,0 +1,114 @@
[
{
"path": "AGENTS.md",
"sha256": "ea4a4eeea6cb08364fb7d9cf5c5757147442df1e9b1907b9118fcd313cd1c4a7"
},
{
"path": "eslint.config.js",
"sha256": "64a85c158e386417d855793d256b9c59be38635ce2febcb5a6a15b17f4143745"
},
{
"path": "package.json",
"sha256": "8ffd2e9c8adb1e0eab863a9abc81bb90a05afdbd625d1348e0f2ba7874d0680f"
},
{
"path": "src/atoms/button/button.stories.tsx",
"sha256": "5ea74a9a4b2431b2a4dbae456bcce6d051842caad19e83ca8e15d99f79d36b5d"
},
{
"path": "src/atoms/button/button.test.tsx",
"sha256": "e768f2c1ebd7e120d543a95c67a2a703a85c5ad6847b7e7f8420fa472771fb82"
},
{
"path": "src/atoms/button/button.tsx",
"sha256": "4d62a3a9703721f64d397e468cdd90225b1d6dd739fd8c17b90962c8e9a7b2c3"
},
{
"path": "src/atoms/button/index.ts",
"sha256": "fb0a14a0c15536e05a58d6d03f41afcee85fb6a24c5ca6910cb172ae26fbcc34"
},
{
"path": "src/atoms/index.ts",
"sha256": "9ec29d14a7a7729b4eca9ba95b4016812a4c1c2cc9a46510d5f2d019744852b3"
},
{
"path": "src/atoms/input/index.ts",
"sha256": "490ba122bc3161dc59d24b274372608b8b319f5c0270699fcdcc3fb6d5d4cffd"
},
{
"path": "src/atoms/input/input.stories.tsx",
"sha256": "c95b51b7b3c4ad42f7b118a9a6569ad6d6a3200f6bf1f2d8f029a92bc307c53e"
},
{
"path": "src/atoms/input/input.test.tsx",
"sha256": "b0fdb584cc9def4c1c7394e6f524c5d614ac74284a50d298ee48254dd4a80162"
},
{
"path": "src/atoms/input/input.tsx",
"sha256": "bd2f477b3a79a29549d18db9605e1a34290fa31ea97d6292d6be77d2e95729f4"
},
{
"path": "src/atoms/label/index.ts",
"sha256": "d9a205b2fd3133823a74c265e09b01dcd5068ce21b82217386d6166bbec68fb6"
},
{
"path": "src/atoms/label/label.test.tsx",
"sha256": "ac8135a7a05a61e34ec9346148d9305190b5f8ec3dd3a5d15df2b2841e24caac"
},
{
"path": "src/atoms/label/label.tsx",
"sha256": "6c6f13125fbc96b55806b9ff2230472e8d24ba45c7cd39e58573460691e23885"
},
{
"path": "src/index.ts",
"sha256": "56303fd5e5d7fe574855887defcb974d2f86667dc926ea690f3d3757bbcc4617"
},
{
"path": "src/lib/utils.ts",
"sha256": "d1f1e0d62cb8d8d1e04c26e14de842d8a151f75812d81b046c65b5d1fe8e4b27"
},
{
"path": "src/molecules/form-field/form-field.stories.tsx",
"sha256": "914c842de560d80091970e3e1ae15c214c396893cc735b6f25460620528e5157"
},
{
"path": "src/molecules/form-field/form-field.test.tsx",
"sha256": "a63244a6a00f7fdba01223c0697a9cfe93a52917e7d09b577e9ffa6547b1399c"
},
{
"path": "src/molecules/form-field/form-field.tsx",
"sha256": "f51a6c4e9550fccaff90a932b9ad3906e8973c82faf0237cf07e1c864536f5f6"
},
{
"path": "src/molecules/form-field/index.ts",
"sha256": "7fe955a7b2c65b1732ee799812be4ac3150346c5bdc91ea6afe0ea7c1034d2cc"
},
{
"path": "src/molecules/index.ts",
"sha256": "eeb24cfbb8b6d2c1a5f5c144eae4db37f6829ad0f62eb163f428a666bc742774"
},
{
"path": "src/organisms/index.ts",
"sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
},
{
"path": "src/styles/globals.css",
"sha256": "58c6e64ac0b26f3897f9a76f86a35dfff1acc7bae8e7e761bca0c8035763cfd2"
},
{
"path": "src/templates/index.ts",
"sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
},
{
"path": "tsconfig.json",
"sha256": "7bbc28ad21b936ca80c7a3ec21094d34012d624ca1d4e63a6f3c3120abba551f"
},
{
"path": "turbo.json",
"sha256": "ae544305202c960399110a31fe976e2c38577956c5aa738989f5d885f156d130"
},
{
"path": "vitest.config.ts",
"sha256": "053df54e347b147d85632286d44e6587aa6c3ae39677fbc1a8599e89ae48a81c"
}
]

View File

@@ -0,0 +1,52 @@
import { describe, it, expect } from "vitest";
import { mkdtempSync, cpSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { computeSnapshot } from "../lib/snapshot";
import expectedSnapshot from "../__snapshots__/core-package/ui.snapshot.json";
// Repo root is 2 levels up from turbo/generators/__tests__
const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "..", "..", "..", "..");
/**
* Strip "@repo/core-ui" from a package.json file in the tmp tree.
* Required when simulating a fresh scaffold: core-ui does not exist yet
* but the snapshot was captured before removal. Apps in the current tree
* still list it as a dependency.
*/
function stripCoreUiDep(pkgJsonPath: string): void {
const raw = readFileSync(pkgJsonPath, "utf8");
const parsed = JSON.parse(raw) as Record<string, Record<string, string>>;
for (const section of ["dependencies", "devDependencies", "peerDependencies"] as const) {
if (parsed[section]?.["@repo/core-ui"]) {
delete parsed[section]["@repo/core-ui"];
}
}
writeFileSync(pkgJsonPath, JSON.stringify(parsed, null, 2) + "\n");
}
describe("e2e: core-package ui", () => {
it("byte-identical reconstruction matches snapshot", { timeout: 120_000 }, () => {
const tmp = mkdtempSync(join(tmpdir(), "e2e-ui-"));
cpSync(REPO_ROOT, tmp, {
recursive: true,
filter: (src) =>
!src.includes("node_modules") &&
!src.includes(".turbo") &&
!src.includes("packages/core-ui"),
});
// Strip @repo/core-ui from app package.json files so pnpm install
// succeeds without the package being present (simulating the post-removal state).
stripCoreUiDep(join(tmp, "apps", "web-next", "package.json"));
stripCoreUiDep(join(tmp, "apps", "web-tanstack", "package.json"));
stripCoreUiDep(join(tmp, "apps", "storybook", "package.json"));
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { stdio: "ignore" });
execSync(`cd ${tmp} && pnpm turbo gen core-package --args ui`, { stdio: "ignore" });
const result = computeSnapshot(join(tmp, "packages/core-ui"));
expect(result).toEqual(expectedSnapshot);
});
});

View File

@@ -654,6 +654,18 @@ import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";
},
printTrpcNextSteps,
],
ui: () => [
() => {
assertOptionalPackageNotPresent("core-ui");
return "Guard passed — packages/core-ui does not exist yet.";
},
...emitTemplateTree("core-package/ui", "packages/core-ui"),
() => {
addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-ui");
return "Added @repo/core-ui to transpilePackages.";
},
printUiNextSteps,
],
};
plop.setGenerator("core-package", {
@@ -663,7 +675,7 @@ import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";
type: "list",
name: "name",
message: "Which optional core package?",
choices: ["realtime", "events", "trpc"],
choices: ["realtime", "events", "trpc", "ui"],
},
],
actions: (answers) => {
@@ -1187,6 +1199,37 @@ function printTrpcNextSteps(): string {
].join("\n");
}
function printUiNextSteps(): string {
return [
"─────────────────────────────────────────────────────────────",
"@repo/core-ui scaffolded.",
"",
"Next steps (manual wiring — not generated):",
"",
" 1. pnpm install # link the new workspace package",
"",
" 2. apps/web-next/package.json:",
' - add "@repo/core-ui": "workspace:*" to dependencies',
"",
" 3. apps/storybook/.storybook/main.ts:",
' - add "../../../packages/core-ui/src/**/*.stories.@(ts|tsx)" to the stories array',
"",
" 4. apps/storybook/.storybook/preview.ts:",
' - add: import "@repo/core-ui/styles/globals.css";',
"",
" 5. apps/storybook/package.json:",
' - add "@repo/core-ui": "workspace:*" to dependencies',
"",
" 6. apps/web-tanstack/package.json:",
' - add "@repo/core-ui": "workspace:*" to dependencies',
" - when TanStack Start gains a real vite.config.ts, add @repo/core-ui",
" to optimizeDeps.include",
"",
" 7. pnpm typecheck && pnpm lint && pnpm test",
"─────────────────────────────────────────────────────────────",
].join("\n");
}
function printEventsNextSteps(): string {
return [
"─────────────────────────────────────────────────────────────",