Skip to content
File

Blob: types/src/transforms/overrides/compiler.ts

typescript116 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 path from 'node:path';
7import { StructureGroups } from '@workerd/jsg/rtti';
8import ts from 'typescript';
9import { getTypeName } from '../../generator';
10import { SourcesMap } from '../../program';
11 
12// If an override matches this RegExp, it will replace the existing definition
13const 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
16const keywordHeritage = /^extends |^implements /;
17 
18function 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 
62const overridesPath = '/$virtual/overrides';
63const 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.
67export 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()`
95export 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()`
108export 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}