Initial Commit
This commit is contained in:
14
packages/devtools/tooling/electrobun/src/schemas/index.ts
Normal file
14
packages/devtools/tooling/electrobun/src/schemas/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export {
|
||||
BuildCommandOptionsSchema,
|
||||
BuildTargetSchema,
|
||||
DevCommandOptionsSchema,
|
||||
ElectrobunConfigSchema,
|
||||
FeatureModuleSchema,
|
||||
GenerateIconsOptionsSchema,
|
||||
NetworkingComplexitySchema,
|
||||
PackageCommandOptionsSchema,
|
||||
ProjectOptionsSchema,
|
||||
parseOrThrow,
|
||||
R3fModuleSchema,
|
||||
SignCommandOptionsSchema,
|
||||
} from "./schemas.js";
|
||||
104
packages/devtools/tooling/electrobun/src/schemas/schemas.md
Normal file
104
packages/devtools/tooling/electrobun/src/schemas/schemas.md
Normal 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`.
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
114
packages/devtools/tooling/electrobun/src/schemas/schemas.ts
Normal file
114
packages/devtools/tooling/electrobun/src/schemas/schemas.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user