fix(generators): run UI tests, guard optional bus, harden e2e clones

Three generator fixes:
- templates/feature/vitest.config.ts.hbs lacked an include for
  .test.{ts,tsx}; the node base only includes .test.ts, so scaffolded
  UI component tests never executed
- gen event consume emitted an unguarded bus.subscribe although
  ctx.bus is optional in BindContext — now wrapped in if (bus) {}
- e2e repo clones now exclude /dist and /.next build outputs, and
  every dep-stripping e2e strips the scaffolded package from EVERY
  workspace package.json via globSync instead of a hardcoded dependent
  list that drifts as packages gain or drop the dependency

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:42:46 +02:00
parent 1e3220aef5
commit a17b984675
11 changed files with 100 additions and 41 deletions

View File

@@ -31,6 +31,10 @@ describe("e2e: core-package analytics", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-analytics"), !src.includes("packages/core-analytics"),
}); });

View File

@@ -3,6 +3,7 @@ import {
mkdtempSync, mkdtempSync,
rmSync, rmSync,
cpSync, cpSync,
globSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
@@ -58,12 +59,22 @@ describe("e2e: core-package audit", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-audit"), !src.includes("packages/core-audit"),
}); });
// Strip @repo/core-audit from apps/web-next/package.json so pnpm install // Strip @repo/core-audit from EVERY workspace package.json so pnpm
// succeeds without the package being present (simulating the post-removal state). // install succeeds without the package being present (simulating the
stripCoreAuditDep(join(tmp, "apps", "web-next", "package.json")); // post-removal state) — a hardcoded dependent list drifts as packages
// gain or drop the dependency.
for (const pkgJson of globSync(
join(tmp, "{apps,packages}", "*", "package.json"),
)) {
stripCoreAuditDep(pkgJson);
}
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
stdio: "ignore", stdio: "ignore",

View File

@@ -3,6 +3,7 @@ import {
mkdtempSync, mkdtempSync,
rmSync, rmSync,
cpSync, cpSync,
globSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
@@ -58,13 +59,22 @@ describe("e2e: core-package consent", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-consent"), !src.includes("packages/core-consent"),
}); });
// Strip @repo/core-consent from package.json files so pnpm install // Strip @repo/core-consent from EVERY workspace package.json so pnpm
// succeeds without the package being present (simulating the post-removal state). // install succeeds without the package being present (simulating the
stripCoreConsentDep(join(tmp, "packages", "core-api", "package.json")); // post-removal state) — a hardcoded dependent list drifts as packages
stripCoreConsentDep(join(tmp, "packages", "core-ui", "package.json")); // gain or drop the dependency.
for (const pkgJson of globSync(
join(tmp, "{apps,packages}", "*", "package.json"),
)) {
stripCoreConsentDep(pkgJson);
}
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
stdio: "ignore", stdio: "ignore",

View File

@@ -3,6 +3,7 @@ import {
mkdtempSync, mkdtempSync,
rmSync, rmSync,
cpSync, cpSync,
globSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
@@ -58,12 +59,22 @@ describe("e2e: core-package dsr", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-dsr"), !src.includes("packages/core-dsr"),
}); });
// Strip @repo/core-dsr from package.json files so pnpm install // Strip @repo/core-dsr from EVERY workspace package.json so pnpm
// succeeds without the package being present (simulating the post-removal state). // install succeeds without the package being present (simulating the
stripCoreDsrDep(join(tmp, "packages", "core-api", "package.json")); // post-removal state) — a hardcoded dependent list drifts as packages
// gain or drop the dependency.
for (const pkgJson of globSync(
join(tmp, "{apps,packages}", "*", "package.json"),
)) {
stripCoreDsrDep(pkgJson);
}
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
stdio: "ignore", stdio: "ignore",

View File

@@ -3,6 +3,7 @@ import {
mkdtempSync, mkdtempSync,
rmSync, rmSync,
cpSync, cpSync,
globSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
@@ -58,24 +59,22 @@ describe("e2e: core-package events", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-events"), !src.includes("packages/core-events"),
}); });
// Strip @repo/core-events from feature package.json files so pnpm install // Strip @repo/core-events from EVERY workspace package.json so pnpm
// succeeds without the package being present (simulating the post-removal state). // install succeeds without the package being present (simulating the
const featurePackages = [ // post-removal state) — a hardcoded dependent list drifts as packages
"auth", // gain or drop the dependency.
"blog", for (const pkgJson of globSync(
"media", join(tmp, "{apps,packages}", "*", "package.json"),
"marketing-pages", )) {
"navigation",
];
for (const pkg of featurePackages) {
const pkgJson = join(tmp, "packages", pkg, "package.json");
stripCoreEventsDep(pkgJson); stripCoreEventsDep(pkgJson);
} }
// Also strip from apps/web-next
stripCoreEventsDep(join(tmp, "apps", "web-next", "package.json"));
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
stdio: "ignore", stdio: "ignore",

View File

@@ -31,6 +31,10 @@ describe("e2e: core-package realtime", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-realtime"), !src.includes("packages/core-realtime"),
}); });
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {

View File

@@ -3,6 +3,7 @@ import {
mkdtempSync, mkdtempSync,
rmSync, rmSync,
cpSync, cpSync,
globSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
@@ -58,21 +59,21 @@ describe("e2e: core-package trpc", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-trpc"), !src.includes("packages/core-trpc"),
}); });
// Strip @repo/core-trpc from every package that references it so pnpm // Strip @repo/core-trpc from EVERY workspace package.json so pnpm
// install succeeds without the package being present (simulating the // install succeeds without the package being present (simulating the
// post-removal state). Apps list it directly; the blog, marketing-pages, // post-removal state) — a hardcoded dependent list drifts as packages
// and navigation features depend on it for their ./ui tRPC hooks. // gain or drop the dependency.
for (const pkgDir of [ for (const pkgJson of globSync(
["apps", "web-next"], join(tmp, "{apps,packages}", "*", "package.json"),
["apps", "web-tanstack"], )) {
["packages", "blog"], stripCoreTrpcDep(pkgJson);
["packages", "marketing-pages"],
["packages", "navigation"],
]) {
stripCoreTrpcDep(join(tmp, ...pkgDir, "package.json"));
} }
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {

View File

@@ -3,6 +3,7 @@ import {
mkdtempSync, mkdtempSync,
rmSync, rmSync,
cpSync, cpSync,
globSync,
readFileSync, readFileSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
@@ -58,14 +59,22 @@ describe("e2e: core-package ui", () => {
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store") && !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next") &&
!src.includes("packages/core-ui"), !src.includes("packages/core-ui"),
}); });
// Strip @repo/core-ui from app package.json files so pnpm install // Strip @repo/core-ui from EVERY workspace package.json so pnpm
// succeeds without the package being present (simulating the post-removal state). // install succeeds without the package being present (simulating the
stripCoreUiDep(join(tmp, "apps", "web-next", "package.json")); // post-removal state) — a hardcoded dependent list drifts as packages
stripCoreUiDep(join(tmp, "apps", "web-tanstack", "package.json")); // gain or drop the dependency.
stripCoreUiDep(join(tmp, "apps", "storybook", "package.json")); for (const pkgJson of globSync(
join(tmp, "{apps,packages}", "*", "package.json"),
)) {
stripCoreUiDep(pkgJson);
}
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
stdio: "ignore", stdio: "ignore",

View File

@@ -38,7 +38,11 @@ describe("e2e: feature generator composition", () => {
filter: (src) => filter: (src) =>
!src.includes("node_modules") && !src.includes("node_modules") &&
!src.includes(".turbo") && !src.includes(".turbo") &&
!src.includes(".pnpm-store"), !src.includes(".pnpm-store") &&
// Build outputs are large and irrelevant to the scaffold — cloning
// dist/.next fills the tmp dir and slows every e2e run.
!src.includes("/dist") &&
!src.includes("/.next"),
}); });
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, { execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {

View File

@@ -1300,7 +1300,10 @@ function handlerBindBlock(a: {
${containerVar}.unbind(${handlerSymbol}); ${containerVar}.unbind(${handlerSymbol});
} }
${containerVar}.bind(${handlerSymbol}).toConstantValue(${wrappedVar}); ${containerVar}.bind(${handlerSymbol}).toConstantValue(${wrappedVar});
bus.subscribe(${eventConst}, "${a.feature}", ${wrappedVar});`; // ctx.bus is an OPTIONAL core (guard per CLAUDE.md binder conventions).
if (bus) {
bus.subscribe(${eventConst}, "${a.feature}", ${wrappedVar});
}`;
} }
function printConsumeNextSteps(a: { function printConsumeNextSteps(a: {

View File

@@ -12,6 +12,9 @@ import {
// manifest when adjusting per-feature bands. // manifest when adjusting per-feature bands.
export default mergeConfig(nodeVitestConfig, { export default mergeConfig(nodeVitestConfig, {
test: { test: {
// The node base only includes .test.ts; UI tests are .test.tsx files
// that opt into jsdom per-file via the @vitest-environment docblock.
include: ["src/**/*.test.{ts,tsx}"],
coverage: { coverage: {
exclude: [ exclude: [
// DI bootstrap — wires InversifyJS at app startup; not unit-testable // DI bootstrap — wires InversifyJS at app startup; not unit-testable