microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
61ba17d2d29dedf4553b21a055aa6b442a304688

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

431lines · 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 "isFinished",
112] as const;
113const omittedPropsSet = new Set(omittedProps);
114type OmittedProps = (typeof omittedProps)[number];
115type NamedTypeUIProps<T extends NamedType> = {
116 type: T;
117 properties: Record<Exclude<keyof T, OmittedProps>, "skip" | "ref" | "nested" | "value">;
118 name?: string;
119};
120
121const NamedTypeUI = <T extends NamedType>({ type, name, properties }: NamedTypeUIProps<T>) => {
122 name ??= type.name;
123 const propsUI: TypeUIBaseProperty[] = Object.entries(type)
124 .map(([key, value]) => {
125 if (omittedPropsSet.has(key as any)) {
126 return undefined;
127 }
128 const action = (properties as any)[key] as "skip" | "ref" | "nested";
129 if (action === "skip") {
130 return undefined;
131 }
132
133 const render = (x: any) =>
134 action === "ref" ? <TypeReference type={value} /> : <TypeUI type={x} />;
135 let valueUI;
136 if (value === undefined) {
137 valueUI = value;
138 } else if (value.kind) {
139 valueUI = render(value);
140 } else if (
141 typeof value === "object" &&
142 "entries" in value &&
143 typeof value.entries === "function"
144 ) {
145 valueUI = <ItemList items={value} render={render} />;
146 } else {
147 valueUI = value;
148 }
149 return {
150 name: key,
151 value: valueUI,
152 } satisfies TypeUIBaseProperty;
153 })
154 .filter((x): x is TypeUIBaseProperty => Boolean(x));
155 return (
156 <TypeUIBase type={type} name={name} properties={propsUI.concat([getDataProperty(type)])} />
157 );
158};
159
160interface TypeUIProps {
161 type: Type;
162}
163
164const TypeUI: FunctionComponent<TypeUIProps> = ({ type }) => {
165 switch (type.kind) {
166 case "Namespace":
167 return <NamespaceUI type={type} />;
168 case "Interface":
169 return <InterfaceUI type={type} />;
170 case "Operation":
171 return <OperationUI type={type} />;
172 case "Model":
173 return <ModelUI type={type} />;
174 case "Scalar":
175 return <ScalarUI type={type} />;
176 case "ModelProperty":
177 return <ModelPropertyUI type={type} />;
178 case "Union":
179 return <UnionUI type={type} />;
180 case "UnionVariant":
181 return <UnionVariantUI type={type} />;
182 case "Enum":
183 return <EnumUI type={type} />;
184 case "EnumMember":
185 return <EnumMemberUI type={type} />;
186 default:
187 return null;
188 }
189};
190
191const NamespaceUI: FunctionComponent<{ type: Namespace }> = ({ type }) => {
192 const name = getNamespaceFullName(type) || "(global)";
193
194 return (
195 <NamedTypeUI
196 name={name}
197 type={type}
198 properties={{
199 namespaces: "skip",
200 models: "nested",
201 scalars: "nested",
202 interfaces: "nested",
203 operations: "nested",
204 unions: "nested",
205 enums: "nested",
206 decoratorDeclarations: "nested",
207 functionDeclarations: "nested",
208 }}
209 />
210 );
211};
212
213const InterfaceUI: FunctionComponent<{ type: Interface }> = ({ type }) => {
214 return (
215 <NamedTypeUI
216 type={type}
217 properties={{
218 operations: "nested",
219 sourceInterfaces: "ref",
220 }}
221 />
222 );
223};
224
225const OperationUI: FunctionComponent<{ type: Operation }> = ({ type }) => {
226 return (
227 <NamedTypeUI
228 type={type}
229 properties={{
230 interface: "skip",
231 parameters: "nested",
232 returnType: "ref",
233 sourceOperation: "ref",
234 }}
235 />
236 );
237};
238
239function getDataProperty(type: Type): TypeUIBaseProperty {
240 return {
241 name: "data",
242 description: "in program.stateMap()",
243 value: <TypeData type={type} />,
244 };
245}
246
247const ModelUI: FunctionComponent<{ type: Model }> = ({ type }) => {
248 return (
249 <NamedTypeUI
250 type={type}
251 properties={{
252 indexer: "skip",
253 baseModel: "ref",
254 derivedModels: "ref",
255 properties: "nested",
256 sourceModel: "ref",
257 }}
258 />
259 );
260};
261
262const ScalarUI: FunctionComponent<{ type: Scalar }> = ({ type }) => {
263 return (
264 <NamedTypeUI
265 type={type}
266 properties={{
267 baseScalar: "ref",
268 derivedScalars: "ref",
269 }}
270 />
271 );
272};
273
274const ModelPropertyUI: FunctionComponent<{ type: ModelProperty }> = ({ type }) => {
275 return (
276 <NamedTypeUI
277 type={type}
278 properties={{
279 model: "skip",
280 type: "ref",
281 optional: "value",
282 sourceProperty: "ref",
283 default: "value",
284 }}
285 />
286 );
287};
288
289const EnumUI: FunctionComponent<{ type: Enum }> = ({ type }) => {
290 return (
291 <NamedTypeUI
292 type={type}
293 properties={{
294 members: "nested",
295 }}
296 />
297 );
298};
299
300const EnumMemberUI: FunctionComponent<{ type: EnumMember }> = ({ type }) => {
301 return (
302 <NamedTypeUI
303 type={type}
304 properties={{
305 enum: "skip",
306 sourceMember: "ref",
307 value: "value",
308 }}
309 />
310 );
311};
312
313const UnionUI: FunctionComponent<{ type: Union }> = ({ type }) => {
314 if (!isNamedUnion(type)) {
315 return <></>;
316 }
317 return (
318 <NamedTypeUI
319 type={type}
320 properties={{
321 expression: "skip",
322 options: "skip",
323 variants: "nested",
324 }}
325 />
326 );
327};
328
329const UnionVariantUI: FunctionComponent<{ type: UnionVariant }> = ({ type }) => {
330 if (typeof type.name === "symbol") {
331 return <></>;
332 }
333 return (
334 <NamedTypeUI
335 type={type as UnionVariant & { name: string }}
336 properties={{
337 union: "skip",
338 type: "ref",
339 }}
340 />
341 );
342};
343
344const NamedTypeRef: FunctionComponent<{ type: NamedType }> = ({ type }) => {
345 const id = getIdForType(type);
346 const href = `#${id}`;
347 return (
348 <a
349 css={{
350 color: "#268bd2",
351 textDecoration: "none",
352
353 "&:hover": {
354 textDecoration: "underline",
355 },
356 }}
357 href={href}
358 title={type.kind + ": " + id}
359 >
360 {getTypeName(type)}
361 </a>
362 );
363};
364const TypeReference: FunctionComponent<{ type: Type }> = ({ type }) => {
365 switch (type.kind) {
366 case "Namespace":
367 case "Operation":
368 case "Interface":
369 case "Enum":
370 case "ModelProperty":
371 case "Scalar":
372 return <NamedTypeRef type={type} />;
373 case "Model":
374 if (type.name === "") {
375 return (
376 <KeyValueSection>
377 <TypeUI type={type} />
378 </KeyValueSection>
379 );
380 } else {
381 return <NamedTypeRef type={type} />;
382 }
383 case "Union":
384 if (isNamedUnion(type)) {
385 return <NamedTypeRef type={type} />;
386 } else {
387 return (
388 <>
389 {[...type.variants.values()].map((variant, i) => {
390 return (
391 <span key={i}>
392 <TypeReference type={variant.type} />
393 {i < type.variants.size - 1 ? " | " : ""}
394 </span>
395 );
396 })}
397 </>
398 );
399 }
400
401 case "TemplateParameter":
402 return <span>Template Param: {type.node.id.sv}</span>;
403 case "String":
404 return <Literal>"{type.value}"</Literal>;
405 case "Number":
406 case "Boolean":
407 return <>{type.value}</>;
408 default:
409 return null;
410 }
411};
412
413const TypeData: FunctionComponent<{ type: Type }> = ({ type }) => {
414 const program = useContext(ProgramContext);
415 const entries = [...program.stateMaps.entries()]
416 .map(([k, v]) => [k, v.get(undefined)?.get(type) as any])
417 .filter(([k, v]) => !!v);
418 if (entries.length === 0) {
419 return null;
420 }
421 return (
422 <KeyValueSection>
423 {entries.map(([k, v], i) => (
424 <div css={{ display: "flex" }} key={i}>
425 <div css={{ color: "#333", marginRight: "5px" }}>{k.toString()}:</div>{" "}
426 <div>{inspect(v)}</div>
427 </div>
428 ))}
429 </KeyValueSection>
430 );
431};
432