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
8 changes: 5 additions & 3 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,8 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
return await generateAPIWorkspaces({
project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, {
commandLineApiWorkspace: argv.api,
defaultToAllApiWorkspaces: false
defaultToAllApiWorkspaces: false,
skipApiWorkspaces: argv.sdkConfig != null
}),
cliContext,
version: argv.version,
Expand Down Expand Up @@ -1087,7 +1088,8 @@ function addGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext)
return await generateAPIWorkspaces({
project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, {
commandLineApiWorkspace: argv.api,
defaultToAllApiWorkspaces: false
defaultToAllApiWorkspaces: false,
skipApiWorkspaces: argv.sdkConfig != null
}),
cliContext,
version: argv.version,
Expand Down Expand Up @@ -2803,7 +2805,7 @@ function addSdkCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
function addSdkMigrateCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext): void {
cli.command(
"migrate",
"Create a Postman SDK Config v1 file from one or more resolved Fern SDK groups",
"Create an SDK Config v1 file from one or more resolved Fern SDK groups",
(yargs) =>
yargs
.option("group", {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { generatorsYml } from "@fern-api/configuration-loader";
import { AbsoluteFilePath, RelativeFilePath } from "@fern-api/fs-utils";
import { ConjureWorkspace, OSSWorkspace } from "@fern-api/lazy-fern-workspace";
import { createGroupedSpecsTarGzArchiveSettled } from "@fern-api/local-workspace-runner";
import {
createGroupedSpecsTarGzArchiveSettled,
validateSdkConfigImportSettings
} from "@fern-api/local-workspace-runner";
import { type FernSourceArchiveRequest } from "@fern-api/remote-workspace-runner";
import { createMockTaskContext } from "@fern-api/task-context";
import { FernFiddle } from "@fern-fern/fiddle-sdk";
Expand All @@ -11,7 +14,8 @@ import { createFernSourceArchiveResolver } from "../createFernSourceArchiveResol

vi.mock("@fern-api/local-workspace-runner", async (importOriginal) => ({
...(await importOriginal<typeof import("@fern-api/local-workspace-runner")>()),
createGroupedSpecsTarGzArchiveSettled: vi.fn()
createGroupedSpecsTarGzArchiveSettled: vi.fn(),
validateSdkConfigImportSettings: vi.fn()
}));

function makeGenerator(): generatorsYml.GeneratorInvocation {
Expand Down Expand Up @@ -39,6 +43,7 @@ function makeGenerator(): generatorsYml.GeneratorInvocation {
describe("createFernSourceArchiveResolver", () => {
beforeEach(() => {
vi.mocked(createGroupedSpecsTarGzArchiveSettled).mockReset();
vi.mocked(validateSdkConfigImportSettings).mockReset();
});

it("returns an actionable error when the workspace cannot expose source specs", async () => {
Expand Down Expand Up @@ -129,7 +134,7 @@ describe("createFernSourceArchiveResolver", () => {
sdkName: "api",
sdkVersion: "1.0.0",
audiences: [],
targets: [{ language: "typescript" }]
targets: [{ language: "typescript", clientPathParameterStyle: "wrapped" }]
}
})([request])
).rejects.toMatchObject({
Expand All @@ -139,5 +144,8 @@ describe("createFernSourceArchiveResolver", () => {
expect(createGroupedSpecsTarGzArchiveSettled).toHaveBeenCalledWith(
expect.objectContaining({ audiences: { type: "select", audiences: [] } })
);
expect(validateSdkConfigImportSettings).toHaveBeenCalledWith([], {
clientPathParameterStyle: "wrapped"
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { getLatestGeneratorVersion } from "@fern-api/configuration-loader";
import { bundleRemoteOpenAPI } from "@fern-api/lazy-fern-workspace";
import { createMockTaskContext } from "@fern-api/task-context";
import { parseSdkConfigV1 } from "@postman/sdk-config/sdk-config/v1";
import { afterEach, describe, expect, it, vi } from "vitest";

import { createSdkConfigWorkspace } from "../createSdkConfigWorkspace.js";

vi.mock("@fern-api/configuration-loader", async (importOriginal) => ({
...(await importOriginal<typeof import("@fern-api/configuration-loader")>()),
getLatestGeneratorVersion: vi.fn()
}));

vi.mock("@fern-api/lazy-fern-workspace", async (importOriginal) => ({
...(await importOriginal<typeof import("@fern-api/lazy-fern-workspace")>()),
bundleRemoteOpenAPI: vi.fn()
}));

describe("createSdkConfigWorkspace", () => {
const temporaryDirectories: string[] = [];

afterEach(async () => {
vi.resetAllMocks();
vi.unstubAllGlobals();
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })));
});

it("constructs sources and resolves an omitted generator version without generators.yml", async () => {
vi.mocked(getLatestGeneratorVersion).mockResolvedValue("4.1.0");
const directory = await mkdtemp(path.join(tmpdir(), "fern-sdk-config-workspace-"));
temporaryDirectories.push(directory);
await mkdir(path.join(directory, "specs"));
await writeFile(
path.join(directory, "specs", "openapi.yml"),
"openapi: 3.0.0\ninfo:\n title: Payments\n version: 1.0.0\npaths: {}\n"
);

const { workspace, cleanup } = await createSdkConfigWorkspace({
sdkConfig: parseSdkConfigV1({
schemaVersion: "sdk-config/v1",
sdkName: "payments",
sdkVersion: "1.0.0",
source: {
specs: [
{
id: "payments",
type: "openapi",
path: "./specs/openapi.yml",
namespace: "payments"
}
]
},
api: { audiences: [] },
client: {},
package: {},
docs: {},
generation: {},
targets: [
{
language: "typescript",
output: { delivery: "files", path: "./generated/typescript" }
}
]
}),
absolutePathToConfig: path.join(directory, "sdk-config.yml"),
cliVersion: "0.0.0",
context: createMockTaskContext()
});

expect(workspace.allSpecs).toMatchObject([
{
type: "openapi",
absoluteFilepath: path.join(directory, "specs", "openapi.yml"),
namespace: "payments"
}
]);
expect(workspace.generatorsConfiguration?.groups).toMatchObject([
{
groupName: "sdk-config",
generators: [
{
name: "fernapi/fern-typescript-sdk",
version: "4.1.0",
language: "typescript",
absolutePathToLocalOutput: path.join(directory, "generated", "typescript")
}
]
}
]);
expect(workspace.generatorsConfiguration?.absolutePathToConfiguration).toBe(
path.join(directory, "sdk-config.yml")
);
expect(getLatestGeneratorVersion).toHaveBeenCalledWith(
expect.objectContaining({
generatorName: "fernapi/fern-typescript-sdk",
cliVersion: "0.0.0"
})
);
await cleanup();
});

it("preserves an explicitly pinned generator version without resolving latest", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "fern-sdk-config-workspace-"));
temporaryDirectories.push(directory);
await writeFile(
path.join(directory, "openapi.yml"),
"openapi: 3.0.0\ninfo:\n title: Payments\n version: 1.0.0\npaths: {}\n"
);

const { workspace, cleanup } = await createSdkConfigWorkspace({
sdkConfig: parseSdkConfigV1({
schemaVersion: "sdk-config/v1",
sdkName: "payments",
source: {
specs: [{ id: "payments", type: "openapi", path: "./openapi.yml" }]
},
api: {},
client: {},
package: {},
docs: {},
generation: {},
targets: [
{
language: "typescript",
generatorVersion: "4.0.0",
output: { delivery: "zip" }
}
]
}),
absolutePathToConfig: path.join(directory, "sdk-config.yml"),
cliVersion: "0.0.0",
context: createMockTaskContext()
});

expect(workspace.generatorsConfiguration?.groups[0]?.generators[0]).toMatchObject({
name: "fernapi/fern-typescript-sdk",
version: "4.0.0"
});
expect(getLatestGeneratorVersion).not.toHaveBeenCalled();
await cleanup();
});

it("materializes a bundled OpenAPI URL source and cleans it up", async () => {
vi.mocked(bundleRemoteOpenAPI).mockResolvedValue({
openapi: "3.0.0",
info: { title: "Payments", version: "1.0.0" },
paths: {}
});
const directory = await mkdtemp(path.join(tmpdir(), "fern-sdk-config-workspace-"));
temporaryDirectories.push(directory);

const created = await createSdkConfigWorkspace({
sdkConfig: parseSdkConfigV1({
schemaVersion: "sdk-config/v1",
sdkName: "payments",
source: {
specs: [
{
id: "payments",
type: "openapi",
url: "https://example.com/openapi.yaml"
}
]
},
api: {},
client: {},
package: {},
docs: {},
generation: {},
targets: [{ language: "typescript", generatorVersion: "4.0.0", output: { delivery: "zip" } }]
}),
absolutePathToConfig: path.join(directory, "sdk-config.yml"),
cliVersion: "0.0.0",
context: createMockTaskContext()
});

const materialized = created.workspace.allSpecs[0];
if (materialized?.type !== "openapi") {
throw new Error("Expected an OpenAPI specification");
}
expect(bundleRemoteOpenAPI).toHaveBeenCalledWith("https://example.com/openapi.yaml");
expect(JSON.parse(await readFile(materialized.absoluteFilepath, "utf-8"))).toMatchObject({
info: { title: "Payments" }
});

await created.cleanup();
await expect(access(materialized.absoluteFilepath)).rejects.toThrow();
});

it.each(["asyncapi", "graphql"] as const)("rejects unsupported %s URL sources", async (type) => {
const directory = await mkdtemp(path.join(tmpdir(), "fern-sdk-config-workspace-"));
temporaryDirectories.push(directory);

await expect(
createSdkConfigWorkspace({
sdkConfig: parseSdkConfigV1({
schemaVersion: "sdk-config/v1",
sdkName: "payments",
source: {
specs: [{ id: "payments", type, url: `https://example.com/${type}.yaml` }]
},
api: {},
client: {},
package: {},
docs: {},
generation: {},
targets: [{ language: "typescript", generatorVersion: "4.0.0", output: { delivery: "zip" } }]
}),
absolutePathToConfig: path.join(directory, "sdk-config.yml"),
cliVersion: "0.0.0",
context: {
failAndThrow: (message: string) => {
throw new Error(message);
}
} as never
})
).rejects.toThrow(`SDK Config ${type} URL source 'payments' is not supported`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ describe("loadSdkConfigV1", () => {
generatorVersion: "4.0.0",
sdkName: "petstore-node",
sdkVersion: "2.0.0",
clientPathParameterStyle: "language-default"
clientPathParameterStyle: "language-default",
requestedOutput: { type: "download" },
absolutePathToLocalOutputArchive: join(directory, "generated", "typescript.zip")
}
]
});
Expand Down Expand Up @@ -95,4 +97,81 @@ describe("loadSdkConfigV1", () => {
payload: { sdkName: "petstore", targets: [{ language: "typescript" }] }
});
});

it("projects SDK Config GitHub delivery and package metadata for the remote request", async () => {
const directory = await mkdtemp(join(tmpdir(), "fern-sdk-config-"));
temporaryDirectories.push(directory);
const configPath = join(directory, "sdk-config.yml");
await writeFile(
configPath,
YAML.stringify({
schemaVersion: "sdk-config/v1",
sdkName: "petstore",
source: { specs: [{ id: "openapi", type: "openapi", path: "./openapi.yml" }] },
api: {},
client: {},
package: { packageName: "@acme/sdk" },
docs: {},
generation: {},
output: {
delivery: "github",
github: { repository: "acme/sdk", mode: "pull-request" },
publish: { registry: "npm" }
},
targets: [{ language: "typescript", generatorVersion: "4.0.0" }]
})
);

await expect(loadSdkConfigV1(configPath)).resolves.toMatchObject({
payload: {
targets: [
{
package: { packageName: "@acme/sdk" },
requestedOutput: {
type: "github",
repository: "acme/sdk",
mode: "pull-request",
publish: { registry: "npm" }
}
}
]
}
});
});

it("resolves a configured ZIP filename relative to sdk-config.yml", async () => {
const directory = await mkdtemp(join(tmpdir(), "fern-sdk-config-"));
temporaryDirectories.push(directory);
const configPath = join(directory, "sdk-config.yml");
await writeFile(
configPath,
YAML.stringify({
schemaVersion: "sdk-config/v1",
sdkName: "petstore",
source: { specs: [{ id: "openapi", type: "openapi", path: "./openapi.yml" }] },
api: {},
client: {},
package: {},
docs: {},
generation: {},
targets: [
{
language: "typescript",
output: { delivery: "zip", fileName: "./artifacts/petstore.zip" }
}
]
})
);

await expect(loadSdkConfigV1(configPath)).resolves.toMatchObject({
payload: {
targets: [
{
requestedOutput: { type: "download" },
absolutePathToLocalOutputArchive: join(directory, "artifacts", "petstore.zip")
}
]
}
});
});
});
Loading
Loading