File
Blob: types/src/transforms/internal-namespace.ts
| 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 | |
| 5 | import assert from 'node:assert'; |
| 6 | import { Structure, StructureGroups } from '@workerd/jsg/rtti'; |
| 7 | import ts from 'typescript'; |
| 8 | import { StructureMap, getTypeName } from '../generator'; |
| 9 | import { maybeExtractGlobalNode } from './globals'; |
| 10 | import { ensureStatementModifiers } from './helpers'; |
| 11 | import { 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`). |
| 68 | export 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 | |
| 84 | function 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 | |
| 99 | function 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 | } |