使用Typescript改造Terser - 2 修改源代码文件后缀

发布于: 2020-05-17作者: 鱼肚最后更新: 2020-06-24

修改源代码文件后缀

这是开始大规模改造terser的第一天,主要做了如下几件事:

  1. 添加typescript相关配置
  2. 修改主要的源码文件(不包含test),从js改成ts

Typescript相关配置

Terser项目是使用的rollup做的打包构建,所以设置TS也主要是对rollup的一些处理。另外原来terser上有eslint配置,也需要处理。

添加相关依赖项

首先是基本的typescript支持,及相关库。

1
npm i -D typescript tslib rollup-plugin-typescript2

这里使用的是rollup-plugin-typescript2而不是@rollup/plugin-typescript,因为后者使用的时候会报一些难以解决的问题。

然后是eslint+typescript相关的一些依赖:

1
npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

添加Typescript基本配置文件

Typescript有一个基本的配置文件tsconfig.json,可以使用工具tsc自动生成。

在项目的顶层目录,执行 tsc init即可添加一个默认的tsconfig.json。

在默认生成的基础上,我修改了一些默认配置。最后的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
  "compilerOptions": {
    /* Basic Options */
    "incremental": true,                   /* Enable incremental compilation */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "es2015",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "lib": ["DOM"],                             /* Specify library files to be included in the compilation. */
    "allowJs": true,                       /* Allow javascript files to be compiled. */
    "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    // "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,                /* Report errors on unused locals. */
    "noUnusedParameters": true,            /* Report errors on unused parameters. */
    "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */

    /* Advanced Options */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}

修改主要的源码文件

首先要确定下修改的范围,我这里修改的时候只修改源码,不改test目录中的文件。

之所以不改test目录,是想保持测试用例与主仓库完全相同。

因为terser的测试用例比较全,如果这些测试都能通过,那么基本可说明改造之后还是可信的。当然这有一个基础条件,就是测试用例未发生变化。

修改节奏

完善的typescript很难一步到位地实现,因为这一是需要对业务有足够多的了解,二是改造的成本较高,牵连面甚广,较难控制改造的规模。

所以我这里控制了TS修改的节奏,主要分成以下两大阶段:

  1. 第一阶段,关闭严格类型检查,仅把所有待改造的js文件修改成.ts后缀,并修复过程中遇到的错误
  2. 第二阶段,逐渐打开严格类型检查,并修复过程中遇到的问题

在第一阶段中,修改主要是以文件为单位进行,因为这个阶段跨文件的问题不会太多。

在第二阶段中,则是以规则为单位进行了。如果打开某个规则之后,出现的问题过多,也可以在规则内,按照文件为单位进行修改,修改完一部分之后,将规则原则以免编译不通过。

今天我的主要修改内容即为第一阶段的内容。将现有的文件(测试用例除外)修改成ts后缀。

修改内容

在将terser从js改造成ts的过程中,我主要做了如下几个方面的事情:

  1. 修改文件引用路径
  2. 修改文件后缀
  3. 修正改成TS之后遇到的错误

其中修改文件引用路径,是指的将 import / require 语句中的.js后缀去掉。

比如原来有 import A from './a.js'这样的文件引用,因为后面涉及到将a.js修改成a.ts,所以继续这么引用会出现错误。

为避免后面出现因为这类写法导致的问题,可将其先改成同时兼容.ts.js的写法,即不加文件后缀。

主要的报错及相应处理方法

TS7030: Not all code paths return a value

出现这种问题,一般是因为函数的末尾没有返回值,但是其中的一些if/else区块中有。

为修复这种问题,可以直接定位到函数的末尾,加一个 return undefined; 即可,既可以返回值,消除这个报错,又不影响程序原有的逻辑。

如将

1
2
3
4
5
6
7
8
9
function(output) {
  var p = output.parent();
  if (p instanceof AST_PropAccess && p.expression === this) {
    var value = this.getValue();
    if (value.startsWith("-")) {
      return true;
    }
  }
}

修改为:

1
2
3
4
5
6
7
8
9
10
function(output) {
  var p = output.parent();
  if (p instanceof AST_PropAccess && p.expression === this) {
    var value = this.getValue();
    if (value.startsWith("-")) {
      return true;
    }
  }
  return undefined; // 此处新加一行
}

TS6133: 'xxx' is declared but its value is never read.

这里的xxx可以是任意变量名。

一般出现这种问题,多是函数的某个参数未被使用导致的。

可以给函数参数重命名,加个下划线前缀即可。

如将

1
2
3
4
function(self, output) {
  output.print("debugger");
  output.semicolon();
});

修改为:

1
2
3
4
function(_self, output) { // self 改成了 _self
  output.print("debugger");
  output.semicolon();
});

TS2554: Expected 2 arguments, but got 1.

此处的2和1都只是一个示例数字。

这个错误是指在某处函数调用处,传的参数比此函数定义的参数要少。

无伤的改法就是将此函数的参数定义中,超出给定参数长度的,设置为可选参数。

如将

1
2
3
function parse($TEXT, options) {
  // 省略具体代码
}

改成

1
2
3
function parse($TEXT, options?) { // options后加了个?号
  // 省略具体代码
}

TS2339: Property 'xxx' does not exist on type YYY

在未打开严格类型检查时,会出现此类错误的,一般是变量定义之后添加元素的情况。

修改的话,需要将此变量的完整类型给出,或直接设置为any。

如将

1
2
var YYY = {};
YYY.xxx = 1;

改成

1
2
var YYY: any = {};
YYY.xxx = 1;

1
2
var YYY: { xxx?: number } = {};
YYY.xxx = 1;

TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

这是类型不匹配导致的错误。常见于 parseInt 等函数,当然所有函数都有可能出现此问题。

以parseInt举例,其参数类型定义如下:

1
declare function parseInt(s: string, radix?: number): number;

这里的第一个参数是string,而不是number。

所以像

1
parseInt(12.3, 10);

这样的用法都是错的,要改成

1
parseInt(String(12.3), 10);

改造结果

现在还剩下最难搞定的几个大文件,compress/index.jsparse.jsscope.js

修改这些文件的时候,遇到过几次test不通过的情况,又推倒重新来了。

后面修改的时候,可以先改一些 JS/TS兼容的部分,之后先改回文件名,保存一部分成果。


0条评论