背景是,基础库迁移到 npm 包,所有引用的地方也要进行迁移。

# 1. ImportDeclaration

对于 AST 而言,有三种引入方式:

import { postA } from 'src/common/post';
import postB from 'src/common/post';
import * as postC from 'src/common/post';

它们都属于 ImportDeclaration,有一个共同的属性 specifiers,其为数组,上面有3个例子分别对应3个 specifier 类型,分别为:

  • ImportSpecifier,可视为“具名导入”
  • ImportDefaultSpecifier,可视为“默认导入”
  • ImportNamespaceSpecifier,可视为“命名空间导入”

下面这种也是 ImportSpecifier

import { postD as request } from 'src/common/post';

知道了它们有不同类型后,怎么拿到具体引入的内容是啥呢?

对于 ImportSpecifier,有 importedlocal 两个属性,分别代表引入的名字和重命名的名字,比如下面的例子,分别是 postDrequest

import { postD as request } from 'src/common/post';

如果没有 as,那么 importedlocal 就是相等的,比如:

import { postA } from 'src/common/post';

对于 ImportDefaultSpecifierImportNamespaceSpecifier,只有 local 属性,对于下面的例子,local 值分别是 postBpostC

import postB from 'src/common/post';
import * as postC from 'src/common/post';

# 2. 替换的数据结构

构造了一个源和目标的替换关系,类型如下:

type IImportType = 'ImportSpecifier' | 'ImportDefaultSpecifier' | 'ImportNamespaceSpecifier';

// 替换关系,为字符串的时候,代表都为 ImportSpecifier 类型,且 sourceName 和 targetName 相同
// 为数组的时候,代表都为 ImportSpecifier 类型,数组的第1项和第2项分别为 sourceName 和 targetName
// 为对象的时候,就比较直白了

type IImportItem = string | Array<string> | {
  sourceName: string;
  sourceType: IImportType;
  targetName: string;
  targetType: IImportType;
};
type IReplaceConfig = {
  importedList: Array<IImportItem>;
  // 源位置
  source: string;
  // 目标位置
  target: string;
}

# 3. 脚本到不了的地方

如果迁移前后的使用方式不一样,则需要手动改代码,因为不仅涉及 import 部分,还涉及使用部分。

下面列举几种脚本可以处理的情况。

# 3.1. 具名转具名

{
  source: 'src/common/network',
  importedList: [
    'post',
  ],
  target: 'npm-network',
}

可以转换的引入是

// 之前:
import { post } from 'src/common/network';

// 之后:
import { post } from 'npm-network';

// 使用不变
post();

# 3.2. 具名转命名

{
  source: 'src/common/tools/dom2image',
  importedList: [
    {
      sourceType: 'ImportSpecifier',
      sourceName: 'dom2image',
      targetType: 'ImportNamespaceSpecifier',
    },
  ],
  target: 'npm-tools/lib/dom2image'
}

可以转换的引入是

// 之前:
import { dom2image } from 'src/common/tools/dom2image';

// 之后:
import * as dom2image from 'npm-tools/lib/dom2image';

// 使用不变
dom2image.url2Base64('xxx')

# 3.3. 默认转默认

{
  sourceType: 'ImportDefaultSpecifier',
  targetType: 'ImportDefaultSpecifier',
}

可以转换的引入是

// 之前:
import Share from 'src/common/tools/share';

// 之后:
import Share from 'npm-tools/lib/share';

// 使用不变
Share.openShareUI();

# 3.4. 默认转具名

{
  sourceType: 'ImportDefaultSpecifier',
  targetName: 'MpExposure',
  targetType: 'ImportSpecifier',
}

可以转换的引入是

// 之前:
import exposure from 'src/common/report/data-center/mp-exposure';

// 之后:
import { MpExposure as exposure } from "npm-report";

// 使用不变
exposure.init();
exposure.destroy();

# 3.5. 默认转命名

{
  sourceType: 'ImportDefaultSpecifier',
  targetType: 'ImportNamespaceSpecifier',
}

可以转换的引入是

// 之前:
import Pvpapp from 'src/common/tools/pvpapp';

// 之后:
import * as Pvpapp from 'npm-tools/lib/pvpapp';

// 使用不变
Pvpapp.invoke('xxx');

# 4. 正则 VS AST

用正则替换好处有:

  • 简单,实现成本低

弊端在于:

  • 无法处理复杂情况,比如夹杂注释

用 AST 替换好处有:

  • 可以处理任意复杂情况

弊端在于

  • 实现成本相对高
  • 可能处理后的结果不符合 eslint 规范,比如一些换行、空格、括号可能会被去掉

综合来看,可以结合二者,具体做法是先用一个错略的正则捕获一个范围,然后对这个范围内的字符串进行 AST 的处理,即 parsetransformgenerate。然后拼接其他部分的字符串即可。大致代码如下:

const IMPORT_RE = /import [\s\S]+ from '.*';\n\n/;

function replaceOneJs(content, file) {
  const match = content.match(IMPORT_RE);
  if (!match) return;

  let output = replaceDependencies(match[0]);

  if (output === match[0]) {
    return;
  }
  output = content.replace(IMPORT_RE, `${output}\n\n`);
  writeFileSync(file, output);
}

# 5. 其他

path.insertAfter 可能会导致条件编译错位,相关issue: https://github.com/babel/babel/issues/7002

没有好的解决办法,目前是在替换过程中记录存在条件编译的文件,然后保存到日志中,二次检查下。