Skip to content
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
57 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
5b7119e
maybe fix publish mode
jsklan Oct 31, 2025
2f86eca
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Nov 5, 2025
b6af440
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Nov 6, 2025
67fbda3
simplify funcs
jsklan Nov 6, 2025
138120f
use parseRepo func from github commons
jsklan Nov 6, 2025
1339f9f
fix get package name from invocation
jsklan Nov 6, 2025
a2f2911
fix version resolution in local mode
jsklan Nov 6, 2025
47a64a3
fix url construction in package.json
jsklan Nov 6, 2025
65f2772
fix license in package.json
jsklan Nov 6, 2025
4d0f9b6
resolve tmp file for snippet json when org is selfhosted
jsklan Nov 6, 2025
f8b49b2
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Nov 6, 2025
bc77ff8
format
jsklan Nov 6, 2025
05f7df9
add reference to remote workspace runner
jsklan Nov 6, 2025
315bcf7
Fix breaking code path in getRemote
jsklan Nov 6, 2025
1ce9b57
update versions files
jsklan Nov 6, 2025
53e2509
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Nov 6, 2025
87d8ab8
copy license file with streams
jsklan Nov 6, 2025
efc554f
nit
jsklan Nov 6, 2025
5a0f485
format
jsklan Nov 6, 2025
f028f38
use copyFile
jsklan Nov 6, 2025
3d0bbd1
Merge branch 'main' into jsklan/ts-remote-local-parity-manual
jsklan Nov 6, 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
31 changes: 29 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,44 @@ 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
});
}

try {
const githubConfig = this.getGitHubConfig({ context });
if (githubConfig.uri != null && githubConfig.token != null) {
return FernGeneratorCli.Remote.github({
repoUrl: this.normalizeRepoUrl(githubConfig.uri),
installationToken: githubConfig.token
});
}
} catch (error) {
return undefined;
}
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 @@ -65,7 +65,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";
47 changes: 47 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,7 @@ import {
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
import { SdkGenerator } from "@fern-typescript/sdk-generator";
import path from "path";

import { SdkCustomConfig } from "./custom-config/SdkCustomConfig";
import { SdkCustomConfigSchema } from "./custom-config/schema/SdkCustomConfigSchema";
Expand Down Expand Up @@ -251,6 +252,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 @@ -265,6 +267,51 @@ 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 ?? "LICENSE";
const licenseFilePath = path.join(rootDirectory, licenseFileName);

await this.copyLicenseFile(licenseFilePath);
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 copyLicenseFile(destinationPath: string): Promise<void> {
const fs = await import("fs");
const { pipeline } = await import("stream/promises");

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

try {
const readStream = fs.createReadStream(dockerLicensePath, { encoding: "utf-8" });
const writeStream = fs.createWriteStream(destinationPath, { encoding: "utf-8" });

await pipeline(readStream, writeStream);
} catch (error) {
throw new Error(
`Could not copy 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 @@ -1274,6 +1274,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.28.2
changelogEntry:
- summary: |
Fix local GitHub generation to match remote generation.
type: fix
createdAt: "2025-11-06"
irVersion: 61

- version: 3.28.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 @@ -73,11 +79,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(config, ir.publishConfig, this.isPackagePrivate(customConfig))
)
: constructNpmPackage({
generatorConfig: config,
isPackagePrivate: this.isPackagePrivate(customConfig)
});

await generatorNotificationService.sendUpdate(
FernGeneratorExec.GeneratorUpdate.initV2({
publishingToRegistry:
Expand All @@ -92,11 +107,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 @@ -299,6 +309,33 @@ export abstract class AbstractGeneratorCli<CustomConfig> {
}
}

function npmPackageInfoFromPublishConfig(
config: FernGeneratorExec.GeneratorConfig,
publishConfig: FernIr.PublishingConfig | undefined,
isPackagePrivate: boolean
): constructNpmPackageArgs {
let args = {};
if (publishConfig?.type === "github") {
if (publishConfig.target?.type === "npm") {
const repoUrl =
publishConfig.repo != null && publishConfig.owner != null
? `https://github.com/${publishConfig.owner}/${publishConfig.repo}`
: publishConfig.uri;
args = {
packageName: publishConfig.target.packageName,
version: publishConfig.target.version,
repoUrl,
publishInfo: undefined,
licenseConfig: config.license
};
}
}
return {
...args,
isPackagePrivate
};
}

class GeneratorContextImpl implements GeneratorContext {
private isSuccess = true;

Expand Down
8 changes: 8 additions & 0 deletions packages/cli/cli/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
- changelogEntry:
- summary: |
Fix TypeScript SDK generator's local GitHub generation to match remote generation.
type: fix
irVersion: 61
createdAt: "2025-11-06"
version: 0.108.1

- changelogEntry:
- summary: |
Add position-based sorting for folder navigation in docs.yml. Pages can now control their order using a `position` field in frontmatter. Pages with position values sort first (ascending), then pages without position sort alphabetically.
Expand Down
1 change: 1 addition & 0 deletions packages/cli/configuration-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@fern-api/fs-utils": "workspace:*",
"@fern-api/task-context": "workspace:*",
"@fern-api/fdr-sdk": "0.139.43-309880147",
"@fern-api/github": "workspace:*",
"@fern-fern/fiddle-sdk": "0.0.656",
"@fern-fern/generators-sdk": "0.114.0-5745f9e74",
"find-up": "^6.3.0",
Expand Down
Loading