修改源代码文件后缀
这是开始大规模改造terser的第一天,主要做了如下几件事:
- 添加typescript相关配置
- 修改主要的源码文件(不包含test),从js改成ts
Typescript相关配置
Terser项目是使用的rollup做的打包构建,所以设置TS也主要是对rollup的一些处理。另外原来terser上有eslint配置,也需要处理。
添加相关依赖项
首先是基本的typescript支持,及相关库。
1npm i -D typescript tslib rollup-plugin-typescript2
这里使用的是rollup-plugin-typescript2
而不是@rollup/plugin-typescript
,因为后者使用的时候会报一些难以解决的问题。
然后是eslint+typescript相关的一些依赖:
1npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
添加Typescript基本配置文件
Typescript有一个基本的配置文件tsconfig.json
,可以使用工具tsc
自动生成。
在项目的顶层目录,执行 tsc init
即可添加一个默认的tsconfig.json。
在默认生成的基础上,我修改了一些默认配置。最后的配置文件如下:
1{ 2 "compilerOptions": { 3 /* Basic Options */ 4 "incremental": true, /* Enable incremental compilation */ 5 "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 "lib": ["DOM"], /* Specify library files to be included in the compilation. */ 8 "allowJs": true, /* Allow javascript files to be compiled. */ 9 "checkJs": true, /* Report errors in .js files. */ 10 // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 "sourceMap": true, /* Generates corresponding '.map' file. */ 14 // "outFile": "./", /* Concatenate and emit output to single file. */ 15 // "outDir": "./", /* Redirect output structure to the directory. */ 16 "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 // "composite": true, /* Enable project compilation */ 18 // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 "removeComments": true, /* Do not emit comments to output. */ 20 // "noEmit": true, /* Do not emit outputs. */ 21 // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 25 /* Strict Type-Checking Options */ 26 // "strict": true, /* Enable all strict type-checking options. */ 27 // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 // "strictNullChecks": true, /* Enable strict null checks. */ 29 // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 35 /* Additional Checks */ 36 "noUnusedLocals": true, /* Report errors on unused locals. */ 37 "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 41 /* Module Resolution Options */ 42 // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 // "typeRoots": [], /* List of folders to include type definitions from. */ 47 // "types": [], /* Type declaration files to be included in compilation. */ 48 // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 53 /* Source Map Options */ 54 // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 59 /* Experimental Options */ 60 // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 63 /* Advanced Options */ 64 "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 } 66}
修改主要的源码文件
首先要确定下修改的范围,我这里修改的时候只修改源码,不改test目录中的文件。
之所以不改test目录,是想保持测试用例与主仓库完全相同。
因为terser的测试用例比较全,如果这些测试都能通过,那么基本可说明改造之后还是可信的。当然这有一个基础条件,就是测试用例未发生变化。
修改节奏
完善的typescript很难一步到位地实现,因为这一是需要对业务有足够多的了解,二是改造的成本较高,牵连面甚广,较难控制改造的规模。
所以我这里控制了TS修改的节奏,主要分成以下两大阶段:
- 第一阶段,关闭严格类型检查,仅把所有待改造的js文件修改成.ts后缀,并修复过程中遇到的错误
- 第二阶段,逐渐打开严格类型检查,并修复过程中遇到的问题
在第一阶段中,修改主要是以文件为单位进行,因为这个阶段跨文件的问题不会太多。
在第二阶段中,则是以规则为单位进行了。如果打开某个规则之后,出现的问题过多,也可以在规则内,按照文件为单位进行修改,修改完一部分之后,将规则原则以免编译不通过。
今天我的主要修改内容即为第一阶段的内容。将现有的文件(测试用例除外)修改成ts后缀。
修改内容
在将terser从js改造成ts的过程中,我主要做了如下几个方面的事情:
- 修改文件引用路径
- 修改文件后缀
- 修正改成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;
即可,既可以返回值,消除这个报错,又不影响程序原有的逻辑。
如将
1function(output) { 2 var p = output.parent(); 3 if (p instanceof AST_PropAccess && p.expression === this) { 4 var value = this.getValue(); 5 if (value.startsWith("-")) { 6 return true; 7 } 8 } 9}
修改为:
1function(output) { 2 var p = output.parent(); 3 if (p instanceof AST_PropAccess && p.expression === this) { 4 var value = this.getValue(); 5 if (value.startsWith("-")) { 6 return true; 7 } 8 } 9 return undefined; // 此处新加一行 10}
TS6133: 'xxx' is declared but its value is never read.
这里的xxx可以是任意变量名。
一般出现这种问题,多是函数的某个参数未被使用导致的。
可以给函数参数重命名,加个下划线前缀即可。
如将
1function(self, output) { 2 output.print("debugger"); 3 output.semicolon(); 4});
修改为:
1function(_self, output) { // self 改成了 _self 2 output.print("debugger"); 3 output.semicolon(); 4});
TS2554: Expected 2 arguments, but got 1.
此处的2和1都只是一个示例数字。
这个错误是指在某处函数调用处,传的参数比此函数定义的参数要少。
无伤的改法就是将此函数的参数定义中,超出给定参数长度的,设置为可选参数。
如将
1function parse($TEXT, options) { 2 // 省略具体代码 3}
改成
1function parse($TEXT, options?) { // options后加了个?号 2 // 省略具体代码 3}
TS2339: Property 'xxx' does not exist on type YYY
在未打开严格类型检查时,会出现此类错误的,一般是变量定义之后添加元素的情况。
修改的话,需要将此变量的完整类型给出,或直接设置为any。
如将
1var YYY = {}; 2YYY.xxx = 1;
改成
1var YYY: any = {}; 2YYY.xxx = 1;
或
1var YYY: { xxx?: number } = {}; 2YYY.xxx = 1;
TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
这是类型不匹配导致的错误。常见于 parseInt 等函数,当然所有函数都有可能出现此问题。
以parseInt举例,其参数类型定义如下:
1declare function parseInt(s: string, radix?: number): number;
这里的第一个参数是string,而不是number。
所以像
1parseInt(12.3, 10);
这样的用法都是错的,要改成
1parseInt(String(12.3), 10);
改造结果
现在还剩下最难搞定的几个大文件,compress/index.js
、parse.js
、scope.js
。
修改这些文件的时候,遇到过几次test不通过的情况,又推倒重新来了。
后面修改的时候,可以先改一些 JS/TS兼容的部分,之后先改回文件名,保存一部分成果。