Skip to content

Commit

Permalink
test: Add snapshot test for barrel files (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Oct 25, 2024
1 parent 052356f commit 1c899b0
Show file tree
Hide file tree
Showing 20 changed files with 176 additions and 53 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"extends": "@tstv/eslint-config",
"ignorePatterns": ["src/test/fixtures/**"],
"rules": {
"@typescript-eslint/consistent-type-imports": "off"
}
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ const __dirname = path.dirname(__filename);

This program was born from an [inspiring conversation](https://twitter.com/bennycode/status/1693362836695585084) I had with [Basarat Ali Syed](https://twitter.com/basarat). I recommend checking out [Basarat's coding tutorials](https://www.youtube.com/@basarat). 👍

## Testimonials

- ts2esm got highlighted in Deno's article on [How to convert CommonJS to ESM](https://deno.com/blog/convert-cjs-to-esm#tools-for-migrating)
- ts2esm helped migrating [cornerstonejs/cornerstone3D](https://github.com/cornerstonejs/cornerstone3D) from CommonJS to ESM

## Used By

[<img src="https://ohif.org/static/c99ccbad57599dbf9f3490519c9b444f/63739/ohif-logo-dark.png" width="256"/>](https://ohif.org/)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"vitest": "^2.0.5"
"vitest": "^2.1.3"
},
"engines": {
"node": ">= 10.9"
Expand Down Expand Up @@ -74,7 +74,7 @@
"release:minor": "npm version minor",
"release:patch": "npm version patch",
"start": "npm run test:types && node --no-warnings=ExperimentalWarning --loader ts-node/esm ./src/cli.ts",
"test": "npm run test:types && npm run test:unit:coverage",
"test": "npm run test:types && npm run test:unit",
"test:types": "tsc --noEmit",
"test:unit": "vitest run --passWithNoTests",
"test:unit:coverage": "npm run test:unit -- --coverage.enabled"
Expand Down
31 changes: 31 additions & 0 deletions src/converter/convertFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import path from 'node:path';
import {ProjectUtil} from '../util/ProjectUtil.js';
import {convertFile} from './convertFile.js';

describe('convertFile', () => {
const fixtures = path.join(process.cwd(), 'src', 'test', 'fixtures');

it('fixes imports from index files', async () => {
const projectDir = path.join(fixtures, 'index-import');
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(projectConfig, sourceFile, true);

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

it('fixes imports when tsconfig has an "include" property', async () => {
const projectDir = path.join(fixtures, 'tsconfig-include');
const projectConfig = path.join(projectDir, 'tsconfig.json');
const snapshot = path.join(projectDir, 'src', 'consumer.snap.ts');
const project = ProjectUtil.getProject(projectConfig);

const sourceFile = project.getSourceFile('consumer.ts')!;
const modifiedFile = convertFile(projectConfig, sourceFile, true);

await expect(modifiedFile.getText()).toMatchFileSnapshot(snapshot);
});
});
49 changes: 49 additions & 0 deletions src/converter/convertFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {SourceFile, SyntaxKind} from 'ts-morph';
import {rewrite} from '../main.js';
import {ProjectUtil} from '../util/ProjectUtil.js';

export function convertFile(tsConfigFilePath: string, sourceFile: SourceFile, dryRun: boolean) {
const filePath = sourceFile.getFilePath();
const project = ProjectUtil.getProject(tsConfigFilePath);
const paths = ProjectUtil.getPaths(project);
const projectDirectory = ProjectUtil.getRootDirectory(tsConfigFilePath);

let madeChanges: boolean = false;

sourceFile.getImportDeclarations().forEach(importDeclaration => {
importDeclaration.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach(stringLiteral => {
const hasAttributesClause = !!importDeclaration.getAttributes();
const adjustedImport = rewrite({
hasAttributesClause,
paths,
projectDirectory,
sourceFilePath: sourceFile.getFilePath(),
stringLiteral,
});
madeChanges ||= adjustedImport;
});
});

sourceFile.getExportDeclarations().forEach(exportDeclaration => {
exportDeclaration.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach(stringLiteral => {
const hasAttributesClause = !!exportDeclaration.getAttributes();
const adjustedExport = rewrite({
hasAttributesClause,
paths,
projectDirectory,
sourceFilePath: filePath,
stringLiteral,
});
madeChanges ||= adjustedExport;
});
});

if (madeChanges) {
if (!dryRun) {
sourceFile.saveSync();
console.log(` Modified (🔧): ${filePath}`);
}
}

return sourceFile;
}
63 changes: 14 additions & 49 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import path from 'node:path';
import {Project, StringLiteral, SyntaxKind} from 'ts-morph';
import {StringLiteral} from 'ts-morph';
import {applyModification} from './codemod/applyModification.js';
import {convertTSConfig} from './codemod/convertTSConfig.js';
import {convertFile} from './converter/convertFile.js';
import {toImport, toImportAttribute} from './converter/ImportConverter.js';
import {parseInfo, type ModuleInfo} from './parser/InfoParser.js';
import {PathFinder} from './util/PathFinder.js';
import {getNormalizedPath} from './util/PathUtil.js';
import {getNormalizedPath, isNodeModuleRoot} from './util/PathUtil.js';
import {ProjectUtil} from './util/ProjectUtil.js';

/**
* Traverses all source code files from a project and checks its import and export declarations.
*/
export async function convert(tsConfigFilePath: string, debugLogging: boolean = false) {
const project = new Project({
// Limit the scope of source files to those directly listed as opposed to also all
// of the dependencies that may be imported. Never want to modify dependencies.
skipFileDependencyResolution: true,

tsConfigFilePath,
});
const projectDirectory = project.getRootDirectories()[0]?.getPath() || '';
// Note: getCompilerOptions() cannot be cached and has to be used everytime the config is accessed
const paths = project.getCompilerOptions().paths;
const project = ProjectUtil.getProject(tsConfigFilePath);
const paths = ProjectUtil.getPaths(project);

// Check "module" and "moduleResolution" in "tsconfig.json"
await convertTSConfig(tsConfigFilePath, project);

// Add "type": "module" to "package.json"
const packageJsonPath = path.join(projectDirectory, 'package.json');
const packageJsonPath = path.join(ProjectUtil.getRootDirectory(tsConfigFilePath), 'package.json');
await applyModification(packageJsonPath, '/type', 'module');

if (paths && debugLogging) {
Expand All @@ -38,45 +32,11 @@ export async function convert(tsConfigFilePath: string, debugLogging: boolean =
if (debugLogging) {
console.log(` Checking (🧪): ${filePath}`);
}

let madeChanges: boolean = false;

sourceFile.getImportDeclarations().forEach(importDeclaration => {
importDeclaration.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach(stringLiteral => {
const hasAttributesClause = !!importDeclaration.getAttributes();
const adjustedImport = rewrite({
hasAttributesClause,
paths,
projectDirectory,
sourceFilePath: filePath,
stringLiteral,
});
madeChanges ||= adjustedImport;
});
});

sourceFile.getExportDeclarations().forEach(exportDeclaration => {
exportDeclaration.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach(stringLiteral => {
const hasAttributesClause = !!exportDeclaration.getAttributes();
const adjustedExport = rewrite({
hasAttributesClause,
paths,
projectDirectory,
sourceFilePath: filePath,
stringLiteral,
});
madeChanges ||= adjustedExport;
});
});

if (madeChanges) {
sourceFile.saveSync();
console.log(` Modified (🔧): ${filePath}`);
}
convertFile(tsConfigFilePath, sourceFile, false);
});
}

function rewrite({
export function rewrite({
hasAttributesClause,
paths,
projectDirectory,
Expand Down Expand Up @@ -131,6 +91,11 @@ function createReplacementPath({

const foundPath = PathFinder.findPath(baseFilePath, info.extension);
if (foundPath) {
// TODO: Write test case for this condition, mock "path" and "fs" calls if necessary
if (foundPath.extension === '/index.js' && isNodeModuleRoot(baseFilePath)) {
// @fixes https://github.com/bennycode/ts2esm/issues/81#issuecomment-2437503011
return null;
}
return toImport({...info, extension: foundPath.extension});
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/test/fixtures/index-import/main.snap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {MY_CONSTANT} from './src/index';

console.log(MY_CONSTANT);
1 change: 1 addition & 0 deletions src/test/fixtures/index-import/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MY_CONSTANT = 1337;
3 changes: 3 additions & 0 deletions src/test/fixtures/index-import/src/main.snap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {MY_CONSTANT} from './lib/index.js';

console.log(MY_CONSTANT);
4 changes: 4 additions & 0 deletions src/test/fixtures/index-import/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @ts-ignore
import {MY_CONSTANT} from './lib';

console.log(MY_CONSTANT);
10 changes: 10 additions & 0 deletions src/test/fixtures/index-import/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "Node16",
"skipLibCheck": true,
"strict": true,
"target": "es2016"
}
}
3 changes: 3 additions & 0 deletions src/test/fixtures/tsconfig-include/consumer.snap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {MY_CONSTANT} from './lib/producer.js';

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

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

console.log(MY_CONSTANT);
1 change: 1 addition & 0 deletions src/test/fixtures/tsconfig-include/src/lib/producer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MY_CONSTANT = 1337;
11 changes: 11 additions & 0 deletions src/test/fixtures/tsconfig-include/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "Node16",
"skipLibCheck": true,
"strict": true,
"target": "es2016"
},
"include": ["src"]
}
8 changes: 8 additions & 0 deletions src/util/PathUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import fs from 'node:fs';
import type {ModuleInfo} from '../parser/InfoParser.js';

/**
Expand Down Expand Up @@ -54,3 +55,10 @@ export function hasRelativePath(path: string) {
}
return path.startsWith('./') || path.startsWith('../');
}

export function isNodeModuleRoot(directory: string): boolean {
const isInNodeModules = directory.includes(`node_modules${path.sep}`);
const packageJsonPath = path.join(directory, 'package.json');
const packageJsonExists = fs.existsSync(packageJsonPath);
return isInNodeModules && packageJsonExists;
}
20 changes: 20 additions & 0 deletions src/util/ProjectUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from 'node:path';
import {Project} from 'ts-morph';

export const ProjectUtil = {
getPaths: (project: Project) => {
// Note: getCompilerOptions() cannot be cached and has to be used everytime the config is accessed
return project.getCompilerOptions().paths;
},
getProject: (tsConfigFilePath: string) => {
return new Project({
// Limit the scope of source files to those directly listed as opposed to also all
// of the dependencies that may be imported. Never want to modify dependencies.
skipFileDependencyResolution: true,
tsConfigFilePath,
});
},
getRootDirectory: (tsConfigFilePath: string): string => {
return path.dirname(tsConfigFilePath);
},
};
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tstv/tsconfig-common/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"types": ["vitest/globals"]
},
"exclude": ["src/test/fixtures/**"],
"extends": "@tstv/tsconfig-common/tsconfig.json",
"include": ["src/**/*", "vitest.config.ts"]
}

0 comments on commit 1c899b0

Please sign in to comment.