Skip to content

Commit

Permalink
fix: Fix build / default modules
Browse files Browse the repository at this point in the history
  • Loading branch information
kingston committed Oct 12, 2024
1 parent 7bc53fc commit 6bd0af9
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 47 deletions.
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import baseConfig from '@ktam/lint-node/eslint';
export default [
...baseConfig,
{
ignores: ['tests', '**/__mocks__/**/*'],
ignores: ['**/__mocks__/**/*'],
},
];
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
},
"devDependencies": {
"@changesets/cli": "2.27.9",
"@ktam/lint-node": "0.1.2",
"@ktam/lint-node": "0.1.3",
"@tsconfig/node20": "20.1.4",
"@types/jscodeshift": "0.12.0",
"@types/node": "20",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

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

15 changes: 12 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
#!/usr/bin/env node

import { Command } from 'commander';
import { readFile } from 'node:fs/promises';

import type { MigrateEsmImportsOptions } from './types.js';

import { description, name, version } from '../package.json';
import { migrateEsmImports } from './index.js';

const packageJson = await readFile(
new URL('../package.json', import.meta.url),
'utf8',
);
const { name, description, version } = JSON.parse(packageJson) as Record<
string,
string
>;

const program = new Command();
program.name(name).description(description).version(version);

program
.argument('[inputs...]', 'Input files or directories to process', 'src')
.argument('[inputs...]', 'Input files or directories to process', ['src'])
// Option to ignore certain directories or files
.option('-x,--exclude <patterns...>', 'Directories or files to exclude', [
'.git',
Expand Down Expand Up @@ -41,4 +50,4 @@ program

const options = program.opts<MigrateEsmImportsOptions>();

await migrateEsmImports(program.args, options);
await migrateEsmImports(program.processedArgs[0] as string[], options);
7 changes: 5 additions & 2 deletions src/migrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,19 @@ const importWithPathsIndexProject: TestProject = {
import { foo } from '@src/utils';
import { bar } from '@src/utils/bar';
import test from 'base/test';
import normalImport from 'vitest';
console.log(foo);`,
'src/utils/index.ts': `export const foo = 'foo';`,
'src/utils/bar.tsx': `export const bar = 'bar';`,
'src/utils/bar.tsx': `export const bar: string = 'bar';`,
'src/base/test.ts': `export default 'test';`,
},
target: {
'src/index.ts': `
import { foo } from '@src/utils/index.js';
import { bar } from '@src/utils/bar.js';
import test from 'base/test.js';
import normalImport from 'vitest';
console.log(foo);`,
},
Expand Down Expand Up @@ -94,10 +96,11 @@ test.for([
exportConversionProject,
])('works with $name project', async ({ source, target }) => {
vol.fromJSON(source, '/root');
await migrateEsmImports(['src'], {
const success = await migrateEsmImports(['src'], {
cwd: '/root',
fs,
});
expect(success).toBe(true);
const result = vol.toJSON('/root', undefined, true);
expect(result).toEqual({
...source,
Expand Down
46 changes: 26 additions & 20 deletions src/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ import { transformFile } from './transformer.js';
export async function migrateEsmImports(
inputs: string[],
options: MigrateEsmImportsOptions,
): Promise<void> {
): Promise<boolean> {
const concurrency = options.concurrency ?? os.cpus().length;

const limit = pLimit(concurrency);
const promises: Promise<void>[] = [];

console.info(`Migrating ESM imports (${concurrency.toString()} threads)...`);
console.info(
`Migrating ESM imports for files in ${inputs.join(',')} (${concurrency.toString()} threads)...`,
);

let filesSucceeded = 0;
let filesFailed = 0;

const resolverCache = new Map<string, string>();
const resolverCache = new Map<string, string | undefined>();
const tsConfigCache = new Map<string, unknown>();

const resolvedInputs = inputs.map((input) => {
Expand All @@ -36,34 +38,36 @@ export async function migrateEsmImports(
return input;
});

for await (const path of globbyStream(resolvedInputs, {
const validExtensions = options.extensions ?? [
'js',
'jsx',
'ts',
'tsx',
'cjs',
'mjs',
'cts',
'mts',
];

for await (const filePath of globbyStream(resolvedInputs, {
cwd: options.cwd ?? process.cwd(),
expandDirectories: {
extensions: options.extensions ?? [
'js',
'jsx',
'ts',
'tsx',
'cjs',
'mjs',
'cts',
'mts',
],
},
onlyFiles: true,
ignore: options.exclude,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
fs: options.fs as any,
})) {
if (typeof path !== 'string') {
if (typeof filePath !== 'string') {
console.error(`Invalid path type: ${typeof path}`);
filesFailed += 1;
continue;
}
if (!validExtensions.includes(path.extname(filePath).slice(1))) {
continue;
}
promises.push(
limit(async () => {
try {
const result = await transformFile(path, {
const result = await transformFile(filePath, {
dryRun: options.dryRun,
resolverCache,
tsConfigCache,
Expand All @@ -74,7 +78,7 @@ export async function migrateEsmImports(
}
} catch (error) {
console.error(
`Failed to migrate ESM imports for ${path}: ${String(error)}`,
`Failed to migrate ESM imports for ${filePath}: ${String(error)}`,
);
filesFailed += 1;
}
Expand All @@ -85,6 +89,8 @@ export async function migrateEsmImports(
await Promise.allSettled(promises);

console.info(
`Successfully migrated ESM imports for ${filesSucceeded.toString()} files. (${filesFailed.toString()} failed).`,
`Successfully migrated ESM imports for ${filesSucceeded.toString()} files. (${filesFailed.toString()} failed)`,
);

return filesFailed === 0;
}
17 changes: 17 additions & 0 deletions src/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,21 @@ describe('fixImportPath', () => {
fixImportPath('/root/src/file.ts', './unknown', resolverCache),
).toThrow('Could not find valid extension for ./unknown');
});

it('should not throw an error for module paths', () => {
fsExistsSyncMock.mockReturnValue(false);
const result = fixImportPath('./root/src/file.ts', 'vitest', resolverCache);
expect(result).toBeUndefined();
});

it('should not throw an error for module paths with tsConfig', () => {
fsExistsSyncMock.mockReturnValue(false);
const result = fixImportPath(
'./root/src/file.ts',
'vitest',
resolverCache,
tsConfig,
);
expect(result).toBeUndefined();
});
});
33 changes: 18 additions & 15 deletions src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function findSpecifierSuffix(file: string): string | undefined {
export function fixImportPath(
filePath: string,
specifier: string,
resolverCache: Map<string, string>,
resolverCache: Map<string, string | undefined>,
tsConfig?: TsConfigResult,
): string | undefined {
// if it's already been fixed we're done :)
Expand Down Expand Up @@ -59,27 +59,30 @@ export function fixImportPath(
return resolverCache.get(resolverCacheKey);
}
const pathsMatcher = createPathsMatcher(tsConfig);
const baseUrl = tsConfig.config.compilerOptions?.baseUrl;
const basePath = baseUrl && path.join(path.dirname(tsConfig.path), baseUrl);
const matchingPaths = pathsMatcher?.(specifier) ?? [];

const possiblePaths = [
...(pathsMatcher?.(specifier) ?? []),
...(basePath ? [path.join(basePath, specifier)] : []),
];

const suffix = possiblePaths
const suffix = matchingPaths
.map((p) => findSpecifierSuffix(p))
.find((s) => s !== undefined);

if (!suffix) {
if (
!suffix &&
// catch scenario where we default to baseUrl which could be a normal module
(matchingPaths.length !== 1 ||
!matchingPaths[0].endsWith(`/${specifier}`))
) {
throw new Error(`Could not find valid extension for ${specifier}`);
}

const resolvedSpecifier = `${specifier}${suffix}`;
if (suffix) {
const resolvedSpecifier = `${specifier}${suffix}`;

resolverCache.set(resolverCacheKey, resolvedSpecifier);
resolverCache.set(resolverCacheKey, resolvedSpecifier);

return resolvedSpecifier;
return resolvedSpecifier;
} else {
resolverCache.set(resolverCacheKey, undefined);
}
}

// don't need to fix otherwise
Expand All @@ -90,7 +93,7 @@ interface TransformFileOptions {
dryRun?: boolean;
verbose?: boolean;
tsConfigCache: Map<string, unknown>;
resolverCache: Map<string, string>;
resolverCache: Map<string, string | undefined>;
}

export async function transformFile(
Expand All @@ -101,7 +104,7 @@ export async function transformFile(

const tsConfig = getTsconfig(file, undefined, tsConfigCache);

const root = jscodeshift(content);
const root = jscodeshift.withParser('tsx')(content);
let wasModified = false as boolean;

const fixDeclaration = (
Expand Down

0 comments on commit 6bd0af9

Please sign in to comment.