File
Blob: types/src/transforms/overrides/compiler.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 path from 'node:path'; |
| 7 | import { StructureGroups } from '@workerd/jsg/rtti'; |
| 8 | import ts from 'typescript'; |
| 9 | import { getTypeName } from '../../generator'; |
| 10 | import { SourcesMap } from '../../program'; |
| 11 | |
| 12 | // If an override matches this RegExp, it will replace the existing definition |
| 13 | const keywordReplace = |
| 14 | /^export |^declare |^type |^abstract |^class |^interface |^enum |^const |^var |^function /; |
| 15 | // If an override matches this RegExp, it will have `class ${name} ` prefixed |
| 16 | const keywordHeritage = /^extends |^implements /; |
| 17 | |
| 18 | function compileOverride( |
| 19 | name: string, |
| 20 | override: string |
| 21 | ): [compiled: string, isReplacement: boolean] { |
| 22 | // If this override is a complete type replacement, return it as is |
| 23 | // Examples: |
| 24 | // - `const WebSocketPair: { new (): { 0: WebSocket; 1: WebSocket }; }` |
| 25 | // - `type ReadableStreamReadResult<R = any> = { done: false, value: R; } | { done: true; value?: undefined; }` |
| 26 | // - `type TransactionOptions = never` (deletes definition) |
| 27 | if (keywordReplace.test(override)) return [override, true]; |
| 28 | |
| 29 | // Fix up overrides, so they can be parsed as TypeScript source files. Whilst |
| 30 | // we convert all overrides to classes, this type classification is ignored |
| 31 | // when merging. Classes just support all possible forms of override (extends, |
| 32 | // implements, constructors, (static) properties/methods). |
| 33 | if (keywordHeritage.test(override) || override.startsWith('{')) { |
| 34 | // Use existing name and type classification, may merge members |
| 35 | // Examples: |
| 36 | // - `extends EventTarget<WorkerGlobalScopeEventMap>` |
| 37 | // - `extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> { constructor(format: "gzip" | "deflate" | "deflate-raw"); }` |
| 38 | // - `{ json<T>(): Promise<T>; }` |
| 39 | override = `class ${name} ${override}`; |
| 40 | } else if (override.startsWith('<')) { |
| 41 | // Use existing name and type classification, may merge members |
| 42 | // Examples: |
| 43 | // - `<R = any> { read(): Promise<ReadableStreamReadResult<R>>; }` |
| 44 | override = `class ${name}${override}`; |
| 45 | } else { |
| 46 | // Use existing type classification, may rename and merge members |
| 47 | // Examples: |
| 48 | // - `KVNamespaceGetOptions<Type> { type: Type; }` |
| 49 | // - `KVNamespaceListOptions` (just rename definition) |
| 50 | // - `WorkerGlobalScope extends EventTarget<WorkerGlobalScopeEventMap>` |
| 51 | override = `class ${override}`; |
| 52 | } |
| 53 | // Purely heritage and rename overrides don't need to define any members, but |
| 54 | // they still need to be valid classes for parsing. |
| 55 | if (!override.endsWith('}')) { |
| 56 | override = `${override} {}`; |
| 57 | } |
| 58 | |
| 59 | return [override, false]; |
| 60 | } |
| 61 | |
| 62 | const overridesPath = '/$virtual/overrides'; |
| 63 | const definesPath = '/$virtual/defines'; |
| 64 | |
| 65 | // Converts and collects all overrides and defines as TypeScript source files. |
| 66 | // Also returns a set of definitions that should be replaced by their override. |
| 67 | export function compileOverridesDefines( |
| 68 | root: StructureGroups |
| 69 | ): [sources: SourcesMap, replacements: Set<string>] { |
| 70 | const sources = new SourcesMap(); |
| 71 | // Types that need their definition completely replaced by their override |
| 72 | const replacements = new Set<string>(); |
| 73 | |
| 74 | root.groups.forEach((group) => { |
| 75 | group.structures.forEach((structure) => { |
| 76 | const name = getTypeName(structure); |
| 77 | const override = structure.tsOverride.trim(); |
| 78 | const define = structure.tsDefine.trim(); |
| 79 | |
| 80 | if (override !== '') { |
| 81 | const [compiled, isReplacement] = compileOverride(name, override); |
| 82 | sources.set(path.join(overridesPath, name + '.ts'), compiled); |
| 83 | if (isReplacement) replacements.add(name); |
| 84 | } |
| 85 | if (define !== '') { |
| 86 | sources.set(path.join(definesPath, name + '.ts'), define); |
| 87 | } |
| 88 | }); |
| 89 | }); |
| 90 | |
| 91 | return [sources, replacements]; |
| 92 | } |
| 93 | |
| 94 | // Try to find override for structure using name obtained from `getTypeName()` |
| 95 | export function maybeGetOverride( |
| 96 | program: ts.Program, |
| 97 | name: string |
| 98 | ): ts.Statement | undefined { |
| 99 | const sourcePath = path.join(overridesPath, name + '.ts'); |
| 100 | const sourceFile = program.getSourceFile(sourcePath); |
| 101 | if (sourceFile !== undefined) { |
| 102 | assert.strictEqual(sourceFile.statements.length, 1); |
| 103 | return sourceFile.statements[0]; |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // Try to find defines for structure using name obtained from `getTypeName()` |
| 108 | export function maybeGetDefines( |
| 109 | program: ts.Program, |
| 110 | name: string |
| 111 | ): ts.NodeArray<ts.Statement> | undefined { |
| 112 | const sourcePath = path.join(definesPath, name + '.ts'); |
| 113 | const sourceFile = program.getSourceFile(sourcePath); |
| 114 | return sourceFile?.statements; |
| 115 | } |