Skip to content

Commit

Permalink
fix(typescript): Fix ESM output (#5554)
Browse files Browse the repository at this point in the history
Fix ESM output
  • Loading branch information
Swimburger authored Jan 8, 2025
1 parent 65113d2 commit a4d83bf
Show file tree
Hide file tree
Showing 10,070 changed files with 40,050 additions and 49,773 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
4 changes: 4 additions & 0 deletions generators/typescript/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.46.1] - 2025-01-08

- Fix: ESModule output is fixed to be compatible with Node.js ESM loading.

## [0.46.0] - 2025-01-06

- Feat: SDKs are now built and exported in both CommonJS (legacy) and ESModule format.
Expand Down
2 changes: 1 addition & 1 deletion generators/typescript/sdk/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.46.0
0.46.1
1 change: 1 addition & 0 deletions generators/typescript/sdk/cli/browser-docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ COPY generators/typescript/utils/core-utilities/auth/src/ /assets/auth
COPY generators/typescript/utils/core-utilities/zurg/src/ /assets/zurg
COPY generators/typescript/utils/core-utilities/base/src/ /assets/base-core-utilities
COPY generators/typescript/utils/core-utilities/utils/src/ /assets/utils
COPY generators/typescript/utils/scripts/ /assets/scripts

COPY generators/typescript/sdk/cli/browser-docker/dist/browserCli.cjs /browserCli.cjs

Expand Down
1 change: 1 addition & 0 deletions generators/typescript/sdk/cli/node-docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ COPY generators/typescript/utils/core-utilities/auth/src/ /assets/auth
COPY generators/typescript/utils/core-utilities/zurg/src/ /assets/zurg
COPY generators/typescript/utils/core-utilities/base/src/ /assets/base-core-utilities
COPY generators/typescript/utils/core-utilities/utils/src/ /assets/utils
COPY generators/typescript/utils/scripts/ /assets/scripts
COPY generators/typescript/sdk/features.yml /assets/features.yml

COPY generators/typescript/sdk/cli/node-docker/dist/nodeCli.cjs /nodeCli.cjs
Expand Down
17 changes: 16 additions & 1 deletion generators/typescript/sdk/cli/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { AbstractGeneratorCli } from "@fern-typescript/abstract-generator-cli";
import { JavaScriptRuntime, NpmPackage, PersistedTypescriptProject } from "@fern-typescript/commons";
import {
JavaScriptRuntime,
NpmPackage,
PersistedTypescriptProject,
ScriptsManager,
fixImportsForEsm
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
import { SdkGenerator } from "@fern-typescript/sdk-generator";

Expand Down Expand Up @@ -152,10 +158,19 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
pathToSrc: persistedTypescriptProject.getSrcDirectory(),
pathToRoot: persistedTypescriptProject.getRootDirectory()
});
const scriptsManager = new ScriptsManager();
await scriptsManager.copyScripts({
pathToRoot: persistedTypescriptProject.getRootDirectory()
});
await this.postProcess(persistedTypescriptProject);

return persistedTypescriptProject;
}

private async postProcess(persistedTypescriptProject: PersistedTypescriptProject): Promise<void> {
await fixImportsForEsm(persistedTypescriptProject.getRootDirectory());
}

protected isPackagePrivate(customConfig: SdkCustomConfig): boolean {
return customConfig.isPackagePrivate;
}
Expand Down
2 changes: 2 additions & 0 deletions generators/typescript/utils/commons/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ export * from "./typescript-project";
export { FernWriters, ObjectWriter } from "./writers";
export { getWriterForMultiLineUnionType } from "./writers/getWriterForMultiLineUnionType";
export * from "@fern-api/typescript-base";
export { ScriptsManager } from "./scripts";
export { fixImportsForEsm } from "./typescript-project/fixImportsForEsm";
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import fs from "fs/promises";
import path from "path";

import { ScriptFile } from "./ScriptFile";

const fileName = "rename-to-esm-files.js";
const filePathOnDockerContainer = `/assets/scripts/${fileName}`;
export const renameToEsmFilesFile: ScriptFile = {
copyToFolder: async (destinationFolder: string): Promise<void> => {
await fs.copyFile(filePathOnDockerContainer, path.join(destinationFolder, fileName));
}
} as const;
3 changes: 3 additions & 0 deletions generators/typescript/utils/commons/src/scripts/ScriptFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ScriptFile = {
copyToFolder: (destinationFolder: string) => Promise<void>;
};
15 changes: 15 additions & 0 deletions generators/typescript/utils/commons/src/scripts/ScriptsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fs from "fs/promises";
import path from "path";

import { AbsoluteFilePath } from "@fern-api/fs-utils";

import { renameToEsmFilesFile } from "./RenameToEsmFilesFile";

export class ScriptsManager {
public async copyScripts({ pathToRoot }: { pathToRoot: AbsoluteFilePath }): Promise<void> {
// make sure the scripts directory exists
const scriptsDir = path.join(pathToRoot, "scripts");
await fs.mkdir(scriptsDir, { recursive: true });
await renameToEsmFilesFile.copyToFolder(scriptsDir);
}
}
1 change: 1 addition & 0 deletions generators/typescript/utils/commons/src/scripts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ScriptsManager";
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import decompress from "decompress";
import { cp, readdir, rm } from "fs/promises";
import tmp from "tmp-promise";
import { Project } from "ts-morph";
import urlJoin from "url-join";

import { AbsoluteFilePath, RelativeFilePath, join } from "@fern-api/fs-utils";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ export class SimpleTypescriptProject extends TypescriptProject {

const cjsFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.CJS_DIRECTORY}/index.js`;
const cjsTypesFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.CJS_DIRECTORY}/index.d.ts`;
const mjsFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/index.js`;
const mjsTypesFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/index.d.ts`;
const mjsFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/index.mjs`;
const mjsTypesFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/index.d.mts`;
const defaultTypesExport = this.outputEsm ? mjsTypesFile : cjsTypesFile;
const defaultExport = this.outputEsm ? mjsFile : cjsFile;

Expand Down Expand Up @@ -251,8 +251,8 @@ export class SimpleTypescriptProject extends TypescriptProject {
...this.getFoldersForExports().reduce((acc, folder) => {
const cjsFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.CJS_DIRECTORY}/${folder}/index.js`;
const cjsTypesFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.CJS_DIRECTORY}/${folder}/index.d.ts`;
const mjsFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/${folder}/index.js`;
const mjsTypesFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/${folder}/index.d.ts`;
const mjsFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/${folder}/index.mjs`;
const mjsTypesFile = `./${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}/${folder}/index.d.mts`;
const defaultTypesExport = this.outputEsm ? mjsTypesFile : cjsTypesFile;
const defaultExport = this.outputEsm ? mjsFile : cjsFile;

Expand All @@ -279,7 +279,10 @@ export class SimpleTypescriptProject extends TypescriptProject {
[SimpleTypescriptProject.FORMAT_SCRIPT_NAME]: "prettier . --write --ignore-unknown",
[SimpleTypescriptProject.BUILD_SCRIPT_NAME]: `yarn ${SimpleTypescriptProject.BUILD_CJS_SCRIPT_NAME} && yarn ${SimpleTypescriptProject.BUILD_ESM_SCRIPT_NAME}`,
[SimpleTypescriptProject.BUILD_CJS_SCRIPT_NAME]: `tsc --project ./${TypescriptProject.TS_CONFIG_CJS_FILENAME}`,
[SimpleTypescriptProject.BUILD_ESM_SCRIPT_NAME]: `tsc --project ./${TypescriptProject.TS_CONFIG_ESM_FILENAME}`
[SimpleTypescriptProject.BUILD_ESM_SCRIPT_NAME]: [
`tsc --project ./${TypescriptProject.TS_CONFIG_ESM_FILENAME}`,
`node ${SimpleTypescriptProject.SCRIPTS_DIRECTORY_NAME}/rename-to-esm-files.js ${SimpleTypescriptProject.DIST_DIRECTORY}/${SimpleTypescriptProject.ESM_DIRECTORY}`
].join(" && ")
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ export abstract class TypescriptProject {
protected static SRC_DIRECTORY = "src" as const;
protected static TEST_DIRECTORY = "tests" as const;
protected static DIST_DIRECTORY = "dist" as const;
protected static SCRIPTS_DIRECTORY_NAME = "scripts" as const;

protected static CJS_DIRECTORY = "cjs" as const;
protected static ESM_DIRECTORY = "esm" as const;
protected static TYPES_DIRECTORY = "types" as const;

protected static BUILD_SCRIPT_FILENAME = "build.js" as const;
protected static NODE_DIST_DIRECTORY = "node" as const;
protected static BROWSER_DIST_DIRECTORY = "browser" as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Project } from "ts-morph";

import { AbsoluteFilePath, RelativeFilePath, join } from "@fern-api/fs-utils";

export async function fixImportsForEsm(pathToProject: AbsoluteFilePath): Promise<void> {
const project = new Project({
tsConfigFilePath: join(pathToProject, RelativeFilePath.of("tsconfig.json"))
});
const typeChecker = project.getTypeChecker();
project.getSourceFiles().forEach((sourceFile) => {
// Get all imports in the file
const imports = sourceFile.getImportDeclarations();
const exports = sourceFile.getExportDeclarations();

[...imports, ...exports].forEach((importDecl) => {
const moduleSpecifier = importDecl.getModuleSpecifierValue();

if (!moduleSpecifier) {
return;
}

// Skip if it's not a relative import or already has .js extension
if (!moduleSpecifier.startsWith(".") || moduleSpecifier.endsWith(".js")) {
return;
}

// Get the referenced file path by using the TypeChecker
const importModuleSpecifier = importDecl.getModuleSpecifier();
if (importModuleSpecifier == null) {
return;
}
const symbol = typeChecker.getSymbolAtLocation(importModuleSpecifier);

const symbolSourceFile = symbol?.getValueDeclaration()?.getSourceFile();
if (symbolSourceFile) {
const filePath = symbolSourceFile.getFilePath();
let newSpecifier = moduleSpecifier;

// Case 1: Directory import resolving to an index file
if (
(filePath.endsWith("index.ts") || filePath.endsWith("index.js")) &&
!moduleSpecifier.endsWith("index") &&
!moduleSpecifier.endsWith(".ts") &&
!moduleSpecifier.endsWith(".js")
) {
newSpecifier = `${moduleSpecifier}/index.js`;
}
// Case 2: Regular .ts file import
else if (filePath.endsWith(".ts") && !moduleSpecifier.endsWith(".ts")) {
newSpecifier = `${moduleSpecifier}.js`;
}
// Case 3: Import with explicit .ts extension
else if (moduleSpecifier.endsWith(".ts")) {
newSpecifier = moduleSpecifier.replace(/\.ts$/, ".js");
}

if (newSpecifier !== moduleSpecifier) {
importDecl.setModuleSpecifier(newSpecifier);
}
}
});
});
await project.save();
}
119 changes: 119 additions & 0 deletions generators/typescript/utils/scripts/rename-to-esm-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node

const fs = require('fs').promises;
const path = require('path');

const extensionMap = {
'.js': '.mjs',
'.d.ts': '.d.mts'
};
const oldExtensions = Object.keys(extensionMap);

async function findFiles(rootPath) {
const files = [];

async function scan(directory) {
const entries = await fs.readdir(directory, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(directory, entry.name);

if (entry.isDirectory()) {
if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
await scan(fullPath);
}
} else if (entry.isFile()) {
if (oldExtensions.some(ext => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
}
}

await scan(rootPath);
return files;
}

async function updateFiles(files) {
const updatedFiles = [];
for (const file of files) {
const updated = await updateFileContents(file);
updatedFiles.push(updated);
}

console.log(`Updated imports in ${updatedFiles.length} files.`);
}

async function updateFileContents(file) {
const content = await fs.readFile(file, 'utf8');

let newContent = content;
// Update each extension type defined in the map
for (const [oldExt, newExt] of Object.entries(extensionMap)) {
const regex = new RegExp(
`(import|export)(.+from\\s+['"])(\\.\\.?\\/[^'"]+)(\\${oldExt})(['"])`,
'g'
);
newContent = newContent.replace(regex, `$1$2$3${newExt}$5`);
}

if (content !== newContent) {
await fs.writeFile(file, newContent, 'utf8');
return true;
}
return false;
}

async function renameFiles(files) {
let counter = 0;
for (const file of files) {
const ext = oldExtensions.find(ext => file.endsWith(ext));
const newExt = extensionMap[ext];

if (newExt) {
const newPath = file.slice(0, -ext.length) + newExt;
await fs.rename(file, newPath);
counter++;
}
}

console.log(`Renamed ${counter} files.`);
}

async function main() {
try {
const targetDir = process.argv[2];
if (!targetDir) {
console.error('Please provide a target directory');
process.exit(1);
}

const targetPath = path.resolve(targetDir);
const targetStats = await fs.stat(targetPath);

if (!targetStats.isDirectory()) {
console.error('The provided path is not a directory');
process.exit(1);
}

console.log(`Scanning directory: ${targetDir}`);

const files = await findFiles(targetDir);

if (files.length === 0) {
console.log('No matching files found.');
process.exit(0);
}

console.log(`Found ${files.length} files.`);
await updateFiles(files);
await renameFiles(files);
console.log('\nDone!');

} catch (error) {
console.error('An error occurred:', error.message);
process.exit(1);
}
}

main();
12 changes: 6 additions & 6 deletions seed/ts-sdk/alias-extends/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a4d83bf

Please sign in to comment.