Content mappers: pass an import's query or attributes to `transform`

未关闭
#64,912 0 条评论 4 个 reaction 已指派 1 人 在 GitHub 查看

维护者通常 1 天内回复

@andrewbranch 已经在做这个了。

开始于 2026年10月26日。

评估

难度
5/5
预计耗时
一周以上
新手友好度
35/100
Issue 类型
功能
描述清晰度
描述清楚
活跃度
活跃
技术栈
go, typescript

调研方向

Start with the content mapper host implementation mentioned in the issue, especially TransformParams in tsc/internal/contentmapper/hostimpl.go, then trace how import queries and attributes reach module resolution. The issue includes a reproduction and a proposed request shape; use it to investigate the resolver and declaration emit questions. Done means opted-in mappers receive the selected variant while plain imports and files claimed by other mappers keep resolving as they do today.

由索引模型根据 Issue 内容生成。

描述

Needs Investigation
🔍 Search Terms

content mapper, contentMappers, query, ?raw, import attributes, module specifier query

✅ Viability Checklist
⭐ Suggestion

Let a content mapper opt in, in its manifest, to seeing which variant an import asks for, through its query, its import attributes, or both. Each distinct variant would be its own module, and the transform request would carry it:

import config from "./app.yaml";                                // as today
import widened from "./app.yaml?widened";                       // query: "?widened"
import alsoWidened from "./app.yaml" with { types: "widened" }; // attributes: { types: "widened" }

// package.json: "typescript": { "contentMapper": { "exec": [...], "variants": true } }
interface TransformParams {
    fileName: string;
    content: string;
    projectHandle: string;
    /** The import's query as written, starting with "?". */
    query?: string;
    /** The import's attributes. */
    attributes?: Record<string, string>;
}

Either key would serve. Attributes are standard syntax and leave resolution as it is: TypeScript already accepts with { types: "widened" } on a mapped file and resolves the path as usual. The query needs the resolver to look past the ?, and in return it also covers variants whose value differs. Node keys modules by URL, so it makes each query its own module, while two imports of one file with different attributes share one module there. That's harmless when only the type differs, as with ?widened, and not when the value differs, as with Vite's ?raw. esbuild separates both.

An import with no query or attributes, and any file no opted-in mapper claims, would resolve as today. Open questions: where to report a variant the mapper doesn't support, and what declaration emit writes for one.

📃 Motivating Example

Vite's ?raw is the prior art: import shader from "./shader.glsl?raw" gives the file's text. vite/client types it with declare module "*?raw" { const src: string; export default src }, which works because a string doesn't depend on the file.

A type that does depend on the file can't be written that way, and the content mapper, which can see the file, never sees the query or the attributes. Motherload types data files as as const would:

import config from "./app.yaml";          // { readonly port: 8080; readonly tags: readonly ["a", "b"] }
import widened from "./app.yaml?widened"; // TS2307; wanted: { port: number; tags: string[] }
Reproduction

With [email protected] (3e92363) and an identity mapper for .foo that logs each request:

// main.ts; tsconfig: "contentMappers": [{ "package": "identity-foo-mapper", "extensions": [".foo"] }]
import config from "./config.foo";
import widened from "./config.foo?widened";
import attributed from "./config.foo" with { types: "widened" };
main.ts(2,21): error TS2307: Cannot find module './config.foo?widened' or its corresponding type declarations.

The attributed import type-checks (under bundler, nodenext and preserve), but the mapper receives one transform, for both the plain and the attributed import:

{"method":"transform","params":{"fileName":"/repro/config.foo","projectHandle":"[email protected]:0","content":"…"}}

On main, TransformParams holds fileName, content and projectHandle.

The mapper, node_modules/identity-foo-mapper/mapper.mjs, with "typescript": { "contentMapper": { "exec": ["node", "mapper.mjs"], "compilerOptions": [] } } in its package.json:

import { appendFileSync } from "node:fs";

const log = (message) => process.env.MAPPER_LOG && appendFileSync(process.env.MAPPER_LOG, JSON.stringify(message) + "\n");

const handlers = {
  initialize: () => ({ positionEncoding: "utf-16", diagnosticSource: "foo" }),
  openProject: () => ({}),
  transform: ({ content }) => ({ text: content, extension: ".ts", mappings: [[0, content.length, 0, content.length, 0]] }),
  closeProject: () => ({}),
};

let buffer = Buffer.alloc(0);
process.stdin.on("data", (chunk) => {
  buffer = Buffer.concat([buffer, chunk]);
  for (;;) {
    const end = buffer.indexOf("\r\n\r\n");
    if (end === -1) return;
    const length = Number(/Content-Length:\s*(\d+)/i.exec(buffer.subarray(0, end).toString())[1]);
    if (buffer.length < end + 4 + length) return;
    const request = JSON.parse(buffer.subarray(end + 4, end + 4 + length).toString());
    buffer = buffer.subarray(end + 4 + length);
    const { content, ...params } = request.params ?? {};
    log({ method: request.method, params: request.method === "transform" ? { ...params, content: "…" } : params });
    if (request.id === undefined) continue;
    const payload = Buffer.from(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: handlers[request.method]?.(request.params) ?? {} }));
    process.stdout.write(`Content-Length: ${payload.length}\r\n\r\n`);
    process.stdout.write(payload);
  }
});
💻 Use Cases
  1. Typing one file differently per import when the type depends on its content, as ?widened does above.
  2. Ambient wildcards can't see the file, and the mapper is never told the query or the attributes. A contentMappers entry's options choose per extension, not per import.
  3. No workaround: Motherload types every data file as const, and users widen by hand.
主要语言
Go
星标
112k
派生
15.4k
平均合并
3 天 1 小时
30 天内合并 PR
136

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/TypeScript 的其他 Issue

查看 microsoft/TypeScript 的全部 Issue

相似的 Issue

更多 Go Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。