diff --git a/src/converter/convertFile.test.ts b/src/converter/convertFile.test.ts index 4346dbd..f6f42cc 100644 --- a/src/converter/convertFile.test.ts +++ b/src/converter/convertFile.test.ts @@ -91,5 +91,17 @@ describe('convertFile', () => { await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot); }); + + it('handles functions exported as default from plain JavaScript files', async () => { + const projectDir = path.join(fixtures, 'module-exports-function-js'); + const projectConfig = path.join(projectDir, 'tsconfig.json'); + const snapshot = path.join(projectDir, 'src', 'build-example-index.snap.js'); + const project = ProjectUtil.getProject(projectConfig); + + const sourceFile = project.getSourceFile('build-example-index.js')!; + const modifiedFile = convertFile(sourceFile); + + await expect(modifiedFile?.getFullText()).toMatchFileSnapshot(snapshot); + }); }); }); diff --git a/src/converter/replacer/replaceModuleExports.ts b/src/converter/replacer/replaceModuleExports.ts index 569e15e..5ebc867 100644 --- a/src/converter/replacer/replaceModuleExports.ts +++ b/src/converter/replacer/replaceModuleExports.ts @@ -1,10 +1,10 @@ import {SourceFile, SyntaxKind} from 'ts-morph'; +import {NodeUtil} from '../../util/NodeUtil.js'; export function replaceModuleExports(sourceFile: SourceFile) { let defaultExport: string | undefined = undefined; const namedExports: string[] = []; - // Iterate through all statements in the source file sourceFile.getStatements().forEach(statement => { try { if (statement.getKind() === SyntaxKind.ExpressionStatement) { @@ -12,19 +12,25 @@ export function replaceModuleExports(sourceFile: SourceFile) { if (!expressionStatement) { return; } + 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(); + const right = binaryExpression.getRight(); - if (left === 'module.exports') { - defaultExport = right; + // Handle `module.exports = ;` + if (left === 'module.exports' && right) { + const {comment} = NodeUtil.extractComment(binaryExpression.getLeft()); + defaultExport = right.getText(); + sourceFile.addStatements(`${comment}export default ${defaultExport};`); statement.remove(); - } else if (left.startsWith('module.exports.')) { + } else if (left.startsWith('module.exports.') && right) { + // Handle `module.exports. = ;` const exportName = left.split('.')[2]; if (exportName) { namedExports.push(exportName); @@ -35,27 +41,19 @@ export function replaceModuleExports(sourceFile: SourceFile) { } } catch (error: unknown) { console.error(` There was an issue with "${sourceFile.getFilePath()}":`, error); - process.exit(1); } }); try { - if (defaultExport) { - sourceFile.addExportAssignment({ - expression: defaultExport, - isExportEquals: false, - }); - } - - namedExports.forEach(name => { + if (namedExports.length > 0) { sourceFile.addExportDeclaration({ - namedExports: [name], + namedExports, }); - }); + } } catch (error: unknown) { console.error(` There was an issue with "${sourceFile.getFilePath()}":`, error); } - const madeChanges = namedExports.length || defaultExport; - return !!madeChanges; + const madeChanges = defaultExport !== undefined || namedExports.length > 0; + return madeChanges; } diff --git a/src/converter/replacer/replaceRequire.ts b/src/converter/replacer/replaceRequire.ts index 47bd81f..6e19ec5 100644 --- a/src/converter/replacer/replaceRequire.ts +++ b/src/converter/replacer/replaceRequire.ts @@ -1,4 +1,5 @@ import {SourceFile, SyntaxKind, VariableStatement} from 'ts-morph'; +import {NodeUtil} from '../../util/NodeUtil.js'; /** * Replaces a CommonJS require statement with an ESM import declaration. @@ -61,10 +62,8 @@ export function replaceRequires(sourceFile: SourceFile) { 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, ''); + const {statement: lineAfterShebang, comment} = NodeUtil.extractComment(firstStatement); + shebangText = comment; // We remove the node containing the shebang comment (and the following statement) to insert only the pure statement. const index = firstStatement.getChildIndex(); firstStatement.remove(); diff --git a/src/test/fixtures/module-exports-function-js/src/build-example-index-markdown.js b/src/test/fixtures/module-exports-function-js/src/build-example-index-markdown.js new file mode 100644 index 0000000..ecb8590 --- /dev/null +++ b/src/test/fixtures/module-exports-function-js/src/build-example-index-markdown.js @@ -0,0 +1,4 @@ +import path from 'path'; +import info from './example-info.json'; + +export default function buildExampleMarkdown(names, exampleBasePaths) {} diff --git a/src/test/fixtures/module-exports-function-js/src/build-example-index-markdown.snap.js b/src/test/fixtures/module-exports-function-js/src/build-example-index-markdown.snap.js new file mode 100644 index 0000000..94912bf --- /dev/null +++ b/src/test/fixtures/module-exports-function-js/src/build-example-index-markdown.snap.js @@ -0,0 +1,4 @@ +const path = require('path'); +const info = require('./example-info.json'); + +module.exports = function buildExampleMarkdown(names, exampleBasePaths) {}; diff --git a/src/test/fixtures/module-exports-function-js/src/build-example-index.js b/src/test/fixtures/module-exports-function-js/src/build-example-index.js new file mode 100644 index 0000000..d855546 --- /dev/null +++ b/src/test/fixtures/module-exports-function-js/src/build-example-index.js @@ -0,0 +1,4 @@ +const path = require('path'); + +/** This comment should be kept. */ +module.exports = function buildExampleIndex(names, exampleBasePaths) {}; diff --git a/src/test/fixtures/module-exports-function-js/src/build-example-index.snap.js b/src/test/fixtures/module-exports-function-js/src/build-example-index.snap.js new file mode 100644 index 0000000..5955619 --- /dev/null +++ b/src/test/fixtures/module-exports-function-js/src/build-example-index.snap.js @@ -0,0 +1,4 @@ +import path from "path"; + +/** This comment should be kept. */ +export default function buildExampleIndex(names, exampleBasePaths) {}; diff --git a/src/test/fixtures/module-exports-function-js/tsconfig.json b/src/test/fixtures/module-exports-function-js/tsconfig.json new file mode 100644 index 0000000..bb35869 --- /dev/null +++ b/src/test/fixtures/module-exports-function-js/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "Node16", + "skipLibCheck": true, + "strict": true, + "target": "ES2020" + } +} diff --git a/src/test/fixtures/module-exports/src/main.snap.ts b/src/test/fixtures/module-exports/src/main.snap.ts index d50ad2e..2bcf316 100644 --- a/src/test/fixtures/module-exports/src/main.snap.ts +++ b/src/test/fixtures/module-exports/src/main.snap.ts @@ -1,6 +1,5 @@ const Benny = 1; const Code = 2; - export default Benny; export { Code }; diff --git a/src/util/NodeUtil.ts b/src/util/NodeUtil.ts new file mode 100644 index 0000000..151b540 --- /dev/null +++ b/src/util/NodeUtil.ts @@ -0,0 +1,14 @@ +import {Expression, Statement} from 'ts-morph'; + +export class NodeUtil { + static extractComment(input: Statement | Expression) { + const statementWithComment = input.getFullText(); + const pureStatement = input.getText(); + const comment = statementWithComment.replace(pureStatement, ''); + const statement = statementWithComment.replace(comment, ''); + return { + comment, + statement, + }; + } +}