microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
9bf31a65c8cb0b05a6ffdb0693c3f07c1313485b

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/html-program-viewer/src/ui.tsx

432lines · modecode

1import { css } from "@emotion/react";
2import {
3 Enum,
4 EnumMember,
5 getNamespaceFullName,
6 getTypeName,
7 Interface,
8 Model,
9 ModelProperty,
10 Namespace,
11 Operation,
12 Program,
13 Scalar,
14 Type,
15 Union,
16 UnionVariant,
17} from "@typespec/compiler";
18import React, { FunctionComponent, ReactElement, useContext } from "react";
19import ReactDOMServer from "react-dom/server";
20import { KeyValueSection, Literal } from "./common.js";
21import { Colors } from "./constants.js";
22import { inspect } from "./inspect.js";
23import { TypeUIBase, TypeUIBaseProperty } from "./type-ui-base.js";
24import { getIdForType, isNamedUnion } from "./utils.js";
25
26function expandNamespaces(namespace: Namespace): Namespace[] {
27 return [namespace, ...[...namespace.namespaces.values()].flatMap(expandNamespaces)];
28}
29
30const ProgramContext = React.createContext<Program>({} as any);
31
32export function renderProgram(program: Program) {
33 const html = ReactDOMServer.renderToString(<TypeSpecProgramViewer program={program} />);
34 return html;
35}
36
37export interface TypeSpecProgramViewerProps {
38 program: Program;
39}
40
41const ProgramViewerStyles = css({
42 fontFamily: "monospace",
43 backgroundColor: Colors.background,
44 li: {
45 margin: 0,
46 listStyle: "none",
47 position: "relative",
48 },
49});
50
51export const TypeSpecProgramViewer: FunctionComponent<TypeSpecProgramViewerProps> = ({
52 program,
53}) => {
54 const root = program.checker!.getGlobalNamespaceType();
55 const namespaces = expandNamespaces(root);
56 return (
57 <ProgramContext.Provider value={program}>
58 <div css={ProgramViewerStyles}>
59 <ul css={{ padding: "0 0 0 10px", margin: 0 }}>
60 {namespaces.map((namespace) => (
61 <li key={getNamespaceFullName(namespace)}>
62 <NamespaceUI type={namespace} />
63 </li>
64 ))}
65 </ul>
66 </div>
67 </ProgramContext.Provider>
68 );
69};
70
71export interface ItemListProps<T> {
72 items: Map<string, T> | T[];
73 render: (t: T) => ReactElement<any, any> | null;
74}
75
76export const ItemList = <T extends object>(props: ItemListProps<T>) => {
77 if (Array.isArray(props.items)) {
78 if (props.items.length === 0) {
79 return <>{"[]"}</>;
80 }
81 } else {
82 if (props.items.size === 0) {
83 return <>{"{}"}</>;
84 }
85 }
86 return (
87 <KeyValueSection>
88 {[...props.items.entries()].map(([k, v]) => (
89 <li key={k}>{props.render(v)}</li>
90 ))}
91 </KeyValueSection>
92 );
93};
94
95type NamedType = Type & { name: string };
96const omittedProps = [
97 "kind",
98 "name",
99 "node",
100 "symbol",
101 "namespace",
102 "templateNode",
103 "templateArguments",
104 "templateMapper",
105 "instantiationParameters",
106 "decorators",
107 "projectionBase",
108 "projectionsByName",
109 "projectionSource",
110 "projector",
111 "projections",
112 "isFinished",
113] as const;
114const omittedPropsSet = new Set(omittedProps);
115type OmittedProps = (typeof omittedProps)[number];
116type NamedTypeUIProps<T extends NamedType> = {
117 type: T;
118 properties: Record<Exclude<keyof T, OmittedProps>, "skip" | "ref" | "nested" | "value">;
119 name?: string;
120};
121
122const NamedTypeUI = <T extends NamedType>({ type, name, properties }: NamedTypeUIProps<T>) => {
123 name ??= type.name;
124 const propsUI: TypeUIBaseProperty[] = Object.entries(type)
125 .map(([key, value]) => {
126 if (omittedPropsSet.has(key as any)) {
127 return undefined;
128 }
129 const action = (properties as any)[key] as "skip" | "ref" | "nested";
130 if (action === "skip") {
131 return undefined;
132 }
133
134 const render = (x: any) =>
135 action === "ref" ? <TypeReference type={value} /> : <TypeUI type={x} />;
136 let valueUI;
137 if (value === undefined) {
138 valueUI = value;
139 } else if (value.kind) {
140 valueUI = render(value);
141 } else if (
142 typeof value === "object" &&
143 "entries" in value &&
144 typeof value.entries === "function"
145 ) {
146 valueUI = <ItemList items={value} render={render} />;
147 } else {
148 valueUI = value;
149 }
150 return {
151 name: key,
152 value: valueUI,
153 } satisfies TypeUIBaseProperty;
154 })
155 .filter((x): x is TypeUIBaseProperty => Boolean(x));
156 return (
157 <TypeUIBase type={type} name={name} properties={propsUI.concat([getDataProperty(type)])} />
158 );
159};
160
161interface TypeUIProps {
162 type: Type;
163}
164
165const TypeUI: FunctionComponent<TypeUIProps> = ({ type }) => {
166 switch (type.kind) {
167 case "Namespace":
168 return <NamespaceUI type={type} />;
169 case "Interface":
170 return <InterfaceUI type={type} />;
171 case "Operation":
172 return <OperationUI type={type} />;
173 case "Model":
174 return <ModelUI type={type} />;
175 case "Scalar":
176 return <ScalarUI type={type} />;
177 case "ModelProperty":
178 return <ModelPropertyUI type={type} />;
179 case "Union":
180 return <UnionUI type={type} />;
181 case "UnionVariant":
182 return <UnionVariantUI type={type} />;
183 case "Enum":
184 return <EnumUI type={type} />;
185 case "EnumMember":
186 return <EnumMemberUI type={type} />;
187 default:
188 return null;
189 }
190};
191
192const NamespaceUI: FunctionComponent<{ type: Namespace }> = ({ type }) => {
193 const name = getNamespaceFullName(type) || "(global)";
194
195 return (
196 <NamedTypeUI
197 name={name}
198 type={type}
199 properties={{
200 namespaces: "skip",
201 models: "nested",
202 scalars: "nested",
203 interfaces: "nested",
204 operations: "nested",
205 unions: "nested",
206 enums: "nested",
207 decoratorDeclarations: "nested",
208 functionDeclarations: "nested",
209 }}
210 />
211 );
212};
213
214const InterfaceUI: FunctionComponent<{ type: Interface }> = ({ type }) => {
215 return (
216 <NamedTypeUI
217 type={type}
218 properties={{
219 operations: "nested",
220 sourceInterfaces: "ref",
221 }}
222 />
223 );
224};
225
226const OperationUI: FunctionComponent<{ type: Operation }> = ({ type }) => {
227 return (
228 <NamedTypeUI
229 type={type}
230 properties={{
231 interface: "skip",
232 parameters: "nested",
233 returnType: "ref",
234 sourceOperation: "ref",
235 }}
236 />
237 );
238};
239
240function getDataProperty(type: Type): TypeUIBaseProperty {
241 return {
242 name: "data",
243 description: "in program.stateMap()",
244 value: <TypeData type={type} />,
245 };
246}
247
248const ModelUI: FunctionComponent<{ type: Model }> = ({ type }) => {
249 return (
250 <NamedTypeUI
251 type={type}
252 properties={{
253 indexer: "skip",
254 baseModel: "ref",
255 derivedModels: "ref",
256 properties: "nested",
257 sourceModel: "ref",
258 }}
259 />
260 );
261};
262
263const ScalarUI: FunctionComponent<{ type: Scalar }> = ({ type }) => {
264 return (
265 <NamedTypeUI
266 type={type}
267 properties={{
268 baseScalar: "ref",
269 derivedScalars: "ref",
270 }}
271 />
272 );
273};
274
275const ModelPropertyUI: FunctionComponent<{ type: ModelProperty }> = ({ type }) => {
276 return (
277 <NamedTypeUI
278 type={type}
279 properties={{
280 model: "skip",
281 type: "ref",
282 optional: "value",
283 sourceProperty: "ref",
284 default: "value",
285 }}
286 />
287 );
288};
289
290const EnumUI: FunctionComponent<{ type: Enum }> = ({ type }) => {
291 return (
292 <NamedTypeUI
293 type={type}
294 properties={{
295 members: "nested",
296 }}
297 />
298 );
299};
300
301const EnumMemberUI: FunctionComponent<{ type: EnumMember }> = ({ type }) => {
302 return (
303 <NamedTypeUI
304 type={type}
305 properties={{
306 enum: "skip",
307 sourceMember: "ref",
308 value: "value",
309 }}
310 />
311 );
312};
313
314const UnionUI: FunctionComponent<{ type: Union }> = ({ type }) => {
315 if (!isNamedUnion(type)) {
316 return <></>;
317 }
318 return (
319 <NamedTypeUI
320 type={type}
321 properties={{
322 expression: "skip",
323 options: "skip",
324 variants: "nested",
325 }}
326 />
327 );
328};
329
330const UnionVariantUI: FunctionComponent<{ type: UnionVariant }> = ({ type }) => {
331 if (typeof type.name === "symbol") {
332 return <></>;
333 }
334 return (
335 <NamedTypeUI
336 type={type as UnionVariant & { name: string }}
337 properties={{
338 union: "skip",
339 type: "ref",
340 }}
341 />
342 );
343};
344
345const NamedTypeRef: FunctionComponent<{ type: NamedType }> = ({ type }) => {
346 const id = getIdForType(type);
347 const href = `#${id}`;
348 return (
349 <a
350 css={{
351 color: Colors.ref,
352 textDecoration: "none",
353
354 "&:hover": {
355 textDecoration: "underline",
356 },
357 }}
358 href={href}
359 title={type.kind + ": " + id}
360 >
361 {getTypeName(type)}
362 </a>
363 );
364};
365const TypeReference: FunctionComponent<{ type: Type }> = ({ type }) => {
366 switch (type.kind) {
367 case "Namespace":
368 case "Operation":
369 case "Interface":
370 case "Enum":
371 case "ModelProperty":
372 case "Scalar":
373 return <NamedTypeRef type={type} />;
374 case "Model":
375 if (type.name === "") {
376 return (
377 <KeyValueSection>
378 <TypeUI type={type} />
379 </KeyValueSection>
380 );
381 } else {
382 return <NamedTypeRef type={type} />;
383 }
384 case "Union":
385 if (isNamedUnion(type)) {
386 return <NamedTypeRef type={type} />;
387 } else {
388 return (
389 <>
390 {[...type.variants.values()].map((variant, i) => {
391 return (
392 <span key={i}>
393 <TypeReference type={variant.type} />
394 {i < type.variants.size - 1 ? " | " : ""}
395 </span>
396 );
397 })}
398 </>
399 );
400 }
401
402 case "TemplateParameter":
403 return <span>Template Param: {type.node.id.sv}</span>;
404 case "String":
405 return <Literal>"{type.value}"</Literal>;
406 case "Number":
407 case "Boolean":
408 return <>{type.value}</>;
409 default:
410 return null;
411 }
412};
413
414const TypeData: FunctionComponent<{ type: Type }> = ({ type }) => {
415 const program = useContext(ProgramContext);
416 const entries = [...program.stateMaps.entries()]
417 .map(([k, v]) => [k, v.get(undefined)?.get(type) as any])
418 .filter(([k, v]) => !!v);
419 if (entries.length === 0) {
420 return null;
421 }
422 return (
423 <KeyValueSection>
424 {entries.map(([k, v], i) => (
425 <div css={{ display: "flex" }} key={i}>
426 <div css={{ color: Colors.indentationGuide, marginRight: "5px" }}>{k.toString()}:</div>{" "}
427 <div>{inspect(v)}</div>
428 </div>
429 ))}
430 </KeyValueSection>
431 );
432};
433