为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

繁花如伊 2021-08-20 17:49:53
在使用 Webpack、TypeScript 或其他转换 ES 模块导入的工具的模块化环境中,使用路径别名,常见约定是@ forsrc。转换具有别名绝对路径的项目是我经常遇到的问题:src/foo/bar/index.jsimport baz from '@/baz';到相对路径:src/foo/bar/index.jsimport baz from '../../baz';例如,使用别名的项目需要与另一个不使用别名的项目合并,由于样式指南或其他原因,无法将后者配置为使用别名。这无法通过简单的搜索和替换来解决,手动修复导入路径既乏味又容易出错。我希望原始 JavaScript/TypeScript 代码库在其他方面保持完整,因此使用转译器对其进行转换可能不是一种选择。我想使用我选择的 IDE(Jetbrains IDEA/Webstorm/Phpstorm)实现这种重构,但会接受任何其他 IDE(VS Code)或普通 Node.js 的解决方案。如何做到这一点?
查看完整描述

3 回答

?
梵蒂冈之花

TA贡献1900条经验 获得超5个赞

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


1. babel-plugin-module-resolver

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


.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并.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",

  ...

};

2. 遍历所有源文件,例如 traverse 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,其source.value的ImportDeclaration节点可以被修饰:

//img1.sycdn.imooc.com//611f7aec0001a30f03500309.jpg

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

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}包含路径别名"@"。可以通过修改第2和第3个捕获组(路径映射+解析为相对路径)来处理新的导入路径。

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

目前,正则表达式仅支持导入。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

  )

);


查看完整回答
反对 回复 2021-08-20
?
鸿蒙传说

TA贡献1865条经验 获得超7个赞

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


它基本上遍历项目树,搜索所有文件,/"@(\/\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);

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


对于 IDE 部分,我知道 Jetbrains Webstorm 让你定义 npm 任务。

创建一个scripts目录来保存脚本。

定义一个脚本package.json


"scripts": {

    ...

    "replaceimports": "node scripts/script.js \"src\""

}

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


查看完整回答
反对 回复 2021-08-20
  • 3 回答
  • 0 关注
  • 189 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信