将别名@Imports重构为相对路径

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

转换带有别名绝对路径的项目是我经常遇到的问题:

src/foo/bar/index.js

import baz from '@/baz';

到相对路径:

src/foo/bar/index.js

import baz from '../../baz';

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

这不能通过简单的搜索和替换来解决,手动修复导入路径既繁琐又容易出错。我希望原始JavaScript/TypeScript代码库在其他方面保持不变,因此可能不会使用代码转换程序对其进行转换。

我希望使用我选择的IDE(JetBrains Idea/WebStorm/PhpStorm)实现这种重构,但接受使用任何其他IDE(VS代码)或普通Node.js的解决方案。

如何实现此目标?


解决方案

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

1.babel-plugin-module-solver

使用babel-plugin-module-resolver,同时省略其他巴别塔插件/预设。

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

生成步骤: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";

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

2.使用Regex进行Codemod jcodesShift

应支持MDNdocs中的所有相关导入/导出变体。该算法的实现方式如下:

1.输入:路径别名映射,格式为alias -> resolved path类似于打字稿tsconfig.jsonpaths或webpack的resolve.alias

const pathMapping = {
  "@": "./custom/app/path",
  ...
};

2.遍历源文件,例如遍历src

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

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();
 ...
};

jscodeshift.js

/**
 * Corresponds to tsconfig.json paths or webpack aliases
 * E.g. "@/app/store/AppStore" -> "./src/app/store/AppStore"
 */
const pathMapping = {
  "@": "./src",
  foo: "bar",
};

const replacePathAlias = require("./replace-path-alias");

module.exports = 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);

  /**
   * Filter out normal module exports, like export function foo(){ ...}
   * Include export {a} from "mymodule" etc.
   */
  root
.find(j.ExportNamedDeclaration, (node) => node.source !== null)
.forEach(replaceNodepathAliases);

  return root.toSource();

  function replaceNodepathAliases(impExpDeclNodePath) {
impExpDeclNodePath.value.source.value = replacePathAlias(
  file.path,
  impExpDeclNodePath.value.source.value,
  pathMapping
);
  }
};

进一步说明:

import { AppStore } from "@/app/store/appStore-types"

创建以下AST,ImportDeclaration个节点的source.value可以修改:

4.对于每个路径声明,测试包含其中一个路径别名的Regex模式。

5.获取别名的解析路径,转换为相对于当前文件位置的路径(积分为@reijo)

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

const path = require("path");

function replacePathAlias(currentFilePath, importPath, pathMap) {
  // if windows env, convert backslashes to "/" first
  currentFilePath = path.posix.join(...currentFilePath.split(path.sep));

  const regex = createRegex(pathMap);
  return importPath.replace(regex, replacer);

  function replacer(_, alias, rest) {
const mappedImportPath = pathMap[alias] + rest;

// use path.posix to also create foward slashes on windows environment
let mappedImportPathRelative = path.posix.relative(
  path.dirname(currentFilePath),
  mappedImportPath
);
// append "./" to make it a relative import path
if (!mappedImportPathRelative.startsWith("../")) {
  mappedImportPathRelative = `./${mappedImportPathRelative}`;
}

logReplace(currentFilePath, mappedImportPathRelative);

return mappedImportPathRelative;
  }
}

function createRegex(pathMap) {
  const mapKeysStr = Object.keys(pathMap).reduce((acc, cur) => `${acc}|${cur}`);
  const regexStr = `^(${mapKeysStr})(.*)$`;
  return new RegExp(regexStr, "g");
}

const log = true;
function logReplace(currentFilePath, mappedImportPathRelative) {
  if (log)
console.log(
  "current processed file:",
  currentFilePath,
  "; Mapped import path relative to current file:",
  mappedImportPathRelative
);
}

module.exports = replacePathAlias;

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

迭代所有源代码并应用正则表达式(未彻底测试):

^(import.*from\s+["|'])(${aliasesKeys})(.*)(["|'])$

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

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

目前,Regex仅支持导入。import "module-name"表单中的副作用导入被排除,其好处是使用搜索/替换更安全。

示例:

const path = require("path");

// here sample file content of one file as hardcoded string for simplicity.
// For your project, read all files (e.g. "fs.readFile" in node.js)
// and foreach file replace content by the return string of replaceImportPathAliases function.
const fileContentSample = `
import { AppStore } from "@/app/store/appStore-types"
import { WidgetService } from "@/app/WidgetService"
import { AppStoreImpl } from "@/app/store/AppStoreImpl"
import { rootReducer } from "@/app/store/root-reducer"
export { appStoreFactory }
`;

// corresponds to tsconfig.json paths or webpack aliases
// e.g. "@/app/store/AppStoreImpl" -> "./custom/app/path/app/store/AppStoreImpl"
const pathMappingSample = {
  "@": "./src",
  foo: "bar"
};

const currentFilePathSample = "./src/sub/a.js";

function replaceImportPathAliases(currentFilePath, fileContent, pathMap) {
  const regex = createRegex(pathMap);
  return fileContent.replace(regex, replacer);

  function replacer(_, g1, aliasGrp, restPathGrp, g4) {
    const mappedImportPath = pathMap[aliasGrp] + restPathGrp;

    let mappedImportPathRelative = path.posix.relative(
      path.dirname(currentFilePath),
      mappedImportPath
    );
    // append "./" to make it a relative import path
    if (!mappedImportPathRelative.startsWith("../")) {
      mappedImportPathRelative = `./${mappedImportPathRelative}`;
    }
    return g1 + mappedImportPathRelative + g4;
  }
}

function createRegex(pathMap) {
  const mapKeysStr = Object.keys(pathMap).reduce((acc, cur) => `${acc}|${cur}`);
  const regexStr = `^(import.*from\s+["|'])(${mapKeysStr})(.*)(["|'])$`;
  return new RegExp(regexStr, "gm");
}

console.log(
  replaceImportPathAliases(
    currentFilePathSample,
    fileContentSample,
    pathMappingSample
  )
);

相关文章