将别名@导入重构为相对路径

Est*_*ask 10 javascript refactoring jetbrains-ide visual-studio-code es6-modules

在使用Webpack,TypeScript或其他工具转换ES模块导入的模块化环境中,使用路径别名,常见的约定是@ forsrc

对我而言,使用别名绝对路径转换项目是一个常见的问题:

src / foo / bar / index.js

import baz from '@/baz';
Run Code Online (Sandbox Code Playgroud)

相对路径:

src / foo / bar / index.js

import baz from '../../baz';
Run Code Online (Sandbox Code Playgroud)

例如,使用别名的项目需要与不使用别名的另一个项目合并,由于样式指南或其他原因,不能将后者配置为使用别名。

简单的搜索和替换无法解决此问题,并且手动修复导入路径非常繁琐且容易出错。我希望原始的JavaScript / TypeScript代码库在其他方面保持不变,因此可能无法选择使用编译器进行转换。

我想用我选择的IDE(Jetbrains IDEA / Webstorm / Phpstorm)实现这种重构,但可以接受任何其他IDE(VS Code)或纯Node.js的解决方案。

如何做到这一点?

for*_*d04 10

将别名导入重新连接到相对路径的三种可能的解决方案:

1. babel-plugin-module-resolver

使用babel-plugin-module-resolver,同时忽略其他 babel 插件/预设。

.babelrc
"plugins": [
  [
    "module-resolver",
    {
      "alias": {
        "^@/(.+)": "./src/\\1"
      }
    }
  ]
]
Run Code Online (Sandbox Code Playgroud)

构建步骤:(babel src --out-dir dist输出dist,不会就地修改)

处理的示例文件:
// input                                // output
import { helloWorld } from "@/sub/b"    // import { helloWorld } from "./sub/b";
import "@/sub/b"                        // import "./sub/b";
export { helloWorld } from "@/sub/b"    // export { helloWorld } from "./sub/b";
export * from "@/sub/b"                 // export * from "./sub/b";
Run Code Online (Sandbox Code Playgroud)

对于 TS,您还需要@babel/preset-typescript.ts通过babel src --out-dir dist --extensions ".ts".

2. Codemod jscodeshift 与正则表达式

应该支持MDN 文档中所有相关的导入/导出变体。该算法是这样实现的:

1. Input: path aliases 以alias -> resolved path类似于 TypeScripttsconfig.json paths或 Webpack 的形式映射resolve.alias

const pathMapping = {
  "@": "./custom/app/path",
  ...
};
Run Code Online (Sandbox Code Playgroud)

2. 遍历所有源文件,例如 traverse src

"plugins": [
  [
    "module-resolver",
    {
      "alias": {
        "^@/(.+)": "./src/\\1"
      }
    }
  ]
]
Run Code Online (Sandbox Code Playgroud)

3.对于每个源文件,找到所有的进出口申报

function transform(file, api) {
  const j = api.jscodeshift;
  const root = j(file.source);

  root.find(j.ImportDeclaration).forEach(replaceNodepathAliases);
  root.find(j.ExportAllDeclaration).forEach(replaceNodepathAliases);
  root
    .find(j.ExportNamedDeclaration, node => node.source !== null)
    .forEach(replaceNodepathAliases);
  return root.toSource();
 ...
};
Run Code Online (Sandbox Code Playgroud)

jscodeshift.js

// input                                // output
import { helloWorld } from "@/sub/b"    // import { helloWorld } from "./sub/b";
import "@/sub/b"                        // import "./sub/b";
export { helloWorld } from "@/sub/b"    // export { helloWorld } from "./sub/b";
export * from "@/sub/b"                 // export * from "./sub/b";
Run Code Online (Sandbox Code Playgroud)

进一步说明:

import { AppStore } from "@/app/store/appStore-types"
Run Code Online (Sandbox Code Playgroud)

创建以下AST,其source.valueImportDeclaration节点可以被修饰:

AST 浏览器

4. 对于每个路径声明,测试包含路径别名之一的正则表达式模式。

5.获取别名的解析路径并转换为相对于当前文件位置的路径(归功于@Reijo)

replace-path-alias.js (4. + 5.):

const pathMapping = {
  "@": "./custom/app/path",
  ...
};
Run Code Online (Sandbox Code Playgroud)

3. 仅正则表达式搜索和替换

遍历所有源并应用正则表达式(未经过彻底测试):

^(import.*from\\s+["|'])(${aliasesKeys})(.*)(["|'])$
Run Code Online (Sandbox Code Playgroud)

,其中${aliasesKeys}包含路径别名"@"。可以通过修改第2和第3个捕获组(路径映射+解析为相对路径)来处理新的导入路径。

此变体无法处理 AST,因此可能被认为不如 jscodeshift 稳定。

目前,正则表达式仅支持导入。import "module-name"排除了表单中的副作用导入,这样可以使搜索/替换更安全。

样本:

jscodeshift -t scripts/jscodeshift.js src # use -d -p options for dry-run + stdout
# or for TS
jscodeshift --extensions=ts --parser=ts -t scripts/jscodeshift.js src
Run Code Online (Sandbox Code Playgroud)


Rei*_*ijo 8

我创建了一个脚本来执行此操作。

它基本上遍历项目树,搜索所有文件,/"@(\/\w+[\w\/.]+)"/gi使用正则表达式查找看起来像“@/my/import”的导入,然后使用nodejs的path模块创建相对路径。

我希望你没有任何我没有在这个简单脚本中涵盖的边缘情况,所以最好备份你的文件。我只在一个简单的场景中测试过它。

这是代码

const path = require("path");
const args = process.argv;

const rootName = args[2];
const rootPath = path.resolve(process.cwd(), rootName);
const alias = "@";

if (!rootPath || !alias) return;

const { promisify } = require("util");
const fs = require("fs");

const readFileAsync = promisify(fs.readFile);
const readDirAsync = promisify(fs.readdir);
const writeFileAsync = promisify(fs.writeFile);
const statsAsync = promisify(fs.stat);

function testForAliasImport(file) {
  if (!file.content) return file;

  const regex = /"@(\/\w+[\w\/.]+)"/gi;

  let match,
    search = file.content;

  while ((match = regex.exec(search))) {
    const matchString = match[0];
    console.log(`found alias import ${matchString} in ${file.filepath}`);
    file.content = file.content.replace(
      matchString,
      aliasToRelative(file, matchString)
    );
    search = search.substring(match.index + matchString.length);
  }

  return file;
}

function aliasToRelative(file, importString) {
  let importPath = importString
    .replace(alias, "")
    .split('"')
    .join("");
  const hasExtension = !!path.parse(importString).ext;

  if (!hasExtension) {
    importPath += ".ext";
  }

  const filepath = file.filepath
    .replace(rootPath, "")
    .split("\\")
    .join("/");

  let relativeImport = path.posix.relative(path.dirname(filepath), importPath);

  if (!hasExtension) {
    relativeImport = relativeImport.replace(".ext", "");
  }

  if (!relativeImport.startsWith("../")) {
    relativeImport = "./" + relativeImport;
  }

  relativeImport = `"${relativeImport}"`;

  console.log(`replaced alias import ${importString} with ${relativeImport}`);
  return relativeImport;
}

async function writeFile(file) {
  if (!file || !file.content || !file.filepath) return file;
  try {
    console.log(`writing new contents to file ${file.filepath}...`);
    await writeFileAsync(file.filepath, file.content);
  } catch (e) {
    console.error(e);
  }
}

async function prepareFile(filepath) {
  const stat = await statsAsync(filepath);
  return { stat, filepath };
}

async function processFile(file) {
  if (file.stat.isFile()) {
    console.log(`reading file ${file.filepath}...`);
    file.content = await readFileAsync(file.filepath);
    file.content = file.content.toString();
  } else if (file.stat.isDirectory()) {
    console.log(`traversing dir ${file.filepath}...`);
    await traverseDir(file.filepath);
  }
  return file;
}

async function traverseDir(dirPath) {
  try {
    const filenames = await readDirAsync(dirPath);
    const filepaths = filenames.map(name => path.join(dirPath, name));
    const fileStats = await Promise.all(filepaths.map(prepareFile));
    const files = await Promise.all(fileStats.map(processFile));
    await Promise.all(files.map(testForAliasImport).map(writeFile));
  } catch (e) {
    console.error(e);
  }
}


traverseDir(rootPath)
  .then(() => console.log("done"))
  .catch(console.error);
Run Code Online (Sandbox Code Playgroud)

请务必提供目录名称作为参数。喜欢src的实例。

对于 IDE 部分,我知道 Jetbrains Webstorm 让你定义 npm 任务。
创建一个scripts目录来保存脚本。
定义一个脚本package.json

"scripts": {
    ...
    "replaceimports": "node scripts/script.js \"src\""
}
Run Code Online (Sandbox Code Playgroud)

在 npm 工具窗口中注册 npm 任务以供使用。


小智 8

显着减少任务时间的一个简单方法是使用正则表达式模式仅匹配位于特定深度级别的目标文件。假设您有一个指向components文件夹的神奇路径和如下所示的项目结构:

\n
...\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 package.json\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 src\n    \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 components\n
Run Code Online (Sandbox Code Playgroud)\n

您可以通过简单的查找和替换来重构它:

\n
find: from "components\nreplace: from "../components\nfiles to include: ./src/*/**.ts\n
Run Code Online (Sandbox Code Playgroud)\n

然后你只需递归:

\n
find: from "components\nreplace: from "../../components\nfiles to include: ./src/*/*/**.ts\n
Run Code Online (Sandbox Code Playgroud)\n

我写了一篇关于此的小博客文章:https://dev.to/fes300/refactoring-absolute-paths-to-relative-ones-in-vscode-3iaj

\n