Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
10f9e18
update .gitignore
jsklan Oct 10, 2025
0a50d9c
Merge branch 'main' into jsklan/ts-remote-local-parity
jsklan Oct 12, 2025
9674004
interim commit for license
jsklan Oct 12, 2025
2228cdf
interim
jsklan Oct 13, 2025
8965eda
readme equivalence
jsklan Oct 13, 2025
b61d682
update claude settings
jsklan Oct 13, 2025
9fcc2dc
Include dynamicGeneratorConfig in local generation
jsklan Oct 14, 2025
8c79cb8
remove claude changes
jsklan Oct 14, 2025
d2aaee0
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 14, 2025
76528ef
update turbo check to verify actual build files exist
jsklan Oct 14, 2025
cdda44c
format
jsklan Oct 14, 2025
5901528
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 15, 2025
7fc36be
update ts sdk generator to pass through local config
jsklan Oct 15, 2025
280c3ee
update
jsklan Oct 15, 2025
401a61e
something
jsklan Oct 15, 2025
109d700
Only version differences
jsklan Oct 16, 2025
aea8d0c
format
jsklan Oct 16, 2025
8530778
add _licenseName back to customconfig
jsklan Oct 16, 2025
1e0875c
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 16, 2025
7dee1e7
versions
jsklan Oct 16, 2025
4460113
fix changelog
jsklan Oct 16, 2025
486e047
simplify getRemote logic
jsklan Oct 16, 2025
5f908da
fix
jsklan Oct 16, 2025
6a5dc36
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 16, 2025
68dc629
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 17, 2025
e88cf53
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 17, 2025
27ce887
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 17, 2025
e050a35
update lock
jsklan Oct 17, 2025
7cc7106
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 17, 2025
a86e4df
Update changelog messages
jsklan Oct 17, 2025
ab20f30
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 17, 2025
a0adbd2
fix pnpm compile
jsklan Oct 17, 2025
1ce9400
relock
jsklan Oct 17, 2025
97c0e58
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Oct 31, 2025
1f0e70a
update
jsklan Oct 31, 2025
ad7b80d
fix compile
jsklan Oct 31, 2025
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,6 @@ seed/python-sdk/**/output.prof
.yarn-cache
.pnpm-cache

**/vitest.config.ts.*.mjs
**/vitest.config.ts.*.mjs

.local
28 changes: 26 additions & 2 deletions generators/base/src/AbstractGeneratorAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export abstract class AbstractGeneratorAgent<GeneratorContext extends AbstractGe
}): Promise<string> {
const readmeConfig = this.getReadmeConfig({
context,
remote: this.getRemote(),
remote: this.getRemote(context),
featureConfig: await this.readFeatureConfig(),
endpointSnippets
});
Expand Down Expand Up @@ -127,17 +127,41 @@ export abstract class AbstractGeneratorAgent<GeneratorContext extends AbstractGe
return loaded;
}

protected getRemote(): FernGeneratorCli.Remote | undefined {
protected getRemote(context: GeneratorContext): FernGeneratorCli.Remote | undefined {
const outputMode = this.config.output.mode.type === "github" ? this.config.output.mode : undefined;
if (outputMode?.repoUrl != null && outputMode?.installationToken != null) {
return FernGeneratorCli.Remote.github({
repoUrl: outputMode.repoUrl,
installationToken: outputMode.installationToken
});
}

const githubConfig = this.getGitHubConfig({ context });
if (githubConfig.uri != null && githubConfig.token != null) {
return FernGeneratorCli.Remote.github({
repoUrl: this.normalizeRepoUrl(githubConfig.uri),
installationToken: githubConfig.token
});
}

return undefined;
}

private normalizeRepoUrl(repoUrl: string): string {
// If it's already a full URL, return as-is
if (repoUrl.startsWith("https://")) {
return repoUrl;
}

// If it's in owner/repo format, convert to full GitHub URL
if (repoUrl.match(/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/)) {
return `https://github.com/${repoUrl}`;
}

// Default: assume it's a GitHub URL and add prefix
return `https://github.com/${repoUrl}`;
}

private async getFeaturesConfig(): Promise<string> {
// try to find the features.yml file using the well-known paths
for (const each of FEATURES_CONFIG_PATHS) {
Expand Down
2 changes: 1 addition & 1 deletion generators/swift/sdk/src/SwiftGeneratorAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class SwiftGeneratorAgent extends AbstractGeneratorAgent<SdkGeneratorCont
}): Promise<string> {
const readmeConfig = this.getReadmeConfig({
context,
remote: this.getRemote(),
remote: this.getRemote(context),
featureConfig: await this.readFeatureConfig(),
endpointSnippets
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ export const TypescriptCustomConfigSchema = z.strictObject({

// deprecated
timeoutInSeconds: z.optional(z.union([z.literal("infinity"), z.number()])),
includeApiReference: z.optional(z.boolean())
includeApiReference: z.optional(z.boolean()),

// internal - license name extracted from custom license file
_fernLicenseName: z.optional(z.string())
});

export type TypescriptCustomConfigSchema = z.infer<typeof TypescriptCustomConfigSchema>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";

import { type NpmPackage } from "../NpmPackage";
import { PublishInfo } from "../PublishInfo";

export interface constructNpmPackageArgs {
packageName?: string;
version?: string;
repoUrl?: string;
publishInfo?: PublishInfo;
licenseConfig?: FernGeneratorExec.LicenseConfig;
isPackagePrivate: boolean;
}

export function constructNpmPackageFromArgs(args: constructNpmPackageArgs): NpmPackage | undefined {
const { packageName, version, repoUrl, publishInfo, licenseConfig, isPackagePrivate } = args;
if (packageName == null || version == null) {
return undefined;
}

return {
packageName,
version,
private: isPackagePrivate,
publishInfo,
license: licenseFromLicenseConfig(licenseConfig),
repoUrl: getRepoUrlFromUrl(repoUrl)
};
}

export function constructNpmPackage({
generatorConfig,
Expand Down Expand Up @@ -50,7 +76,10 @@ export function constructNpmPackage({
}
}

function getRepoUrlFromUrl(repoUrl: string): string {
function getRepoUrlFromUrl(repoUrl: string | undefined): string | undefined {
if (repoUrl == null) {
return undefined;
}
if (repoUrl.startsWith("https://github.com/")) {
return `github:${removeGitSuffix(repoUrl).replace("https://github.com/", "")}`;
}
Expand Down Expand Up @@ -84,3 +113,11 @@ function removeGitSuffix(repoUrl: string): string {
}
return repoUrl;
}

function licenseFromLicenseConfig(licenseConfig: FernGeneratorExec.LicenseConfig | undefined): string | undefined {
return licenseConfig?._visit({
basic: (basic) => basic.id,
custom: (custom) => `See ${custom.filename}`,
_other: () => undefined
});
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { constructNpmPackage } from "./constructNpmPackage";
export * from "./constructNpmPackage";
export { getNamespaceExport } from "./getNamespaceExport";
45 changes: 45 additions & 0 deletions generators/typescript/sdk/cli/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
import { SdkGenerator } from "@fern-typescript/sdk-generator";
import fs from "fs/promises";
import path from "path";

import { SdkCustomConfig } from "./custom-config/SdkCustomConfig";
import { SdkCustomConfigSchema } from "./custom-config/schema/SdkCustomConfigSchema";
Expand Down Expand Up @@ -239,6 +241,7 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
pathToSrc: persistedTypescriptProject.getSrcDirectory()
});
await writeTemplateFiles(rootDirectory, this.getTemplateVariables(customConfig));
await this.writeLicenseFile(config, rootDirectory, generatorContext.logger);
await this.postProcess(persistedTypescriptProject, customConfig);

return persistedTypescriptProject;
Expand All @@ -253,6 +256,48 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
};
}

private async writeLicenseFile(
config: FernGeneratorExec.GeneratorConfig,
rootDirectory: AbsoluteFilePath,
logger: Logger
): Promise<void> {
if (config.license?.type === "custom") {
// For custom licenses, we need to get the license content from the source file
// The CLI should have read the license file content and made it available
// For now, we'll read the license file from the original location

try {
// The license file path is relative to the fern config directory
// We need to construct the full path to read the license content
const licenseFileName = config.license.filename;
const licenseContent = await this.readLicenseFileContent(licenseFileName);

const licenseFilePath = path.join(rootDirectory, "LICENSE");
await fs.writeFile(licenseFilePath, licenseContent, "utf-8");
Comment on lines +273 to +276
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a readstream here?

logger.debug(`Successfully wrote LICENSE file to ${licenseFilePath}`);
} catch (error) {
// If we can't read the license file, we'll skip writing it
// This maintains backwards compatibility
logger.warn(`Failed to write LICENSE file: ${error instanceof Error ? error.message : String(error)}`);
}
}
}

private async readLicenseFileContent(licenseFileName: string): Promise<string> {
const fs = await import("fs/promises");

// In Docker execution environment, the license file is mounted at /tmp/LICENSE
const dockerLicensePath = "/tmp/LICENSE";

try {
return await fs.readFile(dockerLicensePath, "utf-8");
} catch (error) {
throw new Error(
`Could not read license file from ${dockerLicensePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}

private async postProcess(
persistedTypescriptProject: PersistedTypescriptProject,
_customConfig: SdkCustomConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,12 @@ export class GeneratedSdkClientClassImpl implements GeneratedSdkClientClass {
header: context.ir.sdkConfig.platformHeaders.userAgent.header,
value: ts.factory.createStringLiteral(context.ir.sdkConfig.platformHeaders.userAgent.value)
});
} else if (this.npmPackage != null) {
// Fallback: generate User-Agent header from npm package info
headers.push({
header: "User-Agent",
value: ts.factory.createStringLiteral(`${this.npmPackage.packageName}/${this.npmPackage.version}`)
});
}

const generatedVersion = context.versionContext.getGeneratedVersion();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AbstractGeneratorAgent } from "@fern-api/base-generator";
import { AbstractGeneratorAgent, RawGithubConfig } from "@fern-api/base-generator";
import { Logger } from "@fern-api/logger";
import { FernGeneratorCli } from "@fern-fern/generator-cli-sdk";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
Expand Down Expand Up @@ -70,13 +70,14 @@ export class TypeScriptGeneratorAgent extends AbstractGeneratorAgent<SdkContext>
};
}

public getGitHubConfig(args: AbstractGeneratorAgent.GitHubConfigArgs<SdkContext>): FernGeneratorCli.GitHubConfig {
public getGitHubConfig(args: AbstractGeneratorAgent.GitHubConfigArgs<SdkContext>): RawGithubConfig {
// TODO: get from env
return {
sourceDirectory: "NONE",
uri: "NONE",
token: "token",
branch: "NONE"
sourceDirectory: "fern/output",
type: this.publishConfig?.type,
uri: this.publishConfig?.type === "github" ? this.publishConfig.uri : undefined,
token: this.publishConfig?.type === "github" ? this.publishConfig.token : undefined,
mode: this.publishConfig?.type === "github" ? this.publishConfig.mode : undefined
};
}
}
8 changes: 8 additions & 0 deletions generators/typescript/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 3.17.2
changelogEntry:
- summary: |
Fix TypeScript SDK generator's local GitHub generation to match remote generation.
type: fix
createdAt: "2025-10-31"
irVersion: 60

- version: 3.17.1
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import { assertNever } from "@fern-api/core-utils";
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { CONSOLE_LOGGER, createLogger, Logger, LogLevel } from "@fern-api/logger";
import { createLoggingExecutable } from "@fern-api/logging-execa";
import { serialization } from "@fern-fern/ir-sdk";
import { FernIr, serialization } from "@fern-fern/ir-sdk";
import { IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
import { constructNpmPackage, NpmPackage, PersistedTypescriptProject } from "@fern-typescript/commons";
import {
constructNpmPackage,
constructNpmPackageArgs,
constructNpmPackageFromArgs,
NpmPackage,
PersistedTypescriptProject
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
import { writeFile } from "fs/promises";
import tmp from "tmp-promise";
Expand Down Expand Up @@ -72,11 +78,20 @@ export abstract class AbstractGeneratorCli<CustomConfig> {
});
const customConfig = this.parseCustomConfig(config.customConfig, logger);

const npmPackage = constructNpmPackage({
generatorConfig: config,
isPackagePrivate: this.isPackagePrivate(customConfig)
const ir = await parseIR({
absolutePathToIR: AbsoluteFilePath.of(config.irFilepath),
parse: serialization.IntermediateRepresentation.parse
});

const npmPackage = ir.selfHosted
? constructNpmPackageFromArgs(
npmPackageInfoFromPublishConfig(ir.publishConfig, this.isPackagePrivate(customConfig))
)
: constructNpmPackage({
generatorConfig: config,
isPackagePrivate: this.isPackagePrivate(customConfig)
});

await generatorNotificationService.sendUpdate(
FernGeneratorExec.GeneratorUpdate.initV2({
publishingToRegistry:
Expand All @@ -91,11 +106,6 @@ export abstract class AbstractGeneratorCli<CustomConfig> {
_other: () => undefined
});

const ir = await parseIR({
absolutePathToIR: AbsoluteFilePath.of(config.irFilepath),
parse: serialization.IntermediateRepresentation.parse
});

const generatorContext = new GeneratorContextImpl(logger, version);
const typescriptProject = await this.generateTypescriptProject({
config,
Expand Down Expand Up @@ -288,6 +298,54 @@ export abstract class AbstractGeneratorCli<CustomConfig> {
}
}

function npmPackageInfoFromPublishConfig(
publishConfig: FernIr.PublishingConfig | undefined,
isPackagePrivate: boolean
): constructNpmPackageArgs {
return (
publishConfig?._visit({
github: (publishConfig) => {
return {
...npmInfoFromPublishTarget(publishConfig.target),
repoUrl: publishConfig.uri,
publishInfo: undefined,
// TODO: add licence config
licenceConfig: undefined,
isPackagePrivate
};
},
direct: (publishConfig) => {
return { isPackagePrivate };
},
filesystem: () => {
return { isPackagePrivate };
},
_other: () => {
return { isPackagePrivate };
}
}) ?? { isPackagePrivate }
);
}

function npmInfoFromPublishTarget(
target?: FernIr.PublishTarget
): { packageName?: string; version?: string } | undefined {
return (
target?._visit({
npm: (value) => {
return {
packageName: value.packageName,
version: value.version
};
},
postman: () => undefined,
maven: () => undefined,
pypi: () => undefined,
_other: () => undefined
}) ?? undefined
);
}

class GeneratorContextImpl implements GeneratorContext {
private isSuccess = true;

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"bootstrap": "./scripts/bootstrap.sh",
"clean": "turbo clean",
"compile": "turbo compile",
"compile:cli": "turbo compile --filter=@fern-api/cli",
"compile:debug": "turbo compile:debug",
"compile:win": "pnpm -r compile",
"test": "turbo test --filter=!@fern-api/ete-tests",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@fern-api/logging-execa": "workspace:*",
"@fern-api/php-dynamic-snippets": "workspace:*",
"@fern-api/python-dynamic-snippets": "workspace:*",
"@fern-api/remote-workspace-runner": "workspace:*",
"@fern-api/ruby-dynamic-snippets": "workspace:*",
"@fern-api/rust-dynamic-snippets": "workspace:*",
"@fern-api/sdk": "0.12.3",
Expand Down
Loading
Loading