是否可以使用 webpack 加载程序从文件生成 Typescript 接口?

Is it possible to generate Typescript interfaces from files with a webpack loader?

我正在尝试创建一个 webpack 加载程序,将包含 API 数据结构描述的文件转换为一组 TypeScript 接口。

在我的具体案例中,该文件是 JSON,但这最终应该是无关紧要的——该文件只是描述 Web 应用程序后端和前端之间交互的共享数据源.在我下面的 MCVE 中,您可以看到 JSON 文件包含一个空对象,以强调文件的类型和内容与问题无关。

我目前的尝试报告了两个错误(我假设第二个是由第一个引起的):

[at-loader]: Child process failed to process the request:  Error: Could not find file: '/private/tmp/ts-loader/example.api'.
ERROR in ./example.api
Module build failed: Error: Final loader didn't return a Buffer or String

如何使用 webpack 加载程序生成 TypeScript 代码?

package.json

{
  "name": "so-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "awesome-typescript-loader": "^3.2.3",
    "typescript": "^2.6.1",
    "webpack": "^3.8.1"
  }
}

webpack.config.js

const path = require('path');

module.exports = {
  entry: './index.ts',
  output: {
    filename: 'output.js',
  },
  resolveLoader: {
    alias: {
      'my-own-loader': path.resolve(__dirname, "my-own-loader.js"),
    },
  },
  module: {
    rules: [
      {
        test: /\.api$/,
        exclude: /node_modules/,
        use: ["awesome-typescript-loader", "my-own-loader"],
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: "awesome-typescript-loader",
      },
    ]
  },
};

我自己的-loader.js

module.exports = function(source) {
  return `
interface DummyContent {
    name: string;
    age?: number;
}
`;
};

index.ts

import * as foo from './example';

console.log(foo);

example.api

{}

我认识到还有其他代码生成技术。例如,我可以使用一些构建工具将我的 JSON 文件转换为 TypeScript 并签入它们。我正在寻找一个更动态的解决方案。


my-own-loader.js does not export json but string.

没错,就像加载图像文件并不总是导出二进制数据,但有时会输出表示图像元数据的 JavaScript 数据结构。

Why you need to define the typescript interfaces from json file? Would the interfaces be used for typescript compilation?

是的。我想导入一个描述我的 API 数据结构的文件并自动生成相应的 TypeScript 接口。拥有共享文件允许前端和后端始终就将出现的数据达成一致。

如果您的 API 遵循 swagger 规范,您可以使用 npm 包 swagger-ts-generator 从中生成 TypeScript 文件:

Swagger TypeScript code generator

Node module to generate TypeScript code for Angular (2 and above) based on Webapi meta data in Swagger v2 format.

基本上,你给它招摇 URL 它会生成 TypeScript。这些示例适用于 Gulp,但它们应该很好地移植到 WebPack:

var swagger = {
    url: 'http://petstore.swagger.io/v2/swagger.json',
    //url: 'http://127.0.0.1/ZIB.WebApi.v2/swagger/docs/v1',
    swaggerFile: folders.swaggerFolder + files.swaggerJson,
    swaggerFolder: folders.swaggerFolder,
    swaggerTSGeneratorOptions: {
        modelFolder: folders.srcWebapiFolder,
        enumTSFile: folders.srcWebapiFolder + 'enums.ts',
        enumLanguageFiles: [
            folders.srcLanguagesFolder + 'nl.json',
            folders.srcLanguagesFolder + 'en.json',
        ],
        modelModuleName: 'webapi.models',
        enumModuleName: 'webapi.enums',
        enumRef: './enums',
        namespacePrefixesToRemove: [
        ],
        typeNameSuffixesToRemove: [
        ]
    }
}

首先,感谢您提供 MCVE。这是一个 非常 有趣的问题。我用来把这个答案放在一起的代码是基于所说的 MCVE 和 is available here.

缺少文件?

这确实是一条最无用的错误信息。该文件显然位于该位置,但 TypeScript 将拒绝加载任何没有可接受扩展名的内容。

这个错误本质上是隐藏了真正的错误,也就是

TS6054: File 'c:/path/to/project/example.api' has unsupported extension. The only supported extensions are '.ts', '.tsx', '.d.ts', '.js', '.jsx'.

这可以通过侵入 typescript.js 并手动添加文件来验证。这很丑陋,就像侦探工作经常做的那样(从 v2.6.1 中的第 95141 行开始):

for (var _i = 0, rootFileNames_1 = rootFileNames; _i < rootFileNames_1.length; _i++) {
    var fileName = rootFileNames_1[_i];
    this.createEntry(fileName, ts.toPath(fileName, this.currentDirectory, getCanonicalFileName));
}
this.createEntry("c:/path/to/project/example.api", ts.toPath("c:/path/to/project/example.api", this.currentDirectory, getCanonicalFileName));

从概念上讲,您只是在加载器之间传递一个字符串,但事实证明文件名在这里很重要。

可能的修复

我没有看到使用 awesome-typescript-loader 执行此操作的方法,但如果您愿意使用 ts-loader 代替,您当然可以从具有任意扩展名的文件生成 TypeScript,编译它TypeScript,并将其注入您的 output.js.

ts-loader 有一个 appendTsSuffixTo option, that can be used to work around the well-known file extension pain. Your webpack config might look something like this 如果你走那条路:

rules: [
  {
    test: /\.api$/,
    exclude: /node_modules/,
    use: [
      { loader: "ts-loader" },
      { loader: "my-own-loader" }
    ]
  },
  {
    test: /\.tsx?$/,
    exclude: /node_modules/,
    loader: "ts-loader",
    options: {
      appendTsSuffixTo: [/\.api$/]
    }
  }
]

关于接口和 DX 的注意事项

接口被编译器擦除。这可以通过 运行ning tsc 对抗 this

之类的东西来证明
interface DummyContent {
    name: string;
    age?: number;
}

对比this

interface DummyContent {
    name: string;
    age?: number;
}

class DummyClass {
    printMessage = () => {
        console.log("message");
    }
}

var dummy = new DummyClass();
dummy.printMessage();

为了提供良好的开发体验,您可能只需要将这些接口写入开发环境中的文件。您不需要为生产构建写出它们,也不需要(或不想)将它们签入版本控制。

开发人员可能需要将它们写出来,这样他们的 IDE 就有东西可以咬牙切齿了。您可以将 *.api.ts 添加到 .gitignore,并将它们保留在存储库之外,但我怀疑它们需要存在于开发人员的工作区中。

例如,在我的示例存储库中,新开发人员必须 运行 npm install(像往常一样) npm run build(在他们的本地环境中生成界面)以摆脱他们所有的红色波浪形。

我知道这是一个老问题,但我最近 运行 遇到了类似的问题,所以它仍然相关。

一种解决方法是将 declare module "*.api"; 添加到您的 index.d.ts 文件中。但这有失去类型安全的巨大缺点,因为 everything in a shorthand module declaration has type any。所以你最好不要一开始就生成 TypeScript 接口。

我设法使用一些我不完全理解的巫术来解决它,但它似乎有效。

package.json

请注意,我在撰写本文时使用的是最新版本的所有内容,并且 ts-loader 而不是已弃用的 awesome-typescript-loader

{
  "name": "so-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "ts-loader": "^9.2.8",
    "typescript": "^4.6.3",
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2"
  }
}

webpack.config.ts

appendTsSuffixTo 的需求是 well-documented,这是必要的,但还不够。

我发现 ts-loader 几乎没有记录的 resolveModuleName 选项。通过查看 ts-loader 代码并观察函数的输入和输出,我设法拼凑了您在下面看到的自定义解析器函数。我们必须将 .ts 扩展名附加到 resolvedFileName 以欺骗 TypeScript 编译器接受该文件。

请注意,我们需要为这两个规则使用此 ts-loader 配置,因此我将其提取到一个变量中。

const path = require('path');

const tsLoader = {
  loader: "ts-loader",
  options: {
    appendTsSuffixTo: [/\.api$/],
    resolveModuleName: (moduleName, containingFile, compilerOptions, compilerHost, parentResolver) => {
      if (/\.api$/.test(moduleName)) {
        const fileName = path.resolve(path.dirname(containingFile), moduleName);
        return {
          resolvedModule: {
            originalFileName: fileName,
            resolvedFileName: fileName + '.ts',
            resolvedModule: undefined,
            isExternalLibraryImport: false,
          },
          failedLookupLocations: [],
        };
      }
      return parentResolver(moduleName, containingFile, compilerOptions, compilerHost);
    },
  },
};

module.exports = {
  entry: './index.ts',
  output: {
    filename: 'output.js',
  },
  resolveLoader: {
    alias: {
      'my-own-loader': path.resolve(__dirname, "my-own-loader.js"),
    },
  },
  module: {
    rules: [
      {
        test: /\.api$/,
        exclude: /node_modules/,
        use: [tsLoader, "my-own-loader"],
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: [tsLoader],
      },
    ]
  },
};

tsconfig.json

我不知道为什么需要这个,但没有它,我得到 TS18002: The 'files' list in config file 'tsconfig.json' is empty.

{}

my-own-loader.js

我正在添加一个值以表明我们保留类型安全。

module.exports = function(source) {
  return `
export interface DummyContent {
    name: string;
    age?: number;
}

export const DUMMY_VALUE: DummyContent = {
    name: "Jon Snow",
    age: 24,
}
`;
};

index.ts

请注意,我正在导入包含 .api 扩展名的文件。可能可以更改 resolveModuleName 函数使其在没有扩展名的情况下工作,但我没有费心。我实际上喜欢将这里的扩展名视为正在发生一些特别事情的线索。

import { DUMMY_VALUE } from './example.api';

console.log(DUMMY_VALUE.name);
console.log(DUMMY_VALUE.youKnowNothing); // Does not compile