Skip to content
File

Blob: types/src/transforms/import-resolve.ts

typescript153 lines
1// Copyright (c) 2022-2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import assert from "node:assert";
6import ts from "typescript";
7 
8// Adapted from https://github.com/cloudflare/workerd/blob/2182afdd8ca9ac35fb18b76205308fabd5000d01/src/node/tsconfig.json#L27-L32.
9// Maps import specifier patterns to target path pattern. Sorted by target path
10// specificity (highest to lowest). When un-resolving target paths to import
11// specifiers, the most specific will be checked first. Note all target paths
12// must be absolute (i.e. start with "/").
13const TSCONFIG_PATHS: Record<string, string> = {
14 "node-internal:*": "/internal/*",
15 "node:*": "/*",
16};
17 
18// Resolves all relative imports in `declare module` blocks constructed from
19// `*.d.ts` file contents, using `TSCONFIG_PATHS` to convert between specifiers
20// and on-disk paths:
21//
22// ```ts
23// declare module "node:crypto" {
24// const _default: {
25// DiffieHellman: new (key: import("./internal/crypto").ArrayLike, ...) => ...;
26// Hmac: new (..., options?: import("./internal/streams_transform").TransformOptions) => ...;
27// };
28// ...
29// }
30// declare module "node-internal:events" {
31// export namespace EventEmitter {
32// var on: typeof import("./events").on;
33// }
34// }
35// ```
36//
37// --- transforms to --->
38//
39// ```ts
40// declare module "node:crypto" {
41// const _default: {
42// DiffieHellman: new (key: import("node-internal:crypto").ArrayLike, ...) => ...;
43// Hmac: new (..., options?: import("node-internal:streams_transform").TransformOptions) => ...;
44// };
45// ...
46// }
47// declare module "node-internal:events" {
48// export namespace EventEmitter {
49// var on: typeof import("node-internal:events").on;
50// }
51// }
52// ```
53export function createImportResolveTransformer(): ts.TransformerFactory<ts.SourceFile> {
54 return (ctx) => {
55 return (node) => {
56 const visitor = createImportResolveVisitor(ctx);
57 return ts.visitEachChild(node, visitor, ctx);
58 };
59 };
60}
61 
62// `RegExp` in wildcard rule may optionally contain single capturing group
63type WildcardRule = [/* from */ RegExp, /* to */ string];
64function entryWildcardRule([from, to]: [string, string]): WildcardRule {
65 return [new RegExp(`^${from.replace("*", "(.+)")}$`), to];
66}
67function maybeApplyWildcardRule(rules: WildcardRule[], text: string): string | undefined {
68 for (const [from, to] of rules) {
69 const match = from.exec(text);
70 if (match === null) continue;
71 return match.at(1) === undefined ? to : to.replaceAll("*", match[1]);
72 }
73}
74 
75const pathEntries = Object.entries(TSCONFIG_PATHS);
76const pathResolveRules = pathEntries.map(entryWildcardRule);
77const pathUnresolveRules = pathEntries.map(([from, to]) =>
78 entryWildcardRule([to, from])
79);
80 
81function createImportResolveVisitor(ctx: ts.TransformationContext): ts.Visitor {
82 return (node) => {
83 // Visit all `declare module "<specifier>"` nodes
84 if (
85 ts.isModuleDeclaration(node) &&
86 (node.flags & ts.NodeFlags.Namespace) === 0 &&
87 ts.isStringLiteral(node.name)
88 ) {
89 const specifier = node.name.text;
90 const maybePath = maybeApplyWildcardRule(pathResolveRules, specifier);
91 // If we don't know the path of this module, we won't be able to do any
92 // resolving, so return it as is
93 if (maybePath === undefined) return node;
94 const moduleVisitor = createModuleImportResolveVisitor(ctx, maybePath);
95 return ts.visitEachChild(node, moduleVisitor, ctx);
96 }
97 
98 return node;
99 };
100}
101 
102function createModuleImportResolveVisitor(
103 ctx: ts.TransformationContext,
104 referencingPath: string
105): ts.Visitor {
106 assert(referencingPath.startsWith("/"), "Expected absolute referencing path");
107 // `file:` protocol isn't important here, just need something for a valid URL
108 const referencingURL = new URL(referencingPath, "file:");
109 
110 const visitor: ts.Visitor = (node) => {
111 if (
112 ts.isImportTypeNode(node) &&
113 ts.isLiteralTypeNode(node.argument) &&
114 ts.isStringLiteral(node.argument.literal)
115 ) {
116 const relativeSpecifier = node.argument.literal.text;
117 // If import isn't relative, no need to resolve it, so leave it as is
118 if (!relativeSpecifier.startsWith(".")) return node;
119 // Resolve specifier relative to referencing module
120 const resolvedURL = new URL(relativeSpecifier, referencingURL);
121 const resolvedPath = resolvedURL.pathname;
122 // Convert resolved path back to specifier
123 const specifier = maybeApplyWildcardRule(
124 pathUnresolveRules,
125 resolvedPath
126 );
127 assert(
128 specifier !== undefined,
129 `Unable to find matching specifier rule for path: "${resolvedPath}"`
130 );
131 
132 // Update import with new specifier
133 const argument = ctx.factory.updateLiteralTypeNode(
134 node.argument,
135 ctx.factory.createStringLiteral(specifier)
136 );
137 return ctx.factory.updateImportTypeNode(
138 node,
139 argument,
140 node.attributes,
141 node.qualifier,
142 node.typeArguments,
143 node.isTypeOf
144 );
145 }
146 
147 // Recursively visit all nodes (don't need to do this first as visitor never
148 // creates any new import type nodes)
149 return ts.visitEachChild(node, visitor, ctx);
150 };
151 return visitor;
152}