Skip to content

Commit

Permalink
feat: jscodeshiftを利用した置き換えに修正
Browse files Browse the repository at this point in the history
  • Loading branch information
yanagi0602 committed Nov 25, 2024
1 parent 6f38a85 commit 2131f7f
Show file tree
Hide file tree
Showing 4 changed files with 409 additions and 172 deletions.
128 changes: 53 additions & 75 deletions bin/outputExtensionReplace.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,65 @@
const { readFile, writeFile, access } = require('fs').promises;
const { join, dirname, resolve } = require('path');
const glob = require('glob-promise');
const process = require('process');
const { access } = require('fs').promises;
const { dirname, join, resolve } = require('path');

/**
* 非同期の置換処理を行う関数
* @param {string} str - 置換対象の文字列
* @param {RegExp} regex - 正規表現
* @param {(match: string, ...args: any[]) => Promise<string>} asyncFn - 非同期関数
* @return {Promise<string>}
*/
async function replaceAsync(str, regex, asyncFn) {
const promises = [];
str.replace(regex, (match, ...args) => {
const promise = asyncFn(match, ...args);
promises.push(promise);
return match;
});
const data = await Promise.all(promises);
return str.replace(regex, () => data.shift());
async function checkFileExists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}

const shouldReplaceExtension = (value) =>
(value.startsWith('./') || value.startsWith('../')) &&
!value.endsWith('.mjs');

/**
* 指定されたファイルのimport文を置換する
* @param {string} filePath - ファイルのパス
* @return {Promise<void>}
*
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
async function replaceImportsInFile(filePath) {
try {
const data = await readFile(filePath, 'utf8');
const importRegex =
/from\s+['"](\.\/|\.\.\/)(?!.*\.mjs['"])([^'"]+)(['"])/g;
const result = await replaceAsync(
data,
importRegex,
async (_match, p1, p2, p3) => {
const importPath = resolve(dirname(filePath), p1 + p2);
const mjsPath = importPath + '.mjs';
const indexPath = join(importPath, 'index.mjs');
return await Promise.allSettled([
access(mjsPath),
access(indexPath),
]).then((results) => {
const mjsPathResult = results[0];
const indexPathResult = results[1];
if (mjsPathResult.status === 'fulfilled') {
return `from '${p1}${p2}.mjs${p3}`;
} else if (indexPathResult.status === 'fulfilled') {
return `from '${p1}${p2}/index.mjs${p3}`;
module.exports = async function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
await Promise.all(
[
j.ImportDeclaration,
j.ExportNamedDeclaration,
j.ExportAllDeclaration,
j.ExportDefaultDeclaration,
]
.flatMap((declaration) => root.find(declaration).nodes())
.flatMap(async (node) => {
const source = node.source;
if (!source) {
return;
}
const value = source.value;

if (shouldReplaceExtension(value)) {
const importPath = resolve(dirname(file.path), value);
const mjsPath = `${importPath}.mjs`;
const indexPath = join(importPath, 'index.mjs');

const mjsExists = await checkFileExists(mjsPath);
const indexExists = await checkFileExists(indexPath);

if (mjsExists) {
node.source.value = `${value}.mjs`;
} else if (indexExists) {
node.source.value = `${value}/index.mjs`;
} else {
console.error(
`.mjs または /index.mjs が存在しません: ${importPath}`,
);
process.exit(1);
}
});
},
);
await writeFile(filePath, result, 'utf8');
if (data !== result) {
console.log(`置換処理が完了しました: ${filePath}`);
}
} catch (err) {
console.error(`エラーが発生しました: ${filePath}`, err);
process.exit(1);
}
}
}
}),
);

async function main() {
try {
const directories = await glob(join(__dirname, '../packages/*/dist/**/*.mjs'));
await Promise.all(directories.map(replaceImportsInFile));
} catch (err) {
console.error('エラーが発生しました', err);
process.exit(1);
}
}

if (require.main === module) {
main();
}

module.exports = {
replaceAsync,
replaceImportsInFile,
return root.toSource();
};

module.exports.checkFileExists = checkFileExists;
module.exports.shouldReplaceExtension = shouldReplaceExtension;
114 changes: 26 additions & 88 deletions bin/outputExtensionReplace.test.js
Original file line number Diff line number Diff line change
@@ -1,98 +1,36 @@
const fs = require('fs').promises;
const {
replaceImportsInFile,
replaceAsync,
checkFileExists,
shouldReplaceExtension,
} = require('./outputExtensionReplace');

jest.mock('fs', () => {
const originalModule = jest.requireActual('fs');
return {
...originalModule,
promises: {
readdir: jest.fn(),
readFile: jest.fn(),
writeFile: jest.fn(),
stat: jest.fn(),
access: jest.fn(),
},
};
});

describe('replaceImportsInFile', () => {
let originalExit;
let originalConsoleError;
let originalConsoleLog;

beforeAll(() => {
originalExit = process.exit;
process.exit = jest.fn();
originalConsoleError = console.error;
console.error = jest.fn();
originalConsoleLog = console.log;
console.log = jest.fn();
describe('checkFileExists', () => {
it('should return true if the file exists', async () => {
const result = await checkFileExists(__filename);
expect(result).toBe(true);
});

afterAll(() => {
process.exit = originalExit;
console.error = originalConsoleError;
console.log = originalConsoleLog;
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should replace imports in a file', async () => {
const filePath = '/file.mjs';
const fileContent = `
import React, { forwardRef } from 'react';
import module from './module';
import hasIndexDirectory from '../hasIndexDirectory';
`;
const expectedContent = `
import React, { forwardRef } from 'react';
import module from './module.mjs';
import hasIndexDirectory from '../hasIndexDirectory/index.mjs';
`;
fs.readFile.mockResolvedValue(fileContent);
fs.writeFile.mockResolvedValue();
fs.access.mockImplementation((path) => {
if (path.endsWith('module.mjs')) {
return Promise.resolve();
} else if (path.endsWith('hasIndexDirectory/index.mjs')) {
return Promise.resolve();
}
return Promise.reject();
});
await replaceImportsInFile(filePath);
expect(fs.readFile).toHaveBeenCalledWith(filePath, 'utf8');
expect(fs.writeFile).toHaveBeenCalledWith(
filePath,
expectedContent,
'utf8',
);
expect(console.log).toHaveBeenCalledWith(`置換処理が完了しました: ${filePath}`);
});

it('should exit the process if an error occurs', async () => {
const filePath = '/file.mjs';
fs.readFile.mockRejectedValue(new Error('error'));
await replaceImportsInFile(filePath);
expect(process.exit).toHaveBeenCalledWith(1);
expect(console.error).toHaveBeenCalledWith(
`エラーが発生しました: ${filePath}`,
new Error('error'),
);
it('should return false if the file does not exist', async () => {
const filePath = 'non_existent_file.dummy';
const result = await checkFileExists(filePath);
expect(result).toBe(false);
});
});

describe('replaceAsync', () => {
it('should replace a string asynchronously', async () => {
const result = await replaceAsync(
"import module from './module",
/.\/module/g,
async () => './module.mjs',
);
expect(result).toBe("import module from './module.mjs");
describe('shouldReplaceExtension', () => {
it('should return true if the value starts with ./ or ../ and does not end with .mjs', () => {
[
{ value: './foo', expected: true },
{ value: '../foo', expected: true },
{ value: './foo/bar', expected: true },
{ value: '../foo/bar', expected: true },
{ value: './foo.mjs', expected: false },
{ value: '../foo.mjs', expected: false },
{ value: './foo/bar.mjs', expected: false },
{ value: '../foo/bar.mjs', expected: false },
{ value: 'react', expected: false },
].forEach(({ value, expected }) => {
const result = shouldReplaceExtension(value);
expect(result).toBe(expected);
});
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"bootstrap": "lerna bootstrap",
"build": "run-s build:*",
"build:package": "lerna run build",
"build:extensionReplace": "node bin/outputExtensionReplace.js",
"build:extensionReplace": "find packages/*/dist -iname '*.mjs' -type f -print | xargs jscodeshift -t ./bin/outputExtensionReplace.js",
"prepare": "is-ci || husky install"
},
"devDependencies": {
Expand All @@ -32,10 +32,10 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "7.37.2",
"eslint-plugin-react-hooks": "^4.6.0",
"glob-promise": "^6.0.7",
"husky": "^9.1.5",
"is-ci": "^3.0.0",
"jest": "^29.7.0",
"jscodeshift": "^17.1.1",
"lerna": "^8.0.0",
"npm-run-all2": "^6.0.0",
"prettier": "3.3.3",
Expand Down
Loading

0 comments on commit 2131f7f

Please sign in to comment.