Skip to content
File

Blob: types/src/transforms/internal-namespace.ts

typescript210 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 { Structure, StructureGroups } from '@workerd/jsg/rtti';
7import ts from 'typescript';
8import { StructureMap, getTypeName } from '../generator';
9import { maybeExtractGlobalNode } from './globals';
10import { ensureStatementModifiers } from './helpers';
11import { createRenameVisitor } from './overrides';
12 
13// Moves all members (excluding imports) of internal `declare module` blocks
14// into namespaces that are then `export default`ed:
15//
16// ```ts
17// declare module "node-internal:diagnostics_channel" {
18// import _internal1 from "node-internal:async_hooks";
19// import AsyncLocalStorage = _internal1.AsyncLocalStorage;
20// interface DiagnosticsChannelModule {
21// readonly property: boolean;
22// channel<T>(key: PropertyKey): Channel<T>;
23// bindStore(store: AsyncLocalStorage, ...): void;
24// Channel: typeof Channel;
25// }
26// abstract class Channel<T> {
27// ...
28// }
29// }
30// ```
31//
32// --- transforms to --->
33//
34// ```ts
35// declare module "node-internal:diagnostics_channel" {
36// import _internal1 from "node-internal:async_hooks";
37// import AsyncLocalStorage = _internal1.AsyncLocalStorage;
38// namespace _default {
39// const property: boolean;
40// function channel<T>(key: PropertyKey): Channel<T>;
41// function bindStore(store: AsyncLocalStorage, ...): void;
42// abstract class Channel<T> {
43// ...
44// }
45// }
46// export default _default;
47// }
48// ```
49//
50// Note we can't just generate the `namespace _default { ... }` when initially
51// building the AST from RTTI, as the overrides/defines transformer only
52// operates on `interface`/ `class`es, not bare `const`/`function`s.
53//
54// An alternative to exporting a `namespace` would be to export an instance of
55// the root module type:
56//
57// ```
58// declare module "node-internal:diagnostics_channel" {
59// ...
60// const _default: DiagnosticsChannelModule;
61// export default _default;
62// }
63// ```
64//
65// Whilst this correctly exports *values*, it doesn't export *types*.
66// Importantly, it erases type parameters from generic types (i.e. `Channel<T>`
67// becomes `Channel`, with `T`s becoming `unknown`).
68export function createInternalNamespaceTransformer(
69 root: StructureGroups,
70 structureMap: StructureMap
71): ts.TransformerFactory<ts.SourceFile> {
72 return (ctx) => {
73 return (node) => {
74 const moduleStructures = collectInternalModuleStructures(
75 root,
76 structureMap
77 );
78 const visitor = createInternalNamespaceVisitor(moduleStructures, ctx);
79 return ts.visitEachChild(node, visitor, ctx);
80 };
81 };
82}
83 
84function collectInternalModuleStructures(
85 root: StructureGroups,
86 structureMap: StructureMap
87): Map<string, Structure> {
88 const moduleRoots = new Map</* specifier */ string, Structure>(); // TODO: add members here as well?
89 root.modules.forEach((module) => {
90 if (!module._isStructureName) return;
91 const structure = structureMap.get(module.structureName);
92 assert(structure !== undefined, 'Structure is undefined');
93 const specifier = module.specifier;
94 moduleRoots.set(specifier, structure);
95 });
96 return moduleRoots;
97}
98 
99function createInternalNamespaceVisitor(
100 moduleRoots: ReturnType<typeof collectInternalModuleStructures>,
101 ctx: ts.TransformationContext
102): ts.Visitor {
103 const visitor: ts.Visitor = (node) => {
104 if (
105 ts.isModuleDeclaration(node) &&
106 (node.flags & ts.NodeFlags.Namespace) === 0 &&
107 ts.isStringLiteral(node.name) &&
108 node.body !== undefined &&
109 ts.isModuleBlock(node.body)
110 ) {
111 // This transformer should only be called on types generated from C++.
112 // Therefore, all `declare modules` represent internal modules.
113 const moduleRoot = moduleRoots.get(node.name.text);
114 assert(
115 moduleRoot !== undefined,
116 `Expected "${node.name.text}" to be an internal module`
117 );
118 const moduleRootName = getTypeName(moduleRoot);
119 
120 // Ensure all nested types have the correct name. We do this after
121 // overrides as some module members would otherwise have the same name as
122 // global types (e.g. `cloudflare:workers` `DurableObjectBase` and
123 // `DurableObject`)
124 const renames = new Map</* from */ string, /* to */ string>();
125 moduleRoot.members.forEach((member) => {
126 if (member._isNested) {
127 const nested = member.nested;
128 const generatedName = getTypeName(nested.structure);
129 const actualName = nested.name;
130 if (generatedName !== actualName) {
131 renames.set(generatedName, actualName);
132 }
133 }
134 });
135 
136 // Filter array of statements to keep at the top-level of the module, and
137 // build array of statements to include in default namespace export
138 const namespaceStatements: ts.Statement[] = [];
139 const moduleStatements = node.body.statements.filter((statement) => {
140 if (
141 ts.isImportDeclaration(statement) ||
142 ts.isImportEqualsDeclaration(statement)
143 ) {
144 // Keep import statements at top-level of module
145 return true;
146 } else if (
147 ts.isInterfaceDeclaration(statement) &&
148 statement.name.text === moduleRootName
149 ) {
150 // Extract out properties and functions from the internal module root
151 // type. Note internal module root types never have constructors, so
152 // should always be interfaces. Internal module root types should
153 // never have type parameters too, so we don't need to worry about
154 // inlining type arguments, unlike the global scope visitor.
155 for (const member of statement.members) {
156 const maybeNode = maybeExtractGlobalNode(ctx, member);
157 if (maybeNode !== undefined) namespaceStatements.push(maybeNode);
158 }
159 // Remove the root type from the module top-level
160 return false;
161 } else {
162 // Assume all other class/interface definitions are nested types that
163 // should be included in the default namespace export...
164 namespaceStatements.push(
165 ensureStatementModifiers(ctx, statement, { declare: false })
166 );
167 // ...and removed from the module top-level
168 return false;
169 }
170 });
171 
172 // Add default namespace export to top-level statements
173 const defaultIdentifier = ctx.factory.createIdentifier('_default');
174 const namespaceBody = ctx.factory.createModuleBlock(namespaceStatements);
175 const namespaceDeclaration = ctx.factory.createModuleDeclaration(
176 /* modifiers */ undefined,
177 defaultIdentifier,
178 namespaceBody,
179 ts.NodeFlags.Namespace
180 );
181 const exportStatement = ctx.factory.createExportAssignment(
182 /* modifiers */ undefined,
183 /* isExportEquals */ undefined,
184 defaultIdentifier
185 );
186 moduleStatements.push(namespaceDeclaration, exportStatement);
187 
188 // Return updated module declaration with new top-level statements
189 let body = ctx.factory.updateModuleBlock(node.body, moduleStatements);
190 if (renames.size > 0) {
191 const renameVisitor = createRenameVisitor(
192 ctx,
193 renames,
194 /* renameClassesInterfaces */ true
195 );
196 body = ts.visitEachChild(body, renameVisitor, ctx);
197 }
198 return ctx.factory.updateModuleDeclaration(
199 node,
200 node.modifiers,
201 node.name,
202 body
203 );
204 }
205 
206 return node;
207 };
208 return visitor;
209}