Skip to content

Commit

Permalink
feat: Convert files with Shebang (#!) (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Nov 18, 2024
1 parent e826bb8 commit a0ca7a7
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 55 deletions.
3 changes: 3 additions & 0 deletions e2e-tests.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Repository,Commit,First Working Version
https://github.com/cornerstonejs/cornerstone3D,d70e58f4be761fe9d35ece1956bec78073a80e45,ts2esm@2.0.1
https://github.com/southpolecarbon/emission-factors-etl,d98c141653ab253e2e4a837c8e1af2c2d879fc84,ts2esm@2.2.4
2 changes: 0 additions & 2 deletions e2e-tests.txt

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
"clean": "rimraf .nyc_output coverage dist",
"deploy": "exit 0",
"dev": "npm run test:types && node --no-warnings=ExperimentalWarning --inspect --loader ts-node/esm ./src/cli.ts",
"dev:test1": "npm run dev -- ../../temp/cornerstone3D/tsconfig.json --debug",
"dev:test2": "npm run dev -- ../../southpolecarbon/emission-factors-etl/tsconfig.json --debug",
"dev:corner": "npm run dev -- ../../temp/cornerstone3D/tsconfig.json --debug",
"dev:etl": "npm run dev -- ../../southpolecarbon/emission-factors-etl/tsconfig.json --debug",
"dist": "npm run clean && npm run build",
"docs": "exit 0",
"fix": "npm run fix:format && npm run fix:lint",
Expand Down
22 changes: 17 additions & 5 deletions src/converter/convertFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('convertFile', () => {
const sourceFile = project.getSourceFile('main.ts')!;
const modifiedFile = convertFile(sourceFile);

await expect(modifiedFile?.getText()).toMatchFileSnapshot(snapshot);
await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot);
});

it('fixes imports when tsconfig has an "include" property', async () => {
Expand All @@ -27,7 +27,7 @@ describe('convertFile', () => {
const sourceFile = project.getSourceFile('consumer.ts')!;
const modifiedFile = convertFile(sourceFile);

await expect(modifiedFile?.getText()).toMatchFileSnapshot(snapshot);
await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot);
});

it('turns CJS require statements into ESM imports', async () => {
Expand All @@ -39,7 +39,7 @@ describe('convertFile', () => {
const sourceFile = project.getSourceFile('main.ts')!;
const modifiedFile = convertFile(sourceFile);

await expect(modifiedFile?.getText()).toMatchFileSnapshot(snapshot);
await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot);
});

it('handles index files referenced with a trailing slash', async () => {
Expand All @@ -51,7 +51,19 @@ describe('convertFile', () => {
const sourceFile = project.getSourceFile('main.ts')!;
const modifiedFile = convertFile(sourceFile);

await expect(modifiedFile?.getText()).toMatchFileSnapshot(snapshot);
await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot);
});

it('handles files with a Shebang (#!) at the beginning', async () => {
const projectDir = path.join(fixtures, 'cjs-shebang');
const projectConfig = path.join(projectDir, 'tsconfig.json');
const snapshot = path.join(projectDir, 'src', 'main.snap.ts');
const project = ProjectUtil.getProject(projectConfig);

const sourceFile = project.getSourceFile('main.ts')!;
const modifiedFile = convertFile(sourceFile);

await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot);
});
});

Expand All @@ -65,7 +77,7 @@ describe('convertFile', () => {
const sourceFile = project.getSourceFile('main.ts')!;
const modifiedFile = convertFile(sourceFile);

await expect(modifiedFile?.getText()).toMatchFileSnapshot(snapshot);
await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot);
});
});
});
28 changes: 16 additions & 12 deletions src/converter/replacer/replaceFileExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ export function replaceFileExtensions(sourceFile: SourceFile, type: 'import' | '
const identifier = type === 'import' ? 'getImportDeclarations' : 'getExportDeclarations';

sourceFile[identifier]().forEach(declaration => {
declaration.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach(stringLiteral => {
const hasAttributesClause = !!declaration.getAttributes();
const adjustedImport = replaceModulePath({
hasAttributesClause,
paths,
projectDirectory,
sourceFilePath: sourceFile.getFilePath(),
stringLiteral,
try {
declaration.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach(stringLiteral => {
const hasAttributesClause = !!declaration.getAttributes();
const adjustedImport = replaceModulePath({
hasAttributesClause,
paths,
projectDirectory,
sourceFilePath: sourceFile.getFilePath(),
stringLiteral,
});
if (adjustedImport) {
madeChanges = true;
}
});
if (adjustedImport) {
madeChanges = true;
}
});
} catch (error: unknown) {
console.error(` There was an issue with "${sourceFile.getFilePath()}":`, error);
}
});

return madeChanges;
Expand Down
66 changes: 37 additions & 29 deletions src/converter/replacer/replaceModuleExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,55 @@ export function replaceModuleExports(sourceFile: SourceFile) {

// Iterate through all statements in the source file
sourceFile.getStatements().forEach(statement => {
// Check if the statement is an ExpressionStatement
if (statement.getKind() === SyntaxKind.ExpressionStatement) {
const expressionStatement = statement.asKind(SyntaxKind.ExpressionStatement);
if (!expressionStatement) {
return;
}
const expression = expressionStatement.getExpression();
if (expression.getKind() === SyntaxKind.BinaryExpression) {
const binaryExpression = expression.asKind(SyntaxKind.BinaryExpression);
if (!binaryExpression) {
try {
if (statement.getKind() === SyntaxKind.ExpressionStatement) {
const expressionStatement = statement.asKind(SyntaxKind.ExpressionStatement);
if (!expressionStatement) {
return;
}
const left = binaryExpression.getLeft().getText();
const right = binaryExpression.getRight().getText();
const expression = expressionStatement.getExpression();
if (expression.getKind() === SyntaxKind.BinaryExpression) {
const binaryExpression = expression.asKind(SyntaxKind.BinaryExpression);
if (!binaryExpression) {
return;
}
const left = binaryExpression.getLeft().getText();
const right = binaryExpression.getRight().getText();

if (left === 'module.exports') {
defaultExport = right;
statement.remove();
} else if (left.startsWith('module.exports.')) {
const exportName = left.split('.')[2];
if (exportName) {
namedExports.push(exportName);
if (left === 'module.exports') {
defaultExport = right;
statement.remove();
} else if (left.startsWith('module.exports.')) {
const exportName = left.split('.')[2];
if (exportName) {
namedExports.push(exportName);
statement.remove();
}
}
}
}
} catch (error: unknown) {
console.error(` There was an issue with "${sourceFile.getFilePath()}":`, error);
process.exit(1);
}
});

if (defaultExport) {
sourceFile.addExportAssignment({
expression: defaultExport,
isExportEquals: false,
});
}
try {
if (defaultExport) {
sourceFile.addExportAssignment({
expression: defaultExport,
isExportEquals: false,
});
}

namedExports.forEach(name => {
sourceFile.addExportDeclaration({
namedExports: [name],
namedExports.forEach(name => {
sourceFile.addExportDeclaration({
namedExports: [name],
});
});
});
} catch (error: unknown) {
console.error(` There was an issue with "${sourceFile.getFilePath()}":`, error);
}

const madeChanges = namedExports.length || defaultExport;
return !!madeChanges;
Expand Down
43 changes: 38 additions & 5 deletions src/converter/replacer/replaceRequire.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import {SourceFile, SyntaxKind, VariableStatement} from 'ts-morph';

export function replaceRequire(sourceFile: SourceFile, statement: VariableStatement) {
// Get variable declaration
/**
* Replaces a CommonJS require statement with an ESM import declaration.
*
* @param sourceFile - The source file being transformed.
* @param statement - The candidate containing the require statement.
* @returns true if the replacement was successful, false otherwise.
*/
function replaceRequire(sourceFile: SourceFile, statement: VariableStatement) {
// Get the variable declaration
const declaration = statement.getDeclarations()[0];
if (!declaration) {
return false;
Expand Down Expand Up @@ -47,12 +54,38 @@ export function replaceRequire(sourceFile: SourceFile, statement: VariableStatem
export function replaceRequires(sourceFile: SourceFile) {
let madeChanges: boolean = false;

// Handle files with "#! /usr/bin/env node" pragma
const firstStatement = sourceFile.getStatements()[0];
const hasShebang = firstStatement && firstStatement?.getFullText().startsWith('#!');
let shebangText = '';
if (hasShebang) {
// The full text contains both comments and the following statment,
// so we are separating the statement into comments and the instruction that follow on the next line.
const statementWithComment = firstStatement.getFullText();
const pureStatement = firstStatement.getText();
shebangText = statementWithComment.replace(pureStatement, '');
const lineAfterShebang = statementWithComment.replace(shebangText, '');
// We remove the node containing the shebang comment (and the following statement) to insert only the pure statement.
const index = firstStatement.getChildIndex();
firstStatement.remove();
sourceFile.insertStatements(index, lineAfterShebang);
}

sourceFile.getVariableStatements().forEach(statement => {
const updatedRequire = replaceRequire(sourceFile, statement);
if (updatedRequire) {
madeChanges = true;
try {
const updatedRequire = replaceRequire(sourceFile, statement);
if (updatedRequire) {
madeChanges = true;
}
} catch (error: unknown) {
console.error(` There was an issue with "${sourceFile.getFilePath()}":`, error);
}
});

if (shebangText) {
// We reinsert the Shebang at the top of the file to avoid error TS18026.
sourceFile.insertStatements(0, shebangText);
}

return madeChanges;
}
6 changes: 6 additions & 0 deletions src/test/fixtures/cjs-shebang/src/main.snap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#! /usr/bin/env node

/* eslint-disable */
import path from "path";
import shell from "shelljs";
var examples = {};
6 changes: 6 additions & 0 deletions src/test/fixtures/cjs-shebang/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#! /usr/bin/env node

/* eslint-disable */
var path = require('path');
var shell = require('shelljs');
var examples = {};
10 changes: 10 additions & 0 deletions src/test/fixtures/cjs-shebang/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "CommonJS",
"skipLibCheck": true,
"strict": true,
"target": "ES2020"
}
}
1 change: 1 addition & 0 deletions src/test/fixtures/index-import/src/main.snap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-ignore
import {MY_CONSTANT} from './lib/index.js';

console.log(MY_CONSTANT);
1 change: 1 addition & 0 deletions src/test/fixtures/tsconfig-include/src/consumer.snap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-ignore
import {MY_CONSTANT} from './lib/producer.js';

console.log(MY_CONSTANT);

0 comments on commit a0ca7a7

Please sign in to comment.