Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'

This commit is contained in:
-Puter
2026-07-19 03:28:54 +05:30
parent 2daf979036
commit a37e0cc3c9
2163 changed files with 668421 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://effect.website/schemas/ai-codegen.json",
"title": "Effect AI Codegen Configuration",
"description": "Configuration for @effect/ai-codegen code generation",
"type": "object",
"required": ["spec", "output"],
"properties": {
"spec": {
"description": "OpenAPI specification source - URL, file path, or structured resolver config",
"oneOf": [
{
"type": "string",
"description": "URL or file path to the OpenAPI specification"
},
{
"type": "object",
"description": "Stainless stats resolver - fetches stats.yml and extracts openapi_spec_url",
"required": ["type", "statsUrl"],
"additionalProperties": false,
"properties": {
"type": {
"const": "stainless-stats",
"description": "Resolver type"
},
"statsUrl": {
"type": "string",
"format": "uri",
"description": "URL to Stainless .stats.yml file containing openapi_spec_url"
}
}
}
]
},
"output": {
"type": "string",
"description": "Output file path relative to the package directory"
},
"name": {
"type": "string",
"description": "Name for the generated client (defaults to 'Client')"
},
"typeOnly": {
"type": "boolean",
"description": "Generate type-only output without runtime code",
"default": false
},
"header": {
"type": "string",
"description": "Content to prepend to the generated file (e.g. module-level JSDoc)"
},
"patches": {
"type": "array",
"description": "JSON Patch documents to apply to the spec before generation. Each item can be a file path or inline JSON array.",
"items": {
"type": "string"
}
},
"replacements": {
"type": "array",
"description": "Text replacements to apply to generated code.",
"items": {
"type": "object",
"required": ["from", "to"],
"additionalProperties": false,
"properties": {
"from": {
"type": "string",
"description": "The string to search for"
},
"to": {
"type": "string",
"description": "The string to replace with"
}
}
}
},
"excludeAnnotations": {
"type": "array",
"description": "Annotation keys to exclude from generated schema code (e.g. [\"examples\"]).",
"items": {
"type": "string"
}
},
"disableAdditionalProperties": {
"type": "boolean",
"description": "When true, forces additionalProperties to false on all object schemas, preventing StructWithRest emission.",
"default": false
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["**/*.ts"]
}

View File

@@ -0,0 +1,60 @@
{
"name": "@effect/ai-codegen",
"version": "0.0.0",
"type": "module",
"private": true,
"license": "MIT",
"description": "Code generation framework for the Effect AI providers",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/tools/ai-codegen"
},
"sideEffects": [],
"bin": {
"effect-ai-codegen": "./src/bin.ts"
},
"exports": {
"./package.json": "./package.json",
"./*": "./src/*.ts",
"./bin": null,
"./main": null
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"provenance": true,
"bin": {
"effect-ai-codegen": "./dist/bin.js"
},
"exports": {
"./package.json": "./package.json",
"./*": "./dist/*.js",
"./bin": null,
"./main": null
}
},
"scripts": {
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"dependencies": {
"@effect/openapi-generator": "workspace:^",
"@effect/platform-node": "workspace:^",
"effect": "workspace:^",
"glob": "^13.0.6",
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/node": "^26.1.1"
}
}

View File

@@ -0,0 +1,292 @@
/**
* Configuration schema and types for AI provider code generation.
*
* @since 4.0.0
*/
import * as Data from "effect/Data"
import type * as Path from "effect/Path"
import * as Schema from "effect/Schema"
/**
* A text replacement to apply to generated code.
*
* @category models
* @since 4.0.0
*/
export class Replacement extends Schema.Class<Replacement>("Replacement")({
from: Schema.String,
to: Schema.String
}) {}
/**
* Structured spec source configuration for Stainless stats indirection.
*
* @category schemas
* @since 4.0.0
*/
export const SpecSourceConfig = Schema.Struct({
type: Schema.Literal("stainless-stats"),
statsUrl: Schema.String
})
/**
* Configuration for AI provider code generation.
*
* **Example** (Decoding a codegen configuration)
*
* ```ts
* import * as Config from "@effect/ai-codegen/Config"
* import { Schema } from "effect"
*
* const config = Schema.decodeUnknownSync(Config.CodegenConfig)({
* spec: "https://example.com/openapi.json",
* output: "Generated.ts",
* name: "MyClient"
* })
*
* console.log(config.spec)
* // "https://example.com/openapi.json"
* ```
*
* @category models
* @since 4.0.0
*/
export class CodegenConfig extends Schema.Class<CodegenConfig>("CodegenConfig")({
spec: Schema.Union([Schema.String, SpecSourceConfig]),
output: Schema.String,
name: Schema.optional(Schema.String),
typeOnly: Schema.optional(Schema.Boolean),
header: Schema.optional(Schema.String),
patches: Schema.optional(Schema.Array(Schema.String)),
replacements: Schema.optional(Schema.Array(Replacement)),
excludeAnnotations: Schema.optional(Schema.Array(Schema.String)),
disableAdditionalProperties: Schema.optional(Schema.Boolean)
}) {
/**
* Get the client name, defaulting to "Client" if not specified.
*
* @since 4.0.0
*/
get clientName(): string {
return this.name ?? "Client"
}
/**
* Check if type-only generation is enabled.
*
* @since 4.0.0
*/
get isTypeOnly(): boolean {
return this.typeOnly ?? false
}
/**
* Get the list of patch files/strings to apply.
*
* @since 4.0.0
*/
get patchList(): ReadonlyArray<string> {
return this.patches ?? []
}
/**
* Get the list of text replacements to apply.
*
* @since 4.0.0
*/
get replacementList(): ReadonlyArray<Replacement> {
return this.replacements ?? []
}
/**
* Get the header content to prepend to generated files.
*
* @since 4.0.0
*/
get headerContent(): string | undefined {
return this.header
}
/**
* Get the list of annotation keys to exclude from generated code.
*
* @since 4.0.0
*/
get excludeAnnotationsList(): ReadonlyArray<string> | undefined {
return this.excludeAnnotations
}
/**
* Check if additionalProperties should be forced to false on all object schemas.
*
* @since 4.0.0
*/
get shouldDisableAdditionalProperties(): boolean {
return this.disableAdditionalProperties ?? false
}
}
/**
* Represents the source of an OpenAPI specification.
*
* @category models
* @since 4.0.0
*/
export type SpecSource = SpecSource.Url | SpecSource.File | SpecSource.StainlessStats
/**
* Namespace containing the supported OpenAPI specification source variants.
*
* @since 4.0.0
*/
export declare namespace SpecSource {
/**
* A URL-based spec source.
*
* @category models
* @since 4.0.0
*/
export interface Url {
readonly _tag: "Url"
readonly url: string
}
/**
* A file-based spec source.
*
* @category models
* @since 4.0.0
*/
export interface File {
readonly _tag: "File"
readonly path: string
}
/**
* Stainless SDK stats.yml indirection - fetches stats file and extracts openapi_spec_url.
*
* @category models
* @since 4.0.0
*/
export interface StainlessStats {
readonly _tag: "StainlessStats"
readonly statsUrl: string
}
}
/**
* Constructors and utilities for `SpecSource`.
*
* **Example** (Creating spec sources)
*
* ```ts
* import * as Config from "@effect/ai-codegen/Config"
*
* // Create a URL-based source
* const urlSource = Config.SpecSource.Url("https://example.com/openapi.json")
*
* // Create a file-based source
* const fileSource = Config.SpecSource.File("/path/to/spec.json")
* ```
*
* @category constructors
* @since 4.0.0
*/
export const SpecSource = {
/**
* Create a URL-based spec source.
*
* @since 4.0.0
*/
Url: (url: string): SpecSource => ({ _tag: "Url", url }),
/**
* Create a file-based spec source.
*
* @since 4.0.0
*/
File: (path: string): SpecSource => ({ _tag: "File", path }),
/**
* Create a Stainless stats-based spec source.
*
* @since 4.0.0
*/
StainlessStats: (statsUrl: string): SpecSource => ({ _tag: "StainlessStats", statsUrl }),
/**
* Parse a spec string into a `SpecSource`.
* URLs (http:// or https://) become `Url`, otherwise `File`.
*
* @since 4.0.0
*/
fromString: (spec: string, packagePath: string, pathService: Path.Path): SpecSource => {
if (spec.startsWith("http://") || spec.startsWith("https://")) {
return SpecSource.Url(spec)
}
return SpecSource.File(pathService.join(packagePath, spec))
},
/**
* Parse a spec config (string or object) into a `SpecSource`.
*
* @since 4.0.0
*/
fromConfig: (
spec: string | { readonly type: string; readonly statsUrl?: string },
packagePath: string,
pathService: Path.Path
): SpecSource => {
if (typeof spec === "string") {
return SpecSource.fromString(spec, packagePath, pathService)
}
if (spec.type === "stainless-stats" && spec.statsUrl) {
return SpecSource.StainlessStats(spec.statsUrl)
}
throw new Error(`Unknown spec type: ${spec.type}`)
}
}
/**
* Error when parsing a codegen configuration file fails.
*
* **Example** (Creating a config parse error)
*
* ```ts
* import * as Config from "@effect/ai-codegen/Config"
*
* const error = new Config.ConfigParseError({
* path: "/path/to/codegen.json",
* cause: new Error("Invalid JSON")
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class ConfigParseError extends Data.TaggedError("ConfigParseError")<{
readonly path: string
readonly cause: unknown
}> {}
/**
* Error when a codegen configuration file is not found.
*
* **Example** (Creating a config not found error)
*
* ```ts
* import * as Config from "@effect/ai-codegen/Config"
*
* const error = new Config.ConfigNotFoundError({
* provider: "openai",
* expectedPath: "/path/to/packages/ai/openai/codegen.json"
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class ConfigNotFoundError extends Data.TaggedError("ConfigNotFoundError")<{
readonly provider: string
readonly expectedPath: string
}> {}

View File

@@ -0,0 +1,197 @@
/**
* Provider discovery service for AI codegen.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import * as Schema from "effect/Schema"
import * as Yaml from "yaml"
import { CodegenConfig, type SpecSource, SpecSource as SpecSourceUtils } from "./Config.ts"
import * as Glob from "./Glob.ts"
/**
* A discovered AI provider with resolved paths.
*
* **Example** (Inspecting a discovered provider)
*
* ```ts
* import type * as Discovery from "@effect/ai-codegen/Discovery"
*
* declare const provider: Discovery.DiscoveredProvider
*
* console.log(provider.name) // "openai"
* console.log(provider.specSource._tag) // "Url" | "File"
* ```
*
* @category models
* @since 4.0.0
*/
export interface DiscoveredProvider {
readonly name: string
readonly packagePath: string
readonly config: CodegenConfig
readonly specSource: SpecSource
readonly outputPath: string
}
/**
* Service for discovering AI provider configurations.
*
* @category models
* @since 4.0.0
*/
export interface ProviderDiscovery {
readonly discover: () => Effect.Effect<
Array<DiscoveredProvider>,
DiscoveryError | Glob.GlobError
>
readonly discoverOne: (
name: string
) => Effect.Effect<
DiscoveredProvider,
DiscoveryError | ProviderNotFoundError | Glob.GlobError
>
}
/**
* Service tag for discovering AI provider codegen configurations.
*
* @category services
* @since 4.0.0
*/
export const ProviderDiscovery: Context.Service<ProviderDiscovery, ProviderDiscovery> = Context.Service(
"@effect/ai-codegen/ProviderDiscovery"
)
/**
* Error during provider discovery.
*
* **Example** (Creating a discovery error)
*
* ```ts
* import * as Discovery from "@effect/ai-codegen/Discovery"
*
* const error = new Discovery.DiscoveryError({
* message: "Failed to parse config",
* cause: new Error("Invalid JSON")
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class DiscoveryError extends Data.TaggedError("DiscoveryError")<{
readonly message: string
readonly cause?: unknown
}> {}
/**
* Error when a specific provider is not found.
*
* **Example** (Creating a provider not found error)
*
* ```ts
* import * as Discovery from "@effect/ai-codegen/Discovery"
*
* const error = new Discovery.ProviderNotFoundError({
* provider: "openai",
* available: ["anthropic", "google"]
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class ProviderNotFoundError extends Data.TaggedError("ProviderNotFoundError")<{
readonly provider: string
readonly available: ReadonlyArray<string>
}> {}
/**
* Layer providing the ProviderDiscovery service.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<
ProviderDiscovery,
never,
Glob.Glob | FileSystem.FileSystem | Path.Path
> = Effect.gen(function*() {
const glob = yield* Glob.Glob
const fs = yield* FileSystem.FileSystem
const pathService = yield* Path.Path
const parseConfig = Effect.fn("parseConfig")(function*(configPath: string) {
const packagePath = pathService.dirname(configPath)
const name = pathService.basename(packagePath)
const isYaml = configPath.endsWith(".yaml") || configPath.endsWith(".yml")
const content = yield* fs.readFileString(configPath).pipe(
Effect.mapError((cause) =>
new DiscoveryError({
message: `Failed to read config at ${configPath}`,
cause
})
)
)
const parsed = yield* Effect.try({
try: () => isYaml ? Yaml.parse(content) : JSON.parse(content),
catch: (cause) =>
new DiscoveryError({
message: `Failed to parse ${isYaml ? "YAML" : "JSON"} at ${configPath}`,
cause
})
})
const config = yield* Schema.decodeUnknownEffect(CodegenConfig)(parsed).pipe(
Effect.mapError((cause) =>
new DiscoveryError({
message: `Invalid config schema at ${configPath}`,
cause
})
)
)
const provider: DiscoveredProvider = {
name,
packagePath,
config,
specSource: SpecSourceUtils.fromConfig(config.spec, packagePath, pathService),
outputPath: pathService.join(packagePath, config.output)
}
return provider
})
const discover = Effect.fn("discover")(function*() {
const configFiles = yield* glob.glob("packages/ai/*/codegen.{json,yaml,yml}", {
cwd: process.cwd(),
absolute: true
})
return yield* Effect.forEach(configFiles, parseConfig)
})
const discoverOne = Effect.fn("discoverOne")(function*(providerName: string) {
const providers = yield* discover()
const found = providers.find((p) => p.name === providerName)
if (!found) {
return yield* new ProviderNotFoundError({
provider: providerName,
available: providers.map((p) => p.name)
})
}
return found
})
return { discover, discoverOne }
}).pipe(Layer.effect(ProviderDiscovery))

View File

@@ -0,0 +1,222 @@
/**
* Code generator service wrapping @effect/openapi-generator.
*
* @since 4.0.0
*/
import * as OpenApiGenerator from "@effect/openapi-generator/OpenApiGenerator"
import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import type * as FileSystem from "effect/FileSystem"
import type * as JsonSchema from "effect/JsonSchema"
import * as Layer from "effect/Layer"
import * as Path_ from "effect/Path"
import * as Predicate from "effect/Predicate"
import type * as Schema from "effect/Schema"
import type { DiscoveredProvider } from "./Discovery.ts"
/**
* Error during code generation.
*
* **Example** (Creating a generation error)
*
* ```ts
* import * as Generator from "@effect/ai-codegen/Generator"
*
* const error = new Generator.GenerationError({
* provider: "openai",
* cause: new Error("Invalid spec")
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class GenerationError extends Data.TaggedError("GenerationError")<{
readonly provider: string
readonly cause: unknown
}> {}
/**
* Error during patch application.
*
* **Example** (Creating a patch error)
*
* ```ts
* import * as Generator from "@effect/ai-codegen/Generator"
*
* const error = new Generator.PatchError({
* provider: "openai",
* cause: new Error("Invalid patch")
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class PatchError extends Data.TaggedError("PatchError")<{
readonly provider: string
readonly cause: unknown
}> {}
/**
* Service for generating Effect code from OpenAPI specs.
*
* @category models
* @since 4.0.0
*/
export interface CodeGenerator {
readonly generate: (
provider: DiscoveredProvider,
spec: unknown
) => Effect.Effect<string, GenerationError | PatchError, FileSystem.FileSystem | Path_.Path>
}
/**
* Service tag for generating Effect client code from OpenAPI specifications.
*
* @category services
* @since 4.0.0
*/
export const CodeGenerator: Context.Service<CodeGenerator, CodeGenerator> = Context.Service(
"@effect/ai-codegen/CodeGenerator"
)
const isRecord = (u: unknown): u is { readonly [x: string]: unknown } =>
Predicate.isObjectOrArray(u) && !Array.isArray(u)
/** A bare `{ "type": "string" }` branch - the open half of an open enum. */
const isOpenStringBranch = (branch: unknown): boolean =>
isRecord(branch) && branch.type === "string" && Object.keys(branch).length === 1
/** The value of a `{ "const": "..." }` branch, when it is a string const. */
const constBranchValue = (branch: unknown): string | undefined =>
isRecord(branch) && Predicate.isString(branch.const) ? branch.const : undefined
/**
* Collapse an open enum's const branches into a single `enum` branch.
*
* Stainless-generated specs encode an open enum as
* `anyOf: [{ type: "string" }, { const: "a" }, { const: "b" }]` - "any string is valid, and these are
* the known values". Left as-is, each const becomes its own `Schema.Literal` member, so consumers
* cannot recover the literal union from the generated schema (the union's `Type` is just `string`).
*
* Rewriting the const branches to `anyOf: [{ type: "string" }, { enum: ["a", "b"] }]` emits
* `Schema.Union([Schema.String, Schema.Literals(["a", "b"])])` instead, which decodes any string while
* still exposing the known values as `members[1]` for autocomplete.
*/
const normalizeOpenEnum = (js: JsonSchema.JsonSchema): JsonSchema.JsonSchema => {
const anyOf = js.anyOf
if (!Array.isArray(anyOf) || anyOf.length < 2) return js
const [head, ...tail] = anyOf
if (!isOpenStringBranch(head)) return js
const literals: Array<string> = []
for (const branch of tail) {
const value = constBranchValue(branch)
if (value === undefined) return js
literals.push(value)
}
return { ...js, anyOf: [head, { enum: literals }] }
}
/**
* Layer providing the CodeGenerator service.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<
CodeGenerator,
never,
OpenApiGenerator.OpenApiGenerator | FileSystem.FileSystem | Path_.Path
> = Effect.gen(function*() {
const openApiGen = yield* OpenApiGenerator.OpenApiGenerator
const pathService = yield* Path_.Path
const applyPatches = Effect.fn("applyPatches")(function*(
provider: DiscoveredProvider,
spec: Schema.Json
) {
const patchInputs = provider.config.patchList
if (patchInputs.length === 0) {
return spec
}
// Parse all patches, resolving file paths relative to the provider package
const parsedPatches = yield* Effect.forEach(patchInputs, (input) => {
// If it looks like a file path and is not absolute, resolve relative to package
const resolvedInput = !input.startsWith("[") && !pathService.isAbsolute(input)
? pathService.join(provider.packagePath, input)
: input
return OpenApiPatch.parsePatchInput(resolvedInput).pipe(
Effect.map((patch) => ({ source: resolvedInput, patch }))
)
}).pipe(
Effect.mapError((cause) => new PatchError({ provider: provider.name, cause }))
)
// Apply all patches to the spec
return yield* OpenApiPatch.applyPatches(parsedPatches, spec).pipe(
Effect.mapError((cause) => new PatchError({ provider: provider.name, cause }))
)
})
const generate = Effect.fn("generate")(function*(
provider: DiscoveredProvider,
spec: unknown
) {
// Apply patches if any are configured
const patchedSpec = yield* applyPatches(provider, spec as Schema.Json)
const excludeAnnotations = provider.config.excludeAnnotationsList
const disableAdditionalProperties = provider.config.shouldDisableAdditionalProperties
const exclude = excludeAnnotations ? new Set(excludeAnnotations) : undefined
const onEnter = (js: JsonSchema.JsonSchema): JsonSchema.JsonSchema => {
const out = { ...normalizeOpenEnum(js) }
if (exclude) {
for (const key of exclude) delete out[key]
}
if (disableAdditionalProperties && out.type === "object") {
out.additionalProperties = false
}
return out
}
return yield* openApiGen
.generate(patchedSpec as unknown as Parameters<typeof openApiGen.generate>[0], {
name: provider.config.clientName,
format: provider.config.isTypeOnly ? "httpclient-type-only" : "httpclient",
onEnter
})
.pipe(
Effect.mapError((cause) => new GenerationError({ provider: provider.name, cause }))
)
})
return { generate }
}).pipe(Layer.effect(CodeGenerator))
/**
* Layer providing the CodeGenerator with schema transformer (default).
*
* @category layers
* @since 4.0.0
*/
export const layerSchema: Layer.Layer<CodeGenerator, never, FileSystem.FileSystem | Path_.Path> = layer.pipe(
Layer.provide(OpenApiGenerator.layerTransformerSchema)
)
/**
* Layer providing the CodeGenerator with TypeScript-only transformer.
*
* @category layers
* @since 4.0.0
*/
export const layerTypeScript: Layer.Layer<CodeGenerator, never, FileSystem.FileSystem | Path_.Path> = layer.pipe(
Layer.provide(OpenApiGenerator.layerTransformerTs)
)

View File

@@ -0,0 +1,56 @@
/**
* Glob pattern matching service.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as GlobLib from "glob"
/**
* Error during glob pattern matching.
*
* @category errors
* @since 4.0.0
*/
export class GlobError extends Data.TaggedError("GlobError")<{
readonly pattern: string | ReadonlyArray<string>
readonly cause: unknown
}> {}
/**
* Service for glob pattern matching.
*
* @category models
* @since 4.0.0
*/
export interface Glob {
readonly glob: (
pattern: string | ReadonlyArray<string>,
options?: GlobLib.GlobOptions
) => Effect.Effect<Array<string>, GlobError>
}
/**
* Service tag for glob pattern matching used by AI codegen tooling.
*
* @category services
* @since 4.0.0
*/
export const Glob: Context.Service<Glob, Glob> = Context.Service("@effect/ai-codegen/Glob")
/**
* Layer providing the Glob service.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Glob> = Layer.succeed(Glob, {
glob: (pattern, options) =>
Effect.tryPromise({
try: () => GlobLib.glob(pattern as string | Array<string>, options ?? {}) as Promise<Array<string>>,
catch: (cause) => new GlobError({ pattern, cause })
})
})

View File

@@ -0,0 +1,157 @@
/**
* Post-processing service for linting and formatting generated code.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Stream from "effect/Stream"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
/**
* Error during post-processing (lint or format).
*
* **Example** (Creating a post-process error)
*
* ```ts
* import * as PostProcess from "@effect/ai-codegen/PostProcess"
*
* const error = new PostProcess.PostProcessError({
* step: "lint",
* command: "pnpm exec oxlint --fix /path/to/file.ts",
* filePath: "/path/to/file.ts",
* exitCode: 1,
* stdout: "",
* stderr: "error: some lint error",
* cause: new Error("Lint failed")
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class PostProcessError extends Data.TaggedError("PostProcessError")<{
readonly step: "lint" | "format"
readonly command: string
readonly filePath: string
readonly exitCode?: number | undefined
readonly stdout: string
readonly stderr: string
readonly cause: unknown
}> {
override get message(): string {
const lines: Array<string> = [
`${this.step} failed for ${this.filePath}`,
`command: ${this.command}`
]
if (this.exitCode !== undefined) {
lines.push(`exit code: ${this.exitCode}`)
}
if (this.stderr.length > 0) {
lines.push(`stderr:\n${this.stderr}`)
}
if (this.stdout.length > 0) {
lines.push(`stdout:\n${this.stdout}`)
}
return lines.join("\n")
}
}
/**
* Service for post-processing generated code.
*
* @category models
* @since 4.0.0
*/
export interface PostProcessor {
readonly lint: (filePath: string) => Effect.Effect<void, PostProcessError>
readonly format: (filePath: string) => Effect.Effect<void, PostProcessError>
}
/**
* Service tag for linting and formatting generated code.
*
* @category services
* @since 4.0.0
*/
export const PostProcessor: Context.Service<PostProcessor, PostProcessor> = Context.Service(
"@effect/ai-codegen/PostProcessor"
)
/**
* Layer providing the PostProcessor service.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<
PostProcessor,
never,
ChildProcessSpawner.ChildProcessSpawner
> = Effect.gen(function*() {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const collectStream = (stream: Stream.Stream<Uint8Array, any>) =>
stream.pipe(
Stream.decodeText(),
Stream.mkString,
Effect.orElseSucceed(() => "")
)
const runCommand = Effect.fn("runCommand")(function*(
command: string,
args: ReadonlyArray<string>,
step: "lint" | "format",
filePath: string
) {
const fullCommand = [command, ...args].join(" ")
const cmd = ChildProcess.make(command, args, {
stdout: "pipe",
stderr: "pipe"
})
yield* Effect.scoped(Effect.gen(function*() {
const handle = yield* spawner.spawn(cmd).pipe(
Effect.mapError((cause) =>
new PostProcessError({ step, command: fullCommand, filePath, stdout: "", stderr: "", cause })
)
)
const [stdout, stderr] = yield* Effect.all([
collectStream(handle.stdout),
collectStream(handle.stderr)
])
const exitCode = yield* handle.exitCode.pipe(
Effect.mapError((cause) =>
new PostProcessError({ step, command: fullCommand, filePath, stdout, stderr, cause })
)
)
if (exitCode !== 0) {
return yield* new PostProcessError({
step,
command: fullCommand,
filePath,
exitCode,
stdout,
stderr,
cause: new Error(`Command exited with code ${exitCode}`)
})
}
}))
})
const lint = Effect.fn("lint")(function*(filePath: string) {
yield* runCommand("pnpm", ["exec", "oxlint", "--silent", "--quiet", "--fix", filePath], "lint", filePath)
})
const format = Effect.fn("format")(function*(filePath: string) {
yield* runCommand("pnpm", ["exec", "dprint", "--log-level", "silent", "fmt", filePath], "format", filePath)
})
return { lint, format }
}).pipe(Layer.effect(PostProcessor))

View File

@@ -0,0 +1,154 @@
/**
* Service for fetching OpenAPI specifications from URLs or the filesystem.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Match from "effect/Match"
import * as HttpClient from "effect/unstable/http/HttpClient"
import * as Yaml from "yaml"
import type { SpecSource } from "./Config.ts"
/**
* Error when fetching a spec fails.
*
* **Example** (Creating a spec fetch error)
*
* ```ts
* import * as SpecFetcher from "@effect/ai-codegen/SpecFetcher"
*
* const error = new SpecFetcher.SpecFetchError({
* provider: "openai",
* source: "https://example.com/openapi.json",
* cause: new Error("Network error")
* })
* ```
*
* @category errors
* @since 4.0.0
*/
export class SpecFetchError extends Data.TaggedError("SpecFetchError")<{
readonly provider: string
readonly source: string
readonly cause: unknown
}> {}
/**
* Service for fetching OpenAPI specifications.
*
* @category models
* @since 4.0.0
*/
export interface SpecFetcher {
readonly fetch: (
source: SpecSource,
provider: string
) => Effect.Effect<unknown, SpecFetchError>
}
/**
* Service tag for fetching OpenAPI specifications from configured sources.
*
* @category services
* @since 4.0.0
*/
export const SpecFetcher: Context.Service<SpecFetcher, SpecFetcher> = Context.Service(
"@effect/ai-codegen/SpecFetcher"
)
/**
* Layer providing the SpecFetcher service.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<
SpecFetcher,
never,
FileSystem.FileSystem | HttpClient.HttpClient
> = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const httpClient = yield* HttpClient.HttpClient
const fetchFromFile = Effect.fn("fetchFromFile")(function*(
path: string,
provider: string
) {
return yield* fs.readFileString(path).pipe(
Effect.mapError((cause) => new SpecFetchError({ provider, source: path, cause }))
)
})
const fetchFromUrl = Effect.fn("fetchFromUrl")(function*(
url: string,
provider: string
) {
return yield* httpClient.get(url).pipe(
Effect.flatMap((response) => response.text),
Effect.mapError((cause) => new SpecFetchError({ provider, source: url, cause }))
)
})
const parseSpec = (content: string, sourceUrl: string, provider: string) => {
const isYaml = sourceUrl.endsWith(".yaml") || sourceUrl.endsWith(".yml")
return Effect.try({
try: () => isYaml ? Yaml.parse(content) : JSON.parse(content),
catch: (cause) => new SpecFetchError({ provider, source: sourceUrl, cause })
})
}
const fetchViaStainlessStats = Effect.fn("fetchViaStainlessStats")(function*(
statsUrl: string,
provider: string
) {
// Fetch stats.yml
const statsContent = yield* httpClient.get(statsUrl).pipe(
Effect.flatMap((response) => response.text),
Effect.mapError((cause) => new SpecFetchError({ provider, source: `stats:${statsUrl}`, cause }))
)
// Parse YAML and extract openapi_spec_url
const stats = yield* Effect.try({
try: () => Yaml.parse(statsContent),
catch: (cause) => new SpecFetchError({ provider, source: `stats:${statsUrl}`, cause })
})
const specUrl = stats?.openapi_spec_url
if (typeof specUrl !== "string") {
return yield* new SpecFetchError({
provider,
source: `stats:${statsUrl}`,
cause: new Error("Missing or invalid openapi_spec_url in stats file")
})
}
// Fetch and parse the actual spec
const content = yield* fetchFromUrl(specUrl, provider)
return yield* parseSpec(content, specUrl, provider)
})
const fetch = Effect.fn("fetch")(function*(
source: SpecSource,
provider: string
) {
// StainlessStats handles its own parsing since the URL is resolved dynamically
if (source._tag === "StainlessStats") {
return yield* fetchViaStainlessStats(source.statsUrl, provider)
}
const content = yield* Match.value(source).pipe(
Match.tag("File", ({ path }) => fetchFromFile(path, provider)),
Match.tag("Url", ({ url }) => fetchFromUrl(url, provider)),
Match.exhaustive
)
const sourceString = source._tag === "Url" ? source.url : source.path
return yield* parseSpec(content, sourceString, provider)
})
return { fetch }
}).pipe(Layer.effect(SpecFetcher))

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import * as Effect from "effect/Effect"
import { run } from "./main.ts"
run.pipe(
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
)

View File

@@ -0,0 +1,237 @@
/**
* CLI entry point for ai-codegen tool.
*
* @since 4.0.0
*/
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Command from "effect/unstable/cli/Command"
import * as Flag from "effect/unstable/cli/Flag"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
import * as ProviderDiscovery from "./Discovery.ts"
import * as CodeGenerator from "./Generator.ts"
import * as Glob from "./Glob.ts"
import * as PostProcessor from "./PostProcess.ts"
import * as SpecFetcher from "./SpecFetcher.ts"
// =============================================================================
// Flags
// =============================================================================
const providerFlag = Flag.string("provider").pipe(
Flag.withAlias("p"),
Flag.withDescription("Generate for specific provider only"),
Flag.optional
)
const skipLintFlag = Flag.boolean("skip-lint").pipe(
Flag.withDescription("Skip Oxlint step")
)
const skipFormatFlag = Flag.boolean("skip-format").pipe(
Flag.withDescription("Skip Dprint step")
)
// =============================================================================
// ANSI Colors
// =============================================================================
const colors = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
cyan: "\x1b[36m",
green: "\x1b[32m",
yellow: "\x1b[33m",
gray: "\x1b[90m",
white: "\x1b[37m"
} as const
// =============================================================================
// Generate Command
// =============================================================================
interface GenerateOptions {
readonly skipLint: boolean
readonly skipFormat: boolean
}
const formatSpecSource = (source: ProviderDiscovery.DiscoveredProvider["specSource"]): string => {
switch (source._tag) {
case "Url":
return source.url
case "File":
return source.path
case "StainlessStats":
return `stainless-stats(${source.statsUrl})`
}
}
const generateProvider = Effect.fn("generateProvider")(function*(
provider: ProviderDiscovery.DiscoveredProvider,
options: GenerateOptions
) {
const specFetcher = yield* SpecFetcher.SpecFetcher
const generator = yield* CodeGenerator.CodeGenerator
const postProcessor = yield* PostProcessor.PostProcessor
const fs = yield* FileSystem.FileSystem
yield* Console.log(`\n${colors.bold}${colors.cyan}${provider.name}${colors.reset}`)
// Fetch spec
yield* Console.log(` ${colors.dim}Fetching spec...${colors.reset}`)
const spec = yield* specFetcher.fetch(provider.specSource, provider.name)
// Generate code
yield* Console.log(` ${colors.dim}Generating code...${colors.reset}`)
let code = yield* generator.generate(provider, spec)
// Apply replacements
const replacements = provider.config.replacementList
if (replacements.length > 0) {
yield* Console.log(` ${colors.dim}Applying ${replacements.length} replacement(s)...${colors.reset}`)
for (const replacement of replacements) {
code = code.replaceAll(replacement.from, replacement.to)
}
}
// Prepend header if configured
const header = provider.config.headerContent
if (header !== undefined) {
code = `${header}\n${code}`
}
// Write output
yield* Console.log(` ${colors.dim}Writing output...${colors.reset}`)
yield* fs.writeFileString(provider.outputPath, code)
// Post-process
if (!options.skipLint) {
yield* Console.log(` ${colors.dim}Linting...${colors.reset}`)
yield* postProcessor.lint(provider.outputPath)
}
if (!options.skipFormat) {
yield* Console.log(` ${colors.dim}Formatting...${colors.reset}`)
yield* postProcessor.format(provider.outputPath)
}
yield* Console.log(
` ${colors.green}${colors.reset} ${colors.gray}${provider.config.output}${colors.reset}`
)
return provider.outputPath
})
const generate = Command.make("generate", {
provider: providerFlag,
skipLint: skipLintFlag,
skipFormat: skipFormatFlag
}).pipe(
Command.withHandler(Effect.fnUntraced(function*({ provider, skipLint, skipFormat }) {
const discovery = yield* ProviderDiscovery.ProviderDiscovery
const providers = yield* Option.match(provider, {
onNone: () => discovery.discover(),
onSome: (name) => discovery.discoverOne(name).pipe(Effect.map((p) => [p]))
})
if (providers.length === 0) {
yield* Console.log(`${colors.yellow}No providers found.${colors.reset}`)
return
}
yield* Console.log(
`${colors.dim}Generating ${providers.length} provider(s)...${colors.reset}`
)
for (const p of providers) {
yield* generateProvider(p, { skipLint, skipFormat })
}
yield* Console.log(`\n${colors.green}✓ All providers generated successfully!${colors.reset}`)
}))
)
// =============================================================================
// List Command
// =============================================================================
const list = Command.make("list").pipe(
Command.withHandler(Effect.fnUntraced(function*() {
const discovery = yield* ProviderDiscovery.ProviderDiscovery
const providers = yield* discovery.discover()
if (providers.length === 0) {
yield* Console.log(`${colors.yellow}No providers found.${colors.reset}`)
return
}
yield* Console.log(`${colors.green}Found ${providers.length} provider(s):${colors.reset}\n`)
for (const p of providers) {
yield* Console.log(`${colors.bold}${colors.cyan}${p.name}${colors.reset}`)
yield* Console.log(` ${colors.gray}spec:${colors.reset} ${formatSpecSource(p.specSource)}`)
yield* Console.log(` ${colors.gray}output:${colors.reset} ${p.config.output}`)
yield* Console.log(
` ${colors.gray}client:${colors.reset} ${colors.white}${p.config.clientName}${colors.reset}`
)
yield* Console.log(` ${colors.gray}typeOnly:${colors.reset} ${p.config.isTypeOnly}`)
if (p.config.patchList.length > 0) {
yield* Console.log(
` ${colors.gray}patches:${colors.reset} ${colors.yellow}${p.config.patchList.length} file(s)${colors.reset}`
)
}
yield* Console.log("")
}
}))
)
// =============================================================================
// Root Command
// =============================================================================
const root = Command.make("effect-ai-codegen").pipe(
Command.withSubcommands([generate, list])
)
// =============================================================================
// Layer Composition
// =============================================================================
// ProviderDiscovery depends on Glob, FileSystem, and Path
// SpecFetcher depends on FileSystem and HttpClient
// CodeGenerator depends on OpenApiGenerator
// PostProcessor depends on ChildProcessSpawner
// Build up layers with dependencies
const DiscoveryLayer = ProviderDiscovery.layer.pipe(
Layer.provide(Glob.layer)
)
const SpecFetcherLayer = SpecFetcher.layer.pipe(
Layer.provide(FetchHttpClient.layer)
)
const ServicesLayer = Layer.mergeAll(
DiscoveryLayer,
SpecFetcherLayer,
CodeGenerator.layerSchema,
PostProcessor.layer
)
// =============================================================================
// Export
// =============================================================================
/**
* Run the CLI.
*
* @category execution
* @since 4.0.0
*/
export const run = Command.run(root, { version: "0.0.0" }).pipe(
Effect.provide(ServicesLayer)
)

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"references": [
{ "path": "../../effect" },
{ "path": "../../platform-node" },
{ "path": "../openapi-generator" }
],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
}
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["**/*.ts"]
}

View File

@@ -0,0 +1,59 @@
{
"name": "@effect/ai-docgen",
"version": "0.0.0",
"type": "module",
"private": true,
"license": "MIT",
"description": "Generates docs for the clankers",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/tools/ai-docgen"
},
"sideEffects": [],
"bin": {
"effect-ai-docgen": "./src/main.ts"
},
"exports": {
"./package.json": "./package.json",
"./*": "./src/*.ts",
"./bin": null,
"./main": null
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"provenance": true,
"bin": {
"effect-ai-codegen": "./dist/bin.js"
},
"exports": {
"./package.json": "./package.json",
"./*": "./dist/*.js",
"./bin": null,
"./main": null
}
},
"scripts": {
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"dependencies": {
"@effect/platform-node": "workspace:^",
"effect": "workspace:^",
"glob": "^13.0.6",
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/node": "^26.1.1"
}
}

View File

@@ -0,0 +1,48 @@
/**
* Glob pattern matching service.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as GlobLib from "glob"
/**
* Error during glob pattern matching.
*
* @category errors
* @since 4.0.0
*/
export class GlobError extends Data.TaggedError("GlobError")<{
readonly pattern: string | ReadonlyArray<string>
readonly cause: unknown
}> {}
/**
* Context service for glob pattern matching used by AI docgen tooling.
*
* @category services
* @since 4.0.0
*/
export class Glob extends Context.Service<Glob, {
readonly glob: (
pattern: string | ReadonlyArray<string>,
options?: GlobLib.GlobOptions
) => Effect.Effect<Array<string>, GlobError>
}>()("@effect/ai-codegen/Glob") {}
/**
* Layer providing the Glob service.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Glob> = Layer.succeed(Glob, {
glob: (pattern, options) =>
Effect.tryPromise({
try: () => GlobLib.glob(pattern as string | Array<string>, options ?? {}) as Promise<Array<string>>,
catch: (cause) => new GlobError({ pattern, cause })
})
})

View File

@@ -0,0 +1,188 @@
#!/usr/bin/env node
/**
* @since 4.0.0
*/
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import * as Array from "effect/Array"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import { pipe } from "effect/Function"
import * as Path from "effect/Path"
import type * as PlatformError from "effect/PlatformError"
import * as Stream from "effect/Stream"
import * as String from "effect/String"
import * as Argument from "effect/unstable/cli/Argument"
import * as Command from "effect/unstable/cli/Command"
import * as Flag from "effect/unstable/cli/Flag"
const directory = Argument.directory("directory", { mustExist: true })
const output = Flag.path("output").pipe(
Flag.withAlias("o"),
Flag.withDescription("Output file path")
)
const watch = Flag.boolean("watch").pipe(
Flag.withAlias("w"),
Flag.withDescription("Watch for file changes and regenerate documentation")
)
Command.make("effect-ai-docgen", { directory, output, watch }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ directory, output, watch }) {
const fs = yield* FileSystem.FileSystem
const markdown = yield* directoryToMarkdown(directory)
yield* fs.writeFileString(output, markdown)
if (!watch) return
yield* Effect.logInfo("Watching for changes...")
yield* fs.watch(directory).pipe(
Stream.debounce(1000),
Stream.tap(() => Effect.logInfo("Changes detected, regenerating documentation...")),
Stream.switchMap(() =>
directoryToMarkdown(directory).pipe(
Stream.fromEffect
)
),
Stream.runForEach(Effect.fn(function*(markdown) {
yield* fs.writeFileString(output, markdown)
yield* Effect.logInfo("Documentation updated.")
}))
)
})),
Command.run({
version: "0.0.0"
}),
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
)
const directoryToMarkdown = Effect.fn("directoryToMarkdown")(
function*(
directory: string
): Effect.fn.Return<string, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> {
const pathService = yield* Path.Path
const fs = yield* FileSystem.FileSystem
const indexMd = yield* fs.readFileString(pathService.join(directory, "index.md")).pipe(
Effect.map(String.trim),
Effect.orElseSucceed(() => null)
)
const allFiles = yield* fs.readDirectory(directory)
const hasInlineFiles = allFiles.some((file) => pathService.basename(file).startsWith("0") && /\.tsx?$/.test(file))
const tsFileContent = yield* Effect.forEach(
allFiles,
Effect.fn(function*(file) {
const filePath = pathService.join(directory, file)
const stat = yield* fs.stat(filePath)
const basename = pathService.basename(filePath)
if (stat.type === "Directory") {
if (basename === "fixtures") return null
return `${yield* directoryToMarkdown(filePath)}\n`
} else if (/\.tsx?$/.test(file)) {
const metadata = yield* tsFileMetadata(filePath)
if (metadata.fileNameWithoutExt.startsWith("0")) {
return `### ${metadata.title}
${metadata.description ?? ""}
\`\`\`ts
${metadata.content}
\`\`\`
`
}
const relativePath = pathService.relative(process.cwd(), filePath)
const link = `[${metadata.title}](./${relativePath})`
let content = ""
if (hasInlineFiles && metadata.fileNameWithoutExt.startsWith("10")) {
content += `### More examples\n\n`
}
content += `- **${link}**`
if (metadata.description) {
if (metadata.description.includes("\n")) {
const indentedDescription = metadata.description
.split("\n")
.map((line) => ` ${line}`)
.join("\n")
content += `:\n${indentedDescription}`
} else {
content += `: ${metadata.description}`
}
}
return content
}
return null
}),
{ concurrency: 5 }
).pipe(
Effect.map(Array.filter((s): s is string => s !== null && s.trim() !== "")),
Effect.map(Array.join("\n")),
Effect.map(String.trim)
)
return indexMd ? `${indexMd}\n\n${tsFileContent}` : tsFileContent
}
)
const tsFileMetadata = Effect.fn("tsFileMetadata")(function*(filePath: string) {
const pathService = yield* Path.Path
const fs = yield* FileSystem.FileSystem
let content = yield* fs.readFileString(filePath)
const fileNameWithoutExt = pathService.basename(filePath, pathService.extname(filePath))
let title: string = pipe(
fileNameWithoutExt,
String.replaceAll(/^\d+/g, ""),
String.replaceAll(/[-_]/g, " "),
String.trim,
String.capitalize
)
const firstDocString = content.indexOf("/**")
if (firstDocString === -1) {
return {
title,
description: undefined,
content,
fileNameWithoutExt
} as const
}
const linesWithComment = content.slice(firstDocString).split("\n")
let description = ""
for (let i = 1; i < linesWithComment.length; i++) {
const line = linesWithComment[i]
if (!line.startsWith(" *")) break
if (line.endsWith(" */")) {
content = linesWithComment.slice(i + 1).join("\n").trim()
break
}
const lineContent = line.replace(/^ \*\s?/, "")
if (lineContent.startsWith("@title")) {
title = lineContent.replace("@title", "").trim()
continue
}
description += lineContent + "\n"
}
return {
title,
description: String.trim(description) || undefined,
content,
fileNameWithoutExt
} as const
})

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"references": [
{ "path": "../../effect" },
{ "path": "../../platform-node" }
],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
}
}

View File

@@ -0,0 +1,226 @@
# Bundle Size Development Workflow
This package contains the internal bundle-size tooling used to measure Effect
entrypoints with Rollup, minification, and gzip.
## AI Agent Instructions
Use this workflow when a user wants to quickly understand how a local source
change affects bundle size for one or more explicitly selected fixture files.
Do not scan `scratchpad/` or `packages/tools/bundle/fixtures/` for this workflow.
Only measure files that the user explicitly names, or files you create
specifically for the current investigation.
Before running a comparison, ask the user which mode they want:
- default cleanup mode for a single check or one-off investigation;
- keep-base mode for many repeated measurements, followed by explicit cleanup at
the end.
Use cleanup mode unless the user says they expect to run multiple trials.
If the user asks what a generated bundle is made of, use the bundle composition
workflow instead of the size comparison workflow.
### Analyze Bundle Composition
Use this workflow when the user gives an explicit fixture and wants to
understand which modules make up the produced bundle.
Agent procedure:
1. Confirm the exact fixture path or paths to analyze. Do not scan directories.
2. Run `pnpm bundle-analyze` with only those explicit paths.
3. Read the printed Markdown table and identify the generated `*.raw-data.json`
path for each fixture.
4. Analyze `*.raw-data.json` first. Use `*.treemap.html` only when the user wants
a visual artifact to open.
5. Report the largest modules, notable dependency groups, and any surprising
inclusions. Mention the generated artifact paths in the answer.
Prefer the repository-level wrapper:
```sh
pnpm bundle-analyze scratchpad/my-fixture.ts
```
You can pass multiple explicit files:
```sh
pnpm bundle-analyze \
scratchpad/schema-codec.ts \
scratchpad/schema-arbitrary.ts
```
The wrapper builds the current checkout with `pnpm build` before generating
analysis artifacts, so the bundle is not produced from stale `dist` files.
By default, artifacts are written to `tmp/bundle-analysis`. Use `--output-dir`
to choose another directory:
```sh
pnpm bundle-analyze --output-dir tmp/schema-analysis scratchpad/my-fixture.ts
```
For each selected fixture, the tool writes:
- `<name>.min.js`: the generated Rollup output for inspection;
- `<name>.treemap.html`: an interactive treemap for human inspection;
- `<name>.raw-data.json`: raw visualizer data suitable for AI analysis.
The analysis build disables identifier mangling so module and export names stay
readable in the visualizer output. Use `bundle-compare-selected` or `report`
when exact size measurement is the goal.
After running the command, inspect the Markdown table it prints to find the exact
paths. For an AI analysis, read the `*.raw-data.json` file first and summarize
the largest modules and dependency groups. Use the `*.treemap.html` file when
the user wants to inspect the bundle visually.
The raw data contains a tree plus module metadata. For a quick size-oriented
summary, rank module parts by `renderedLength` or `gzipLength`, then map each
part through `metaUid` to `nodeMetas[metaUid].id`.
Example inspection command:
```sh
node -e 'const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); console.log(Object.entries(data.nodeParts).map(([uid, part]) => ({ uid, id: data.nodeMetas[part.metaUid]?.id, rendered: part.renderedLength, gzip: part.gzipLength })).sort((a, b) => b.rendered - a.rendered).slice(0, 20))' tmp/bundle-analysis/my-fixture.raw-data.json
```
Do not treat the generated `.min.js` size from this workflow as the exact bundle
size comparison number. The analysis build keeps names readable for the
visualizer. Use `bundle-compare-selected` or `report` for exact size reporting.
Do not use this workflow to compare against a base ref. Use
`bundle-compare-selected` for size impact and `bundle-analyze` for current bundle
composition.
### Compare Explicit Scratchpad Fixtures
Prefer the repository-level wrapper:
```sh
pnpm bundle-compare-selected --base main scratchpad/my-fixture.ts
```
You can pass multiple explicit files:
```sh
pnpm bundle-compare-selected --base main \
scratchpad/schema-codec.ts \
scratchpad/schema-arbitrary.ts
```
If `--base` is omitted, the script compares against `main`.
By default, the wrapper removes `tmp/bundle-base` before exiting, including on
failure. This avoids leaving extra git worktree state after a one-off
measurement.
For repeated measurements where the user explicitly wants a faster feedback loop,
pass `--keep-base`:
```sh
pnpm bundle-compare-selected --base main --keep-base scratchpad/my-fixture.ts
```
When using `--keep-base`, clean up `tmp/bundle-base` after the last measurement.
The explicit final cleanup command is:
```sh
git worktree remove --force tmp/bundle-base
```
The wrapper:
- builds the current checkout with `pnpm build`;
- creates or reuses `tmp/bundle-base` at the requested base ref;
- builds the base checkout when needed and writes
`tmp/bundle-base/.bundle-build-stamp` only after a successful build;
- reuses a kept base checkout only when its HEAD and `.bundle-build-stamp`
both match the requested base ref;
- copies only the selected files into the base checkout at the same relative
paths;
- invokes the internal bundle CLI to compare current vs base sizes.
The selected files are copied into the base checkout so the report isolates
source changes instead of changes to the fixture text.
Expected output is a Markdown table:
```md
| File Name | Current Size | Previous Size | Difference |
| :------------------------- | :----------: | :-----------: | :---------------: |
| `scratchpad/my-fixture.ts` | 42.10 KB | 40.80 KB | +1.30 KB (+3.19%) |
```
### Internal CLI
Use the internal CLI directly only when the base checkout is already prepared
and built:
```sh
node packages/tools/bundle/src/bin.ts compare-selected \
--base-dir tmp/bundle-base \
scratchpad/my-fixture.ts
```
The `--base-dir` value must be the root of the base checkout, not the base
fixture directory.
### Current Size Only
If the user only asks for the current bundled size, use:
```sh
pnpm --dir packages/tools/bundle report ../../../scratchpad/my-fixture.ts
```
This does not compare against a base ref.
### Fixture Guidance
Keep temporary fixtures in `scratchpad/`.
Good temporary fixtures are small, focused entrypoints that import public
package APIs:
```ts
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
name: Schema.String
})
Schema.decodeUnknownEffect(schema)({ name: "effect" }).pipe(Effect.runFork)
```
Prefer self-contained fixtures. The compare-selected workflow copies only the
explicit entry files into the base checkout. If a fixture imports local relative
helpers, either avoid that shape or make the entry fixture self-contained before
measuring.
Do not add temporary fixtures to `packages/tools/bundle/fixtures/`. That
directory is for stable comparison fixtures used by the regular bundle-size
workflow.
### Cleanup
The wrapper cleans up `tmp/bundle-base` by default. If `--keep-base` was used for
a multi-run investigation, remove the worktree when the user is done.
To remove it:
```sh
git worktree remove --force tmp/bundle-base
```
To verify that cleanup succeeded:
```sh
git worktree list
```
Only the main repository worktree should remain.

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["src/bin.ts"]
}

View File

@@ -0,0 +1,3 @@
import * as Effect from "effect/Effect"
Effect.succeed(123).pipe(Effect.runFork)

View File

@@ -0,0 +1,25 @@
import * as Array from "effect/Array"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Request from "effect/Request"
import * as Resolver from "effect/RequestResolver"
class GetNameById extends Request.TaggedClass("GetNameById")<{
readonly id: number
}, string> {}
const UserResolver = Resolver.make<GetNameById>((entries) =>
Effect.sync(() => {
for (const entry of entries) {
entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`))
}
})
)
const effect = Effect.forEach(
Array.range(1, 100_000),
(id) => Effect.request(new GetNameById({ id }), UserResolver),
{ concurrency: "unbounded" }
)
Effect.runFork(effect)

View File

@@ -0,0 +1,5 @@
import * as Brand from "effect/Brand"
import * as Schema from "effect/Schema"
type Positive = number & Brand.Brand<"Positive">
const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))

View File

@@ -0,0 +1,10 @@
import * as Cache from "effect/Cache"
import * as Effect from "effect/Effect"
Cache.make({
capacity: 1024,
lookup: (key: string) => Effect.succeed(key)
}).pipe(
Effect.flatMap(Cache.get("1")),
Effect.runFork
)

View File

@@ -0,0 +1,13 @@
import * as Config from "effect/Config"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
API_KEY: Schema.String,
PORT: Schema.Int,
LOCALHOST: Schema.URL
})
const config = Config.schema(schema)
Effect.runFork(config)

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
id: Schema.Number,
name: Schema.String,
price: Schema.Number
})
Schema.toDifferJsonPatch(schema)

View File

@@ -0,0 +1,12 @@
import * as Effect from "effect/Effect"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
import * as HttpClient from "effect/unstable/http/HttpClient"
Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
const res = yield* client.get("https://jsonplaceholder.typicode.com/posts/1")
yield* res.json
}).pipe(
Effect.provide(FetchHttpClient.layer),
Effect.runPromise
)

View File

@@ -0,0 +1,5 @@
import * as Effect from "effect/Effect"
Effect.log("hello").pipe(
Effect.runFork
)

View File

@@ -0,0 +1,13 @@
import * as Effect from "effect/Effect"
import * as Metric from "effect/Metric"
const program = Effect.gen(function*() {
yield* Effect.succeed(1).pipe(
Effect.forkChild({ startImmediately: true })
)
})
program.pipe(
Metric.enableRuntimeMetrics,
Effect.runFork
)

View File

@@ -0,0 +1,6 @@
import * as Optic from "effect/Optic"
type S = { readonly a: number }
const optic = Optic.id<S>().key("a")
optic.getResult({ a: 1 })

View File

@@ -0,0 +1,22 @@
import * as Effect from "effect/Effect"
import * as PubSub from "effect/PubSub"
const program = Effect.gen(function*() {
const pubsub = yield* PubSub.unbounded<number>()
yield* Effect.gen(function*() {
const subscription = yield* PubSub.subscribe(pubsub)
while (true) {
const element = yield* PubSub.take(subscription)
console.log(element)
}
}).pipe(Effect.forkScoped({ startImmediately: true }))
yield* PubSub.publishAll(pubsub, [1, 2])
yield* PubSub.publishAll(pubsub, [3, 4]).pipe(Effect.delay("100 millis"), Effect.forkScoped)
yield* PubSub.publishAll(pubsub, [5, 6, 7, 8]).pipe(Effect.delay("200 millis"), Effect.forkScoped)
yield* Effect.sleep("500 millis")
})
Effect.runFork(Effect.scoped(program))

View File

@@ -0,0 +1,18 @@
import * as Effect from "effect/Effect"
import * as Queue from "effect/Queue"
const program = Effect.gen(function*() {
const queue = yield* Queue.make<number>()
yield* Effect.gen(function*() {
yield* Queue.takeN(queue, 3)
}).pipe(Effect.forever, Effect.forkScoped)
yield* Queue.offerAll(queue, [1, 2])
yield* Queue.offerAll(queue, [3, 4]).pipe(Effect.delay("100 millis"), Effect.forkScoped)
yield* Queue.offerAll(queue, [5, 6, 7, 8]).pipe(Effect.delay("200 millis"), Effect.forkScoped)
yield* Effect.sleep("500 millis")
})
Effect.runFork(Effect.scoped(program))

View File

@@ -0,0 +1,9 @@
import * as Effect from "effect/Effect"
import * as Schedule from "effect/Schedule"
Effect.succeed(123).pipe(
Effect.repeat({
schedule: Schedule.spaced("100 millis")
}),
Effect.runFork
)

View File

@@ -0,0 +1,12 @@
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
class A extends Schema.Class<A>("A")({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
}) {}
Schema.decodeUnknownEffect(A)({ a: "a", b: 1, c: ["c"] }).pipe(
Effect.runFork
)

View File

@@ -0,0 +1,16 @@
import * as SchemaRepresentation from "effect/SchemaRepresentation"
const doc = SchemaRepresentation.fromJsonSchemaDocument({
"dialect": "draft-2020-12",
"schema": {
"type": "object",
"properties": {
"a": {
"type": "string"
}
}
},
"definitions": {}
})
console.dir(doc, { depth: null })

View File

@@ -0,0 +1,16 @@
import * as Schema from "effect/Schema"
import * as SchemaRepresentation from "effect/SchemaRepresentation"
const schema = Schema.toCodecJson(Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
}))
const json = Schema.encodeSync(SchemaRepresentation.DocumentFromJson)(
SchemaRepresentation.fromAST(schema.ast)
)
SchemaRepresentation.toSchema(
Schema.decodeSync(SchemaRepresentation.DocumentFromJson)(JSON.parse(JSON.stringify(json)))
)

View File

@@ -0,0 +1,20 @@
import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
import * as SchemaTransformation from "effect/SchemaTransformation"
const schema = Schema.String.pipe(Schema.decodeTo(
Schema.String,
SchemaTransformation.transformOrFail({
decode: (s) =>
Effect.gen(function*() {
yield* Effect.clockWith((clock) => clock.sleep(Duration.millis(300)))
return s
}),
encode: (_) => Effect.succeed(_)
})
))
Schema.decodeUnknownEffect(schema)({ a: "a", b: 1, c: ["c"] }).pipe(
Effect.runFork
)

View File

@@ -0,0 +1,8 @@
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
const schema = Schema.String
Schema.decodeUnknownEffect(schema)("a").pipe(
Effect.runFork
)

View File

@@ -0,0 +1,8 @@
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
const schema = Schema.TemplateLiteral(["a", Schema.String])
Schema.decodeUnknownEffect(schema)("abc").pipe(
Effect.runFork
)

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.toArbitraryLazy(schema)

View File

@@ -0,0 +1,12 @@
import * as Schema from "effect/Schema"
import * as SchemaRepresentation from "effect/SchemaRepresentation"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
const representation = Schema.toRepresentation(schema)
SchemaRepresentation.toCodeDocument(SchemaRepresentation.toMultiDocument(representation))

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.toCodecJson(schema)

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.toEquivalence(schema)

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.toFormatter(schema)

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.toJsonSchemaDocument(schema)

View File

@@ -0,0 +1,9 @@
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.toRepresentation(schema)

View File

@@ -0,0 +1,12 @@
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.FiniteFromString),
c: Schema.Array(Schema.String)
})
Schema.decodeUnknownEffect(schema)({ a: "a", b: 1, c: ["c"] }).pipe(
Effect.runFork
)

View File

@@ -0,0 +1,21 @@
import * as Effect from "effect/Effect"
import * as TxRef from "effect/TxRef"
const program = Effect.gen(function*() {
const ref = yield* TxRef.make(0)
yield* Effect.forkChild(Effect.forever(
TxRef.update(ref, (n) => n + 1).pipe(Effect.delay("100 millis"))
))
yield* Effect.tx(Effect.gen(function*() {
const value = yield* TxRef.get(ref)
if (value < 10) {
yield* Effect.log(`retry due to value: ${value}`)
return yield* Effect.txRetry
}
yield* Effect.log(`transaction done with value: ${value}`)
}))
})
Effect.runPromise(program).catch(console.error)

View File

@@ -0,0 +1,7 @@
import * as Effect from "effect/Effect"
import * as Stream from "effect/Stream"
Stream.range(1, 100_000).pipe(
Stream.runDrain,
Effect.runSync
)

View File

@@ -0,0 +1,51 @@
{
"name": "@effect/bundle",
"version": "0.0.0",
"type": "module",
"private": true,
"license": "MIT",
"description": "Bundle size testing infrastructure for Effect packages",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/tools/bundle"
},
"sideEffects": [],
"bin": {
"effect-bundle": "./src/bin.ts"
},
"exports": {
"./package.json": "./package.json",
"./*": "./src/*.ts"
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"scripts": {
"build": "tsc -b tsconfig.src.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"compare": "node src/bin.ts compare",
"report": "node src/bin.ts report",
"visualize": "node src/bin.ts visualize"
},
"dependencies": {
"@effect/platform-node": "workspace:^",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^1.0.0",
"effect": "workspace:^",
"glob": "^13.0.6",
"rollup": "^4.62.2",
"rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-visualizer": "^7.0.1"
},
"devDependencies": {
"@types/node": "^26.1.1"
}
}

View File

@@ -0,0 +1,123 @@
/**
* Command definitions for the `effect-bundle` bundle-size CLI.
*
* This module wires the top-level `bundle` command to the reporting service and
* exposes the workflows used when maintaining fixture bundle sizes. `compare`
* builds the package's local fixtures and compares them with matching fixture
* files from another checkout, `report` bundles an explicit list of entrypoints
* and prints a Markdown table, `compare-selected` compares explicit entrypoints
* against a base checkout, `visualize-selected` analyzes explicit entrypoints,
* and `visualize` prompts for local fixtures before producing visualization
* output for inspection.
*
* Command output is intentionally split by workflow. `compare` requires an
* existing `--base-dir` (`-b`) and writes its Markdown report to `--output-path`
* (`-o`), defaulting to `stats.txt` resolved from the current working directory.
* `report` accepts one or more existing files and writes to stdout. `visualize`
* uses `--output-dir` (`-o`) for generated bundle artifacts, so `-o` names a
* file for `compare` but a directory for `visualize`.
*
* @since 4.0.0
*/
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Path from "effect/Path"
import * as Argument from "effect/unstable/cli/Argument"
import * as Command from "effect/unstable/cli/Command"
import * as Flag from "effect/unstable/cli/Flag"
import * as Prompt from "effect/unstable/cli/Prompt"
import { Fixtures } from "./Fixtures.ts"
import { Reporter } from "./Reporter.ts"
const baseDirectory = Flag.directory("base-dir", { mustExist: true }).pipe(
Flag.withAlias("b"),
Flag.withDescription("The base directory to use for bundle size comparisons")
)
const outputPath = Flag.file("output-path").pipe(
Flag.withAlias("o"),
Flag.withDescription("The name of the file to write the bundle size report to"),
Flag.withDefault("stats.txt"),
Flag.mapEffect(Effect.fnUntraced(function*(outputPath) {
const path = yield* Path.Path
return path.resolve(outputPath)
}))
)
const compare = Command.make("compare", { baseDirectory, outputPath }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ baseDirectory, outputPath }) {
const fs = yield* FileSystem.FileSystem
const reporter = yield* Reporter
const report = yield* reporter.report({ baseDirectory })
yield* fs.writeFileString(outputPath, report)
yield* Effect.log(`Bundle size report written to: '${outputPath}'`)
}))
)
const outputDirectory = Flag.directory("output-dir").pipe(
Flag.withAlias("o"),
Flag.withDescription("The name of the directory to write the bundle size visualizations to"),
Flag.mapEffect(Effect.fnUntraced(function*(outputPath) {
const path = yield* Path.Path
return path.resolve(outputPath)
}))
)
const visualize = Command.make("visualize", { outputDirectory }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ outputDirectory }) {
const path = yield* Path.Path
const { fixtures, fixturesDir } = yield* Fixtures
const reporter = yield* Reporter
const paths = yield* Prompt.multiSelect({
message: "Select files whose bundle size you would like to visualize",
choices: fixtures.map((fixture) => ({
title: fixture,
value: path.join(fixturesDir, fixture)
}))
})
const report = yield* reporter.visualize({ paths, outputDirectory })
yield* Console.log(report)
}))
)
const reportPaths = Argument.file("paths", { mustExist: true }).pipe(
Argument.withDescription("Fixture files to include in the report"),
Argument.variadic({ min: 1 })
)
const report = Command.make("report", { paths: reportPaths }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ paths }) {
const reporter = yield* Reporter
const report = yield* reporter.reportSelected({ paths })
yield* Console.log(report)
}))
)
const compareSelected = Command.make("compare-selected", { baseDirectory, paths: reportPaths }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ baseDirectory, paths }) {
const reporter = yield* Reporter
const report = yield* reporter.reportSelectedComparison({ baseDirectory, paths })
yield* Console.log(report)
}))
)
const visualizeSelected = Command.make("visualize-selected", { outputDirectory, paths: reportPaths }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ outputDirectory, paths }) {
const reporter = yield* Reporter
const report = yield* reporter.visualize({ outputDirectory, paths })
yield* Console.log(report)
}))
)
/**
* Bundle analysis CLI command with subcommands for comparing fixture bundle sizes, reporting selected fixtures, and generating visualizations.
*
* @category commands
* @since 4.0.0
*/
export const cli = Command.make("bundle").pipe(
Command.withSubcommands([compare, compareSelected, report, visualize, visualizeSelected])
)

View File

@@ -0,0 +1,51 @@
/**
* Discovers the TypeScript entrypoint fixtures used by the bundle-size tools.
*
* The bundle CLI uses these fixture names to build current bundle reports,
* compare them against a base directory, and populate the visualization
* selector. Fixtures are intentionally discovered from the package's local
* `fixtures` directory as top-level `.ts` files and sorted by name so reports
* are deterministic.
*
* When adding or renaming fixtures, keep in mind that comparison reports match
* files by basename between the current fixtures directory and the provided
* base directory. New fixtures without a matching base file are reported as
* unchanged. Each fixture is bundled as its own Rollup entrypoint, so it should
* represent the import shape being measured and avoid depending on incidental
* fixture discovery order.
*
* @since 4.0.0
*/
import * as Array from "effect/Array"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Order from "effect/Order"
import * as Glob from "glob"
/**
* Context service that discovers and sorts TypeScript fixture files used by the bundle size tooling.
*
* @category services
* @since 4.0.0
*/
export class Fixtures extends Context.Service<Fixtures>()(
"@effect/bundle/Fixtures",
{
make: Effect.gen(function*() {
const fixturesDir = new URL("../fixtures/", import.meta.url).pathname
const fixtures = yield* Effect.promise(() => Glob.glob("*.ts", { cwd: fixturesDir })).pipe(
Effect.map(Array.sort(Order.String)),
Effect.orDie
)
return {
fixtures,
fixturesDir
} as const
})
}
) {
static readonly layer = Layer.effect(this, this.make)
}

View File

@@ -0,0 +1,176 @@
/**
* Utilities for assembling the Rollup plugin pipeline used by the Effect
* bundle-size tooling.
*
* This module is responsible for the bundler-specific work that turns local
* fixture entrypoints into comparable ESM output: resolving Effect package
* imports against each package's built `dist` files, replacing production
* environment checks, lowering TypeScript with esbuild, minifying with terser,
* and optionally adding a bundle visualizer. It is primarily used by the
* Rollup service when measuring gzipped fixture sizes or opening a
* visualization for bundle inspection.
*
* Keep plugin ordering intentional when changing this module. Local package
* resolution must run before normal node resolution so workspace imports are
* measured from built artifacts, esbuild must emit ESM for Rollup to continue
* tree-shaking, and terser mangling is disabled while visualizing so reported
* module names stay readable.
*
* @since 4.0.0
*/
import { nodeResolve } from "@rollup/plugin-node-resolve"
import replace from "@rollup/plugin-replace"
import terser from "@rollup/plugin-terser"
import type * as Path from "effect/Path"
import * as Predicate from "effect/Predicate"
import type { Plugin } from "rollup"
import esbuild from "rollup-plugin-esbuild"
import { type PluginVisualizerOptions, visualizer } from "rollup-plugin-visualizer"
const EFFECT_PACKAGE_REGEX = /^(@effect\/[\w-]+|effect)(\/.*)?$/
const TYPE_SCRIPT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
const toLocalDistPath = (pathService: Path.Path, packageDir: string, resolvedId: string): string => {
const srcDir = pathService.join(packageDir, "src")
const relative = pathService.relative(srcDir, resolvedId)
if (relative === "" || relative.startsWith("..") || pathService.isAbsolute(relative)) {
return resolvedId
}
const extension = pathService.extname(relative)
if (!TYPE_SCRIPT_EXTENSIONS.has(extension)) {
return resolvedId
}
return pathService.join(packageDir, "dist", relative.slice(0, -extension.length) + ".js")
}
/**
* Options for configuring Rollup plugins.
*
* @category options
* @since 4.0.0
*/
export interface PluginOptions {
readonly nodeTarget?: string | undefined
readonly minify?: boolean | undefined
readonly mangle?: boolean | undefined
readonly visualize?: boolean | undefined
readonly visualizations?: ReadonlyArray<VisualizationOutput> | undefined
}
/**
* Output generated by the Rollup visualizer plugin.
*
* @category options
* @since 4.0.0
*/
export interface VisualizationOutput {
readonly filename: string
readonly template: NonNullable<PluginVisualizerOptions["template"]>
readonly title?: string | undefined
}
interface ResolvedPluginOptions {
readonly nodeTarget: string
readonly minify: boolean
readonly mangle: boolean
readonly visualize: boolean
readonly visualizations: ReadonlyArray<VisualizationOutput>
}
const defaultPluginOptions: ResolvedPluginOptions = {
nodeTarget: "node20",
minify: true,
mangle: true,
visualize: false,
visualizations: []
}
/**
* Merges provided options with defaults.
*/
const resolvePluginOptions = (options: PluginOptions): ResolvedPluginOptions => ({
nodeTarget: options.nodeTarget ?? defaultPluginOptions.nodeTarget,
minify: options.minify ?? defaultPluginOptions.minify,
mangle: options.mangle ?? defaultPluginOptions.mangle,
visualize: options.visualize ?? defaultPluginOptions.visualize,
visualizations: options.visualizations ?? defaultPluginOptions.visualizations
})
/**
* Creates a custom Rollup plugin that resolves Effect package imports to their
* local dist directories.
*
* @category constructors
* @since 4.0.0
*/
export const createResolveLocalPackageImports = (pathService: Path.Path): Plugin => ({
name: "rollup-plugin-resolve-imports",
async resolveId(source, importer) {
const match = source.match(EFFECT_PACKAGE_REGEX)
if (Predicate.isNotNull(match)) {
const packageName = match[1]
const packageJson = await this.resolve(`${packageName}/package.json`, importer, { skipSelf: true })
if (packageJson === null) return null
const resolved = await this.resolve(source, importer, { skipSelf: true })
if (resolved === null) return null
return {
...resolved,
id: toLocalDistPath(pathService, pathService.dirname(packageJson.id), resolved.id),
external: false
}
}
return null
}
})
/**
* Creates the full Rollup plugin pipeline for bundling.
*
* @category constructors
* @since 4.0.0
*/
export const createPlugins = (pathService: Path.Path, options: PluginOptions = {}): Array<Plugin> => {
const resolved = resolvePluginOptions(options)
const plugins: Array<Plugin> = [
createResolveLocalPackageImports(pathService),
nodeResolve(),
// @ts-expect-error see https://github.com/rollup/plugins/issues/1662
replace({
"process.env.NODE_ENV": JSON.stringify("production"),
preventAssignment: true
}),
esbuild({
target: resolved.nodeTarget,
format: "esm",
treeShaking: true
}),
// @ts-expect-error see https://github.com/rollup/plugins/issues/1662
terser({
format: { comments: false },
compress: resolved.minify,
mangle: resolved.mangle && !resolved.visualize
})
]
if (resolved.visualizations.length > 0) {
for (const output of resolved.visualizations) {
const visualizerOptions: PluginVisualizerOptions = {
filename: output.filename,
gzipSize: true,
open: false,
template: output.template
}
if (output.title !== undefined) {
visualizerOptions.title = output.title
}
plugins.push(visualizer(visualizerOptions))
}
} else if (resolved.visualize) {
plugins.push(visualizer({
open: true,
gzipSize: true
}))
}
return plugins
}

View File

@@ -0,0 +1,299 @@
/**
* Bundle report generation for the Effect bundle-size tooling.
*
* The reporter coordinates fixture discovery with the Rollup service to turn
* measured fixture bundles into Markdown tables or visualization output. It is
* used by the bundle CLI to compare the current workspace against a checked-out
* base directory, to print a one-off report for selected entry files, and to
* generate visualizations when a size change needs inspection.
*
* Reports compare files by basename and display gzipped Rollup output sizes in
* decimal kilobytes. Base fixtures are bundled only when the matching file
* exists; if a current fixture has no matching basename in the base directory it
* is reported as unchanged. Visualization artifacts are named from entry file
* stems in the requested output directory, so duplicate names can make the
* output misleading.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import { constFalse } from "effect/Function"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import { fileURLToPath } from "node:url"
import { Fixtures } from "./Fixtures.ts"
import type { BundleStats } from "./Rollup.ts"
import { Rollup } from "./Rollup.ts"
/**
* Error raised when generating a bundle size report or visualization fails.
*
* @category errors
* @since 4.0.0
*/
export class ReporterError extends Data.TaggedError("ReporterError")<{
readonly cause: unknown
}> {}
/**
* Options for generating a bundle size comparison report against fixture files from a base directory.
*
* @category options
* @since 4.0.0
*/
export interface ReportOptions {
readonly baseDirectory: string
}
/**
* Options for generating bundle visualizations for selected entry files into an output directory.
*
* @category options
* @since 4.0.0
*/
export interface VisualizeOptions {
readonly paths: ReadonlyArray<string>
readonly outputDirectory: string
}
/**
* Options for generating a bundle size report for an explicit list of entry files.
*
* @category options
* @since 4.0.0
*/
export interface ReportSelectedOptions {
readonly paths: ReadonlyArray<string>
}
/**
* Options for generating a bundle size comparison report for explicit entry files against a base checkout.
*
* @category options
* @since 4.0.0
*/
export interface ReportSelectedComparisonOptions {
readonly baseDirectory: string
readonly paths: ReadonlyArray<string>
}
/**
* Context service for producing bundle size reports and visualizations from Rollup-generated fixture stats.
*
* @category services
* @since 4.0.0
*/
export class Reporter extends Context.Service<Reporter>()(
"@effect/bundle/Reporter",
{
make: Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const { fixtures, fixturesDir } = yield* Fixtures
const rollup = yield* Rollup
const currentDirectory = path.resolve(fileURLToPath(new URL("../../../../", import.meta.url)))
const calculateDifference = (current: BundleStats, previous: BundleStats) => {
const currSize = current.sizeInBytes
const prevSize = previous.sizeInBytes
const diff = currSize - prevSize
const diffPct = prevSize === 0 ? 0 : (Math.abs(diff) / prevSize) * 100
const currKb = (currSize / 1000).toFixed(2)
const prevKb = (prevSize / 1000).toFixed(2)
const diffKb = (Math.abs(diff) / 1000).toFixed(2)
const filename = path.basename(current.path)
return {
diff,
diffPct,
currKb,
prevKb,
diffKb,
filename
}
}
const createComparisonReport = (
entries: ReadonlyArray<{
readonly current: BundleStats
readonly previous: BundleStats
readonly filename: string
}>
): string => {
const lines: Array<string> = [
"| File Name | Current Size | Previous Size | Difference |",
"|:----------|:------------:|:-------------:|:----------:|"
]
for (const { current, previous, filename } of entries) {
const comparison = calculateDifference(current, previous)
const currKb = `${comparison.currKb} KB`
const prevKb = `${comparison.prevKb} KB`
const diffKb = `${comparison.diffKb} KB`
const diffPct = `${comparison.diffPct.toFixed(2)}%`
const sign = comparison.diff === 0 ? "" : comparison.diff > 0 ? "+" : "-"
const line = `| \`${filename}\` | ${currKb} | ${prevKb} | ${sign}${diffKb} (${sign}${diffPct}) |`
lines.push(line)
}
return lines.join("\n") + "\n"
}
const createReport = (curr: ReadonlyArray<BundleStats>, prev: ReadonlyArray<BundleStats>): string => {
const entries: Array<{
readonly current: BundleStats
readonly previous: BundleStats
readonly filename: string
}> = []
for (const current of curr) {
const previous = prev.find((previous) => {
return path.basename(previous.path) === path.basename(current.path)
}) ?? current
entries.push({
current,
previous,
filename: path.basename(current.path)
})
}
return createComparisonReport(entries)
}
const createSelectedReport = (stats: ReadonlyArray<BundleStats>): string => {
const lines: Array<string> = [
"| File Name | Current Size |",
"|:----------|:------------:|"
]
for (const current of stats) {
const filename = `\`${path.basename(current.path)}\``
const currKb = `${(current.sizeInBytes / 1000).toFixed(2)} KB`
const line = `| ${filename} | ${currKb} |`
lines.push(line)
}
return lines.join("\n") + "\n"
}
const createVisualizationReport = (paths: ReadonlyArray<string>, outputDirectory: string): string => {
const lines: Array<string> = [
"| File Name | Generated Bundle | Treemap | Raw Data |",
"|:----------|:----------------|:--------|:---------|"
]
for (const entryPath of paths) {
const name = path.parse(entryPath).name
const filename = path.relative(currentDirectory, path.resolve(entryPath))
const minified = path.join(outputDirectory, `${name}.min.js`)
const treemap = path.join(outputDirectory, `${name}.treemap.html`)
const rawData = path.join(outputDirectory, `${name}.raw-data.json`)
lines.push(`| \`${filename}\` | \`${minified}\` | \`${treemap}\` | \`${rawData}\` |`)
}
return lines.join("\n") + "\n"
}
const report = Effect.fn("Reporter.report")(
function*(options: ReportOptions) {
yield* Effect.logInfo(`Found ${fixtures.length} files to bundle`)
const currentPaths = fixtures.map((fixture) => path.join(fixturesDir, fixture))
const previousPaths = yield* Effect.filter(
fixtures.map((fixture) => path.join(options.baseDirectory, fixture)),
(previousPath) => fs.exists(previousPath).pipe(Effect.orElseSucceed(constFalse)),
{ concurrency: fixtures.length }
)
const [currentStats, previousStats] = yield* Effect.all([
rollup.bundleAll({
paths: currentPaths
}),
rollup.bundleAll({
paths: previousPaths
})
], { concurrency: 2 })
yield* Effect.logInfo("Bundling complete! Generating bundle size report...")
return createReport(currentStats, previousStats)
}
)
const visualize = Effect.fn("Reporter.visualize")(
function*(options: VisualizeOptions) {
yield* fs.makeDirectory(options.outputDirectory, { recursive: true })
yield* rollup.bundleAll({
paths: options.paths,
outputDirectory: options.outputDirectory,
visualize: true
})
return createVisualizationReport(options.paths, options.outputDirectory)
}
)
const reportSelected = Effect.fn("Reporter.reportSelected")(
function*(options: ReportSelectedOptions) {
yield* Effect.logInfo(`Found ${options.paths.length} files to bundle`)
const stats = yield* rollup.bundleAll({ paths: options.paths })
yield* Effect.logInfo("Bundling complete! Generating bundle size report...")
return createSelectedReport(stats)
}
)
const reportSelectedComparison = Effect.fn("Reporter.reportSelectedComparison")(
function*(options: ReportSelectedComparisonOptions) {
yield* Effect.logInfo(`Found ${options.paths.length} files to compare`)
const baseDirectory = path.resolve(options.baseDirectory)
const currentPaths = options.paths.map((currentPath) => path.resolve(currentPath))
const previousPaths = yield* Effect.forEach(
currentPaths,
Effect.fnUntraced(function*(currentPath) {
const relativePath = path.relative(currentDirectory, currentPath)
if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
return yield* Effect.fail(
new ReporterError({
cause: `Selected bundle entry must be inside ${currentDirectory}: ${currentPath}`
})
)
}
const previousPath = path.join(baseDirectory, relativePath)
yield* fs.makeDirectory(path.dirname(previousPath), { recursive: true })
yield* fs.copy(currentPath, previousPath, { overwrite: true })
return previousPath
}),
{ concurrency: currentPaths.length }
)
const [currentStats, previousStats] = yield* Effect.all([
rollup.bundleAll({ paths: currentPaths }),
rollup.bundleAll({ paths: previousPaths })
], { concurrency: 2 })
const entries: Array<{
readonly current: BundleStats
readonly previous: BundleStats
readonly filename: string
}> = []
for (let i = 0; i < currentStats.length; i++) {
entries.push({
current: currentStats[i]!,
previous: previousStats[i]!,
filename: path.relative(currentDirectory, currentStats[i]!.path)
})
}
yield* Effect.logInfo("Bundling complete! Generating bundle size report...")
return createComparisonReport(entries)
}
)
return {
report,
reportSelectedComparison,
reportSelected,
visualize
} as const
})
}
) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(Fixtures.layer),
Layer.provide(Rollup.layer)
)
}

View File

@@ -0,0 +1,201 @@
/**
* Rollup-backed bundling and size measurement for the Effect bundle-size tools.
*
* This module provides the service used by the bundle CLI and reporter to turn
* fixture or selected TypeScript entrypoints into ESM Rollup output, optionally
* write a minified artifact, and return the gzipped byte count used in
* bundle-size comparisons.
*
* Bundles are generated in memory so the emitted code can be streamed to both
* gzip measurement and optional file output. Only Rollup `chunk` outputs are
* included; assets are ignored, and when Rollup creates multiple chunks (for
* example because of dynamic imports or shared chunks) their code is streamed
* together for measurement. The optional output file is named from the
* entrypoint stem, so it is best treated as an inspection artifact rather than
* a complete Rollup output directory.
*
* @since 4.0.0
*/
import * as NodeStream from "@effect/platform-node/NodeStream"
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as FiberSet from "effect/FiberSet"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Path from "effect/Path"
import * as Stream from "effect/Stream"
import { createGzip } from "node:zlib"
import type { RollupOptions } from "rollup"
import { rollup } from "rollup"
import { createPlugins, type VisualizationOutput } from "./Plugins.ts"
/**
* Error raised when Rollup bundling, output generation, or bundle size measurement fails.
*
* @category errors
* @since 4.0.0
*/
export class RollupError extends Data.TaggedError("RollupError")<{
readonly cause: unknown
}> {}
/**
* Bundle size statistics for an entry file, including its path and gzipped size in bytes.
*
* @category models
* @since 4.0.0
*/
export class BundleStats extends Data.TaggedClass("BundleStats")<{
readonly path: string
readonly sizeInBytes: number
}> {}
/**
* Options for bundling one entry file, optionally writing a minified output and generating a visualization.
*
* @category options
* @since 4.0.0
*/
export interface BundleOptions {
readonly path: string
readonly visualize?: boolean | undefined
readonly outputDirectory?: string | undefined
}
/**
* Options for bundling multiple entry files with shared visualization and output-directory settings.
*
* @category options
* @since 4.0.0
*/
export interface BundleAllOptions {
readonly paths: ReadonlyArray<string>
readonly visualize?: boolean | undefined
readonly outputDirectory?: string | undefined
}
/**
* Context service for bundling entry files with Rollup and measuring their gzipped output size.
*
* @category services
* @since 4.0.0
*/
export class Rollup extends Context.Service<Rollup>()(
"@effect/bundle/Rollup",
{
make: Effect.gen(function*() {
const pathService = yield* Path.Path
const fs = yield* FileSystem.FileSystem
const createVisualizationOutputs = (options: BundleOptions): ReadonlyArray<VisualizationOutput> => {
if (!options.visualize || !options.outputDirectory) {
return []
}
const name = pathService.parse(options.path).name
return [
{
filename: pathService.join(options.outputDirectory, `${name}.treemap.html`),
template: "treemap",
title: `${name} bundle treemap`
},
{
filename: pathService.join(options.outputDirectory, `${name}.raw-data.json`),
template: "raw-data",
title: `${name} bundle raw data`
}
]
}
const getRollupOptions = (options: BundleOptions): RollupOptions => ({
input: options.path,
output: {
format: "esm"
},
plugins: createPlugins(pathService, {
visualize: options.visualize,
visualizations: createVisualizationOutputs(options)
}),
onwarn: (warning, next) => {
if (warning.code === "THIS_IS_UNDEFINED") return
next(warning)
}
})
const bundle = Effect.fn("Rollup.bundle")(
function*(options: BundleOptions) {
const bundle = yield* Effect.acquireRelease(
Effect.tryPromise({
try: () => rollup(getRollupOptions(options)),
catch: (cause) => new RollupError({ cause })
}),
(bundle) => Effect.promise(() => bundle.close())
)
const fibers = yield* FiberSet.make()
const { output } = yield* Effect.tryPromise({
try: () => bundle.generate({ format: "esm" }),
catch: (cause) => new RollupError({ cause })
})
const stream = yield* Stream.fromIterable(output).pipe(
Stream.filter((output) => output.type === "chunk"),
Stream.map((chunk) => chunk.code),
Stream.encodeText,
Stream.broadcast({ capacity: 8, replay: 8 })
)
if (options.outputDirectory) {
const outputPath = pathService.join(
options.outputDirectory,
`${pathService.parse(options.path).name}.min.js`
)
yield* FiberSet.run(
fibers,
stream.pipe(
Stream.run(fs.sink(outputPath))
)
)
}
const sizeInBytes = yield* stream.pipe(
NodeStream.pipeThroughDuplex({
evaluate: () => createGzip({ level: 9 }),
onError: (cause) => new RollupError({ cause })
}),
Stream.runFold(
() => 0,
(totalBytes, chunkBytes) => chunkBytes.length + totalBytes
)
)
yield* FiberSet.awaitEmpty(fibers)
yield* Effect.log(`Bundled ${options.path}`).pipe(
Effect.annotateLogs({ size: `${(sizeInBytes / 1000).toFixed(2)} kB` })
)
return new BundleStats({ path: options.path, sizeInBytes })
},
Effect.scoped
)
const bundleAll = Effect.fn("Rollup.bundleAll")(
function*(options: BundleAllOptions) {
return yield* Effect.forEach(
options.paths,
(path) => bundle({ path, visualize: options.visualize, outputDirectory: options.outputDirectory }),
{ concurrency: options.paths.length }
)
}
)
return {
bundle,
bundleAll
} as const
})
}
) {
static readonly layer = Layer.effect(this, this.make)
}

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
/**
* @since 4.0.0
*/
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Command from "effect/unstable/cli/Command"
import PackageJson from "../package.json" with { type: "json" }
import { cli } from "./Cli.ts"
import { Fixtures } from "./Fixtures.ts"
import { Reporter } from "./Reporter.ts"
const MainLayer = Layer.mergeAll(
Fixtures.layer,
Reporter.layer
).pipe(Layer.provideMerge(NodeServices.layer))
Command.run(cli, { version: PackageJson["version"] }).pipe(
Effect.provide(MainLayer),
NodeRuntime.runMain
)

View File

@@ -0,0 +1,74 @@
import { createResolveLocalPackageImports } from "@effect/bundle/Plugins"
import { assert, describe, it } from "@effect/vitest"
import type * as EffectPath from "effect/Path"
import * as path from "node:path"
import { fileURLToPath } from "node:url"
import type { Plugin } from "rollup"
type Resolved = {
readonly id: string
readonly external: false
}
type ResolveId = (
this: {
readonly resolve: (
source: string,
importer?: string,
options?: { readonly skipSelf?: boolean }
) => Promise<Resolved | null>
},
source: string,
importer?: string
) => Promise<Resolved | null>
const packageDir = fileURLToPath(new URL("../../../effect", import.meta.url))
const pathService = path as unknown as EffectPath.Path
const getResolveId = (plugin: Plugin): ResolveId => {
assert.strictEqual(typeof plugin.resolveId, "function")
return plugin.resolveId as ResolveId
}
const resolved = (id: string): Resolved => ({
id,
external: false
})
describe("createResolveLocalPackageImports", () => {
it("resolves directory package exports to dist index files", async () => {
const resolveId = getResolveId(createResolveLocalPackageImports(pathService))
const result = await resolveId.call({
resolve: async (source) => {
switch (source) {
case "effect/package.json":
return resolved(path.join(packageDir, "package.json"))
case "effect/testing":
return resolved(path.join(packageDir, "src", "testing", "index.ts"))
default:
return null
}
}
}, "effect/testing")
assert.deepStrictEqual(result, resolved(path.join(packageDir, "dist", "testing", "index.js")))
})
it("keeps flat package exports on flat dist files", async () => {
const resolveId = getResolveId(createResolveLocalPackageImports(pathService))
const result = await resolveId.call({
resolve: async (source) => {
switch (source) {
case "effect/package.json":
return resolved(path.join(packageDir, "package.json"))
case "effect/Schema":
return resolved(path.join(packageDir, "src", "Schema.ts"))
default:
return null
}
}
}, "effect/Schema")
assert.deepStrictEqual(result, resolved(path.join(packageDir, "dist", "Schema.js")))
})
})

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["fixtures"],
"compilerOptions": {
"rootDir": "fixtures",
"noEmit": true
},
"references": [
{ "path": "../../effect" },
{ "path": "../../platform-node" }
]
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": [],
"references": [
{ "path": "tsconfig.fixtures.json" },
{ "path": "tsconfig.src.json" }
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"resolveJsonModule": true,
"types": ["node"]
},
"references": [
{ "path": "../../effect" },
{ "path": "../../platform-node" }
]
}

View File

@@ -0,0 +1,6 @@
import { mergeConfig, type ViteUserConfig } from "vitest/config"
import shared from "../../../vitest.shared.ts"
const config: ViteUserConfig = {}
export default mergeConfig(shared, config)

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["**/*.ts"]
}

View File

@@ -0,0 +1,63 @@
{
"name": "@effect/jsdocs",
"version": "0.0.0",
"type": "module",
"private": true,
"license": "MIT",
"description": "JSDoc extraction tools for Effect",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/tools/jsdocs"
},
"sideEffects": [],
"bin": {
"effect-jsdocs": "./src/bin.ts"
},
"exports": {
"./package.json": "./package.json",
".": "./src/Jsdocs.ts",
"./*": "./src/*.ts",
"./bin": null
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"provenance": true,
"bin": {
"effect-jsdocs": "./dist/bin.js"
},
"exports": {
"./package.json": "./package.json",
".": "./dist/Jsdocs.js",
"./*": "./dist/*.js",
"./bin": null
}
},
"scripts": {
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"dependencies": {
"effect": "workspace:^",
"glob": "^13.0.6"
},
"peerDependencies": {
"typescript": ">=5.0.0 <7.0.0"
},
"devDependencies": {
"@effect/vitest": "workspace:^",
"@types/node": "^26.1.1",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env node
import * as path from "node:path"
import { computeJSDocInputHash, extractJSDocsSync, loadJSDocConfig, readJSDocModel, writeJSDocModel } from "./Jsdocs.ts"
function reportDiagnostics(model: ReturnType<typeof extractJSDocsSync>, check: boolean) {
const diagnostics = model.files.reduce((count, file) => count + file.diagnostics.length, 0)
if (diagnostics > 0) {
for (const file of model.files) {
for (const diagnostic of file.diagnostics) {
process.stderr.write(`${file.file}: ${diagnostic.message}\n`)
}
}
if (check) process.exitCode = 1
}
}
try {
const cwd = process.cwd()
const check = process.argv.includes("--check")
const config = loadJSDocConfig(cwd)
const inputHash = computeJSDocInputHash({ cwd, ...config })
const cached = readJSDocModel(path.resolve(cwd, config.output))
if (cached._tag === "Success" && cached.value.inputHash === inputHash) {
process.stdout.write(`Skipped ${config.output}\n`)
reportDiagnostics(cached.value, check)
} else {
const model = extractJSDocsSync({ cwd, ...config })
writeJSDocModel(cwd, config.output, model)
process.stdout.write(`Wrote ${config.output}\n`)
reportDiagnostics(model, check)
}
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"types": ["node"]
}
}

View File

@@ -0,0 +1,6 @@
import { mergeConfig, type ViteUserConfig } from "vitest/config"
import shared from "../../../vitest.shared.ts"
const config: ViteUserConfig = {}
export default mergeConfig(shared, config)

View File

@@ -0,0 +1,815 @@
# @effect/openapi-generator
## 4.0.0-beta.98
### Patch Changes
- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]:
- effect@4.0.0-beta.98
- @effect/platform-node@4.0.0-beta.98
## 4.0.0-beta.97
### Patch Changes
- Updated dependencies []:
- effect@4.0.0-beta.97
- @effect/platform-node@4.0.0-beta.97
## 4.0.0-beta.96
### Patch Changes
- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]:
- effect@4.0.0-beta.96
- @effect/platform-node@4.0.0-beta.96
## 4.0.0-beta.95
### Patch Changes
- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]:
- effect@4.0.0-beta.95
- @effect/platform-node@4.0.0-beta.95
## 4.0.0-beta.94
### Patch Changes
- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]:
- effect@4.0.0-beta.94
- @effect/platform-node@4.0.0-beta.94
## 4.0.0-beta.93
### Patch Changes
- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]:
- effect@4.0.0-beta.93
- @effect/platform-node@4.0.0-beta.93
## 4.0.0-beta.92
### Patch Changes
- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]:
- effect@4.0.0-beta.92
- @effect/platform-node@4.0.0-beta.92
## 4.0.0-beta.91
### Patch Changes
- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]:
- effect@4.0.0-beta.91
- @effect/platform-node@4.0.0-beta.91
## 4.0.0-beta.90
### Patch Changes
- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]:
- effect@4.0.0-beta.90
- @effect/platform-node@4.0.0-beta.90
## 4.0.0-beta.89
### Patch Changes
- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]:
- effect@4.0.0-beta.89
- @effect/platform-node@4.0.0-beta.89
## 4.0.0-beta.88
### Patch Changes
- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]:
- effect@4.0.0-beta.88
- @effect/platform-node@4.0.0-beta.88
## 4.0.0-beta.87
### Patch Changes
- [#2469](https://github.com/Effect-TS/effect-smol/pull/2469) [`12fcf35`](https://github.com/Effect-TS/effect-smol/commit/12fcf35b92879f70a0e3b32b18359986439b9283) Thanks @jbmusso! - Fix the generated SSE `sseRequest` helper to reference `Schema.ConstraintDecoder` instead of the no-longer-exported `Schema.Decoder`.
For specs with `text/event-stream` responses the generator emitted a helper typed as `Schema.Decoder<Type, DecodingServices>`, but `Schema` exports that decode-only interface as `ConstraintDecoder`, so generated clients failed to compile (`'"effect/Schema"' has no exported member named 'Decoder'`). The emitted helper now uses `Schema.ConstraintDecoder`.
- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]:
- effect@4.0.0-beta.87
- @effect/platform-node@4.0.0-beta.87
## 4.0.0-beta.86
### Patch Changes
- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]:
- effect@4.0.0-beta.86
- @effect/platform-node@4.0.0-beta.86
## 4.0.0-beta.85
### Patch Changes
- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]:
- effect@4.0.0-beta.85
- @effect/platform-node@4.0.0-beta.85
## 4.0.0-beta.84
### Patch Changes
- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]:
- effect@4.0.0-beta.84
- @effect/platform-node@4.0.0-beta.84
## 4.0.0-beta.83
### Patch Changes
- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]:
- effect@4.0.0-beta.83
- @effect/platform-node@4.0.0-beta.83
## 4.0.0-beta.82
### Patch Changes
- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]:
- effect@4.0.0-beta.82
- @effect/platform-node@4.0.0-beta.82
## 4.0.0-beta.81
### Patch Changes
- [#2270](https://github.com/Effect-TS/effect-smol/pull/2270) [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36) Thanks @IMax153! - Add HTTP API streaming response support
- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]:
- effect@4.0.0-beta.81
- @effect/platform-node@4.0.0-beta.81
## 4.0.0-beta.80
### Patch Changes
- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]:
- effect@4.0.0-beta.80
- @effect/platform-node@4.0.0-beta.80
## 4.0.0-beta.79
### Patch Changes
- [#2350](https://github.com/Effect-TS/effect-smol/pull/2350) [`3bbdf8c`](https://github.com/Effect-TS/effect-smol/commit/3bbdf8cbbdad27c889fa30436fdcfd8eeb606b6c) Thanks @wer416182-afk! - Declare `swagger2openapi` as a runtime dependency so published `openapigen` installs can resolve the converter imported by `OpenApiGenerator`.
- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]:
- effect@4.0.0-beta.79
- @effect/platform-node@4.0.0-beta.79
## 4.0.0-beta.78
### Patch Changes
- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]:
- effect@4.0.0-beta.78
- @effect/platform-node@4.0.0-beta.78
## 4.0.0-beta.77
### Patch Changes
- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]:
- effect@4.0.0-beta.77
- @effect/platform-node@4.0.0-beta.77
## 4.0.0-beta.76
### Patch Changes
- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]:
- effect@4.0.0-beta.76
- @effect/platform-node@4.0.0-beta.76
## 4.0.0-beta.75
### Patch Changes
- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]:
- effect@4.0.0-beta.75
- @effect/platform-node@4.0.0-beta.75
## 4.0.0-beta.74
### Patch Changes
- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]:
- effect@4.0.0-beta.74
- @effect/platform-node@4.0.0-beta.74
## 4.0.0-beta.73
### Patch Changes
- [#2292](https://github.com/Effect-TS/effect-smol/pull/2292) [`ba21055`](https://github.com/Effect-TS/effect-smol/commit/ba2105521173fbef145aa6d986bed1facb3bd94e) Thanks @tim-smart! - Add HttpApi generation support for custom OpenAPI HTTP security schemes.
- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]:
- effect@4.0.0-beta.73
- @effect/platform-node@4.0.0-beta.73
## 4.0.0-beta.72
### Patch Changes
- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]:
- effect@4.0.0-beta.72
- @effect/platform-node@4.0.0-beta.72
## 4.0.0-beta.71
### Patch Changes
- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]:
- effect@4.0.0-beta.71
- @effect/platform-node@4.0.0-beta.71
## 4.0.0-beta.70
### Patch Changes
- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]:
- effect@4.0.0-beta.70
- @effect/platform-node@4.0.0-beta.70
## 4.0.0-beta.69
### Patch Changes
- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]:
- effect@4.0.0-beta.69
- @effect/platform-node@4.0.0-beta.69
## 4.0.0-beta.68
### Patch Changes
- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]:
- effect@4.0.0-beta.68
- @effect/platform-node@4.0.0-beta.68
## 4.0.0-beta.67
### Patch Changes
- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]:
- effect@4.0.0-beta.67
- @effect/platform-node@4.0.0-beta.67
## 4.0.0-beta.66
### Patch Changes
- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]:
- effect@4.0.0-beta.66
- @effect/platform-node@4.0.0-beta.66
## 4.0.0-beta.65
### Patch Changes
- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]:
- effect@4.0.0-beta.65
- @effect/platform-node@4.0.0-beta.65
## 4.0.0-beta.64
### Patch Changes
- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]:
- effect@4.0.0-beta.64
- @effect/platform-node@4.0.0-beta.64
## 4.0.0-beta.63
### Patch Changes
- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]:
- effect@4.0.0-beta.63
- @effect/platform-node@4.0.0-beta.63
## 4.0.0-beta.62
### Patch Changes
- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]:
- effect@4.0.0-beta.62
- @effect/platform-node@4.0.0-beta.62
## 4.0.0-beta.61
### Patch Changes
- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]:
- effect@4.0.0-beta.61
- @effect/platform-node@4.0.0-beta.61
## 4.0.0-beta.60
### Patch Changes
- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]:
- effect@4.0.0-beta.60
- @effect/platform-node@4.0.0-beta.60
## 4.0.0-beta.59
### Patch Changes
- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]:
- effect@4.0.0-beta.59
- @effect/platform-node@4.0.0-beta.59
## 4.0.0-beta.58
### Patch Changes
- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]:
- effect@4.0.0-beta.58
- @effect/platform-node@4.0.0-beta.58
## 4.0.0-beta.57
### Patch Changes
- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]:
- effect@4.0.0-beta.57
- @effect/platform-node@4.0.0-beta.57
## 4.0.0-beta.56
### Patch Changes
- Updated dependencies []:
- effect@4.0.0-beta.56
- @effect/platform-node@4.0.0-beta.56
## 4.0.0-beta.55
### Patch Changes
- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]:
- effect@4.0.0-beta.55
- @effect/platform-node@4.0.0-beta.55
## 4.0.0-beta.54
### Patch Changes
- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]:
- effect@4.0.0-beta.54
- @effect/platform-node@4.0.0-beta.54
## 4.0.0-beta.53
### Patch Changes
- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]:
- effect@4.0.0-beta.53
- @effect/platform-node@4.0.0-beta.53
## 4.0.0-beta.52
### Patch Changes
- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]:
- effect@4.0.0-beta.52
- @effect/platform-node@4.0.0-beta.52
## 4.0.0-beta.51
### Patch Changes
- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]:
- effect@4.0.0-beta.51
- @effect/platform-node@4.0.0-beta.51
## 4.0.0-beta.50
### Patch Changes
- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]:
- effect@4.0.0-beta.50
- @effect/platform-node@4.0.0-beta.50
## 4.0.0-beta.49
### Patch Changes
- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]:
- effect@4.0.0-beta.49
- @effect/platform-node@4.0.0-beta.49
## 4.0.0-beta.48
### Patch Changes
- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]:
- effect@4.0.0-beta.48
- @effect/platform-node@4.0.0-beta.48
## 4.0.0-beta.47
### Patch Changes
- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]:
- effect@4.0.0-beta.47
- @effect/platform-node@4.0.0-beta.47
## 4.0.0-beta.46
### Patch Changes
- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]:
- effect@4.0.0-beta.46
- @effect/platform-node@4.0.0-beta.46
## 4.0.0-beta.45
### Patch Changes
- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]:
- effect@4.0.0-beta.45
- @effect/platform-node@4.0.0-beta.45
## 4.0.0-beta.44
### Patch Changes
- [#1910](https://github.com/Effect-TS/effect-smol/pull/1910) [`698296f`](https://github.com/Effect-TS/effect-smol/commit/698296f919859010dfcc713ad29927157f8aec00) Thanks @craigsmitham! - Support `application/x-www-form-urlencoded` request bodies in `httpclient` output format. Previously, form-urlencoded request bodies were silently dropped, producing operations with no payload parameter. The generator now emits `HttpClientRequest.bodyUrlParams` for these endpoints, matching the existing pattern for `multipart/form-data` (`bodyFormData`) and `application/json` (`bodyJsonUnsafe`). The `httpapi` format was already handling this content type correctly.
- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests.
- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]:
- effect@4.0.0-beta.44
- @effect/platform-node@4.0.0-beta.44
## 4.0.0-beta.43
### Patch Changes
- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]:
- effect@4.0.0-beta.43
- @effect/platform-node@4.0.0-beta.43
## 4.0.0-beta.42
### Patch Changes
- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]:
- effect@4.0.0-beta.42
- @effect/platform-node@4.0.0-beta.42
## 4.0.0-beta.41
### Patch Changes
- [#1852](https://github.com/Effect-TS/effect-smol/pull/1852) [`54769ed`](https://github.com/Effect-TS/effect-smol/commit/54769ed9aa8ee513bbdc4a15d51e2e4042e67394) Thanks @tim-smart! - Finalize the OpenAPI generator public migration by replacing the `typeOnly` option and `--type-only` CLI flag with the `format` option and `--format` flag, and by adding `httpapi` as a supported output alongside `httpclient` and `httpclient-type-only`.
- [#1852](https://github.com/Effect-TS/effect-smol/pull/1852) [`54769ed`](https://github.com/Effect-TS/effect-smol/commit/54769ed9aa8ee513bbdc4a15d51e2e4042e67394) Thanks @tim-smart! - Fix generated schema declaration ordering when non-recursive schemas reference recursive schemas, preventing TypeScript use-before-declaration errors in generated clients and HttpApi modules.
- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]:
- effect@4.0.0-beta.41
- @effect/platform-node@4.0.0-beta.41
## 4.0.0-beta.40
### Patch Changes
- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]:
- effect@4.0.0-beta.40
- @effect/platform-node@4.0.0-beta.40
## 4.0.0-beta.39
### Patch Changes
- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]:
- effect@4.0.0-beta.39
- @effect/platform-node@4.0.0-beta.39
## 4.0.0-beta.38
### Patch Changes
- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]:
- effect@4.0.0-beta.38
- @effect/platform-node@4.0.0-beta.38
## 4.0.0-beta.37
### Patch Changes
- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]:
- effect@4.0.0-beta.37
- @effect/platform-node@4.0.0-beta.37
## 4.0.0-beta.36
### Patch Changes
- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]:
- effect@4.0.0-beta.36
- @effect/platform-node@4.0.0-beta.36
## 4.0.0-beta.35
### Patch Changes
- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7), [`3015c2d`](https://github.com/Effect-TS/effect-smol/commit/3015c2dc25fb44694978b4ff921af9b24178fcc0)]:
- effect@4.0.0-beta.35
- @effect/platform-node@4.0.0-beta.35
## 4.0.0-beta.34
### Patch Changes
- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]:
- effect@4.0.0-beta.34
- @effect/platform-node@4.0.0-beta.34
## 4.0.0-beta.33
### Patch Changes
- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]:
- effect@4.0.0-beta.33
- @effect/platform-node@4.0.0-beta.33
## 4.0.0-beta.32
### Patch Changes
- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]:
- effect@4.0.0-beta.32
- @effect/platform-node@4.0.0-beta.32
## 4.0.0-beta.31
### Patch Changes
- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]:
- effect@4.0.0-beta.31
- @effect/platform-node@4.0.0-beta.31
## 4.0.0-beta.30
### Patch Changes
- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]:
- effect@4.0.0-beta.30
- @effect/platform-node@4.0.0-beta.30
## 4.0.0-beta.29
### Patch Changes
- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]:
- effect@4.0.0-beta.29
- @effect/platform-node@4.0.0-beta.29
## 4.0.0-beta.28
### Patch Changes
- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]:
- effect@4.0.0-beta.28
- @effect/platform-node@4.0.0-beta.28
## 4.0.0-beta.27
### Patch Changes
- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]:
- effect@4.0.0-beta.27
- @effect/platform-node@4.0.0-beta.27
## 4.0.0-beta.26
### Patch Changes
- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]:
- effect@4.0.0-beta.26
- @effect/platform-node@4.0.0-beta.26
## 4.0.0-beta.25
### Patch Changes
- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]:
- effect@4.0.0-beta.25
- @effect/platform-node@4.0.0-beta.25
## 4.0.0-beta.24
### Patch Changes
- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]:
- effect@4.0.0-beta.24
- @effect/platform-node@4.0.0-beta.24
## 4.0.0-beta.23
### Patch Changes
- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]:
- effect@4.0.0-beta.23
- @effect/platform-node@4.0.0-beta.23
## 4.0.0-beta.22
### Patch Changes
- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]:
- effect@4.0.0-beta.22
- @effect/platform-node@4.0.0-beta.22
## 4.0.0-beta.21
### Patch Changes
- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]:
- effect@4.0.0-beta.21
- @effect/platform-node@4.0.0-beta.21
## 4.0.0-beta.20
### Patch Changes
- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]:
- effect@4.0.0-beta.20
- @effect/platform-node@4.0.0-beta.20
## 4.0.0-beta.19
### Patch Changes
- Updated dependencies []:
- effect@4.0.0-beta.19
- @effect/platform-node@4.0.0-beta.19
## 4.0.0-beta.18
### Patch Changes
- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]:
- effect@4.0.0-beta.18
- @effect/platform-node@4.0.0-beta.18
## 4.0.0-beta.17
### Patch Changes
- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]:
- effect@4.0.0-beta.17
- @effect/platform-node@4.0.0-beta.17
## 4.0.0-beta.16
### Patch Changes
- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]:
- effect@4.0.0-beta.16
- @effect/platform-node@4.0.0-beta.16
## 4.0.0-beta.15
### Patch Changes
- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]:
- effect@4.0.0-beta.15
- @effect/platform-node@4.0.0-beta.15
## 4.0.0-beta.14
### Patch Changes
- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]:
- effect@4.0.0-beta.14
- @effect/platform-node@4.0.0-beta.14
## 4.0.0-beta.13
### Patch Changes
- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]:
- effect@4.0.0-beta.13
- @effect/platform-node@4.0.0-beta.13
## 4.0.0-beta.12
### Patch Changes
- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]:
- effect@4.0.0-beta.12
- @effect/platform-node@4.0.0-beta.12
## 4.0.0-beta.11
### Patch Changes
- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]:
- effect@4.0.0-beta.11
- @effect/platform-node@4.0.0-beta.11
## 4.0.0-beta.10
### Patch Changes
- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]:
- effect@4.0.0-beta.10
- @effect/platform-node@4.0.0-beta.10
## 4.0.0-beta.9
### Patch Changes
- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]:
- effect@4.0.0-beta.9
- @effect/platform-node@4.0.0-beta.9
## 4.0.0-beta.8
### Patch Changes
- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]:
- effect@4.0.0-beta.8
- @effect/platform-node@4.0.0-beta.8
## 4.0.0-beta.7
### Patch Changes
- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]:
- effect@4.0.0-beta.7
- @effect/platform-node@4.0.0-beta.7
## 4.0.0-beta.6
### Patch Changes
- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]:
- effect@4.0.0-beta.6
- @effect/platform-node@4.0.0-beta.6
## 4.0.0-beta.5
### Patch Changes
- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]:
- effect@4.0.0-beta.5
- @effect/platform-node@4.0.0-beta.5
## 4.0.0-beta.4
### Patch Changes
- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]:
- effect@4.0.0-beta.4
- @effect/platform-node@4.0.0-beta.4
## 4.0.0-beta.3
### Patch Changes
- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]:
- effect@4.0.0-beta.3
- @effect/platform-node@4.0.0-beta.3
## 4.0.0-beta.2
### Patch Changes
- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]:
- effect@4.0.0-beta.2
- @effect/platform-node@4.0.0-beta.2
## 4.0.0-beta.1
### Patch Changes
- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]:
- effect@4.0.0-beta.1
- @effect/platform-node@4.0.0-beta.1
## 4.0.0-beta.0
### Major Changes
- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta
### Patch Changes
- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]:
- @effect/platform-node@4.0.0-beta.0
- effect@4.0.0-beta.0

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Effectful Technologies Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["**/*.ts"]
}

View File

@@ -0,0 +1,77 @@
{
"name": "@effect/openapi-generator",
"type": "module",
"version": "4.0.0-beta.98",
"license": "MIT",
"description": "Generate Effect Schema types, HTTP clients, and HttpApi modules from OpenAPI specifications",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/tools/openapi-generator"
},
"bugs": {
"url": "https://github.com/Effect-TS/effect-smol/issues"
},
"tags": [
"typescript",
"openapi",
"codegen"
],
"keywords": [
"typescript",
"openapi",
"codegen"
],
"bin": {
"openapigen": "./src/bin.ts"
},
"sideEffects": [],
"exports": {
"./package.json": "./package.json",
"./*": "./src/*.ts",
"./bin": null,
"./main": null
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"access": "public",
"provenance": true,
"bin": {
"openapigen": "./dist/bin.js"
},
"exports": {
"./package.json": "./package.json",
"./*": "./dist/*.js",
"./bin": null,
"./main": null
}
},
"scripts": {
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"peerDependencies": {
"@effect/platform-node": "workspace:^",
"effect": "workspace:^"
},
"dependencies": {
"swagger2openapi": "^7.0.8"
},
"devDependencies": {
"@types/swagger2openapi": "^7.0.4",
"effect": "workspace:^",
"json-schema-typed": "^8.0.2",
"openapi-typescript": "^7.13.0",
"yaml": "^2.9.0"
}
}

View File

@@ -0,0 +1,552 @@
/**
* Renders parsed OpenAPI operations into generated Effect HttpApi source code.
*
* This module maps normalized OpenAPI metadata, tags, request bodies,
* responses, and security requirements into import declarations, HttpApi
* groups, endpoint definitions, schema annotations, and middleware classes.
* The rendered TypeScript can be emitted by the OpenAPI generator as an
* executable Effect `HttpApi` module.
*
* @since 4.0.0
*/
import type {
ParsedOpenApi,
ParsedOpenApiSecurityScheme,
ParsedOpenApiTag,
ParsedOperation,
ParsedOperationMediaTypeSchema,
ParsedOperationResponse
} from "./ParsedOperation.ts"
import * as Utils from "./Utils.ts"
interface GroupRenderModel {
readonly identifier: string
readonly topLevel: boolean
readonly metadata: ParsedOpenApiTag | undefined
readonly operations: ReadonlyArray<ParsedOperation>
readonly constName: string
}
interface SecurityRenderModel {
readonly securityDeclarations: ReadonlyArray<string>
readonly middlewareDeclarations: ReadonlyArray<string>
readonly endpointMiddlewares: ReadonlyMap<string, ReadonlyArray<string>>
}
const fallbackGroupIdentifier = "default"
/**
* Render the import declarations required by generated HttpApi source.
*
* **Details**
*
* The schema namespace import is named by the caller so generated code can
* avoid collisions with symbols already present in the output module. Multipart
* support is included only when the parsed OpenAPI document needs it.
*
* @category code generation
* @since 4.0.0
*/
export const imports = (
importName: string,
options?: {
readonly multipart?: boolean | undefined
}
): string =>
[
`import * as ${importName} from "effect/Schema"`,
...(options?.multipart === true ? [`import { Multipart } from "effect/unstable/http"`] : []),
`import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, OpenApi } from "effect/unstable/httpapi"`
].join("\n")
/**
* Convert a parsed OpenAPI document into Effect HttpApi source code.
*
* **Details**
*
* The generated implementation contains security declarations, reusable
* middleware classes, HttpApi groups, endpoint definitions, and OpenAPI
* annotations derived from the parsed operation metadata.
*
* @category code generation
* @since 4.0.0
*/
export const toImplementation = (
_importName: string,
name: string,
parsed: ParsedOpenApi
): string => {
const security = buildSecurityRenderModel(parsed)
const groups = groupOperations(parsed)
const groupSources = groups.map((group) => renderGroup(group, security.endpointMiddlewares))
const metadataAnnotations = renderApiAnnotations(parsed)
let apiValue = `export class ${name} extends HttpApi.make(${JSON.stringify(name)})`
for (const annotation of metadataAnnotations) {
apiValue += `\n .${annotation}`
}
if (groups.length > 0) {
apiValue += `\n .add(${groups.map((group) => group.constName).join(", ")})`
}
apiValue += ` {}`
return [
...security.securityDeclarations,
...security.middlewareDeclarations,
...groupSources,
apiValue
].join("\n\n")
}
const groupOperations = (parsed: ParsedOpenApi): ReadonlyArray<GroupRenderModel> => {
const tagMetadata = new Map(parsed.tags.map((tag) => [tag.name, tag]))
const byIdentifier = new Map<string, {
readonly identifier: string
topLevel: boolean
readonly metadata: ParsedOpenApiTag | undefined
readonly operations: Array<ParsedOperation>
}>()
for (const operation of parsed.operations) {
const identifier = operation.tags[0] ?? fallbackGroupIdentifier
const topLevel = operation.tags.length === 0
const existing = byIdentifier.get(identifier)
if (existing) {
if (topLevel) {
existing.topLevel = true
}
existing.operations.push(operation)
continue
}
byIdentifier.set(identifier, {
identifier,
topLevel,
metadata: tagMetadata.get(identifier),
operations: [operation]
})
}
const allocateName = makeNameAllocator()
const groups: Array<GroupRenderModel> = []
for (const group of byIdentifier.values()) {
const baseName = ensureIdentifier(group.identifier, "Group")
groups.push({
...group,
constName: allocateName(`${baseName}Group`)
})
}
return groups
}
const renderGroup = (
group: GroupRenderModel,
endpointMiddlewares: ReadonlyMap<string, ReadonlyArray<string>>
): string => {
let source = `class ${group.constName} extends HttpApiGroup.make(${JSON.stringify(group.identifier)}${
group.topLevel ? ", { topLevel: true }" : ""
})`
const allocateEndpointName = makeNameAllocator()
const endpointSources = group.operations.map((operation) =>
renderEndpoint(
operation,
allocateEndpointName(operation.id),
endpointMiddlewares.get(toOperationKey(operation)) ?? []
)
)
if (endpointSources.length > 0) {
source += `\n .add(${endpointSources.join(", \n ")})`
}
if (group.metadata?.description !== undefined) {
source += `\n .annotate(OpenApi.Description, ${JSON.stringify(group.metadata.description)})`
}
if (group.metadata?.externalDocs !== undefined) {
source += `\n .annotate(OpenApi.ExternalDocs, ${JSON.stringify(group.metadata.externalDocs)})`
}
source += ` {}`
return source
}
const renderEndpoint = (
operation: ParsedOperation,
endpointName: string,
endpointMiddlewares: ReadonlyArray<string>
): string => {
const options: Array<string> = []
if (operation.pathSchema !== undefined) {
options.push(`params: ${operation.pathSchema}`)
}
if (operation.querySchema !== undefined) {
options.push(`query: ${operation.querySchema}`)
}
if (operation.headersSchema !== undefined) {
options.push(`headers: ${operation.headersSchema}`)
}
const payload = renderPayload(operation)
if (payload !== undefined) {
options.push(`payload: ${payload}`)
}
const success = renderResponseSet(operation.responses, "success")
if (success !== undefined) {
options.push(`success: ${success}`)
}
const error = renderResponseSet(operation.responses, "error")
if (error !== undefined) {
options.push(`error: ${error}`)
}
const endpoint = options.length === 0
? `HttpApiEndpoint.${operation.method}(${JSON.stringify(endpointName)}, ${
JSON.stringify(toHttpApiPath(operation.path))
})`
: `HttpApiEndpoint.${operation.method}(${JSON.stringify(endpointName)}, ${
JSON.stringify(toHttpApiPath(operation.path))
}, { ${options.join(", ")} })`
const annotations: Array<string> = []
if (operation.operationId !== undefined) {
annotations.push(`annotate(OpenApi.Identifier, ${JSON.stringify(operation.operationId)})`)
}
if (operation.metadata.summary !== undefined) {
annotations.push(`annotate(OpenApi.Summary, ${JSON.stringify(operation.metadata.summary)})`)
}
if (operation.metadata.description !== undefined) {
annotations.push(`annotate(OpenApi.Description, ${JSON.stringify(operation.metadata.description)})`)
}
if (operation.metadata.deprecated) {
annotations.push(`annotate(OpenApi.Deprecated, true)`)
}
if (operation.metadata.externalDocs !== undefined) {
annotations.push(`annotate(OpenApi.ExternalDocs, ${JSON.stringify(operation.metadata.externalDocs)})`)
}
if (annotations.length === 0 && endpointMiddlewares.length === 0) {
return endpoint
}
let out = endpoint
for (const middleware of endpointMiddlewares) {
out += `\n .middleware(${middleware})`
}
for (const annotation of annotations) {
out += `\n .${annotation}`
}
return out
}
const renderPayload = (operation: ParsedOperation): string | undefined => {
if (!methodSupportsBody(operation.method)) {
return
}
const payloads = operation.requestBodyRepresentable.map((schema) => renderMediaSchema(schema))
if (payloads.length === 0) {
return
}
if (operation.requestBody?.required === false) {
payloads.unshift("HttpApiSchema.NoContent")
}
return joinSchemas(payloads)
}
const renderResponseSet = (
responses: ReadonlyArray<ParsedOperationResponse>,
target: "success" | "error"
): string | undefined => {
const rendered: Array<string> = []
for (const response of responses) {
const status = toStatus(response.status)
if (status === undefined) {
continue
}
const isSuccess = status < 400
if ((target === "success") !== isSuccess) {
continue
}
if (response.isEmpty) {
rendered.push(`HttpApiSchema.Empty(${status})`)
continue
}
for (const media of response.representable) {
rendered.push(applyStatus(renderMediaSchema(media), status, target))
}
}
if (rendered.length === 0) {
return
}
return joinSchemas(rendered)
}
const joinSchemas = (schemas: ReadonlyArray<string>): string =>
schemas.length === 1 ? schemas[0] : `[${schemas.join(", ")}]`
const renderMediaSchema = (media: ParsedOperationMediaTypeSchema): string => {
if (media.effectStream === "sse") {
const options = media.contentType === "text/event-stream"
? `{ events: ${media.schema}, error: ${media.errorSchema} }`
: `{ contentType: ${JSON.stringify(media.contentType)}, events: ${media.schema}, error: ${media.errorSchema} }`
return `HttpApiSchema.StreamSse(${options})`
}
if (media.effectStream === "uint8array") {
if (media.contentType === "application/octet-stream") {
return "HttpApiSchema.StreamUint8Array()"
}
return `HttpApiSchema.StreamUint8Array({ contentType: ${JSON.stringify(media.contentType)} })`
}
switch (media.encoding) {
case "json": {
if (media.contentType === "application/json") {
return media.schema
}
return `${media.schema}.pipe(HttpApiSchema.asJson({ contentType: ${JSON.stringify(media.contentType)} }))`
}
case "multipart": {
return `${media.schema}.pipe(HttpApiSchema.asMultipart())`
}
case "form-url-encoded": {
if (media.contentType === "application/x-www-form-urlencoded") {
return `${media.schema}.pipe(HttpApiSchema.asFormUrlEncoded())`
}
return `${media.schema}.pipe(HttpApiSchema.asFormUrlEncoded({ contentType: ${
JSON.stringify(media.contentType)
} }))`
}
case "text": {
if (media.contentType === "text/plain") {
return `${media.schema}.pipe(HttpApiSchema.asText())`
}
return `${media.schema}.pipe(HttpApiSchema.asText({ contentType: ${JSON.stringify(media.contentType)} }))`
}
case "binary": {
if (media.contentType === "application/octet-stream") {
return `${media.schema}.pipe(HttpApiSchema.asUint8Array())`
}
return `${media.schema}.pipe(HttpApiSchema.asUint8Array({ contentType: ${JSON.stringify(media.contentType)} }))`
}
}
}
const renderApiAnnotations = (parsed: ParsedOpenApi): ReadonlyArray<string> => {
const annotations: Array<string> = [
`annotate(OpenApi.Title, ${JSON.stringify(parsed.metadata.title)})`,
`annotate(OpenApi.Version, ${JSON.stringify(parsed.metadata.version)})`
]
if (parsed.metadata.summary !== undefined) {
annotations.push(`annotate(OpenApi.Summary, ${JSON.stringify(parsed.metadata.summary)})`)
}
if (parsed.metadata.description !== undefined) {
annotations.push(`annotate(OpenApi.Description, ${JSON.stringify(parsed.metadata.description)})`)
}
if (parsed.metadata.license !== undefined) {
annotations.push(`annotate(OpenApi.License, ${JSON.stringify(parsed.metadata.license)})`)
}
if (parsed.metadata.servers !== undefined) {
annotations.push(`annotate(OpenApi.Servers, ${JSON.stringify(parsed.metadata.servers)})`)
}
return annotations
}
const buildSecurityRenderModel = (parsed: ParsedOpenApi): SecurityRenderModel => {
const allocateName = makeNameAllocator()
const securityDeclarations: Array<string> = []
const middlewareDeclarations: Array<string> = []
const endpointMiddlewares = new Map<string, ReadonlyArray<string>>()
const schemeDeclarations = new Map<string, string>()
const middlewareNames = new Map<string, string>()
for (const securityScheme of parsed.securitySchemes) {
const baseName = ensureIdentifier(securityScheme.name, "Security")
const declarationName = allocateName(`${baseName}Security`)
schemeDeclarations.set(securityScheme.name, declarationName)
securityDeclarations.push(`export const ${declarationName} = ${renderSecurityScheme(securityScheme)}`)
}
for (const operation of parsed.operations) {
if (operation.effectiveSecurity.length === 0) {
continue
}
if (operation.effectiveSecurity.some((requirement) => Object.keys(requirement).length === 0)) {
continue
}
const operationMiddlewareNames: Array<string> = []
const seenOrSchemes = new Set<string>()
const andRequirements: Array<ReadonlyArray<string>> = []
for (const requirement of operation.effectiveSecurity) {
const schemes = Object.keys(requirement)
if (schemes.length === 1) {
const schemeName = schemes[0]
if (schemeDeclarations.has(schemeName) && !seenOrSchemes.has(schemeName)) {
seenOrSchemes.add(schemeName)
}
} else if (schemes.length > 1) {
andRequirements.push([...schemes].sort())
}
}
const orSchemeNames = Array.from(seenOrSchemes).sort()
if (orSchemeNames.length > 0) {
const className = getOrSecurityMiddlewareName(orSchemeNames)
operationMiddlewareNames.push(className)
}
const seenAndRequirements = new Set<string>()
for (const requirement of andRequirements) {
const key = requirement.join("\u0000")
if (seenAndRequirements.has(key)) {
continue
}
seenAndRequirements.add(key)
const className = getAndSecurityMiddlewareName(requirement)
operationMiddlewareNames.push(className)
}
if (operationMiddlewareNames.length > 0) {
endpointMiddlewares.set(toOperationKey(operation), operationMiddlewareNames)
}
}
return {
securityDeclarations,
middlewareDeclarations,
endpointMiddlewares
}
function getOrSecurityMiddlewareName(schemes: ReadonlyArray<string>): string {
const key = `or:${schemes.join("\u0000")}`
const existing = middlewareNames.get(key)
if (existing !== undefined) {
return existing
}
const className = allocateName(`${getSecurityMiddlewareBaseName(schemes, "Or")}SecurityMiddleware`)
const securityEntries = schemes.map((name) => `${JSON.stringify(name)}: ${schemeDeclarations.get(name)!}`).join(
", "
)
middlewareDeclarations.push(
`export class ${className} extends HttpApiMiddleware.Service<${className}>()(${
JSON.stringify(`${schemes.join(" | ")} security`)
}, { security: { ${securityEntries} } }) {}`
)
middlewareNames.set(key, className)
return className
}
function getAndSecurityMiddlewareName(schemes: ReadonlyArray<string>): string {
const key = `and:${schemes.join("\u0000")}`
const existing = middlewareNames.get(key)
if (existing !== undefined) {
return existing
}
const className = allocateName(`${getSecurityMiddlewareBaseName(schemes, "And")}SecurityMiddleware`)
middlewareDeclarations.push(
`class ${className} extends HttpApiMiddleware.Service<${className}>()(${
JSON.stringify(`${schemes.join(" & ")} security`)
}) {}`
)
middlewareNames.set(key, className)
return className
}
}
const getSecurityMiddlewareBaseName = (
schemes: ReadonlyArray<string>,
joiner: "And" | "Or"
): string => {
const [head, ...tail] = schemes.map((scheme) => ensureIdentifier(scheme, "Security"))
return tail.length === 0 ? head : [head, ...tail.map((scheme) => `${joiner}${scheme}`)].join("")
}
const renderSecurityScheme = (securityScheme: ParsedOpenApiSecurityScheme): string => {
let source: string
switch (securityScheme.type) {
case "basic": {
source = "HttpApiSecurity.basic"
break
}
case "bearer": {
source = "HttpApiSecurity.bearer"
break
}
case "http": {
source = `HttpApiSecurity.http({ scheme: ${JSON.stringify(securityScheme.scheme!)} })`
break
}
case "apiKey": {
source = `HttpApiSecurity.apiKey({ key: ${JSON.stringify(securityScheme.key!)}, in: ${
JSON.stringify(securityScheme.in!)
} })`
break
}
}
if (securityScheme.description !== undefined) {
source += `.pipe(HttpApiSecurity.annotate(OpenApi.Description, ${JSON.stringify(securityScheme.description)}))`
}
if (
(securityScheme.type === "bearer" || securityScheme.type === "http") && securityScheme.bearerFormat !== undefined
) {
source += `.pipe(HttpApiSecurity.annotate(OpenApi.Format, ${JSON.stringify(securityScheme.bearerFormat)}))`
}
return source
}
const toOperationKey = (operation: ParsedOperation): string => `${operation.method}:${operation.path}`
const toHttpApiPath = (path: string): string => path.replace(/{([^}]+)}/g, ":$1")
const toStatus = (status: string): number | undefined => {
if (!/^\d{3}$/.test(status)) {
return
}
return Number(status)
}
const applyStatus = (schema: string, status: number, target: "success" | "error"): string => {
if ((target === "success" && status === 200) || (target === "error" && status === 500)) {
return schema
}
return `${schema}.pipe(HttpApiSchema.status(${status}))`
}
const methodSupportsBody = (method: ParsedOperation["method"]): boolean =>
method !== "get" && method !== "head" && method !== "options" && method !== "trace"
const ensureIdentifier = (value: string, fallback: string): string => {
const sanitized = Utils.identifier(value)
return sanitized.length > 0 ? sanitized : fallback
}
const makeNameAllocator = () => {
const used = new Set<string>()
return (base: string) => {
let candidate = base
let index = 2
while (used.has(candidate)) {
candidate = `${base}${index}`
index += 1
}
used.add(candidate)
return candidate
}
}

View File

@@ -0,0 +1,319 @@
/**
* Generate TypeScript source for JSON Schema declarations extracted from
* OpenAPI documents.
*
* This module is the schema-rendering stage of the OpenAPI generator. Callers
* register named OpenAPI 3.0 or 3.1 schemas, provide the reusable component
* definitions for the document, and receive source text containing exported
* TypeScript aliases plus Effect Schema runtime values. The generator first
* normalizes OpenAPI-specific schema shapes into Effect's JSON Schema model,
* then delegates recursive analysis and runtime expression construction to
* `SchemaRepresentation`.
*
* The renderer keeps the emitted module usable for both human maintainers and
* automated code-generation consumers by grouping recursive declarations,
* reusable references, and locally registered schemas. It also contains the
* HttpApi-specific multipart file substitutions that map generated schemas to
* Effect's multipart runtime types.
*
* @since 4.0.0
*/
import * as Arr from "effect/Array"
import * as JsonSchema from "effect/JsonSchema"
import * as Rec from "effect/Record"
import * as SchemaRepresentation from "effect/SchemaRepresentation"
type Source = "openapi-3.0" | "openapi-3.1"
interface GenerateOptions {
readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined
}
interface GenerateHttpApiOptions extends GenerateOptions {
readonly multipartSchemaRefs?: {
readonly singleFile: string
readonly files: string
} | undefined
}
/**
* Create a stateful JSON Schema code generator for OpenAPI-derived schemas.
*
* **Details**
*
* Schemas registered with the returned generator are converted into TypeScript
* type aliases and Effect Schema runtime declarations, with reusable OpenAPI
* component definitions supplied at generation time.
*
* @category code generation
* @since 4.0.0
*/
export function make() {
const store: Record<string, JsonSchema.JsonSchema> = {}
function addSchema(name: string, schema: JsonSchema.JsonSchema): string {
if (name in store) {
throw new Error(`Schema ${name} already exists`)
}
store[name] = schema
return name
}
function generate(
source: Source,
components: JsonSchema.Definitions,
typeOnly: boolean,
options?: GenerateOptions
) {
const generated = makeCodeDocument(source, components, options)
if (generated === undefined) {
return ""
}
const nonRecursiveReferences = generated.codeDocument.references.nonRecursives
const recursiveReferences = Object.entries(generated.codeDocument.references.recursives)
const nonRecursives = nonRecursiveReferences.map(({ $ref, code }) =>
renderSchemaTypeAndRuntime($ref, code, typeOnly)
)
const recursiveDeclarations: Array<string> = []
const recursives: Array<string> = []
if (typeOnly) {
for (const [$ref, code] of recursiveReferences) {
recursives.push(renderSchemaTypeAndRuntime($ref, code, true))
}
} else {
const recursivelyForwardReferenced = collectForwardReferencedRecursives(
nonRecursiveReferences,
recursiveReferences
)
const recursiveInternalNames = makeRecursiveInternalNameMap(
recursivelyForwardReferenced,
[
...nonRecursiveReferences.map(({ $ref }) => $ref),
...recursiveReferences.map(([$ref]) => $ref),
...generated.nameMap
]
)
for (const [$ref, code] of recursiveReferences) {
if (recursivelyForwardReferenced.has($ref)) {
const internalName = recursiveInternalNames.get($ref)!
recursiveDeclarations.push(renderRecursiveReferenceDeclaration($ref, code, internalName))
recursives.push(`const ${internalName} = ${code.runtime}`)
continue
}
recursives.push(renderSchemaTypeAndRuntime($ref, code, false))
}
}
const codes = generated.codeDocument.codes.map((code, i) =>
renderSchemaTypeAndRuntime(generated.nameMap[i], code, typeOnly)
)
return render("recursive declarations", recursiveDeclarations) +
render("non-recursive definitions", nonRecursives) +
render("recursive definitions", recursives) +
render("schemas", codes)
}
function generateHttpApi(
source: Source,
components: JsonSchema.Definitions,
options?: GenerateHttpApiOptions
) {
const generated = makeCodeDocument(source, components, options)
if (generated === undefined) {
return ""
}
const nonRecursiveReferences = generated.codeDocument.references.nonRecursives
const recursiveReferences = Object.entries(generated.codeDocument.references.recursives)
const nonRecursives = nonRecursiveReferences.map(({ $ref, code }) =>
renderSchemaTypeAndRuntime($ref, code, false, options?.multipartSchemaRefs)
)
const recursivelyForwardReferenced = collectForwardReferencedRecursives(nonRecursiveReferences, recursiveReferences)
const recursiveInternalNames = makeRecursiveInternalNameMap(
recursivelyForwardReferenced,
[
...nonRecursiveReferences.map(({ $ref }) => $ref),
...recursiveReferences.map(([$ref]) => $ref),
...generated.nameMap
]
)
const recursiveDeclarations: Array<string> = []
const recursives: Array<string> = []
for (const [$ref, code] of recursiveReferences) {
if (recursivelyForwardReferenced.has($ref)) {
const internalName = recursiveInternalNames.get($ref)!
recursiveDeclarations.push(renderRecursiveReferenceDeclaration($ref, code, internalName))
recursives.push(`const ${internalName} = ${code.runtime}`)
continue
}
recursives.push(renderSchemaTypeAndRuntime($ref, code, false, options?.multipartSchemaRefs))
}
const codes = generated.codeDocument.codes.map((code, i) =>
renderSchemaTypeAndRuntime(generated.nameMap[i], code, false, options?.multipartSchemaRefs)
)
return render("recursive declarations", recursiveDeclarations) +
render("non-recursive definitions", nonRecursives) +
render("recursive definitions", recursives) +
render("schemas", codes)
}
function makeCodeDocument(
source: Source,
components: JsonSchema.Definitions,
options?: GenerateOptions
): {
readonly nameMap: Array<string>
readonly codeDocument: SchemaRepresentation.CodeDocument
} | undefined {
const nameMap: Array<string> = []
const schemas: Array<JsonSchema.JsonSchema> = []
const definitions: JsonSchema.Definitions = Rec.map(
components,
(js) => fromSchemaOpenApi(source, js).schema
)
for (const [name, js] of Object.entries(store)) {
nameMap.push(name)
schemas.push(fromSchemaOpenApi(source, js).schema)
}
if (!Arr.isArrayNonEmpty(schemas)) {
return
}
const multiDocument: SchemaRepresentation.MultiDocument = SchemaRepresentation.fromJsonSchemaMultiDocument({
dialect: "draft-2020-12",
schemas,
definitions
}, {
onEnter(js) {
const out = { ...js }
if (out.type === "object" && out.additionalProperties === undefined) {
out.additionalProperties = false
}
return options?.onEnter?.(out) ?? out
}
})
return {
nameMap,
codeDocument: SchemaRepresentation.toCodeDocument(multiDocument)
}
}
return { addSchema, generate, generateHttpApi } as const
}
function fromSchemaOpenApi(source: Source, jsonSchema: JsonSchema.JsonSchema) {
switch (source) {
case "openapi-3.1":
return JsonSchema.fromSchemaOpenApi3_1(jsonSchema)
case "openapi-3.0":
return JsonSchema.fromSchemaOpenApi3_0(jsonSchema)
}
}
function renderSchemaTypeAndRuntime(
$ref: string,
code: SchemaRepresentation.Code,
typeOnly: boolean,
multipartSchemaRefs?: {
readonly singleFile: string
readonly files: string
}
) {
if (!typeOnly && multipartSchemaRefs !== undefined) {
if ($ref === multipartSchemaRefs.singleFile) {
return [
`export type ${$ref} = Multipart.PersistedFile`,
`export const ${$ref} = Multipart.SingleFileSchema`
].join("\n")
}
if ($ref === multipartSchemaRefs.files) {
return [
`export type ${$ref} = ReadonlyArray<Multipart.PersistedFile>`,
`export const ${$ref} = Multipart.FilesSchema`
].join("\n")
}
}
const strings = [`export type ${$ref} = ${code.Type}`]
if (!typeOnly) {
strings.push(`export const ${$ref} = ${code.runtime}`)
}
return strings.join("\n")
}
function renderRecursiveReferenceDeclaration(
$ref: string,
code: SchemaRepresentation.Code,
internalName: string
): string {
return [
`export type ${$ref} = ${code.Type}`,
`export const ${$ref} = Schema.suspend((): Schema.Codec<${$ref}> => ${internalName})`
].join("\n")
}
function render(title: string, as: ReadonlyArray<string>) {
if (as.length === 0) return ""
return "// " + title + "\n" + as.join("\n") + "\n"
}
const tokenPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g
function collectForwardReferencedRecursives(
nonRecursives: ReadonlyArray<{
readonly $ref: string
readonly code: SchemaRepresentation.Code
}>,
recursives: ReadonlyArray<readonly [string, SchemaRepresentation.Code]>
): Set<string> {
const recursiveNames = new Set(recursives.map(([name]) => name))
const referenced = new Set<string>()
for (const { code } of nonRecursives) {
for (const token of code.runtime.matchAll(tokenPattern)) {
const identifier = token[0]
if (recursiveNames.has(identifier)) {
referenced.add(identifier)
}
}
}
return referenced
}
function makeRecursiveInternalNameMap(
recursiveNames: ReadonlySet<string>,
existingNames: ReadonlyArray<string>
): Map<string, string> {
const usedNames = new Set(existingNames)
const internalNames = new Map<string, string>()
for (const name of recursiveNames) {
let candidate = `__recursive_${name}`
while (usedNames.has(candidate)) {
candidate = `_${candidate}`
}
usedNames.add(candidate)
internalNames.set(name, candidate)
}
return internalNames
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,539 @@
/**
* OpenAPI spec patching utilities.
*
* Handles parsing and applying JSON Patch documents (RFC 6902) to OpenAPI
* specs. Supports patches from:
* - JSON files (.json)
* - YAML files (.yaml, .yml)
* - Inline JSON strings
*
* @since 4.0.0
*/
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import { constFalse, constUndefined } from "effect/Function"
import * as JsonPatch from "effect/JsonPatch"
import * as Path from "effect/Path"
import * as Predicate from "effect/Predicate"
import * as Schema from "effect/Schema"
import * as Yaml from "yaml"
// =============================================================================
// Error Types
// =============================================================================
/**
* Error thrown when parsing a JSON Patch input fails.
*
* **Details**
*
* This error occurs when:
* - A patch file cannot be read
* - JSON or YAML syntax is invalid
* - The file format is unsupported
*
* **Example** (Creating a parse error)
*
* ```ts
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* const error = new OpenApiPatch.JsonPatchParseError({
* source: "./patches/fix.json",
* reason: "Unexpected token at position 42"
* })
*
* console.log(error.message)
* // "Failed to parse patch from ./patches/fix.json: Unexpected token at position 42"
* ```
*
* @category errors
* @since 4.0.0
*/
export class JsonPatchParseError extends Schema.ErrorClass<JsonPatchParseError>("JsonPatchParseError")({
_tag: Schema.tag("JsonPatchParseError"),
source: Schema.String,
reason: Schema.String
}) {
override get message() {
return `Failed to parse patch from ${this.source}: ${this.reason}`
}
}
/**
* Error thrown when a parsed value does not conform to the JSON Patch schema.
*
* **Details**
*
* This error occurs when:
* - The patch is not an array
* - An operation is missing required fields (op, path)
* - An operation has an unsupported op value
* - An add/replace operation is missing the value field
*
* **Example** (Creating a validation error)
*
* ```ts
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* const error = new OpenApiPatch.JsonPatchValidationError({
* source: "inline",
* reason: "Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'"
* })
*
* console.log(error.message)
* // "Invalid JSON Patch from inline: Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'"
* ```
*
* @category errors
* @since 4.0.0
*/
export class JsonPatchValidationError extends Schema.ErrorClass<JsonPatchValidationError>("JsonPatchValidationError")({
_tag: Schema.tag("JsonPatchValidationError"),
source: Schema.String,
reason: Schema.String
}) {
override get message() {
return `Invalid JSON Patch from ${this.source}: ${this.reason}`
}
}
/**
* Error thrown when applying a JSON Patch operation fails.
*
* **Details**
*
* This error occurs when:
* - A path does not exist for remove/replace operations
* - An array index is out of bounds
* - The target location is not a valid container
*
* **Example** (Creating an application error)
*
* ```ts
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* const error = new OpenApiPatch.JsonPatchApplicationError({
* source: "./patches/fix.json",
* operationIndex: 2,
* operation: "remove",
* path: "/paths/~1users",
* reason: "Property \"users\" does not exist"
* })
*
* console.log(error.message)
* // "Failed to apply patch from ./patches/fix.json: operation 2 (remove at /paths/~1users): Property \"users\" does not exist"
* ```
*
* @category errors
* @since 4.0.0
*/
export class JsonPatchApplicationError
extends Schema.ErrorClass<JsonPatchApplicationError>("JsonPatchApplicationError")({
_tag: Schema.tag("JsonPatchApplicationError"),
source: Schema.String,
operationIndex: Schema.Number,
operation: Schema.String,
path: Schema.String,
reason: Schema.String
})
{
override get message() {
return `Failed to apply patch from ${this.source}: operation ${this.operationIndex} ` +
`(${this.operation} at ${this.path}): ${this.reason}`
}
}
/**
* Error thrown when multiple JSON Patch operations fail.
*
* **Details**
*
* This error aggregates all application errors so users can see every
* failing operation at once instead of fixing them one at a time.
*
* **Example** (Creating an aggregate error)
*
* ```ts
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* const error = new OpenApiPatch.JsonPatchAggregateError({
* errors: [
* new OpenApiPatch.JsonPatchApplicationError({
* source: "./fix.json",
* operationIndex: 0,
* operation: "replace",
* path: "/info/x",
* reason: "Property does not exist"
* }),
* new OpenApiPatch.JsonPatchApplicationError({
* source: "./fix.json",
* operationIndex: 2,
* operation: "remove",
* path: "/paths/~1users",
* reason: "Property does not exist"
* })
* ]
* })
*
* console.log(error.message)
* // "2 patch operations failed:\n 1. ..."
* ```
*
* @category errors
* @since 4.0.0
*/
export class JsonPatchAggregateError extends Schema.ErrorClass<JsonPatchAggregateError>("JsonPatchAggregateError")({
_tag: Schema.tag("JsonPatchAggregateError"),
errors: Schema.Array(Schema.Unknown)
}) {
override get message() {
const errors = this.errors as ReadonlyArray<JsonPatchApplicationError>
const count = errors.length
const plural = count === 1 ? "operation" : "operations"
const details = errors
.map((e, i) => ` ${i + 1}. [${e.source}] op ${e.operationIndex} (${e.operation} at ${e.path}): ${e.reason}`)
.join("\n")
return `${count} patch ${plural} failed:\n${details}`
}
}
// =============================================================================
// Schema
// =============================================================================
/**
* Schema for a JSON Patch "add" operation.
*
* @category schemas
* @since 4.0.0
*/
export const JsonPatchAdd: Schema.Codec<
Extract<
JsonPatch.JsonPatchOperation,
{ op: "add" }
>
> = Schema.Struct({
op: Schema.Literal("add"),
path: Schema.String,
value: Schema.Json,
description: Schema.optionalKey(Schema.String)
})
/**
* Schema for a JSON Patch "remove" operation.
*
* @category schemas
* @since 4.0.0
*/
export const JsonPatchRemove: Schema.Codec<
Extract<
JsonPatch.JsonPatchOperation,
{ op: "remove" }
>
> = Schema.Struct({
op: Schema.Literal("remove"),
path: Schema.String,
description: Schema.optionalKey(Schema.String)
})
/**
* Schema for a JSON Patch "replace" operation.
*
* @category schemas
* @since 4.0.0
*/
export const JsonPatchReplace: Schema.Codec<
Extract<
JsonPatch.JsonPatchOperation,
{ op: "replace" }
>
> = Schema.Struct({
op: Schema.Literal("replace"),
path: Schema.String,
value: Schema.Json,
description: Schema.optionalKey(Schema.String)
})
/**
* Schema for a single JSON Patch operation.
*
* **Details**
*
* Supports the subset of RFC 6902 operations that Effect's JsonPatch module
* implements: `add`, `remove`, and `replace`.
*
* @category schemas
* @since 4.0.0
*/
export const JsonPatchOperation: Schema.Codec<JsonPatch.JsonPatchOperation> = Schema.Union([
JsonPatchAdd,
JsonPatchRemove,
JsonPatchReplace
])
/**
* Schema for a JSON Patch document (array of operations).
*
* **Details**
*
* A JSON Patch document is an ordered list of operations to apply to a JSON
* document. Operations are applied in sequence, with each operation seeing
* the result of previous operations.
*
* **Example** (Decoding a patch document)
*
* ```ts
* import { Schema } from "effect"
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* const patch = Schema.decodeUnknownSync(OpenApiPatch.JsonPatchDocument)([
* { op: "add", path: "/foo", value: "bar" },
* { op: "remove", path: "/baz" },
* { op: "replace", path: "/qux", value: 42 }
* ])
* ```
*
* @category schemas
* @since 4.0.0
*/
export const JsonPatchDocument = Schema.Array(JsonPatchOperation)
/**
* Type for a JSON Patch document.
*
* @category types
* @since 4.0.0
*/
export type JsonPatchDocument = typeof JsonPatchDocument.Type
// =============================================================================
// Parsing Functions
// =============================================================================
const decodeJsonPatchDocument = Schema.decodeUnknownEffect(JsonPatchDocument)
/**
* Check if a string looks like it could be a file path.
*
* Heuristic: contains path separators or ends with a known extension.
*/
const looksLikeFilePath = (input: string): boolean => {
const trimmed = input.trim()
if (trimmed.startsWith("[")) return false
if (trimmed.includes("/") || trimmed.includes("\\")) return true
if (/\.(json|yaml|yml)$/i.test(trimmed)) return true
return false
}
/**
* Determine file format from extension.
*/
const getFileFormat = Effect.fn(function*(filePath: string) {
const path = yield* Path.Path
const { ext } = path.parse(filePath)
if (ext === ".json") return "json"
if (ext === ".yaml" || ext === ".yml") return "yaml"
return undefined
})
/**
* Check if a file path exists and is a file.
*/
const checkFileExists = Effect.fn("checkFileExists")(function*(filePath: string) {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath)
const exists = yield* Effect.orElseSucceed(fs.exists(absolutePath), constFalse)
if (!exists) return false
const stat = yield* Effect.orElseSucceed(fs.stat(absolutePath), constUndefined)
return Predicate.isNotUndefined(stat) && stat.type === "File"
})
/**
* Parse content as JSON.
*/
const parseJsonContent = Effect.fnUntraced(function*(content: string, source: string) {
return yield* Effect.try({
try: () => JSON.parse(content) as unknown,
catch: (error) =>
new JsonPatchParseError({
source,
reason: error instanceof Error ? error.message : String(error)
})
})
})
/**
* Parse content as YAML.
*/
const parseYamlContent = Effect.fnUntraced(function*(content: string, source: string) {
return yield* Effect.try({
try: () => Yaml.parse(content) as unknown,
catch: (error) =>
new JsonPatchParseError({
source,
reason: error instanceof Error ? error.message : String(error)
})
})
})
/**
* Read and parse a patch file.
*/
const parsePatchFile = Effect.fn("parsePatchFile")(function*(filePath: string) {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath)
const fileFormat = yield* getFileFormat(filePath)
if (Predicate.isUndefined(fileFormat)) {
return yield* new JsonPatchParseError({
source: filePath,
reason: `Unsupported file format. Expected .json, .yaml, or .yml`
})
}
const content = yield* Effect.mapError(fs.readFileString(absolutePath), (error) =>
new JsonPatchParseError({
source: filePath,
reason: `Failed to read file: ${error.message}`
}))
const parsed = fileFormat === "json"
? yield* parseJsonContent(content, filePath)
: yield* parseYamlContent(content, filePath)
return yield* Effect.mapError(decodeJsonPatchDocument(parsed), (error) =>
new JsonPatchValidationError({
source: filePath,
reason: error.message
}))
})
/**
* Parse inline JSON string as a patch document.
*/
const parseInlinePatch = Effect.fn("parseInlinePatch")(function*(input: string) {
const parsed = yield* parseJsonContent(input, "inline")
return yield* Effect.mapError(decodeJsonPatchDocument(parsed), (error) =>
new JsonPatchValidationError({
source: "inline",
reason: error.message
}))
})
/**
* Parse a JSON Patch from either a file path or inline JSON string.
*
* **Details**
*
* The input is first checked as a file path. If the file exists, it is read
* and parsed based on its extension (.json, .yaml, .yml). Otherwise, the
* input is parsed as inline JSON.
*
* **Example** (Parsing patch input)
*
* ```ts
* import { Effect } from "effect"
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* // From inline JSON
* const fromInline = OpenApiPatch.parsePatchInput(
* '[{"op":"replace","path":"/info/title","value":"My API"}]'
* )
*
* // From file path
* const fromFile = OpenApiPatch.parsePatchInput("./patches/fix-api.json")
*
* const program = Effect.gen(function*() {
* const patch = yield* fromInline
* console.log(patch)
* // [{ op: "replace", path: "/info/title", value: "My API" }]
* })
* ```
*
* @category parsing
* @since 4.0.0
*/
export const parsePatchInput = Effect.fn("parsePatchInput")(function*(input: string) {
if (looksLikeFilePath(input)) {
const exists = yield* checkFileExists(input)
if (exists) {
return yield* parsePatchFile(input)
}
}
return yield* parseInlinePatch(input)
})
// =============================================================================
// Application Functions
// =============================================================================
/**
* Apply a sequence of JSON patches to a document.
*
* **Details**
*
* Patches are applied in order, with each patch operating on the result of
* the previous one. All operations are attempted, and if any fail, the errors
* are accumulated and reported together so users can fix all issues at once.
*
* **Example** (Applying patches)
*
* ```ts
* import { Effect } from "effect"
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
*
* const document = { info: { title: "Old Title" }, paths: {} }
* const patches = [
* {
* source: "inline",
* patch: [{ op: "replace" as const, path: "/info/title", value: "New Title" }]
* }
* ]
*
* const program = Effect.gen(function*() {
* const result = yield* OpenApiPatch.applyPatches(patches, document)
* console.log(result)
* // { info: { title: "New Title" }, paths: {} }
* })
* ```
*
* @category application
* @since 4.0.0
*/
export const applyPatches = Effect.fn("applyPatches")(function*(
patches: ReadonlyArray<{ readonly source: string; readonly patch: JsonPatchDocument }>,
document: Schema.Json
) {
let result: Schema.Json = document
const errors: Array<JsonPatchApplicationError> = []
for (const { source, patch } of patches) {
for (let i = 0; i < patch.length; i++) {
const op = patch[i]
yield* Effect.ignore(Effect.try({
try: () => {
result = JsonPatch.apply([op], result)
},
catch: (error) =>
errors.push(
new JsonPatchApplicationError({
source,
operationIndex: i,
operation: op.op,
path: op.path,
reason: error instanceof Error ? error.message : String(error)
})
)
}))
}
}
if (errors.length > 0) {
return yield* new JsonPatchAggregateError({ errors })
}
return result
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,277 @@
/**
* Normalized OpenAPI operation model shared by the generator pipeline.
*
* This module records the shape produced after an OpenAPI document is resolved
* into stable generator inputs: document metadata, tags, security schemes,
* per-operation parameters, request bodies, response media types, derived
* schema references, path templates, and streaming capabilities. Renderers
* consume this representation to emit HttpClient or HttpApi modules without
* reinterpreting raw OpenAPI path-item structures.
*
* @since 4.0.0
*/
import type * as Types from "effect/Types"
import type {
OpenAPISecurityRequirement,
OpenAPISpecExternalDocs,
OpenAPISpecLicense,
OpenAPISpecMethodName,
OpenAPISpecServer
} from "effect/unstable/httpapi/OpenApi"
/**
* Root OpenAPI metadata preserved for generated client and HttpApi output.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOpenApiMetadata {
readonly title: string
readonly version: string
readonly summary: string | undefined
readonly description: string | undefined
readonly license: OpenAPISpecLicense | undefined
readonly servers: ReadonlyArray<OpenAPISpecServer> | undefined
}
/**
* Tag metadata used to group and annotate generated operations.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOpenApiTag {
readonly name: string
readonly description: string | undefined
readonly externalDocs: OpenAPISpecExternalDocs | undefined
}
/**
* Supported security scheme extracted from an OpenAPI components section.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOpenApiSecurityScheme {
readonly name: string
readonly type: "basic" | "bearer" | "apiKey" | "http"
readonly description: string | undefined
readonly bearerFormat: string | undefined
readonly scheme: string | undefined
readonly key: string | undefined
readonly in: "header" | "query" | "cookie" | undefined
}
/**
* Normalized OpenAPI document consumed by the generator renderers.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOpenApi {
readonly metadata: ParsedOpenApiMetadata
readonly tags: ReadonlyArray<ParsedOpenApiTag>
readonly securitySchemes: ReadonlyArray<ParsedOpenApiSecurityScheme>
readonly operations: ReadonlyArray<ParsedOperation>
}
/**
* Documentation and lifecycle metadata associated with an operation.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOperationMetadata {
readonly summary: string | undefined
readonly description: string | undefined
readonly deprecated: boolean
readonly externalDocs: OpenAPISpecExternalDocs | undefined
}
/**
* Resolved OpenAPI parameter grouped by where it appears in the request.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOperationParameter {
readonly name: string
readonly in: "path" | "query" | "header" | "cookie"
readonly required: boolean
readonly description: string | undefined
readonly schema: {}
}
/**
* Summary of the request body declaration before per-media schemas are rendered.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOperationRequestBody {
readonly required: boolean
readonly contentTypes: Array<string>
}
/**
* Encoding strategy the generator can use for a request or response media type.
*
* @category models
* @since 4.0.0
*/
export type ParsedOperationMediaTypeEncoding =
| "json"
| "multipart"
| "form-url-encoded"
| "text"
| "binary"
/**
* Media type whose schema can be represented in generated Effect code.
*
* @category models
* @since 4.0.0
*/
export type ParsedOperationMediaTypeSchema =
| {
readonly contentType: string
readonly encoding: ParsedOperationMediaTypeEncoding
readonly schema: string
readonly effectStream?: undefined
}
| {
readonly contentType: string
readonly encoding: "text"
readonly schema: string
readonly effectStream: "sse"
readonly errorSchema: string
}
| {
readonly contentType: string
readonly encoding: "binary"
readonly schema?: undefined
readonly effectStream: "uint8array"
}
/**
* Parsed response metadata together with generated schema references.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOperationResponse {
readonly status: string
readonly description: string | undefined
readonly contentTypes: Array<string>
readonly hasHeaders: boolean
readonly isEmpty: boolean
readonly representable: ReadonlyArray<ParsedOperationMediaTypeSchema>
}
/**
* Resolved security requirement applied to a parsed operation.
*
* @category models
* @since 4.0.0
*/
export type ParsedOperationSecurityRequirement = Readonly<OpenAPISecurityRequirement>
/**
* Normalized operation model shared by all OpenAPI generator backends.
*
* @category models
* @since 4.0.0
*/
export interface ParsedOperation {
readonly id: string
readonly operationId: string | undefined
readonly path: string
readonly method: OpenAPISpecMethodName
readonly tags: ReadonlyArray<string>
readonly metadata: ParsedOperationMetadata
readonly parameters: {
readonly path: ReadonlyArray<ParsedOperationParameter>
readonly query: ReadonlyArray<ParsedOperationParameter>
readonly header: ReadonlyArray<ParsedOperationParameter>
readonly cookie: ReadonlyArray<ParsedOperationParameter>
}
readonly requestBody: ParsedOperationRequestBody | undefined
readonly responses: ReadonlyArray<ParsedOperationResponse>
readonly defaultResponse: ParsedOperationResponse | undefined
readonly effectiveSecurity: ReadonlyArray<ParsedOperationSecurityRequirement>
readonly description: string | undefined
readonly params?: string
readonly paramsOptional: boolean
readonly urlParams: ReadonlyArray<string>
readonly headers: ReadonlyArray<string>
readonly cookies: ReadonlyArray<string>
readonly payload?: string
readonly payloadFormData: boolean
readonly payloadFormUrlEncoded: boolean
readonly pathSchema: string | undefined
readonly querySchema: string | undefined
readonly querySchemaOptional: boolean
readonly headersSchema: string | undefined
readonly headersSchemaOptional: boolean
readonly requestBodyRepresentable: ReadonlyArray<ParsedOperationMediaTypeSchema>
readonly pathIds: ReadonlyArray<string>
readonly pathTemplate: string
readonly successSchemas: ReadonlyMap<string, string>
readonly errorSchemas: ReadonlyMap<string, string>
readonly voidSchemas: ReadonlySet<string>
// SSE streaming response schema (text/event-stream)
readonly sseSchema?: string
// Binary stream response (application/octet-stream)
readonly binaryResponse: boolean
}
/**
* Creates a mutable operation accumulator populated with parser defaults.
*
* @category constructors
* @since 4.0.0
*/
export const makeDeepMutable = (options: {
readonly id: string
readonly method: OpenAPISpecMethodName
readonly pathIds: Array<string>
readonly pathTemplate: string
readonly description: string | undefined
}): Types.DeepMutable<ParsedOperation> => ({
...options,
operationId: undefined,
path: "",
tags: [],
metadata: {
summary: undefined,
description: options.description,
deprecated: false,
externalDocs: undefined
},
parameters: {
path: [],
query: [],
header: [],
cookie: []
},
requestBody: undefined,
responses: [],
defaultResponse: undefined,
effectiveSecurity: [],
urlParams: [],
headers: [],
cookies: [],
payloadFormData: false,
payloadFormUrlEncoded: false,
pathSchema: undefined,
querySchema: undefined,
querySchemaOptional: true,
headersSchema: undefined,
headersSchemaOptional: true,
requestBodyRepresentable: [],
successSchemas: new Map(),
errorSchemas: new Map(),
voidSchemas: new Set(),
paramsOptional: true,
binaryResponse: false
})

View File

@@ -0,0 +1,111 @@
/**
* Shared utility helpers for the OpenAPI generator.
*
* This module centralizes the small transformations used while rendering
* generated TypeScript, including operation-name normalization, optional
* description handling, safe JSDoc comment emission, and direct array merging
* for code-generation accumulators.
*
* @since 4.0.0
*/
import * as String from "effect/String"
import * as UndefinedOr from "effect/UndefinedOr"
/**
* Converts an OpenAPI name into the generator's camel-case form.
*
* **Details**
*
* Separators are removed, leading digits are ignored, and letters following a
* separator or digit are upper-cased without otherwise changing letter casing.
*
* @category converting
* @since 4.0.0
*/
export const camelize = (self: string): string => {
let str = ""
let hadSymbol = false
for (let i = 0; i < self.length; i++) {
const charCode = self.charCodeAt(i)
if (
(charCode >= 65 && charCode <= 90) ||
(charCode >= 97 && charCode <= 122)
) {
str += hadSymbol ? self[i].toUpperCase() : self[i]
hadSymbol = false
} else if (charCode >= 48 && charCode <= 57) {
if (str.length > 0) {
str += self[i]
hadSymbol = true
}
} else if (str.length > 0) {
hadSymbol = true
}
}
return str
}
/**
* Converts an OpenAPI operation id into the exported operation identifier used
* by generated TypeScript modules.
*
* @category converting
* @since 4.0.0
*/
export const identifier = (operationId: string) => String.capitalize(camelize(operationId))
/**
* Extracts a trimmed, non-empty string from an unknown value.
*
* **Details**
*
* Returns `undefined` for non-string values and for strings containing only
* whitespace.
*
* @category filtering
* @since 4.0.0
*/
export const nonEmptyString = (a: unknown): string | undefined => {
if (typeof a === "string") {
const trimmed = String.trim(a)
if (String.isNonEmpty(trimmed)) {
return trimmed
}
}
}
/**
* Renders an optional description as a JSDoc block for generated TypeScript.
*
* **Details**
*
* Returns an empty string when the description is absent and escapes any
* closing comment marker so generated source remains syntactically valid.
*
* @category converting
* @since 4.0.0
*/
export const toComment = UndefinedOr.match({
onUndefined: () => "",
onDefined: (description: string) =>
`/**
* ${description.replace(/\*\//g, " * /").split("\n").join("\n* ")}
*/\n`
})
/**
* Appends every element from `source` into `destination` in order.
*
* **Details**
*
* This mutates `destination` directly, which avoids allocating an intermediate
* array when generator code needs to merge collections.
*
* @category concatenating
* @since 4.0.0
*/
export const spreadElementsInto = <A>(source: Array<A>, destination: Array<A>): void => {
for (let i = 0; i < source.length; i++) {
destination.push(source[i])
}
}

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import * as NodeServices from "@effect/platform-node/NodeServices"
import * as Effect from "effect/Effect"
import { run } from "./main.ts"
run.pipe(
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
)

View File

@@ -0,0 +1,116 @@
/**
* Command-line entry point for generating Effect HTTP clients or HttpApi
* definitions from an OpenAPI specification.
*
* The CLI reads a spec file, optionally applies JSON patches in order, selects
* the generator layer for the requested output format, reports generation
* warnings to stderr, and writes the generated source to stdout.
*
* @since 4.0.0
*/
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import type * as Schema from "effect/Schema"
import * as CliError from "effect/unstable/cli/CliError"
import * as Command from "effect/unstable/cli/Command"
import * as Flag from "effect/unstable/cli/Flag"
import type { OpenAPISpec } from "effect/unstable/httpapi/OpenApi"
import * as OpenApiGenerator from "./OpenApiGenerator.ts"
import * as OpenApiPatch from "./OpenApiPatch.ts"
const spec = Flag.fileParse("spec").pipe(
Flag.withAlias("s"),
Flag.withDescription("The OpenAPI spec file to generate output from")
)
const name = Flag.string("name").pipe(
Flag.withAlias("n"),
Flag.withDescription("The name of the generated output"),
Flag.withDefault("Client")
)
const format = Flag.choice("format", ["httpclient", "httpclient-type-only", "httpapi"] as const).pipe(
Flag.withAlias("f"),
Flag.withDescription(
"Output format to generate: httpclient | httpclient-type-only | httpapi (default: httpclient)"
),
Flag.withDefault("httpclient")
)
const patch = Flag.string("patch").pipe(
Flag.withAlias("p"),
Flag.withDescription(
"JSON patch to apply to OpenAPI spec before generation. " +
"Can be a file path (.json, .yaml, .yml) or inline JSON array. " +
"Multiple patches are applied in order."
),
Flag.between(0, Infinity)
)
const root = Command.make("openapigen", { spec, format, name, patch }).pipe(
Command.withHandler(Effect.fnUntraced(function*({ name, spec, format, patch }) {
let patchedSpec: Schema.Json = spec as Schema.Json
if (patch.length > 0) {
const parsedPatches = yield* Effect.forEach(
patch,
(input) =>
OpenApiPatch.parsePatchInput(input).pipe(
Effect.map((p) => ({ source: input, patch: p })),
Effect.mapError((error) => new CliError.UserError({ cause: error }))
)
)
patchedSpec = yield* OpenApiPatch.applyPatches(parsedPatches, patchedSpec).pipe(
Effect.mapError((error) => new CliError.UserError({ cause: error }))
)
}
const generator = yield* OpenApiGenerator.OpenApiGenerator
const warnings: Array<OpenApiGenerator.OpenApiGeneratorWarning> = []
const source = yield* generator.generate(patchedSpec as unknown as OpenAPISpec, {
name,
format,
onWarning: (warning) => {
warnings.push(warning)
}
})
yield* Effect.forEach(
warnings,
(warning) => Console.error(formatWarning(warning)),
{ discard: true }
)
return yield* Console.log(source)
})),
Command.provide(({ format }) =>
format === "httpclient-type-only"
? OpenApiGenerator.layerTransformerTs
: OpenApiGenerator.layerTransformerSchema
)
)
/**
* Runs the OpenAPI generator command-line program.
*
* **Details**
*
* The command reads an OpenAPI specification, optionally applies JSON patches,
* generates source code in the selected format, writes any generation warnings
* to stderr, and prints the generated source to stdout.
*
* @category running
* @since 4.0.0
*/
export const run: Effect.Effect<void, CliError.CliError, Command.Environment> = Command.run(root, {
version: "0.0.0"
})
const formatWarning = (warning: OpenApiGenerator.OpenApiGeneratorWarning): string => {
const context = [
warning.method?.toUpperCase(),
warning.path,
warning.operationId ? `(${warning.operationId})` : undefined
].filter((value): value is string => value !== undefined)
return context.length > 0
? `WARNING [${warning.code}] ${context.join(" ")}: ${warning.message}`
: `WARNING [${warning.code}] ${warning.message}`
}

View File

@@ -0,0 +1,173 @@
import * as JsonSchemaGenerator from "@effect/openapi-generator/JsonSchemaGenerator"
import { describe, expect, it } from "@effect/vitest"
describe("JsonSchemaGenerator", () => {
it("schema & no definitions", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string" })
const definitions = {}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// schemas
export type A = string
export const A = Schema.String
`)
})
it("schema & definitions", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/B" })
const definitions = {
B: { type: "string" }
}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// non-recursive definitions
export type B = string
export const B = Schema.String
// schemas
export type A = B
export const A = B
`)
})
it("onEnter strips specified keys", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string", description: "desc", examples: ["ex"] })
const definitions = {}
const result = generator.generate("openapi-3.1", definitions, false, {
onEnter: (js) => {
const out = { ...js }
delete out.examples
return out
}
})
expect(result).toBe(`// schemas
export type A = string
export const A = Schema.String.annotate({ "description": "desc" })
`)
})
it("default preserves all annotations", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string", description: "desc", examples: ["ex"] })
const definitions = {}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// schemas
export type A = string
export const A = Schema.String.annotate({ "description": "desc", "examples": ["ex"] })
`)
})
it("generateHttpApi emits explicit type and const declarations", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string" })
generator.addSchema("B", {
type: "object",
properties: {
id: {
type: "string"
}
},
required: ["id"],
additionalProperties: false
})
const result = generator.generateHttpApi("openapi-3.1", {})
expect(result).toContain(`export type A = string
export const A = Schema.String`)
expect(result).toContain(`export type B = { readonly "id": string }
export const B = Schema.Struct({ "id": Schema.String })`)
expect(result).not.toContain("Schema.Class<")
expect(result).not.toContain("Schema.Opaque<")
})
it("recursive schema", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/B" })
const definitions = {
B: {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/B"
}
}
},
"required": [
"name",
"children"
],
"additionalProperties": false
}
}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// recursive definitions
export type B = { readonly "name": string, readonly "children": ReadonlyArray<B> }
export const B = Schema.Struct({ "name": Schema.String, "children": Schema.Array(Schema.suspend((): Schema.Codec<B> => B)) })
// schemas
export type A = B
export const A = B
`)
})
it("renders recursive definitions before non-recursive references for runtime generation", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/ErrorResponse" })
const definitions = {
InnerErrors: {
type: "object",
properties: {
field: {
type: "string"
}
},
required: ["field"],
additionalProperties: false
},
ErrorDetails: {
oneOf: [
{
type: "object",
additionalProperties: {
$ref: "#/components/schemas/ErrorDetails"
}
},
{
$ref: "#/components/schemas/InnerErrors"
}
]
},
ErrorResponse: {
type: "object",
properties: {
errors: {
$ref: "#/components/schemas/ErrorDetails"
}
},
additionalProperties: false
}
}
const runtimeResult = generator.generate("openapi-3.1", definitions, false)
const recursiveDeclaration =
"export const ErrorDetails = Schema.suspend((): Schema.Codec<ErrorDetails> => __recursive_ErrorDetails)"
expect(runtimeResult).toContain(recursiveDeclaration)
expect(runtimeResult).toContain("const __recursive_ErrorDetails =")
expect(runtimeResult.indexOf(recursiveDeclaration)).toBeLessThan(
runtimeResult.indexOf("export const ErrorResponse =")
)
const httpApiResult = generator.generateHttpApi("openapi-3.1", definitions)
expect(httpApiResult).toContain(recursiveDeclaration)
expect(httpApiResult).toContain("const __recursive_ErrorDetails =")
expect(httpApiResult.indexOf(recursiveDeclaration)).toBeLessThan(
httpApiResult.indexOf("export const ErrorResponse =")
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,147 @@
import * as NodeServices from "@effect/platform-node/NodeServices"
import { assert, describe, it } from "@effect/vitest"
import { Effect, Layer, Stdio, Stream } from "effect"
import * as Exit from "effect/Exit"
import { TestConsole } from "effect/testing"
import { CliOutput } from "effect/unstable/cli"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
const makeLayer = (args: ReadonlyArray<string>) =>
Layer.mergeAll(
TestConsole.layer,
CliOutput.layer(CliOutput.defaultFormatter({ colors: false })),
NodeServices.layer,
Stdio.layerTest({ args: Effect.succeed(args) })
)
const fixturePath = (fileName: string) => `${import.meta.dirname}/fixtures/${fileName}`
const cliProcessPath = `${import.meta.dirname}/../src/bin.ts`
type CliMainModule = {
readonly run: Effect.Effect<void>
}
const runCli = Effect.fnUntraced(function*(args: ReadonlyArray<string>) {
const module = (yield* Effect.promise(
() => import(new URL("../src/main.ts", import.meta.url).href)
)) as CliMainModule
return yield* Effect.gen(function*() {
const exit = yield* Effect.exit(module.run)
const stdoutLines = yield* TestConsole.logLines
const stderrLines = yield* TestConsole.errorLines
const stdout = stdoutLines.length > 0 ? String(stdoutLines[stdoutLines.length - 1]) : ""
const stderr = stderrLines.map(String).join("\n")
return { exit, stdout, stderr } as const
}).pipe(Effect.provide(makeLayer(args)))
})
const runCliProcess = Effect.fnUntraced(function*(args: ReadonlyArray<string>) {
const handle = yield* ChildProcess.make("node", [cliProcessPath, ...args])
return yield* Effect.all({
exitCode: handle.exitCode,
stdout: Stream.mkString(Stream.decodeText(handle.stdout)),
stderr: Stream.mkString(Stream.decodeText(handle.stderr))
}, { concurrency: "unbounded" })
})
describe("openapigen CLI", () => {
it.effect("documents --format values and default in --help", () =>
Effect.gen(function*() {
const result = yield* runCli(["--help"])
assert.isTrue(Exit.isSuccess(result.exit))
assert.include(result.stdout, "--format")
assert.include(result.stdout, "httpclient")
assert.include(result.stdout, "httpclient-type-only")
assert.include(result.stdout, "httpapi")
assert.include(result.stdout, "default: httpclient")
assert.strictEqual(result.stderr, "")
}))
it.effect("routes --format values and defaults to httpclient", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-basic-spec.json")
const defaultResult = yield* runCli(["--spec", spec, "--name", "CliClient"])
const httpclientResult = yield* runCli([
"--spec",
spec,
"--name",
"CliClient",
"--format",
"httpclient"
])
const typeOnlyResult = yield* runCli([
"--spec",
spec,
"--name",
"CliClient",
"--format",
"httpclient-type-only"
])
const httpapiResult = yield* runCli([
"--spec",
spec,
"--name",
"CliClient",
"--format",
"httpapi"
])
assert.isTrue(Exit.isSuccess(defaultResult.exit))
assert.isTrue(Exit.isSuccess(httpclientResult.exit))
assert.isTrue(Exit.isSuccess(typeOnlyResult.exit))
assert.isTrue(Exit.isSuccess(httpapiResult.exit))
assert.strictEqual(defaultResult.stderr, "")
assert.strictEqual(httpclientResult.stderr, "")
assert.strictEqual(typeOnlyResult.stderr, "")
assert.strictEqual(httpapiResult.stderr, "")
assert.strictEqual(defaultResult.stdout, httpclientResult.stdout)
assert.include(httpclientResult.stdout, "import * as Schema from \"effect/Schema\"")
assert.notInclude(typeOnlyResult.stdout, "import * as Schema from \"effect/Schema\"")
assert.include(typeOnlyResult.stdout, "import type * as HttpClient from \"effect/unstable/http/HttpClient\"")
assert.include(httpapiResult.stdout, "export class CliClient extends HttpApi.make(\"CliClient\")")
}))
it.effect("rejects legacy --type-only flag", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-basic-spec.json")
const result = yield* runCli(["--spec", spec, "--name", "CliClient", "--type-only"])
assert.isTrue(Exit.isFailure(result.exit))
assert.include(result.stdout, "USAGE")
assert.include(result.stderr, "Unrecognized flag: --type-only")
}))
it.effect("writes warnings to stderr and keeps stdout as generated source", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-warning-spec.json")
const result = yield* runCli(["--spec", spec, "--name", "CliClient"])
assert.isTrue(Exit.isSuccess(result.exit))
assert.include(result.stdout, "export const make = (")
assert.include(result.stderr, "WARNING [cookie-parameter-dropped]")
assert.include(result.stderr, "cookie-parameter-dropped")
assert.notInclude(result.stdout, "cookie-parameter-dropped")
assert.notInclude(result.stderr, "export const make = (")
}))
it.effect("separates generated source and warnings when spawned as a child process", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-warning-spec.json")
const result = yield* runCliProcess(["--spec", spec, "--name", "CliClient"])
assert.strictEqual(result.exitCode, ChildProcessSpawner.ExitCode(0))
assert.include(result.stdout, "export const make = (")
assert.notInclude(result.stdout, "WARNING [")
assert.notInclude(result.stdout, "cookie-parameter-dropped")
assert.include(
result.stderr,
"WARNING [cookie-parameter-dropped] GET /users/{id} (getUser): Cookie parameter \"session\" was dropped because non-security cookie parameters are not supported."
)
assert.notInclude(result.stderr, "export const make = (")
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)))
})

View File

@@ -0,0 +1,380 @@
import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Path from "effect/Path"
const testLayer = NodeServices.layer
describe("OpenApiPatch", () => {
describe("parsePatchInput", () => {
describe("inline JSON", () => {
it.effect("parses valid inline JSON patch", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"add\",\"path\":\"/foo\",\"value\":\"bar\"}]"
)
assert.deepStrictEqual(result, [{ op: "add", path: "/foo", value: "bar" }])
}).pipe(Effect.provide(testLayer)))
it.effect("parses inline JSON with multiple operations", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"add\",\"path\":\"/a\",\"value\":1},{\"op\":\"remove\",\"path\":\"/b\"},{\"op\":\"replace\",\"path\":\"/c\",\"value\":true}]"
)
assert.strictEqual(result.length, 3)
assert.strictEqual(result[0].op, "add")
assert.strictEqual(result[1].op, "remove")
assert.strictEqual(result[2].op, "replace")
}).pipe(Effect.provide(testLayer)))
it.effect("fails on invalid JSON syntax", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"add\" \"path\":\"/foo\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on unsupported operation", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"copy\",\"from\":\"/a\",\"path\":\"/b\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on missing path field", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"add\",\"value\":\"test\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on missing value for add operation", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"add\",\"path\":\"/foo\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("allows missing value for remove operation", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"remove\",\"path\":\"/foo\"}]"
)
assert.deepStrictEqual(result, [{ op: "remove", path: "/foo" }])
}).pipe(Effect.provide(testLayer)))
})
describe("file paths", () => {
it.effect("parses valid JSON file", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/valid-add.json"
)
const result = yield* OpenApiPatch.parsePatchInput(filePath)
assert.strictEqual(result.length, 1)
assert.strictEqual(result[0].op, "add")
assert.strictEqual(result[0].path, "/info/x-custom")
}).pipe(Effect.provide(testLayer)))
it.effect("parses valid YAML file", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/valid-patch.yaml"
)
const result = yield* OpenApiPatch.parsePatchInput(filePath)
assert.strictEqual(result.length, 2)
assert.strictEqual(result[0].op, "replace")
assert.strictEqual(result[1].op, "add")
}).pipe(Effect.provide(testLayer)))
it.effect("parses multiple operations from JSON file", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/valid-multiple.json"
)
const result = yield* OpenApiPatch.parsePatchInput(filePath)
assert.strictEqual(result.length, 3)
}).pipe(Effect.provide(testLayer)))
it.effect("fails on file with unsupported operation", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/invalid-op.json"
)
const exit = yield* Effect.exit(OpenApiPatch.parsePatchInput(filePath))
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on file with missing path", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/missing-path.json"
)
const exit = yield* Effect.exit(OpenApiPatch.parsePatchInput(filePath))
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("falls back to inline JSON when file does not exist", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"add\",\"path\":\"/x\",\"value\":1}]"
)
assert.deepStrictEqual(result, [{ op: "add", path: "/x", value: 1 }])
}).pipe(Effect.provide(testLayer)))
})
})
describe("applyPatches", () => {
it.effect("applies single add patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test",
patch: [{ op: "add" as const, path: "/info/version", value: "1.0.0" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, { info: { title: "Test", version: "1.0.0" } })
}))
it.effect("applies single remove patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Test", deprecated: true } }
const patches = [{
source: "test",
patch: [{ op: "remove" as const, path: "/info/deprecated" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, { info: { title: "Test" } })
}))
it.effect("applies single replace patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Old Title" } }
const patches = [{
source: "test",
patch: [{ op: "replace" as const, path: "/info/title", value: "New Title" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, { info: { title: "New Title" } })
}))
it.effect("applies multiple patches in sequence", () =>
Effect.gen(function*() {
const document = { info: { title: "Original", version: "0.0.1" } }
const patches = [
{
source: "patch1",
patch: [{ op: "replace" as const, path: "/info/title", value: "Step 1" }]
},
{
source: "patch2",
patch: [{ op: "replace" as const, path: "/info/version", value: "1.0.0" }]
},
{
source: "patch3",
patch: [{ op: "add" as const, path: "/info/x-patched", value: true }]
}
]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, {
info: { title: "Step 1", version: "1.0.0", "x-patched": true }
})
}))
it.effect("applies multiple operations within a single patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" }, paths: {} }
const patches = [{
source: "test",
patch: [
{ op: "replace" as const, path: "/info/title", value: "Updated" },
{ op: "add" as const, path: "/info/version", value: "2.0.0" }
]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, {
info: { title: "Updated", version: "2.0.0" },
paths: {}
})
}))
it.effect("fails when path does not exist for replace", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test",
patch: [{ op: "replace" as const, path: "/info/nonexistent", value: "x" }]
}]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
}))
it.effect("fails when path does not exist for remove", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test",
patch: [{ op: "remove" as const, path: "/info/nonexistent" }]
}]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
}))
it.effect("accumulates multiple errors", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test.json",
patch: [
{ op: "replace" as const, path: "/info/nonexistent1", value: "x" },
{ op: "remove" as const, path: "/info/nonexistent2" },
{ op: "replace" as const, path: "/info/nonexistent3", value: "y" }
]
}]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
const failure = exit.cause.reasons[0]
if (failure._tag === "Fail") {
assert.strictEqual(failure.error._tag, "JsonPatchAggregateError")
assert.strictEqual(failure.error.errors.length, 3)
assert.include(failure.error.message, "3 patch operations failed")
assert.include(failure.error.message, "/info/nonexistent1")
assert.include(failure.error.message, "/info/nonexistent2")
assert.include(failure.error.message, "/info/nonexistent3")
}
}
}))
it.effect("accumulates errors across multiple patches", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [
{
source: "patch1.json",
patch: [{ op: "remove" as const, path: "/info/missing1" }]
},
{
source: "patch2.json",
patch: [{ op: "remove" as const, path: "/info/missing2" }]
}
]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
const failure = exit.cause.reasons[0]
if (failure._tag === "Fail") {
assert.strictEqual(failure.error.errors.length, 2)
assert.include(failure.error.message, "patch1.json")
assert.include(failure.error.message, "patch2.json")
}
}
}))
it.effect("preserves unmodified parts of document", () =>
Effect.gen(function*() {
const document = {
info: { title: "Test", description: "Unchanged" },
paths: { "/users": { get: {} } },
components: { schemas: {} }
}
const patches = [{
source: "test",
patch: [{ op: "replace" as const, path: "/info/title", value: "Changed" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.strictEqual((result as { info: { description: string } }).info.description, "Unchanged")
assert.deepStrictEqual((result as { paths: object }).paths, { "/users": { get: {} } })
assert.deepStrictEqual((result as { components: object }).components, { schemas: {} })
}))
it.effect("returns original document when no patches provided", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const result = yield* OpenApiPatch.applyPatches([], document)
assert.deepStrictEqual(result, document)
}))
})
describe("error messages", () => {
it.effect("JsonPatchParseError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchParseError({
source: "./fix.json",
reason: "Unexpected token"
})
assert.strictEqual(
error.message,
"Failed to parse patch from ./fix.json: Unexpected token"
)
}))
it.effect("JsonPatchValidationError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchValidationError({
source: "inline",
reason: "Missing 'path' field"
})
assert.strictEqual(
error.message,
"Invalid JSON Patch from inline: Missing 'path' field"
)
}))
it.effect("JsonPatchApplicationError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchApplicationError({
source: "./fix.json",
operationIndex: 2,
operation: "remove",
path: "/info/x",
reason: "Property does not exist"
})
assert.strictEqual(
error.message,
"Failed to apply patch from ./fix.json: operation 2 (remove at /info/x): Property does not exist"
)
}))
it.effect("JsonPatchAggregateError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchAggregateError({
errors: [
new OpenApiPatch.JsonPatchApplicationError({
source: "./fix.json",
operationIndex: 0,
operation: "replace",
path: "/info/x",
reason: "Property does not exist"
}),
new OpenApiPatch.JsonPatchApplicationError({
source: "./other.json",
operationIndex: 1,
operation: "remove",
path: "/paths/~1users",
reason: "Path not found"
})
]
})
assert.include(error.message, "2 patch operations failed")
assert.include(error.message, "1. [./fix.json] op 0 (replace at /info/x)")
assert.include(error.message, "2. [./other.json] op 1 (remove at /paths/~1users)")
}))
})
})

View File

@@ -0,0 +1,33 @@
import * as Utils from "@effect/openapi-generator/Utils"
import { describe, expect, it } from "vitest"
describe("Utils", () => {
describe("camelize", () => {
it("removes hyphens and capitalizes following letters", () => {
expect(Utils.camelize("my-operation-id")).toBe("myOperationId")
})
it("removes slashes and capitalizes following letters", () => {
expect(Utils.camelize("my/operation/id")).toBe("myOperationId")
})
it("handles numbers", () => {
expect(Utils.camelize("operation-2")).toBe("operation2")
})
it("removes leading numbers", () => {
expect(Utils.camelize("2operation")).toBe("operation")
})
it("handles empty string", () => {
expect(Utils.camelize("")).toBe("")
})
})
describe("identifier", () => {
it("capitalizes camelized string", () => {
expect(Utils.identifier("my-operation")).toBe("MyOperation")
expect(Utils.identifier("operation-2")).toBe("Operation2")
})
})
})

View File

@@ -0,0 +1,35 @@
{
"openapi": "3.0.0",
"info": {
"title": "CLI Basic API",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"operationId": "listUsers",
"responses": {
"200": {
"description": "List users",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@@ -0,0 +1,57 @@
{
"openapi": "3.0.0",
"info": {
"title": "CLI Warning API",
"version": "1.0.0"
},
"paths": {
"/users/{id}": {
"get": {
"operationId": "getUser",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "session",
"in": "cookie",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "User response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": ["id"],
"additionalProperties": false
}
}
}
}
}
}
}
},
"components": {
"schemas": {},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@@ -0,0 +1,7 @@
[
{
"op": "copy",
"from": "/info/title",
"path": "/info/x-title"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"op": "add",
"value": "test"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"op": "add",
"path": "/info/x-missing"
}
]

View File

@@ -0,0 +1,8 @@
[
{
"op": "add",
"path": "/info/x-custom",
"value": "custom-value",
"description": "Add custom extension"
}
]

View File

@@ -0,0 +1,17 @@
[
{
"op": "replace",
"path": "/info/title",
"value": "My Custom API"
},
{
"op": "add",
"path": "/info/x-generator",
"value": "effect-openapi"
},
{
"op": "replace",
"path": "/info/version",
"value": "2.0.0"
}
]

View File

@@ -0,0 +1,6 @@
- op: replace
path: /info/title
value: YAML Patched Title
- op: add
path: /info/x-yaml-patch
value: true

View File

@@ -0,0 +1,6 @@
[
{
"op": "remove",
"path": "/paths/~1deprecated"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"op": "replace",
"path": "/info/title",
"value": "Updated API Title"
}
]

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src"],
"references": [
{ "path": "../../effect" },
{ "path": "../../platform-node" }
]
}

View File

@@ -0,0 +1,6 @@
import { mergeConfig, type ViteUserConfig } from "vitest/config"
import shared from "../../../vitest.shared.ts"
const config: ViteUserConfig = {}
export default mergeConfig(shared, config)

View File

@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@effect/docgen/schema.json",
"exclude": ["**/*.ts"]
}

View File

@@ -0,0 +1,84 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "oxc", "eslint", "unicorn", "node"],
"categories": {
"correctness": "error",
"suspicious": "error",
"perf": "error"
},
"rules": {
// Effect custom rules
"effect/no-bigint-literals": "error",
"effect/no-import-from-barrel-package": ["error", {
"checkPatterns": [
"^effect$",
"^effect/(.+/)?[a-z][a-z0-9]*$",
"^@effect/[^/]+$",
"^@effect/[^/]+/(.+/)?[a-z][a-z0-9]*$"
],
"checkRelativeIndexImports": true
}],
"effect/no-js-extension-imports": "error",
"effect/no-opaque-instance-fields": "error",
"effect/no-unused-internal": "error",
"effect/jsdocs": "error",
// Tune native rules
// Import rules
"typescript/consistent-type-imports": ["error", {
"fixStyle": "inline-type-imports"
}],
"typescript/no-import-type-side-effects": "error",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"import/no-empty-named-blocks": "error",
// TypeScript cleanup
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-useless-empty-export": "error",
// Code quality
"eslint/no-console": "error",
"eslint/no-var": "error",
"eslint/no-useless-constructor": "error",
"unicorn/no-abusive-eslint-disable": "error",
"eslint/no-unneeded-ternary": "error",
"eslint/no-useless-concat": "error",
"oxc/misrefactored-assign-op": "error",
// Unicorn
"unicorn/prefer-array-flat-map": "error",
"unicorn/no-accessor-recursion": "error",
"oxc/no-map-spread": "off",
"eslint/object-shorthand": "off",
"eslint/no-shadow": "off",
"eslint/no-unused-vars": "off",
"eslint/require-yield": "off",
"eslint/no-fallthrough": "off",
"eslint/no-await-in-loop": "off",
"unicorn/no-new-array": "off",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-array-sort": "off",
"unicorn/no-array-reverse": "off",
"unicorn/require-post-message-target-origin": "off",
"unicorn/prefer-add-event-listener": "off",
"unicorn/prefer-set-has": "off",
"no-dangling-underscore": "off",
"typescript/no-explicit-any": "off",
"typescript/no-empty-interface": "off",
"typescript/ban-ts-comment": "off",
"typescript/no-namespace": "off",
"typescript/no-non-null-assertion": "off",
"typescript/no-dynamic-delete": "off",
"typescript/no-invalid-void-type": "off",
"typescript/no-unsafe-function-type": "off",
"typescript/unified-signatures": "off",
"typescript/no-empty-object-type": "off",
"typescript/no-confusing-non-null-assertion": "off",
"typescript/array-type": ["error", {
"default": "generic",
"readonly": "generic"
}],
"typescript/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}]
}
}

View File

@@ -0,0 +1,71 @@
{
"name": "@effect/oxc",
"version": "0.0.0",
"type": "module",
"private": true,
"license": "MIT",
"description": "Opinionated linting and formatting configuration for Effect",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/tools/oxc"
},
"sideEffects": [],
"bugs": {
"url": "https://github.com/Effect-TS/effect-smol/issues"
},
"tags": [
"typescript",
"formatting",
"linting",
"oxc"
],
"keywords": [
"typescript",
"formatting",
"linting",
"oxc"
],
"exports": {
"./package.json": "./package.json",
"./oxlint": "./src/oxlint/index.ts",
"./oxlint/rules/*": "./src/oxlint/rules/*.ts",
"./oxlintrc.json": "./oxlintrc.json"
},
"files": [
"oxlintrc.json",
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"provenance": true,
"exports": {
"./package.json": "./package.json",
"./oxlint": "./dist/oxlint/index.js",
"./oxlint/rules/*": "./dist/oxlint/rules/*.js",
"./oxlintrc.json": "./oxlintrc.json"
}
},
"scripts": {
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"dependencies": {
"@effect/jsdocs": "workspace:^"
},
"peerDependencies": {
"typescript": ">=5.0.0 <7.0.0"
},
"devDependencies": {
"@types/node": "^26.1.1",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
}
}

Some files were not shown because too many files have changed in this diff Show More