Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
beb6561
perf(typescript): optimize fixImportsForEsm with merged regex, early …
devin-ai-integration[bot] Mar 26, 2026
4000e5b
chore(typescript): restore batch=100 for file processing (unbounded i…
devin-ai-integration[bot] Mar 26, 2026
fa96766
style: fix biome formatting (single-line function call)
devin-ai-integration[bot] Mar 26, 2026
0b0b775
perf(typescript): add in-memory ESM import processing (Tier 1 optimiz…
devin-ai-integration[bot] Mar 26, 2026
3e74192
feat(typescript): eliminate full disk pass with targeted core-only pr…
devin-ai-integration[bot] Mar 26, 2026
3bdaef9
perf(typescript): optimize post-persist pass to target only core/ + s…
devin-ai-integration[bot] Mar 26, 2026
c66d3c2
fix: add existsSync guard in fixImportsForCoreFiles for missing direc…
devin-ai-integration[bot] Mar 26, 2026
fd08be6
test: add comprehensive unit tests for fixImportsForEsm
devin-ai-integration[bot] Mar 26, 2026
b8c28df
fix: remove duplicate file write in test (Graphite review)
devin-ai-integration[bot] Mar 26, 2026
b2218b0
perf(typescript): move copyCoreUtilities into memfs Volume, eliminate…
devin-ai-integration[bot] Mar 26, 2026
272633b
style: fix biome formatting issues
devin-ai-integration[bot] Mar 26, 2026
dfde05c
fix: correct write order — copyCoreUtilitiesToVolume runs after write…
devin-ai-integration[bot] Mar 26, 2026
8dec2c6
style: fix biome formatting for persist() method signature
devin-ai-integration[bot] Mar 26, 2026
1af6c68
refactor: replace .then() chain with async/await per CLAUDE.md rules
devin-ai-integration[bot] Mar 26, 2026
55dc68d
refactor: deduplicate core utility copy logic, encapsulate volume, re…
devin-ai-integration[bot] Mar 26, 2026
d1779ca
refactor: simplify persist() API, remove dead exports, clean up copyC…
devin-ai-integration[bot] Mar 26, 2026
191ad01
feat: move public exports and template processing into memfs Volume
devin-ai-integration[bot] Mar 27, 2026
11179ac
fix: self-review fixes + add unit tests for generatePublicExportsToVo…
Swimburger Mar 27, 2026
2fef785
refactor: replace MemfsVolume interface with direct memfs Volume impo…
Swimburger Mar 28, 2026
2189ffa
fix: add dirent type assertions for memfs Volume.readdirSync in Publi…
Swimburger Mar 28, 2026
b1f9f9c
refactor: consolidate to single in-memory code path for both legacy a…
Swimburger Mar 28, 2026
9b31393
fix: normalize packagePath before passing to fixImportsInVolume
Swimburger Mar 28, 2026
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 @@ -96,7 +96,6 @@ export class ExpressGeneratorCli extends AbstractGeneratorCli<ExpressCustomConfi
const typescriptProject = await expressGenerator.generate();
const persistedTypescriptProject = await typescriptProject.persist();
await expressGenerator.copyCoreUtilities({
pathToSrc: persistedTypescriptProject.getSrcDirectory(),
pathToRoot: persistedTypescriptProject.getRootDirectory()
});

Expand Down
13 changes: 2 additions & 11 deletions generators/typescript/express/generator/src/ExpressGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,17 +315,8 @@ export class ExpressGenerator {
});
}

public async copyCoreUtilities({
pathToSrc,
pathToRoot
}: {
pathToSrc: AbsoluteFilePath;
pathToRoot: AbsoluteFilePath;
}): Promise<void> {
await this.coreUtilitiesManager.copyCoreUtilities({
pathToSrc,
pathToRoot
});
public async copyCoreUtilities({ pathToRoot }: { pathToRoot: AbsoluteFilePath }): Promise<void> {
await this.coreUtilitiesManager.copyCoreUtilities({ pathToRoot });
}

private getTypesToGenerate(): Record<FernIr.TypeId, FernIr.TypeDeclaration> {
Expand Down
39 changes: 25 additions & 14 deletions generators/typescript/sdk/cli/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { FernIr } from "@fern-fern/ir-sdk";
import { AbstractGeneratorCli } from "@fern-typescript/abstract-generator-cli";
import {
convertJestImportsToVitest,
fixImportsForEsm,
fixImportsInVolume,
type MemfsVolume,
NpmPackage,
PersistedTypescriptProject,
writeTemplateFiles
writeTemplateFilesToVolume
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
import { SdkGenerator } from "@fern-typescript/sdk-generator";
Expand Down Expand Up @@ -257,16 +258,29 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
}
});
const typescriptProject = await sdkGenerator.generate();
const persistedTypescriptProject = await typescriptProject.persist();
const rootDirectory = persistedTypescriptProject.getRootDirectory();
await sdkGenerator.copyCoreUtilities({
pathToSrc: persistedTypescriptProject.getSrcDirectory(),
pathToRoot: rootDirectory
});
await sdkGenerator.generatePublicExports({
pathToSrc: persistedTypescriptProject.getSrcDirectory()
// Single in-memory code path for both legacy and non-legacy exports.
// The volumeHook runs AFTER writeSrcToVolume inside persist() but
// BEFORE writing to disk. Operations MUST execute in this order:
// 1. copyCoreUtilitiesToVolume — populates Volume with core files
// (overwrites ts-morph barrel exports at overlapping paths)
// 2. writeTemplateFilesToVolume — renders .template.ts → .ts
// (needs core files from step 1 to exist)
// 3. generatePublicExportsToVolume — creates exports.ts re-export chain
// (needs core/*/exports.ts from step 1)
// 4. fixImportsInVolume — adds .js extensions to all import specifiers
// (needs all files from steps 1-3; only for non-legacy exports)
const templateVariables = this.getTemplateVariables(customConfig);
const rawPackagePath = customConfig.packagePath ?? "src";
const packagePath = rawPackagePath.replace(/^\//, "").replace(/\/$/, "") || "src";
const persistedTypescriptProject = await typescriptProject.persist(async (volume: MemfsVolume) => {
await sdkGenerator.copyCoreUtilitiesToVolume(volume);
writeTemplateFilesToVolume(volume, templateVariables);
sdkGenerator.generatePublicExportsToVolume(volume);
if (!customConfig.useLegacyExports) {
fixImportsInVolume(volume, packagePath);
}
});
await writeTemplateFiles(rootDirectory, this.getTemplateVariables(customConfig));
const rootDirectory = persistedTypescriptProject.getRootDirectory();
await this.writeLicenseFile(config, rootDirectory, generatorContext.logger);
await this.postProcess(persistedTypescriptProject, customConfig);

Expand Down Expand Up @@ -325,9 +339,6 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
_customConfig: SdkCustomConfig
): Promise<void> {
const customConfig = this.customConfigWithOverrides(_customConfig);
if (customConfig.useLegacyExports === false) {
await fixImportsForEsm(persistedTypescriptProject.getRootDirectory());
}
if (customConfig.testFramework === "vitest") {
await convertJestImportsToVitest(
persistedTypescriptProject.getRootDirectory(),
Expand Down
19 changes: 6 additions & 13 deletions generators/typescript/sdk/generator/src/SdkGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getFullPathForEndpoint,
getTextOfTsNode,
ImportsManager,
type MemfsVolume,
NpmPackage,
PackageId,
PublicExportsManager,
Expand Down Expand Up @@ -782,21 +783,13 @@ export class SdkGenerator {
return this.intermediateRepresentation.types;
}

public async copyCoreUtilities({
pathToSrc,
pathToRoot
}: {
pathToSrc: AbsoluteFilePath;
pathToRoot: AbsoluteFilePath;
}): Promise<void> {
await this.coreUtilitiesManager.copyCoreUtilities({ pathToSrc, pathToRoot });
public async copyCoreUtilitiesToVolume(volume: MemfsVolume): Promise<void> {
await this.coreUtilitiesManager.copyCoreUtilitiesToVolume(volume);
}

public async generatePublicExports({ pathToSrc }: { pathToSrc: AbsoluteFilePath }): Promise<void> {
await this.publicExportsManager.generatePublicExportsFiles({
pathToSrc
});
this.context.logger.debug("Generated public exports");
public generatePublicExportsToVolume(volume: MemfsVolume): void {
this.publicExportsManager.generatePublicExportsToVolume(volume, this.relativePackagePath);
this.context.logger.debug("Generated public exports to volume");
}

private generateTypeDeclarations() {
Expand Down
13 changes: 13 additions & 0 deletions generators/typescript/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.59.5
changelogEntry:
- summary: |
Optimize ESM import fixup to process all files in a single memfs Volume
before writing to disk. All file generation (source files, core utilities,
public exports, and template rendering) now happens in-memory, eliminating
the post-persist disk passes for fixImportsForCoreFiles and
generatePublicExports entirely.
At Stripe scale (5K files), import fixup time drops from ~777ms to ~158ms (80% faster).
type: chore
createdAt: "2026-03-26"
irVersion: 65

- version: 3.59.4
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Volume } from "memfs/lib/volume";
import { beforeEach, describe, expect, it } from "vitest";
import { writeTemplateFilesToVolume } from "../writeTemplateFiles.js";

describe("writeTemplateFilesToVolume", () => {
let volume: InstanceType<typeof Volume>;

beforeEach(() => {
volume = new Volume();
});

it("renders a template file and removes the original", () => {
volume.mkdirSync("/src/core/fetcher", { recursive: true });
volume.writeFileSync(
"/src/core/fetcher/getFetchFn.template.ts",
'export async function getFetchFn(): Promise<any> {\n <% if (it.fetchSupport === "native") { %>return fetch;<% } else { %>return (await import("node-fetch")).default;<% } %>\n}\n'
);

writeTemplateFilesToVolume(volume, { fetchSupport: "native" });

// Rendered file should exist
expect(volume.existsSync("/src/core/fetcher/getFetchFn.ts")).toBe(true);
const content = volume.readFileSync("/src/core/fetcher/getFetchFn.ts", "utf-8").toString();
expect(content).toContain("return fetch;");
expect(content).not.toContain("<%");
expect(content).not.toContain("%>");

// Template file should be removed
expect(volume.existsSync("/src/core/fetcher/getFetchFn.template.ts")).toBe(false);
});

it("renders template with different variable values", () => {
volume.mkdirSync("/src/core/fetcher", { recursive: true });
volume.writeFileSync(
"/src/core/fetcher/getFetchFn.template.ts",
'<% if (it.fetchSupport === "native") { %>NATIVE<% } else { %>NODE_FETCH<% } %>'
);

writeTemplateFilesToVolume(volume, { fetchSupport: "node-fetch" });

const content = volume.readFileSync("/src/core/fetcher/getFetchFn.ts", "utf-8").toString();
expect(content).toContain("NODE_FETCH");
expect(content).not.toContain("NATIVE");
});

it("processes multiple template files", () => {
volume.mkdirSync("/src/core/fetcher", { recursive: true });
volume.writeFileSync(
"/src/core/fetcher/getFetchFn.template.ts",
"export const fetch = <%= it.fetchSupport %>;"
);
volume.writeFileSync(
"/src/core/fetcher/getResponseBody.template.ts",
"export const stream = <%= it.streamType %>;"
);

writeTemplateFilesToVolume(volume, { fetchSupport: '"native"', streamType: '"web"' });

expect(volume.existsSync("/src/core/fetcher/getFetchFn.ts")).toBe(true);
expect(volume.existsSync("/src/core/fetcher/getResponseBody.ts")).toBe(true);
expect(volume.existsSync("/src/core/fetcher/getFetchFn.template.ts")).toBe(false);
expect(volume.existsSync("/src/core/fetcher/getResponseBody.template.ts")).toBe(false);
});

it("does nothing when no template files exist", () => {
volume.mkdirSync("/src/core/fetcher", { recursive: true });
volume.writeFileSync("/src/core/fetcher/Fetcher.ts", "export class Fetcher {}\n");

writeTemplateFilesToVolume(volume, {});

// Existing file should be untouched
const content = volume.readFileSync("/src/core/fetcher/Fetcher.ts", "utf-8").toString();
expect(content).toBe("export class Fetcher {}\n");
});

it("only processes files with .template. in the name", () => {
volume.mkdirSync("/src", { recursive: true });
volume.writeFileSync("/src/regular.ts", "export const x = 1;\n");
volume.writeFileSync("/src/template.ts", "export const y = 2;\n");
volume.writeFileSync("/src/myFile.template.ts", "export const z = <%= it.value %>;");

writeTemplateFilesToVolume(volume, { value: "42" });

// Only the .template. file should be processed
expect(volume.readFileSync("/src/regular.ts", "utf-8").toString()).toBe("export const x = 1;\n");
expect(volume.readFileSync("/src/template.ts", "utf-8").toString()).toBe("export const y = 2;\n");
expect(volume.existsSync("/src/myFile.ts")).toBe(true);
expect(volume.readFileSync("/src/myFile.ts", "utf-8").toString()).toBe("export const z = 42;");
expect(volume.existsSync("/src/myFile.template.ts")).toBe(false);
});

it("handles nested directory structures", () => {
volume.mkdirSync("/src/core/fetcher/internal", { recursive: true });
volume.writeFileSync("/src/core/fetcher/internal/helper.template.ts", "export const x = <%= it.val %>;");

writeTemplateFilesToVolume(volume, { val: "true" });

expect(volume.existsSync("/src/core/fetcher/internal/helper.ts")).toBe(true);
expect(volume.readFileSync("/src/core/fetcher/internal/helper.ts", "utf-8").toString()).toBe(
"export const x = true;"
);
expect(volume.existsSync("/src/core/fetcher/internal/helper.template.ts")).toBe(false);
});
});
Loading
Loading