将 TypeScript 添加为开发依赖,并初始化项目,初始化完毕之后,就可以写 .ts 文件了。

  1. npm install typescript -D
  2. npx tsc --init

获得更好的编辑支持

项目文件

通过配置 tsconfig.json 中的 rootDirfilesincludeexclude 字段,让 TypeScript 知道项目包括哪些核心文件,如果不详细定义的话,TypeScript 会检查当前目录下的所有文件,其中可能包括我们的构建脚本和配置文件(scripts/*, *.config.js)等,这并不是期待的表现。

  1. // ./tsconfig.json
  2. {
  3. "rootDir": "./src",
  4. "include": [
  5. /**
  6. * 如果开启了 allowJS 且源文件中包含 .js 文件,则必需开启 emitDeclarationOnly 或者将 outDir 设置为其它目录
  7. * 如果源文件中还包含 .js 文件对应的 .d.ts,还需要将 declarationDir 设置为其它目录
  8. * 这样 tsconfig.json 就不会报错,但实际上 outDir declarationDir 我们都是不需要的,
  9. * 因为 tsconfig.json 中的选项只用于 ts-loader transpile 和给编辑器看,
  10. * .d.ts .js 的生成使用 typescript compile API 自定义
  11. */
  12. "src/ts/**/*",
  13. "src/*.ts"
  14. ],
  15. "exclude": [
  16. "node_modules",
  17. "dist",
  18. "build",
  19. "dev",
  20. "release",
  21. "config",
  22. "scripts"
  23. ]
  24. }

出于一些个人需求,上述配置并不是最精简的配置。

注意上述示例中的注释,具体情况如下:

  • TypeScript 可以将 .ts 文件转译为 .js 文件,也只有转译为 .js 之后我们的代码才能够运行,转译的同时会生成 .d.ts 文件,其中记录的是类型信息,当我们写的代码被其他项目使用的时候,这些类型信息就能够为开发者提供类型支持。转译输出 .js 文件和 .d.ts 文件的具体行为是支持详细定义的,可以生成,也可以不生成,也可以自定义输出的路径。默认情况下调用 tsc 进行转译并不会生成 .d.ts 类型文件,转译后的 .js 文件默认会输出到原始 .ts 文件同级目录。
  • TypeScript 可以允许我们同时在项目中书写 TypeScript 和 JavaScript,前者文件后缀是 .ts,后者是 .js,我们也可以为 JavaScript 文件手动编写类型文件,比如为 filename.js 编写类型文件 filename.d.ts,获得对相应 JavaScript 代码的类型提示。

当上述两种情形交汇的时候,冲突就会产生(比如 TypeScript 会认为,根据我们上面的配置文件,编译器生成的 filename.d.ts 会覆盖我们手动写的 filename.d.ts 这是不合理的,然后 tsconfig.json 文件就会报错),我们需要调整 TypeScript 对于 .js 文件和 .d.ts 文件的输出位置来规避冲突:

  1. // ./tsconfig.js
  2. {
  3. "compilerOptions": {
  4. "allowJs": true,
  5. "checkJs": true,
  6. "declaration": true,
  7. "declarationMap": true,
  8. "emitDeclarationOnly": false,
  9. "sourceMap": true,
  10. "outDir": "./.temp/transpiled",
  11. "declarationDir": "./.temp/typings",
  12. }
  13. }

以上配置可以避免上述问题,同时也指定了这样的行为:

当在命令行调用 npx tsc 的时候,TypeScript 会将转译结果放置在 ./.temp/transpiled 目录下,保持与源文件夹相同的文件结构,类型文件会单独放在 ./.temp/typings 目录下,也保持相同的结构,如果想将转译结果和类型文件放在一起的话,将 declarationDir 配置注释掉即可。

还有,别忘了设置 .gitignore,别不小心把 ./.temp 提交出去。

虽然我们这么配置了,但一般情况下并不推荐使用 npx tsc 直接进行转译,这种方式更多地用在想快速预览转译效果的时候。

路径别名

通过配置 tsconfig.json 让编辑器能够识别路径别名,涉及的配置项主要是 compilerOptions.baseUrlcompilerOptions.paths

  1. // ./tsconfig.json
  2. {
  3. "compilerOptions": {
  4. /* Specify how TypeScript looks up a file from a given module specifier. */
  5. "moduleResolution": "node",
  6. /* Specify the base directory to resolve non-relative module names. */
  7. "baseUrl": ".",
  8. /* Specify a set of entries that re-map imports to additional lookup locations. */
  9. "paths": {
  10. "ES": [
  11. "./src/es/index.js",
  12. "./src/es/index.mjs"
  13. ],
  14. "ES/*": [
  15. "./src/es/*"
  16. ],
  17. "CJS": [
  18. "./src/cjs/index.js",
  19. "./src/cjs/index.cjs"
  20. ],
  21. "CJS/*": [
  22. "./src/cjs/*"
  23. ],
  24. "TS": [
  25. "./src/ts/index.ts"
  26. ],
  27. "TS/*": [
  28. "./src/ts/*"
  29. ],
  30. "Statics/*": [
  31. "./src/statics/*"
  32. ],
  33. "Images/*": [
  34. "./src/statics/images/*"
  35. ],
  36. "Styles/*": [
  37. "./src/statics/styles/*"
  38. ]
  39. },
  40. }
  41. }

如果与 webpack 一起使用的话,记得同时为 webpack.config.js 设置 resolve.extensionsresolve.alias,也有 webpack 插件可以帮忙将指定 tsconfig.json 中的 compilerOptions.paths 自动同步到 webpack.config.js 的相关配置中,比如 tsconfig-paths-webpack-plugin,但我觉得这种配置更新频率较低,引入一个插件实在不值得,手动配置灵活度也很高。

代码格式化

:::tips 配置过程中如果 ESLint 表现异常的话,比如 VSCode 插件提示:ESLint can not lint: *****,可以尝试重启 ESLint 服务,重启方式为:敲击 F1 打开命令输入框,输入 ESLint: Restart ESLint Server。 :::

使用 ESLint 为 TypeScript 添加代码格式化功能,配置完成之后,我们为 VSCode 安装的 ESLint 插件能够同时为 TypeScript 和 JavaScript 文件提供格式化,首先安装相关依赖。

  1. npm install -D typescript eslint
  2. npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
  3. # optional
  4. npm install -D eslint-config-standard-with-typescript@latest

依赖安装完成之后,开始配置 .eslintrc.json,主要是 extendsparserplugins 三项。

  1. // .eslintrc.json
  2. {
  3. "env": {
  4. "browser": true,
  5. "es6": true,
  6. "node": true
  7. },
  8. "extends": [
  9. "standard",
  10. "standard-with-typescript",
  11. "eslint:recommended",
  12. "plugin:@typescript-eslint/recommended"
  13. ],
  14. "globals": {
  15. "Atomics": "readonly",
  16. "SharedArrayBuffer": "readonly"
  17. },
  18. "parser": "@typescript-eslint/parser",
  19. "parserOptions": {
  20. "ecmaVersion": 2021,
  21. "sourceType": "module",
  22. "project": "./tsconfig.json"
  23. },
  24. "plugins": [
  25. "@typescript-eslint/eslint-plugin"
  26. ],
  27. "rules": {
  28. "max-len": [
  29. 1,
  30. {
  31. "code": 140,
  32. "ignoreUrls": true,
  33. "ignoreTemplateLiterals": true
  34. }
  35. ]
  36. }
  37. }

如果使用 standard-with-typescript 规则集的话,需要额外安装依赖(小节开头提到的可选依赖),并且在配置的时候通过 parserOptions.project 指定 TypeScript 配置文件,指定配置文件的本质其实是指定配置文件中的 filesincludeexclude 等项目文件信息,从而使 Linter 知道哪些文件是要被解析的。于是存在这样一种情况,我们的 tsconfig.json 中只希望指定项目核心的代码文件(需要被转译的代码文件,如 src 目录下的文件),此时如果我们访问项目目录下的其它代码文件,Linter 会报错,提示我们这些文件不在 Lint 名单之内,我们的预期有两种:检查该文件、忽略该文件。

检查该文件的做法是:提供一个单独的 tsconfig.json 文件,比如 tsconfig.eslint.json,它的定义如下:

  1. {
  2. // extend your base config so you don't have to redefine your compilerOptions
  3. "extends": "./tsconfig.json",
  4. "include": [
  5. "src/**/*.ts",
  6. // if you have a mixed JS/TS codebase, don't forget to include your JS files
  7. "src/**/*.js",
  8. "tests/**/*.ts"
  9. ]
  10. }

然后将它指定给 parserOptions.project,如下:

  1. {
  2. "parserOptions": {
  3. "ecmaVersion": 2021,
  4. "sourceType": "module",
  5. - "project": "./tsconfig.json"
  6. + "project": "./tsconfig.eslint.json"
  7. }
  8. }


忽略该文件的做法是:将该文件添加到 .eslintigore 中或者使用 ESLint 的 overrides 选项对个别文件的配置进行覆盖,在此不做详细描述。

这个问题相关的资料如下:

此时,我们的 ESLint 已经能够正确格式化 .ts 文件了,但我们的规则还是比较笼统的,.ts.js 文件在 ESLint 眼中一视同仁,实际上 .ts 的很多规则并不适用于 .js 文件,所以我们选择使用 .eslintrc.jsonoverrides 字段将二者区分开,最终示例代码如下:

  1. // ./eslintrc.json
  2. {
  3. "env": {
  4. "browser": true,
  5. "es6": true,
  6. "node": true
  7. },
  8. "globals": {
  9. "Atomics": "readonly",
  10. "SharedArrayBuffer": "readonly"
  11. },
  12. "overrides": [
  13. {
  14. "files": [
  15. "*.js"
  16. ],
  17. "extends": [
  18. "standard"
  19. ],
  20. "plugins": [],
  21. "parser": "espree",
  22. "parserOptions": {
  23. "ecmaVersion": 2021,
  24. "sourceType": "module"
  25. }
  26. },
  27. {
  28. "files": [
  29. "*.ts"
  30. ],
  31. "extends": [
  32. "standard-with-typescript",
  33. "eslint:recommended",
  34. "plugin:@typescript-eslint/recommended"
  35. ],
  36. "parser": "@typescript-eslint/parser",
  37. "parserOptions": {
  38. "ecmaVersion": 2021,
  39. "sourceType": "module",
  40. "project": "./tsconfig.eslint.json"
  41. },
  42. "plugins": [
  43. "@typescript-eslint/eslint-plugin"
  44. ]
  45. }
  46. ],
  47. "rules": {
  48. "max-len": [
  49. 1,
  50. {
  51. "code": 140,
  52. "ignoreUrls": true,
  53. "ignoreTemplateLiterals": true
  54. }
  55. ]
  56. }
  57. }

隔离 TypeScript 和 JavaScript 的 Lint 规则之后,tsconfig.eslint.json 中的 *.js 也可以移除啦!

{
  // extend your base config so you don't have to redefine your compilerOptions
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.ts",
-    // if you have a mixed JS/TS codebase, don't forget to include your JS files
-   "src/**/*.js",
    "tests/**/*.ts"
  ]
}

本小节参考以下内容:

获得更好的开发支持

手动转译

目前为止,如果我们在项目根目录执行 npx tsc,我们就可以在 ./.temp/ 目录下看到源代码转译之后的结果,代码和 SourceMap 在 transpiled 文件夹中,DTS 在 typings 文件夹中(我们也可以将他们放置在一起)。

不出意外的话,这些文件是可以直接运行的,我们可以添加一些选项来更加精准地控制转译的行为,这些选项的含义可以查阅 TypeScript TSConfig Reference,在此不做过多解读,示例如下:

// ./tsconfig.json
{
    "compilerOptions": {
      "incremental": true,
    "composite": true,
    "tsBuildInfoFile": "./.temp/.tsbuildinfo",
    "target": "es6",
    "module": "es6",
    "rootDir": "./src",
    "isolatedModules": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "typeAcquisition": {
    "enable": false
  }
}

并不是每次编辑完文件之后,都要手动运行一下 npx tsc,TypeScript 提供了 watch 模式,使用 npx tsc --watch 或者 npx tsc -w 开启,这种情况下会开启一个常驻的编译进程,源文件每次发生变更,就会进行重新编译,重新编译的时候并不是全量编译,开启 compilerOptions.incremental 即为增量编译模式,增量编译模式下有一个记录编译信息的缓存文件,可以通过 compilerOptions.tsBuildInfoFile 指定其输出位置。

需要注意的是,如果开启了增量编译模式,且 .tsbuildinfo 文件存在,则下次编译优先考虑增量编译,如果要全量编译,请在编译之前将 .tsbuildinfo 文件移除。

自动转译

如果是临时写个脚本或者体量较小的玩意儿,手动编译也就够了。对于大型的、长期维护的、依赖复杂的项目,手动编译就显得很麻烦,这些项目中一般也会使用其它构建工具,用于处理非代码文件或者对输出的代码进行打包优化等,一方面 TypeScript Compiler 并不很擅长做这些事情,另一方面 .ts 代码转译成 .js 是构建流程的第一步,我们希望它能够与原有的构建流程无缝兼容。

我们需要做的事情很简单,以 webpack 集成为例,我们只要将 ts-loader 添加到 .ts 文件解析的第一个环节并作简单配置即可:

npm install -D ts-loader
// ./config/loaders.config.js

const jsLoader = {
- test: /\.m?js$/,
+ test: /\.[m|c]?[j|t]sx?$/,
  exclude: /(node_modules|bower_components)/,
  use: [
    {
      loader: 'babel-loader'
    },
+   {
+     loader: 'ts-loader',
+     options: {
+       transpileOnly: true
+     }
+   }
  ]
}

现在,当 webpack 解析 .ts 或者 .tsx 或者 .js 文件的时候,会首先传递给 ts-loader 进行处理,ts-loader 会将 TypeScript 代码按照 tsconfig.json 中的配置转译为 JavaScript 代码,然后交给 babel-loader 处理,babel-loader 之后就是常规的构建流程啦。另外,由于我们配置了 transpileOnly: true,所以 ts-loader 除了转译代码之外,并不会做多余的操作。

如果项目直接引用第三方库的 .ts 文件的话,上述配置会忽略 node_modules 中的 ts 文件,可以将 exclude 修改为 /(bower_components)/,这样我们的 ts-loader 也会解析第三方库的 .ts 文件啦。

至此,我们使用 TypeScript 写代码的目的就初步达成啦,除了你可以把 .js 文件变成 .ts 文件之外,一切都跟之前一样。

转译检查

在我们配置 ts-loader 的过程中,将 transpileOnly 设置为 true,这使得 ts-loader 只专注于转译 .ts 文件,不进行类型检查,从而获得性能提升。但大多数时候,我们可能需要开启类型检查,此时需要用到另一个插件:fork-ts-checker-webpack-plugin

运行以下命令安装这个插件:

npm install fork-ts-checker-webpack-plugin -D

初始化一个插件实例:

const forkTsCheckerWebpackPlugin = new ForkTsCheckerWebpackPlugin({
  typescript: {
    syntactic: true, semantic: true, declaration: false, global: false
  }
})

把这个实例传给 Webpack 的插件配置中就好啦。

项目引用

但是……如果涉及到多个不同的项目同时开发,又相互依赖呢?好像又多了一些麻烦。

假设一个情境,我在本地同时开发两个项目,A 项目和 B 项目,A 项目依赖 B 项目,我有以下需求:

  1. A 能够像使用第三方依赖一样使用 B 项目,当我开发 A 的时候,不要关心 B,不需要对 B 进行任何操作
  2. 当我在开发 A 的过程中修改 B 的时候,我希望 A 能够响应 B 的变更
  3. 当我在 A 中使用 B 的时候,我希望得到 B 完善的类型提示

第一个需求是很容易达成的,使用 NPM Workspaces 或者 TypeScript References 都可以实现,前者对项目目录结构有要求,后者对开发启动方式或者构建流程有要求。

第二个需求也是可以达成的,将 webpack devServer 的 watchOptions.ignored 关掉即可,这个选项默认会忽视 /node_modules/ 中的变更,关闭之后会有一定的性能损失,建议进行定制化屏蔽(正则匹配),同时,最好将 webpack 的 config.resolve.symlinks 设置为 false,避免 workspaces 模式下模块解析的一些问题。

第三个需求可以通过配置 package.json 模块导出的相关字段解决,我们都知道,当 A 项目引用 B 项目时,模块解析的方式由 B 项目决定,当 A 在引用时指定不同的路径,指向的文件是不同的。当 A 引用到 .ts 文件时,可以获得类型提示,当 A 引用到 .d.ts 文件时,可以获得类型提示,只有这两种方式。

将这三个需求的各种解决方式权衡考虑,我们就能够得到适合自己的解决方案,以下是我的:

对于一个支持 TypeScript 的项目来说,typings 字段是一定要指定一个总体的类型文件的,我倾向于把它放在根目录的 typings 文件夹中,以 B 项目为例,package.json 这样配置:

// ./package.json
{
    "typings": "./typings/main.d.ts",
  "types": "./typings/main.d.ts",
}

如果这个 DTS 文件存在,当我在 A 中 import * as B from 'packageB' 时,类型提示就来自这个文件。这个文件一直存在嘛?它可以一直存在,但不合适,它并不在开发构建流程中,即我会在开发完 B 之后通过 npm run dist 来生成它,而不是 npm run dev 过程中,这就意味着,如果我在开发 A,引用了 B,然后修改了 B,B 的类型提示文件是不会更新的,它应该更新,但它应该在我开发完 A 的事情,回头提交 B 的更改时再更新,而不是在我关注 A 项目的时候分心去更新。

那么我就只能通过直接引用 .ts 文件来获得类型提示了,解析要定义在哪里呢?是 main 字段嘛?当然不是,main 字段是生产可用模块导出的地方,.ts 不算是生产可用模块,那就只能是 exports (以及 typesVersion)中了。

// ./package.json
{
  "exports": {
      "./ts": "./src/main.ts",
    "./ts/*": "./src/ts/*.ts",
    "./ts-js/": "./src/ts/*.js",
  },
  "typesVersions": {
    "*": {
      "ts": [
        "./src/main.ts"
      ],
      "ts/*": [
        "./src/ts/*"
      ],
      "ts-js/*": [
        "./src/ts/*"
      ],
    }
  }
}

然后我们在 A 项目中引用 B 的方式要切换为 import * as B from 'packageB/ts',我们可以获得最新的类型提示啦,来自 TypeScript 源码而不是类型文件的类型提示。

然后又有朋友说了,如果项目 A 中引用 B 的地方很多,岂不是每个文件都要去加个 /ts 无意义后缀,好麻烦啊,我的实践是,永远不直接引用第三方包,建立一个 src/libs 专门用来管理引入的依赖,相当于是一层防腐层,在代码层面将依赖和源码隔离开来,将来替换的时候也方便,对第三方 API 做包装也方便,香的很。

这样引用有一个弊端,就是被引用的项目会被当做当前项目,代码体积相对较大,会有一定的性能损失。社区中有另外一种方案,既没有 monorepo 那么复杂,又具备 monorepo 的一些优点,他们将 NPM Workspaces 和 TypeScripts Reference 结合起来,也能够解决上述需求,对于不像我一样介意多开一个 tsc 编译终端的朋友们算是一个更好的方案,我把相关的资料放在下面,大家自行查阅。

获得更好的打包支持

开发的问题是解决了,那最终输出的文件呢?当然是写脚本自定义了,灵活度那么高,总不可能还用终端吧,TypeScript 提供了 Compiler API。

首先确定最终要对外界导出哪些东西,Mobius Template 提倡的构建目标有四种,dev 用于开发,build 用于查看开发构建配置下生成的代码,没有打包速度提升相关的优化,dist 用于生产,release 用于发布,要到处给外界使用的东西,应该在 release 中。

// ./package.json

{
    "typings": "./typings/main.d.ts",
  "types": "./typings/main.d.ts",
  "main": "./release/modules/cjs/main.cjs",
  "module": "./release/modules/esm/main.js",
  "exports": {
    ".": {
      "require": "./release/modules/cjs/main.cjs",
      "import": "./release/modules/esm/main.js",
      "node": "./release/modules/esm/main.js",
      "default": "./release/modules/esm/main.js"
    },

    "./es": "./release/modules/es/main.js",
    "./es/*": "./release/modules/es/*",
    "./es-js/*": "./release/modules/es/*.js",

    "./ts": "./src/main.ts",
    "./ts/*": "./src/ts/*.ts",
    "./ts-js/*": "./src/ts/*.js",

    "./esm": "./release/modules/esm/main.js",
    "./umd": {
      "require": "./release/modules/umd/main.cjs",
      "import": "./release/modules/umd/main.js",
      "node": "./release/modules/umd/main.js",
      "default": "./release/modules/umd/main.js"
    },
    "./cjs": "./release/modules/cjs/main.cjs",

    "./src/*": "./src/*",
    "./src-ts/*": "./src/*.ts",
    "./src-js/*": "./src/*.js",
    "./statics/*": "./statics/*",
    "./release/*": "./release/*",
    "./release-js/*": "./release/*.js",
    "./release-cjs/*": "./release/*.cjs",
    "./esm/*": "./release/modules/esm/*",
    "./cjs/*": "./release/modules/cjs/*",
    "./umd/*": "./release/modules/umd/*",

    "./package.json": "./package.json"
  },
  "typesVersions": {
    "*": {
      "es": [
        "./release/modules/es/main.d.ts"
      ],
      "es/*": [
        "./release/modules/es/*"
      ],
      "es-js/*": [
        "./release/modules/es/*"
      ],
      "ts": [
        "./src/main.ts"
      ],
      "ts/*": [
        "./src/ts/*"
      ],
      "ts-js/*": [
        "./src/ts/*"
      ],
      "esm": [
        "./typings/main.d.ts"
      ],
      "umd": [
        "./typings/main.d.ts"
      ],
      "cjs": [
        "./typings/main.d.ts"
      ],
      "*": [
        "./typings/main.d.ts"
      ]
    }
  }
}

然后写脚本啦,注意看上面我们的导出需求,我们在 release 目录下设立 modules 目录用来放置所有导出的模块,按照模块类型区分,包括 cjsumdes 三种,当另一个项目引用的时候,取决于引用方式,会拿到不同的文件和类型提示。

  • cjs:从 ./release/modules/cjs/main.js 拿代码,类型提示来自 typings 字段配置,指向 ./typings/main.d.ts
  • umd: 从 ./release/modules/umd/main.js 拿代码,类型提示来自 typings 字段配置,指向 ./typings/main.d.ts
  • es: 从 ./release/modules/es/main.js 拿代码,类型提示来自 typesVersion 字段配置,指向 ./release/modules/es/main.d.ts(或者具体到子文件的代码和类型)。
  • esm: 从 ./release/modules/esm/main.js 拿代码,类型提示来自 typings 字段配置,指向 ./typings/main.d.ts

要特别说明的是,这里的 es 并不代表这些代码在浏览器或 Node 环境中可以直接执行,它是面向打包的,原因如下:

  • TypeScript 转译 .ts 文件不会变更 import/export 语句,即如果原先引用路径无后缀,转译之后也没有后缀,如果原先后缀是 .ts,转译之后也是 .ts 后缀。
  • TypeScript 可以正确处理 .js 后缀和无后缀两种情况,即如果我们引用的目标文件实际上是 import * as Tar from './target.ts',当我们写成 import * as Tar from './target.js' 或者 import * as Tar from './target' 的时候,都是没问题的
  • 如果要让 es 文件可以执行,需要在建立模块引用关系的时候补全 .js 后缀,无后缀或者 .ts 后缀都是不行的。
  • webpack 打包需要能够根据路径解析到文件,它能够解析无后缀情况,如果我们引用的目标文件实际上是 import * as Tar from './target.ts',那我们就只能写 import * as Tar from './target.ts'或者 import * as Tar from './target'

看似没什么问题,直接用 .js 后缀就好啦,别忘了我们在上一个环节中的考虑,我们希望另一个项目在引用当前项目的时候,直接引用 src 中的源文件,而如果源文件使用 .js 做引用路径后缀,webpack 就找不到目标文件(因为引用的文件实际上是 .ts 后缀,TypeScript 可以正确处理这种情况,但 webpack 不可以),为了尽可能满足需求,我们只能选择使用无后缀的模块引用方式。

esm 是可以在浏览器或 Node 环境中直接执行的版本,它由 webpack 的打包而成,不保证可用且不建议在生产环境中使用(webpack 面向 esmodule 的构建目标是实验功能,尚不完善)。

// ./scripts/bundle.js

const getTSConfigJSONString = () => readFileSync(rootResolvePath('./tsconfig.json'), { encoding: 'utf8' })
  .replace(/\s\/\*.*\*\//g, '')
  .replace(/\s\/\/.*,/g, '')
  .replace(/\s\/\*.*/g, '')
  .replace(/\s\*(.)*/g, '')
const getTSConfig = () => JSON.parse(getTSConfigJSONString())
const collectFiles = (rootPath, results = []) => {
  const files = fs.readdirSync(rootPath)
  files.forEach(item => {
    const filepath = path.resolve(rootPath, item)
    if (fs.statSync(filepath).isDirectory()) {
      results.push(...collectFiles(filepath))
    } else {
      results.push(filepath)
    }
  })
  return results
}

const packES = () => {
  return new Promise((resolve) => {
    const compilerOptions = getTSConfig().compilerOptions
    compilerOptions.incremental = false
    delete compilerOptions.tsBuildInfoFile
    compilerOptions.composite = false
    compilerOptions.target = ts.ScriptTarget.ES2015
    compilerOptions.module = ts.ModuleKind.ES2015
    compilerOptions.outDir = rootResolvePath('./release/modules/es')
    compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs
    delete compilerOptions.declarationDir

    const program = ts.createProgram([rootResolvePath('./src/main.ts')], compilerOptions)
    const emitResult = program.emit()

    const targetFiles = collectFiles(rootResolvePath('./src/ts'))
      .filter(filepath => filepath.endsWith('.js') || filepath.endsWith('.d.ts'))

    targetFiles.forEach(filepath => {
      const relativePathToSrc = path.relative(rootResolvePath('./src'), filepath)
      const relativePathToDest = path.relative(filepath, rootResolvePath('./release/modules/es'))
      copyFileSync(
        filepath,
        path.resolve(filepath, relativePathToDest, relativePathToSrc)
      )
    })

    console.log('【packES】 emitResult', emitResult)
    console.log('【packES】 source file copyed', targetFiles)
    resolve(emitResult)
  })
}

const packTypings = () => {
  return new Promise((resolve) => {
    const compilerOptions = getTSConfig().compilerOptions
    compilerOptions.emitDeclarationOnly = true
    compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs
    compilerOptions.declarationDir = rootResolvePath('./typings')

    const program = ts.createProgram([rootResolvePath('./src/main.ts')], compilerOptions)
    const emitResult = program.emit()

    const dtsFiles = collectFiles(rootResolvePath('./src/ts'))
      .filter(filepath => filepath.endsWith('.d.ts'))

    dtsFiles.forEach(filepath => {
      const relativePathToSrc = path.relative(rootResolvePath('./src'), filepath)
      const relativePathToDest = path.relative(filepath, rootResolvePath('./typings'))
      copyFileSync(
        filepath,
        path.resolve(filepath, relativePathToDest, relativePathToSrc)
      )
    })

    console.log('【packTypings】 emitResult', emitResult)
    console.log('【packTypings】 dts file copyed', dtsFiles)
    resolve(emitResult)
  })
}

注意,tsconfig.json 的配置读进来之后,并不可以直接当作 compilerOptions 使用,targetmodulemoduleResolution 等字段需要使用 typescript 提供的常量重新定义。

其它

# 下载所有依赖
npm install -D typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-standard-with-typescript@latest ts-loader
# 移除 VCS 冗余项
git rm -f -r dev
git rm -f -r build
git rm -r -f dist
git rm -r -f release
git rm -f -r typings