microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3e669c74c1e16a47afe2b98706bdcf8dad4434af

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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