Skip to content
Closed
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 @@ -76,7 +76,7 @@ export function constructNpmPackage({
}
}

function getRepoUrlFromUrl(repoUrl: string | undefined): string | undefined {
export function getRepoUrlFromUrl(repoUrl: string | undefined): string | undefined {
if (repoUrl == null) {
return undefined;
}
Expand Down
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.39.3
changelogEntry:
- summary: |
Make package.json generation more reliable.
type: fix
createdAt: "2025-12-08"
irVersion: 62

- version: 3.39.2
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ 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 { FernIr, serialization } from "@fern-fern/ir-sdk";
import { serialization } from "@fern-fern/ir-sdk";
import { IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
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";
import { npmPackageInfoFromPublishConfig } from "./npmPackageInfoFromPublishConfig";
import { publishPackage } from "./publishPackage";
import { writeGenerationMetadata } from "./writeGenerationMetadata";
import { writeGitHubWorkflows } from "./writeGitHubWorkflows";
Expand Down Expand Up @@ -85,9 +85,13 @@ export abstract class AbstractGeneratorCli<CustomConfig> {
});

const npmPackage = ir.selfHosted
? constructNpmPackageFromArgs(
? (constructNpmPackageFromArgs(
npmPackageInfoFromPublishConfig(config, ir.publishConfig, this.isPackagePrivate(customConfig))
)
) ??
constructNpmPackage({
generatorConfig: config,
isPackagePrivate: this.isPackagePrivate(customConfig)
}))
: constructNpmPackage({
generatorConfig: config,
isPackagePrivate: this.isPackagePrivate(customConfig)
Expand Down Expand Up @@ -299,33 +303,6 @@ 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { FernGeneratorExec } from "@fern-api/base-generator";
import { FernIr } from "@fern-fern/ir-sdk";
import { describe, expect, it } from "vitest";
import { npmPackageInfoFromPublishConfig } from "../npmPackageInfoFromPublishConfig";

describe("npmPackageInfoFromPublishConfig", () => {
const createMockConfig = (license?: FernGeneratorExec.LicenseConfig): FernGeneratorExec.GeneratorConfig => {
return {
dryRun: false,
irFilepath: "/path/to/ir.json",
output: {
path: "/output",
mode: FernGeneratorExec.OutputMode.downloadFiles()
},
organization: "test-org",
workspaceName: "test-workspace",
environment: {
type: "local"
},
customConfig: {},
whitelabel: false,
writeUnitTests: false,
generateOauthClients: false,
generatePaginatedClients: false,
license
} as FernGeneratorExec.GeneratorConfig;
};

describe("github publishConfig with npm target", () => {
it("extracts packageName and version from github publishConfig with npm target", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.github({
owner: "payabli",
repo: "sdk-node",
uri: undefined,
token: undefined,
mode: undefined,
target: FernIr.PublishTarget.npm({
packageName: "@payabli/sdk-node",
version: "0.0.116",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.packageName).toBe("@payabli/sdk-node");
expect(result.version).toBe("0.0.116");
expect(result.repoUrl).toBe("github:payabli/sdk-node");
expect(result.publishInfo).toBeUndefined();
expect(result.isPackagePrivate).toBe(false);
});

it("uses uri as fallback when owner/repo are null", () => {
const config = createMockConfig();
// Note: The GithubPublish type requires owner and repo as strings,
// but the code checks for != null, so we test with the uri fallback
// by providing a uri and checking that owner/repo take precedence when present
const publishConfig = FernIr.PublishingConfig.github({
owner: "custom",
repo: "repo",
uri: "https://github.com/fallback/uri",
token: undefined,
mode: undefined,
target: FernIr.PublishTarget.npm({
packageName: "@custom/sdk",
version: "1.0.0",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.packageName).toBe("@custom/sdk");
expect(result.version).toBe("1.0.0");
// owner/repo take precedence over uri
expect(result.repoUrl).toBe("github:custom/repo");
});

it("preserves license config", () => {
const license = FernGeneratorExec.LicenseConfig.basic({
id: "MIT"
});
const config = createMockConfig(license);
const publishConfig = FernIr.PublishingConfig.github({
owner: "test",
repo: "repo",
uri: undefined,
token: undefined,
mode: undefined,
target: FernIr.PublishTarget.npm({
packageName: "@test/sdk",
version: "1.0.0",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.licenseConfig).toBe(license);
});
});

describe("filesystem publishConfig with npm publishTarget", () => {
it("extracts packageName and version from filesystem publishConfig with npm publishTarget", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.filesystem({
generateFullProject: true,
publishTarget: FernIr.PublishTarget.npm({
packageName: "@payabli/sdk-node",
version: "0.0.116",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.packageName).toBe("@payabli/sdk-node");
expect(result.version).toBe("0.0.116");
expect(result.repoUrl).toBeUndefined();
expect(result.publishInfo).toBeUndefined();
expect(result.isPackagePrivate).toBe(false);
});

it("handles filesystem publishConfig without publishTarget", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.filesystem({
generateFullProject: true,
publishTarget: undefined
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.packageName).toBeUndefined();
expect(result.version).toBeUndefined();
expect(result.isPackagePrivate).toBe(false);
});
});

describe("direct publishConfig with npm target", () => {
it("extracts packageName and version from direct publishConfig with npm target", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.direct({
target: FernIr.PublishTarget.npm({
packageName: "@direct/sdk",
version: "2.0.0",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, true);

expect(result.packageName).toBe("@direct/sdk");
expect(result.version).toBe("2.0.0");
expect(result.repoUrl).toBeUndefined();
expect(result.publishInfo).toBeUndefined();
expect(result.isPackagePrivate).toBe(true);
});
});

describe("non-npm publish targets", () => {
it("returns empty args for maven target", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.filesystem({
generateFullProject: true,
publishTarget: FernIr.PublishTarget.maven({
coordinate: "com.example:sdk",
version: "1.0.0",
usernameEnvironmentVariable: "MAVEN_USERNAME",
passwordEnvironmentVariable: "MAVEN_PASSWORD",
mavenUrlEnvironmentVariable: "MAVEN_URL"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.packageName).toBeUndefined();
expect(result.version).toBeUndefined();
expect(result.isPackagePrivate).toBe(false);
});

it("returns empty args for pypi target", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.github({
owner: "test",
repo: "repo",
uri: undefined,
token: undefined,
mode: undefined,
target: FernIr.PublishTarget.pypi({
packageName: "test-sdk",
version: "1.0.0"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.packageName).toBeUndefined();
expect(result.version).toBeUndefined();
expect(result.isPackagePrivate).toBe(false);
});
});

describe("undefined publishConfig", () => {
it("returns only isPackagePrivate when publishConfig is undefined", () => {
const config = createMockConfig();

const result = npmPackageInfoFromPublishConfig(config, undefined, true);

expect(result.packageName).toBeUndefined();
expect(result.version).toBeUndefined();
expect(result.repoUrl).toBeUndefined();
expect(result.publishInfo).toBeUndefined();
expect(result.isPackagePrivate).toBe(true);
});
});

describe("isPackagePrivate flag", () => {
it("sets isPackagePrivate to true when specified", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.filesystem({
generateFullProject: true,
publishTarget: FernIr.PublishTarget.npm({
packageName: "@private/sdk",
version: "1.0.0",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, true);

expect(result.isPackagePrivate).toBe(true);
});

it("sets isPackagePrivate to false when specified", () => {
const config = createMockConfig();
const publishConfig = FernIr.PublishingConfig.filesystem({
generateFullProject: true,
publishTarget: FernIr.PublishTarget.npm({
packageName: "@public/sdk",
version: "1.0.0",
tokenEnvironmentVariable: "NPM_TOKEN"
})
});

const result = npmPackageInfoFromPublishConfig(config, publishConfig, false);

expect(result.isPackagePrivate).toBe(false);
});
});
});
Loading
Loading