Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/client-foldkit-scaffold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"stack-effect": minor
---

add `client-foldkit` target kind.

Includes four feature modules:

- `http-api-foldkit-client`
- `http-rpc-foldkit-client`
- `ws-presence-foldkit-client`
- `chat-foldkit-client`

also added new deterministic AST-based composition via new `ts-object-field` and `namespaceImport` contribution primitives.
104 changes: 104 additions & 0 deletions apps/cli/e2e/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,109 @@ describe("add", () => {
}).pipe(Effect.provide(CLI.layer)),
{ timeout: 90_000 },
);

it.effect(
"client-foldkit target scaffolds with rest module when server exists",
() =>
Effect.gen(function* () {
const cli = yield* CLI;
const root = `${cli.workdir}/foldkit-test`;

yield* cli.run(
"init",
"foldkit-test",
"--yes",
"--root",
cli.workdir,
);
yield* cli.expectExitCode(0);

// Add domain-api (required by server and foldkit rest)
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
"package/domain",
"--modules",
"domain-api",
);
yield* cli.expectExitCode(0);

// Add server (satisfies implication)
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
"server/api",
"--modules",
"http-api-server",
);
yield* cli.expectExitCode(0);

// Add client-foldkit with rest module
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
"client-foldkit/app",
"--modules",
"http-api-foldkit-client",
);
yield* cli.expectExitCode(0);

yield* cli.withinProject("foldkit-test", function* (project) {
yield* project.expectFileExists(
"apps/client-foldkit-app/package.json",
);
yield* project.expectFileExists(
"apps/client-foldkit-app/src/main.ts",
);
yield* project.expectFileExists(
"apps/client-foldkit-app/src/features/rest.ts",
);
yield* project.expectTypeCheckPasses();
});
}).pipe(Effect.provide(CLI.layer)),
{ timeout: 120_000 },
);

it.effect(
"rejects client-foldkit cross-target implications in non-interactive mode",
() =>
Effect.gen(function* () {
const cli = yield* CLI;
const root = `${cli.workdir}/foldkit-impl`;

yield* cli.run(
"init",
"foldkit-impl",
"--yes",
"--root",
cli.workdir,
);
yield* cli.expectExitCode(0);

// http-api-foldkit-client implies http-api-server — rejected non-interactively
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
"client-foldkit/app",
"--modules",
"http-api-foldkit-client",
);
yield* cli.expectExitCode(1);
yield* cli.expectOutputContaining("implies");
}).pipe(Effect.provide(CLI.layer)),
{ timeout: 30_000 },
);
});
});
180 changes: 106 additions & 74 deletions apps/cli/e2e/matrix.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, layer } from "@effect/vitest";
import { CatalogService } from "@repo/catalog";
import { TargetKind } from "@repo/domain/Catalog";
import { Array as Arr, Effect } from "effect";
import { Effect } from "effect";
import { CLI } from "./harness";

// ---------------------------------------------------------------------------
Expand All @@ -26,6 +26,7 @@ interface MatrixEntry {
const defaultTargetNames = new Map([
["server", "api"],
["client-react", "web"],
["client-foldkit", "app"],
["cli", "app"],
["package", "domain"], // fallback; overridden by identity modules
]);
Expand Down Expand Up @@ -105,7 +106,9 @@ const matrix = Effect.runSync(

// Separate entries that have cross-target implications (client modules)
const singleTargetEntries = matrix.filter(
(e) => !e.target.startsWith("client-react"),
(e) =>
!e.target.startsWith("client-react") &&
!e.target.startsWith("client-foldkit"),
);

// ---------------------------------------------------------------------------
Expand All @@ -115,29 +118,43 @@ const singleTargetEntries = matrix.filter(
const buildFullStackMatrix = Effect.gen(function* () {
const catalog = yield* CatalogService;

const clientModules = yield* catalog.getSupportedModules(
const clientKinds = [
TargetKind.make("client-react"),
);

return Arr.map(clientModules, (clientMod) => {
const implies = clientMod.implies ?? [];
if (implies.length === 0) return null;

const serverModuleIds = implies
.filter((imp) => imp.targetKind === "server")
.map((imp) => imp.moduleId);

if (serverModuleIds.length > 0) {
return {
label: `full-stack: ${clientMod.id} → server [${serverModuleIds.join(", ")}]`,
serverTarget: `server/${defaultTargetNames.get("server")}`,
serverModules: serverModuleIds,
clientTarget: `client-react/${defaultTargetNames.get("client-react")}`,
clientModules: [clientMod.id],
};
TargetKind.make("client-foldkit"),
] as const;

const results: Array<{
label: string;
serverTarget: string;
serverModules: ReadonlyArray<string>;
clientTarget: string;
clientModules: ReadonlyArray<string>;
}> = [];

for (const kind of clientKinds) {
const clientModules = yield* catalog.getSupportedModules(kind);

for (const clientMod of clientModules) {
const implies = clientMod.implies ?? [];
if (implies.length === 0) continue;

const serverModuleIds = implies
.filter((imp) => imp.targetKind === "server")
.map((imp) => imp.moduleId);

if (serverModuleIds.length > 0) {
results.push({
label: `full-stack: ${clientMod.id} → server [${serverModuleIds.join(", ")}]`,
serverTarget: `server/${defaultTargetNames.get("server")}`,
serverModules: serverModuleIds,
clientTarget: `${kind}/${defaultTargetNames.get(kind)}`,
clientModules: [clientMod.id],
});
}
}
return null;
}).filter((entry) => entry !== null);
}

return results;
});

const fullStackMatrix = Effect.runSync(
Expand Down Expand Up @@ -241,56 +258,71 @@ describe("matrix", () => {
});

layer(CLI.layer)("full-stack all client modules together", (it) => {
it.effect(
"all client modules with all server dependencies",
() =>
Effect.gen(function* () {
const cli = yield* CLI;
const name = "matrix-fullstack-all";
const root = `${cli.workdir}/${name}`;

// Init
yield* cli.run("init", name, "--yes", "--root", cli.workdir);
yield* cli.expectExitCode(0);

// Add all server modules
const allServerModules = [
...new Set(fullStackMatrix.flatMap((e) => e.serverModules)),
];
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
`server/${defaultTargetNames.get("server")}`,
"--modules",
allServerModules.join(","),
);
yield* cli.expectExitCode(0);

// Add all client modules
const allClientModules = [
...new Set(fullStackMatrix.flatMap((e) => e.clientModules)),
];
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
`client-react/${defaultTargetNames.get("client-react")}`,
"--modules",
allClientModules.join(","),
);
yield* cli.expectExitCode(0);

// Validate
yield* cli.withinProject(name, function* (project) {
yield* project.expectTypeCheckPasses();
});
}).pipe(Effect.provide(CLI.layer)),
{ timeout: 180_000 },
);
// Group full-stack entries by client target kind
const byClientKind = new Map<
string,
{ serverModules: Set<string>; clientModules: Set<string> }
>();
for (const entry of fullStackMatrix) {
const kind = entry.clientTarget;
if (!byClientKind.has(kind)) {
byClientKind.set(kind, {
serverModules: new Set(),
clientModules: new Set(),
});
}
const group = byClientKind.get(kind)!;
for (const m of entry.serverModules) group.serverModules.add(m);
for (const m of entry.clientModules) group.clientModules.add(m);
}

for (const [clientTarget, group] of byClientKind) {
const kindSlug = clientTarget.replace("/", "-");
it.effect(
`all ${clientTarget} modules with server dependencies`,
() =>
Effect.gen(function* () {
const cli = yield* CLI;
const name = `matrix-fullstack-all-${kindSlug}`;
const root = `${cli.workdir}/${name}`;

// Init
yield* cli.run("init", name, "--yes", "--root", cli.workdir);
yield* cli.expectExitCode(0);

// Add all server modules
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
`server/${defaultTargetNames.get("server")}`,
"--modules",
[...group.serverModules].join(","),
);
yield* cli.expectExitCode(0);

// Add all client modules
yield* cli.run(
"add",
"--yes",
"--root",
root,
"--target",
clientTarget,
"--modules",
[...group.clientModules].join(","),
);
yield* cli.expectExitCode(0);

// Validate
yield* cli.withinProject(name, function* (project) {
yield* project.expectTypeCheckPasses();
});
}).pipe(Effect.provide(CLI.layer)),
{ timeout: 180_000 },
);
}
});
});
Loading
Loading