Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
export {
BuildCommandOptionsSchema,
BuildTargetSchema,
DevCommandOptionsSchema,
ElectrobunConfigSchema,
FeatureModuleSchema,
GenerateIconsOptionsSchema,
NetworkingComplexitySchema,
PackageCommandOptionsSchema,
ProjectOptionsSchema,
parseOrThrow,
R3fModuleSchema,
SignCommandOptionsSchema,
} from "./schemas.js";

View File

@@ -0,0 +1,104 @@
# Schemas — `schemas.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the Zod schemas that gate every public boundary of
`@suaveplan/electrobun`. Each interface in `src/types/electrobun.types.ts`
has a matching schema here, and the `parseOrThrow` helper folds the
parse + throw pattern into one call so every entry point reads the same
way: validate, then operate on the inferred type. The schemas are the
runtime contract — when a downstream library passes a stale
`ProjectOptions` shape, the schema fails at the boundary rather than
silently producing a broken project.
The schemas use Zod 4's enum / object / readonly-array helpers to keep
the TypeScript inference cheap, and `parseOrThrow` always raises
`ElectrobunError(ELECTROBUN.INVALID_OPTIONS)` so consumers can branch on
`error.code === "ELECTROBUN.INVALID_OPTIONS"` regardless of which entry
point validated.
## Features
- Enum schemas (`FeatureModuleSchema`, `NetworkingComplexitySchema`,
`R3fModuleSchema`, `BuildTargetSchema`).
- Composite schemas (`ProjectOptionsSchema`, `ElectrobunConfigSchema`,
`GenerateIconsOptionsSchema`, `DevCommandOptionsSchema`,
`BuildCommandOptionsSchema`, `PackageCommandOptionsSchema`,
`SignCommandOptionsSchema`).
- `parseOrThrow(schema, value, context)` — single helper that converts a
Zod failure into an `ElectrobunError` carrying `details.issues`.
## Usage: Basic
```ts
import { ProjectOptionsSchema } from "@suaveplan/electrobun/schemas";
const parsed = ProjectOptionsSchema.safeParse(input);
if (!parsed.success) {
console.error(parsed.error.issues);
}
```
## Usage: Advanced
```ts
import {
ProjectOptionsSchema,
parseOrThrow,
} from "@suaveplan/electrobun/schemas";
import { ElectrobunError } from "@suaveplan/electrobun/error-codes";
try {
const validated = parseOrThrow(
ProjectOptionsSchema,
input,
"ProjectOptions"
);
await generate(validated);
} catch (err) {
if (err instanceof ElectrobunError) {
process.stderr.write(`${err.code}: ${err.message}\n`);
for (const issue of err.details["issues"] as Array<{
path: ReadonlyArray<string>;
message: string;
}>) {
process.stderr.write(` - ${issue.path.join(".")}: ${issue.message}\n`);
}
}
}
```
## API Reference
- `FeatureModuleSchema`, `NetworkingComplexitySchema`, `R3fModuleSchema`,
`BuildTargetSchema` — closed Zod enums.
- `ProjectOptionsSchema` — full `ProjectOptions` shape; rejects empty
`name`, `identifier`, `version`, `outputDir`.
- `ElectrobunConfigSchema` — partial config; every field optional.
- `GenerateIconsOptionsSchema` — `{ inputPath, outputDir }`.
- `DevCommandOptionsSchema`, `BuildCommandOptionsSchema`,
`PackageCommandOptionsSchema`, `SignCommandOptionsSchema` — per-command
option shapes.
- `parseOrThrow<T>(schema, value, context)` — returns the parsed value or
throws `ElectrobunError(ELECTROBUN.INVALID_OPTIONS, "${context}: invalid
options", { details: { context, issues } })`.
## Implementation Notes
`z.array(...).readonly()` produces a `readonly`-typed inference that
matches the `readonly`-array fields on `ProjectOptions`, so the consumer
gets a parsed object whose mutability matches the interface. This is
important because the generator iterates the feature list and would
otherwise need an explicit `Array.from(...)` to avoid a structural
mismatch.
`parseOrThrow` uses `safeParse` rather than `parse` so the Zod issue list
lands on `details.issues`. The thrown error carries the original `cause`
slot empty — the Zod issues are the cause, surfaced as structured data
rather than a child `Error`. Consumers who want to log a single line
should format `err.message`; consumers who want to drive remediation
should walk `details.issues`.

View File

@@ -0,0 +1,97 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
ELECTROBUN_CODES,
ElectrobunError,
} from "../error-codes/error-codes.js";
import type { ProjectOptions } from "../types/electrobun.types.js";
import {
BuildCommandOptionsSchema,
ProjectOptionsSchema,
parseOrThrow,
SignCommandOptionsSchema,
} from "./schemas.js";
function makeOptions(overrides: Partial<ProjectOptions> = {}): ProjectOptions {
return {
name: "test-app",
identifier: "com.test.app",
description: "An app",
version: "1.0.0",
outputDir: "/tmp/x",
features: [],
networkingComplexity: "basic",
r3fModules: [],
...overrides,
};
}
describe("schemas", () => {
it("ProjectOptionsSchema rejects empty name", { timeout: 5000 }, () => {
const result = ProjectOptionsSchema.safeParse(
makeOptions({ name: "" })
);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.some((i) => i.path[0] === "name")).toBe(
true
);
}
});
it("ProjectOptionsSchema accepts valid options", { timeout: 5000 }, () => {
const result = ProjectOptionsSchema.safeParse(
makeOptions({ features: ["i18n"], r3fModules: ["camera"] })
);
expect(result.success).toBe(true);
});
it("BuildCommandOptionsSchema enforces target enum", {
timeout: 5000,
}, () => {
const result = BuildCommandOptionsSchema.safeParse({
cwd: "/tmp",
target: "bsd",
});
expect(result.success).toBe(false);
});
it("SignCommandOptionsSchema requires identity", { timeout: 5000 }, () => {
const result = SignCommandOptionsSchema.safeParse({
cwd: "/tmp",
identity: "",
});
expect(result.success).toBe(false);
});
it("parseOrThrow raises ELECTROBUN.INVALID_OPTIONS on bad input", {
timeout: 5000,
}, () => {
try {
parseOrThrow(
ProjectOptionsSchema,
makeOptions({ name: "" }),
"ProjectOptions"
);
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(ElectrobunError);
expect((err as ElectrobunError).code).toBe(
ELECTROBUN_CODES.INVALID_OPTIONS
);
expect(
Array.isArray((err as ElectrobunError).details["issues"])
).toBe(true);
}
});
it("parseOrThrow returns parsed value on success", {
timeout: 5000,
}, () => {
const parsed = parseOrThrow(
ProjectOptionsSchema,
makeOptions(),
"ProjectOptions"
);
expect(parsed.name).toBe("test-app");
});
});

View File

@@ -0,0 +1,114 @@
import z from "zod";
import {
ELECTROBUN_CODES,
ElectrobunError,
} from "../error-codes/error-codes.js";
const FEATURE_VALUES = [
"i18n",
"networking",
"api",
"api-schemas",
"storage",
"auth",
"form-builder",
"r3f",
] as const;
const NETWORKING_VALUES = ["basic", "full"] as const;
const R3F_VALUES = [
"camera",
"physics",
"character",
"particles",
"pathfinding",
] as const;
const TARGET_VALUES = ["mac", "linux", "windows"] as const;
export const FeatureModuleSchema = z.enum(FEATURE_VALUES);
export const NetworkingComplexitySchema = z.enum(NETWORKING_VALUES);
export const R3fModuleSchema = z.enum(R3F_VALUES);
export const BuildTargetSchema = z.enum(TARGET_VALUES);
const NonEmptyString = z.string().min(1);
export const ProjectOptionsSchema = z.object({
name: NonEmptyString,
identifier: NonEmptyString,
description: z.string(),
version: NonEmptyString,
outputDir: NonEmptyString,
iconSource: z.string().optional(),
features: z.array(FeatureModuleSchema).readonly(),
networkingComplexity: NetworkingComplexitySchema,
r3fModules: z.array(R3fModuleSchema).readonly(),
});
export const ElectrobunConfigSchema = z.object({
name: z.string().optional(),
identifier: z.string().optional(),
description: z.string().optional(),
version: z.string().optional(),
iconSource: z.string().optional(),
features: z.array(FeatureModuleSchema).optional(),
networkingComplexity: NetworkingComplexitySchema.optional(),
r3fModules: z.array(R3fModuleSchema).optional(),
});
export const GenerateIconsOptionsSchema = z.object({
inputPath: NonEmptyString,
outputDir: NonEmptyString,
});
export const DevCommandOptionsSchema = z.object({
cwd: NonEmptyString,
extraArgs: z.array(z.string()).optional(),
});
export const BuildCommandOptionsSchema = z.object({
cwd: NonEmptyString,
target: BuildTargetSchema,
update: z.boolean().optional(),
extraArgs: z.array(z.string()).optional(),
});
export const PackageCommandOptionsSchema = z.object({
cwd: NonEmptyString,
target: BuildTargetSchema,
extraArgs: z.array(z.string()).optional(),
});
export const SignCommandOptionsSchema = z.object({
cwd: NonEmptyString,
identity: NonEmptyString,
entitlements: z.string().optional(),
notarize: z.boolean().optional(),
extraArgs: z.array(z.string()).optional(),
});
/**
* Parse a value against a Zod schema and raise an `ElectrobunError` with
* `ELECTROBUN.INVALID_OPTIONS` when the parse fails. The Zod issue list is
* carried on `details.issues` so callers can drive diagnostics without
* re-parsing the message.
*/
export function parseOrThrow<T>(
schema: z.ZodType<T>,
value: unknown,
context: string
): T {
const result = schema.safeParse(value);
if (result.success) {
return result.data;
}
throw new ElectrobunError(
ELECTROBUN_CODES.INVALID_OPTIONS,
`${context}: invalid options`,
{
details: {
context,
issues: result.error.issues,
},
}
);
}