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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,23 @@ export class DynamicTypeLiteralMapper {
}): java.TypeLiteral {
switch (named.type) {
case "alias":
if (named.typeReference.type === "unknown") {
const convertedValue = this.convert({
typeReference: named.typeReference,
value,
as,
inUndiscriminatedUnion
});
return java.TypeLiteral.reference(
java.invokeMethod({
on: this.context.getJavaClassReferenceFromDeclaration({
declaration: named.declaration
}),
method: "of",
arguments_: [convertedValue]
})
);
}
return this.convert({ typeReference: named.typeReference, value, as, inUndiscriminatedUnion });
case "discriminatedUnion":
return this.convertDiscriminatedUnion({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ public TypeName visitNamed(NamedType named) {
if (isAlias) {
AliasTypeDeclaration aliasTypeDeclaration =
typeDeclaration.getShape().getAlias().get();
return aliasTypeDeclaration.getResolvedType().visit(this);
if (!aliasTypeDeclaration.getAliasOf().isUnknown()) {
return aliasTypeDeclaration.getResolvedType().visit(this);
}
}
}
return applyEnclosing(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ public SingleTypeGenerator(

@Override
public Optional<AbstractTypeGenerator> visitAlias(AliasTypeDeclaration value) {
if (generatorContext.getCustomConfig().wrappedAliases() || fromErrorDeclaration) {
if (generatorContext.getCustomConfig().wrappedAliases()
|| fromErrorDeclaration
|| value.getAliasOf().isUnknown()) {
AliasGenerator aliasGenerator =
new AliasGenerator(className, generatorContext, value, reservedTypeNamesInScope, isTopLevelClass);
return Optional.of(aliasGenerator);
Expand Down
10 changes: 9 additions & 1 deletion generators/java/sdk/src/main/java/com/fern/java/client/Cli.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,15 @@ public GeneratedRootClient generateClient(
NullableNonemptyFilterGenerator nullableNonemptyFilterGenerator = new NullableNonemptyFilterGenerator(context);
this.addGeneratedFile(nullableNonemptyFilterGenerator.generateFile());

if (context.getCustomConfig().wrappedAliases()) {
boolean hasUnknownAliasTypes = ir.getTypes().values().stream()
.anyMatch(typeDeclaration -> typeDeclaration.getShape().isAlias()
&& typeDeclaration
.getShape()
.getAlias()
.get()
.getAliasOf()
.isUnknown());
if (context.getCustomConfig().wrappedAliases() || hasUnknownAliasTypes) {
WrappedAliasGenerator wrappedAliasGenerator = new WrappedAliasGenerator(context);
this.addGeneratedFile(wrappedAliasGenerator.generateFile());
}
Expand Down
13 changes: 13 additions & 0 deletions generators/java/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 3.44.2
changelogEntry:
- summary: |
Generate named Java wrapper classes for unknown type aliases. Previously,
when an API schema had no `type` and no `properties` (representing an
"any" type), the generator dropped the named type definition and inlined
`Object` wherever it was referenced. Unknown type aliases now generate a
proper wrapper class (e.g. `DocumentedUnknownType`) based on
`java.lang.Object`, consistent with how other alias types are handled.
type: fix
createdAt: "2026-03-16"
irVersion: 65

- version: 3.44.1
changelogEntry:
- summary: |
Expand Down
5 changes: 3 additions & 2 deletions generators/php/sdk/src/wire-tests/WireTestGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export class WireTestGenerator {
parameters: [],
body: php.codeblock((writer) => {
writer.writeTextStatement("parent::setUp()");
writer.writeTextStatement("$wiremockUrl = getenv('WIREMOCK_URL') ?: 'http://localhost:8080'");

// Build auth parameters
const authParams = this.buildAuthParamsForTest();
Expand Down Expand Up @@ -174,7 +175,7 @@ export class WireTestGenerator {
name: "Environments"
})
);
writer.write(`::custom(${envValues.map((value) => `'${value}'`).join(", ")}),`);
writer.write(`::custom(${envValues.map(() => "$wiremockUrl").join(", ")}),`);
if (authParams.length === 0) {
writer.write("\n");
writer.dedent();
Expand All @@ -190,7 +191,7 @@ export class WireTestGenerator {
}
writer.writeLine("options: [");
writer.indent();
writer.writeLine("'baseUrl' => getenv('WIREMOCK_URL') ?: 'http://localhost:8080',");
writer.writeLine("'baseUrl' => $wiremockUrl,");
writer.dedent();
writer.write("]");
if (authParams.length === 0) {
Expand Down
12 changes: 12 additions & 0 deletions generators/php/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.2.1
changelogEntry:
- summary: |
Fix Wire test files to read `WIREMOCK_URL` environment variable instead of
hardcoding `http://localhost:8080`. The `setUp()` method now declares a
`$wiremockUrl` variable that reads from the environment with a fallback,
enabling tests to work with dynamically assigned Docker ports in local
development and CI/CD environments.
type: fix
createdAt: "2026-03-17"
irVersion: 62

- version: 2.2.0
changelogEntry:
- summary: |
Expand Down
10 changes: 10 additions & 0 deletions generators/typescript/sdk/generator/src/SdkGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,16 @@ export class SdkGenerator {
namedExports: [clientClassName]
});

if (this.generateWebSocketClients && package_.websocket != null) {
const socketClassName = this.websocketSocketDeclarationReferencer.getExportedName(
packageId.subpackageId
);
sourceFile.addExportDeclaration({
moduleSpecifier: "./client/Socket",
namedExports: [socketClassName]
});
}

sourceFile.addExportDeclaration({
moduleSpecifier: "./client/index"
});
Expand Down
11 changes: 11 additions & 0 deletions generators/typescript/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 3.58.1
changelogEntry:
- summary: |
Export the generated Socket class from subpackage `exports.ts` when
WebSocket clients are enabled. Previously only the Client class was
exported, making the Socket class inaccessible through the public
exports path.
type: fix
createdAt: "2026-03-17"
irVersion: 65

- version: 3.58.0
changelogEntry:
- summary: |
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/ai/baml_src/diff_analyzer.baml
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@ function AnalyzeSdkDiff(
- PATCH: leave empty string — patch changes don't warrant changelog entries
- Do not use conventional commit prefixes (no "feat:", "fix:", etc.)
- Write in third person ("The SDK now supports..." not "Add support for...")
- IMPORTANT: Wrap any type references containing angle brackets in backticks
to prevent MDX parsing issues. For example, write `Optional<String>` not
Optional<String>, and `Map<String, Object>` not Map<String, Object>.

Remember again that YOU MUST return a structured JSON response with these four fields:
- message: A git commit message formatted like the example previously provided
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/ai/src/baml_client/inlinedbaml.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/cli/cli-v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.bun-build
87 changes: 87 additions & 0 deletions packages/cli/cli-v2/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# CLI-V2 Package

The next-generation Fern CLI, built on yargs with a class-based command architecture.

## Command Structure

Commands use one of three registration patterns from `src/commands/_internal/`:

### Leaf Command (`command()` helper)
Single command with no subcommands. This is the most common pattern.

```typescript
export declare namespace SplitCommand {
export interface Args extends GlobalArgs {
api?: string;
format?: SplitFormatInput;
}
}

export class SplitCommand {
public async handle(context: Context, args: SplitCommand.Args): Promise<void> {
// ...
}

private async splitAsOverlay(/* ... */): Promise<void> {
// Private helpers for internal logic
}
}

export function addSplitCommand(cli: Argv<GlobalArgs>): void {
const cmd = new SplitCommand();
command(
cli,
"split",
"Description shown in help",
(context, args) => cmd.handle(context, args as SplitCommand.Args),
(yargs) =>
yargs
.option("api", { type: "string", description: "Filter by API name" })
.option("format", { type: "string", default: "overlay" })
);
}
```

### Command Group (`commandGroup()` helper)
Routes to required subcommands, no default handler.

```typescript
export function addAuthCommand(cli: Argv<GlobalArgs>): void {
commandGroup(cli, "auth", "Authenticate fern", [
addLoginCommand,
addLogoutCommand,
addStatusCommand
]);
}
```

### Command with Subcommands (`commandWithSubcommands()` helper)
Has both a default handler AND subcommands (git-stash pattern).

```typescript
commandWithSubcommands(cli, "preview", "Preview docs", handler, builder, [addDeleteCommand]);
```

## Key Conventions

- **Class-based handlers**: Commands are classes with a public `handle(context, args)` method. Private methods for internal logic. The class is instantiated once in the `add*Command` registration function.
- **Args via declare namespace**: Each command declares its args interface inside a `declare namespace` block extending `GlobalArgs`.
- **File naming**: `command.ts` for implementation, `index.ts` for single-line re-export, `__test__/` for tests alongside source.
- **Import paths**: Always use `.js` extensions (ESM). Use `@fern-api/*` for workspace packages, relative paths within this package.
- **Context-first**: All handlers receive `Context` as first argument — provides logging, workspace loading, auth, telemetry, and shutdown hooks.
- **Errors**: Throw `CliError` (with static factories like `CliError.authRequired()`, `CliError.notFound()`). Never swallow errors silently.
- **UI output**: Use `Icons.success`/`Icons.error` from `ui/format.ts` with `chalk` for colored output. Info/debug to `context.stderr`, structured output to `context.stdout`.

## Testing

```typescript
// Silent context for logic tests
const context = createTestContext({ cwd: AbsoluteFilePath.of(testDir) });

// Context with output capture for asserting messages
const { context, getStdout, getStderr } = createTestContextWithCapture({ cwd });
await cmd.handle(context, args);
expect(getStderr()).toContain("expected message");
```

Test utilities live in `src/__test__/utils/`. Command tests live in `src/commands/*/__test__/`.
Loading
Loading