diff --git a/.github/workflows/tryit-comment.yml b/.github/workflows/tryit-comment.yml index 53985e4041..b94778efa7 100644 --- a/.github/workflows/tryit-comment.yml +++ b/.github/workflows/tryit-comment.yml @@ -30,7 +30,7 @@ jobs: `Changes in this PR will be published to the following url to try(check status of TypeSpec Pull Request Try It pipeline for publish status):`, `Playground: https://cadlplayground.z22.web.core.windows.net/prs/${prNumber}/`, "", - `Website: https://cadlwebsite.z1.web.core.windows.net/prs/${prNumber}/`, + `Website: https://tspwebsitepr.z5.web.core.windows.net/prs/${prNumber}/`, ].join("\n") }) diff --git a/common/changes/@typespec/compiler/playground-linters_2023-09-07-22-00.json b/common/changes/@typespec/compiler/playground-linters_2023-09-07-22-00.json new file mode 100644 index 0000000000..d87aadf949 --- /dev/null +++ b/common/changes/@typespec/compiler/playground-linters_2023-09-07-22-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/compiler", + "comment": "Expose `CompilerOptions` TypeScript type", + "type": "none" + } + ], + "packageName": "@typespec/compiler" +} \ No newline at end of file diff --git a/eng/pipelines/pr-tryit.yaml b/eng/pipelines/pr-tryit.yaml index b7c813ad00..151ec88304 100644 --- a/eng/pipelines/pr-tryit.yaml +++ b/eng/pipelines/pr-tryit.yaml @@ -46,7 +46,7 @@ jobs: inlineScript: | az storage blob upload-batch \ --destination \$web \ - --account-name "cadlwebsite" \ + --account-name "tspwebsitepr" \ --destination-path $(TYPESPEC_WEBSITE_BASE_PATH) \ --source "./packages/website/build/" \ --overwrite diff --git a/eng/scripts/create-tryit-comment.js b/eng/scripts/create-tryit-comment.js index 5762c709a9..ad4e87bd1b 100644 --- a/eng/scripts/create-tryit-comment.js +++ b/eng/scripts/create-tryit-comment.js @@ -44,7 +44,7 @@ async function main() { ``, `You can try these changes at https://cadlplayground.z22.web.core.windows.net${folderName}/prs/${prNumber}/`, "", - `Check the website changes at https://cadlwebsite.z1.web.core.windows.net${folderName}/prs/${prNumber}/`, + `Check the website changes at https://tspwebsitepr.z5.web.core.windows.net${folderName}/prs/${prNumber}/`, ].join("\n"); await writeComment(repo, prNumber, comment, ghAuth); } diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index 5dde2a7fb2..069a13d230 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -18,6 +18,7 @@ export { } from "./library.js"; export * from "./module-resolver.js"; export * from "./node-host.js"; +export * from "./options.js"; export * from "./parser.js"; export * from "./path-utils.js"; export * from "./program.js"; diff --git a/packages/playground-website/vite.config.ts b/packages/playground-website/vite.config.ts index 744e63bf54..920f60f6db 100644 --- a/packages/playground-website/vite.config.ts +++ b/packages/playground-website/vite.config.ts @@ -22,8 +22,16 @@ const config = definePlaygroundViteConfig({ filename: "samples/unions.tsp", preferredEmitter: "@typespec/openapi3", }, - "HTTP service": { filename: "samples/http.tsp", preferredEmitter: "@typespec/openapi3" }, - "REST framework": { filename: "samples/rest.tsp", preferredEmitter: "@typespec/openapi3" }, + "HTTP service": { + filename: "samples/http.tsp", + preferredEmitter: "@typespec/openapi3", + compilerOptions: { linterRuleSet: { extends: ["@typespec/http/all"] } }, + }, + "REST framework": { + filename: "samples/rest.tsp", + preferredEmitter: "@typespec/openapi3", + compilerOptions: { linterRuleSet: { extends: ["@typespec/http/all"] } }, + }, "Protobuf Kiosk": { filename: "samples/kiosk.tsp", preferredEmitter: "@typespec/protobuf", diff --git a/packages/playground/src/index.ts b/packages/playground/src/index.ts index 1c21c3c09e..10a85b36b2 100644 --- a/packages/playground/src/index.ts +++ b/packages/playground/src/index.ts @@ -3,4 +3,4 @@ export { registerMonacoDefaultWorkers } from "./monaco-worker.js"; export { registerMonacoLanguage } from "./services.js"; export { createUrlStateStorage } from "./state-storage.js"; export { PlaygroundSample } from "./types.js"; -export { filterEmitters } from "./utils.js"; +export { resolveLibraries as filterEmitters } from "./utils.js"; diff --git a/packages/playground/src/react/editor-command-bar.tsx b/packages/playground/src/react/editor-command-bar.tsx index b8a76aab58..8c20f103e6 100644 --- a/packages/playground/src/react/editor-command-bar.tsx +++ b/packages/playground/src/react/editor-command-bar.tsx @@ -10,22 +10,23 @@ import { Tooltip, } from "@fluentui/react-components"; import { Bug16Regular, Save16Regular, Settings24Regular } from "@fluentui/react-icons"; +import { CompilerOptions } from "@typespec/compiler"; import { FunctionComponent } from "react"; import { PlaygroundSample } from "../types.js"; import { EmitterDropdown } from "./emitter-dropdown.js"; -import { OutputSettings } from "./output-settings.js"; import { SamplesDropdown } from "./samples-dropdown.js"; -import { EmitterOptions } from "./types.js"; +import { CompilerSettings } from "./settings/compiler-settings.js"; +import { PlaygroundTspLibrary } from "./types.js"; export interface EditorCommandBarProps { documentationUrl?: string; saveCode: () => Promise | void; newIssue?: () => Promise | void; - emitters: string[]; + libraries: PlaygroundTspLibrary[]; selectedEmitter: string; onSelectedEmitterChange: (emitter: string) => void; - emitterOptions: EmitterOptions; - onEmitterOptionsChange: (options: EmitterOptions) => void; + compilerOptions: CompilerOptions; + onCompilerOptionsChange: (options: CompilerOptions) => void; samples?: Record; selectedSampleName: string; @@ -35,11 +36,11 @@ export const EditorCommandBar: FunctionComponent = ({ documentationUrl, saveCode, newIssue, - emitters, + libraries, selectedEmitter, onSelectedEmitterChange, - emitterOptions, - onEmitterOptionsChange, + compilerOptions: emitterOptions, + onCompilerOptionsChange, samples, selectedSampleName, onSelectedSampleNameChange, @@ -73,7 +74,7 @@ export const EditorCommandBar: FunctionComponent = ({ /> )} x.isEmitter).map((x) => x.name)} onSelectedEmitterChange={onSelectedEmitterChange} selectedEmitter={selectedEmitter} /> @@ -83,10 +84,11 @@ export const EditorCommandBar: FunctionComponent = ({ - diff --git a/packages/playground/src/react/hooks.ts b/packages/playground/src/react/hooks.ts index 1d9201f0bb..67b663642f 100644 --- a/packages/playground/src/react/hooks.ts +++ b/packages/playground/src/react/hooks.ts @@ -50,3 +50,18 @@ export function useControllableValue( return [currentValue, setValueOrCallOnChange] as any; } + +export function useAsyncMemo( + callback: () => Promise, + defaultValue: T, + deps?: React.DependencyList +): T { + const [value, setValue] = useState(defaultValue); + useEffect(() => { + callback() + .then(setValue) + // eslint-disable-next-line no-console + .catch(() => console.error("Failed to load async memo")); + }, deps); + return value; +} diff --git a/packages/playground/src/react/playground.tsx b/packages/playground/src/react/playground.tsx index 88027592c0..f4f1a9f2e8 100644 --- a/packages/playground/src/react/playground.tsx +++ b/packages/playground/src/react/playground.tsx @@ -1,3 +1,4 @@ +import { CompilerOptions } from "@typespec/compiler"; import debounce from "debounce"; import { KeyCode, KeyMod, MarkerSeverity, Uri, editor } from "monaco-editor"; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react"; @@ -7,12 +8,13 @@ import { BrowserHost } from "../browser-host.js"; import { importTypeSpecCompiler } from "../core.js"; import { getMarkerLocation } from "../services.js"; import { PlaygroundSample } from "../types.js"; +import { resolveLibraries } from "../utils.js"; import { EditorCommandBar } from "./editor-command-bar.js"; import { useMonacoModel } from "./editor.js"; import { Footer } from "./footer.js"; -import { useControllableValue } from "./hooks.js"; +import { useAsyncMemo, useControllableValue } from "./hooks.js"; import { OutputView } from "./output-view.js"; -import { CompilationState, EmitterOptions, FileOutputViewer } from "./types.js"; +import { CompilationState, FileOutputViewer } from "./types.js"; import { TypeSpecEditor } from "./typespec-editor.js"; export interface PlaygroundProps { @@ -21,8 +23,8 @@ export interface PlaygroundProps { /** Default emitter if leaving this unmanaged. */ defaultContent?: string; - /** List of available emitters */ - emitters: string[]; + /** List of available libraries */ + libraries: string[]; /** Emitter to use */ emitter?: string; @@ -32,11 +34,11 @@ export interface PlaygroundProps { onEmitterChange?: (emitter: string) => void; /** Emitter options */ - emitterOptions?: EmitterOptions; + compilerOptions?: CompilerOptions; /** Default emitter options if leaving this unmanaged. */ - defaultEmitterOptions?: EmitterOptions; + defaultCompilerOptions?: CompilerOptions; /** Callback when emitter options change */ - onEmitterOptionsChange?: (emitter: EmitterOptions) => void; + onCompilerOptionsChange?: (emitter: CompilerOptions) => void; /** Samples available */ samples?: Record; @@ -65,7 +67,7 @@ export interface PlaygroundSaveData { emitter: string; /** Emitter options. */ - options?: EmitterOptions; + options?: CompilerOptions; /** If a sample is selected and the content hasn't changed since. */ sampleName?: string; @@ -85,10 +87,10 @@ export const Playground: FunctionComponent = (props) => { props.defaultEmitter, props.onEmitterChange ); - const [emitterOptions, onEmitterOptionsChange] = useControllableValue( - props.emitterOptions, - props.defaultEmitterOptions ?? {}, - props.onEmitterOptionsChange + const [compilerOptions, onCompilerOptionsChange] = useControllableValue( + props.compilerOptions, + props.defaultCompilerOptions ?? {}, + props.onCompilerOptionsChange ); const [selectedSampleName, onSelectedSampleNameChange] = useControllableValue( props.sampleName, @@ -107,7 +109,7 @@ export const Playground: FunctionComponent = (props) => { setContent(content); const typespecCompiler = await importTypeSpecCompiler(); - const state = await compile(host, content, selectedEmitter, emitterOptions); + const state = await compile(host, content, selectedEmitter, compilerOptions); setCompilationState(state); if ("program" in state) { const markers: editor.IMarkerData[] = state.program.diagnostics.map((diag) => ({ @@ -121,7 +123,7 @@ export const Playground: FunctionComponent = (props) => { } else { editor.setModelMarkers(typespecModel, "owner", []); } - }, [host, selectedEmitter, emitterOptions, typespecModel, setContent]); + }, [host, selectedEmitter, compilerOptions, typespecModel, setContent]); const updateTypeSpec = useCallback( (value: string) => { @@ -143,6 +145,9 @@ export const Playground: FunctionComponent = (props) => { if (config.preferredEmitter) { onSelectedEmitterChange(config.preferredEmitter); } + if (config.compilerOptions) { + onCompilerOptionsChange(config.compilerOptions); + } } } }, [updateTypeSpec, selectedSampleName]); @@ -165,7 +170,7 @@ export const Playground: FunctionComponent = (props) => { onSave({ content: typespecModel.getValue(), emitter: selectedEmitter, - options: emitterOptions, + options: compilerOptions, sampleName: isSampleUntouched ? selectedSampleName : undefined, }); } @@ -173,7 +178,7 @@ export const Playground: FunctionComponent = (props) => { typespecModel, onSave, selectedEmitter, - emitterOptions, + compilerOptions, selectedSampleName, isSampleUntouched, ]); @@ -193,6 +198,12 @@ export const Playground: FunctionComponent = (props) => { [saveCode] ); + const libraries = useAsyncMemo( + async () => resolveLibraries(props.libraries), + [], + [props.libraries] + ); + return (
= (props) => { >
> + options: CompilerOptions ): Promise { await host.writeFile("main.tsp", content); await emptyOutputDir(host); try { const typespecCompiler = await importTypeSpecCompiler(); const program = await typespecCompiler.compile(host, "main.tsp", { - outputDir: "tsp-output", - emit: [selectedEmitter], + ...options, options: { - ...emittersOptions, + ...options.options, [selectedEmitter]: { - ...emittersOptions[selectedEmitter], + ...options.options?.[selectedEmitter], "emitter-output-dir": "tsp-output", }, }, + outputDir: "tsp-output", + emit: [selectedEmitter], }); const outputFiles = await findOutputFiles(host); return { program, outputFiles }; diff --git a/packages/playground/src/react/settings/compiler-settings.tsx b/packages/playground/src/react/settings/compiler-settings.tsx new file mode 100644 index 0000000000..420aed574b --- /dev/null +++ b/packages/playground/src/react/settings/compiler-settings.tsx @@ -0,0 +1,81 @@ +import { CompilerOptions, LinterRuleSet, TypeSpecLibrary } from "@typespec/compiler"; +import { FunctionComponent, useCallback, useEffect, useState } from "react"; +import { EmitterOptions, PlaygroundTspLibrary } from "../types.js"; +import { EmitterOptionsForm } from "./emitter-options-form.js"; +import { LinterForm } from "./linter-form.js"; + +export type CompilerSettingsProps = { + libraries: PlaygroundTspLibrary[]; + selectedEmitter: string; + options: CompilerOptions; + onOptionsChanged: (options: CompilerOptions) => void; +}; + +export const CompilerSettings: FunctionComponent = ({ + selectedEmitter, + libraries, + options, + onOptionsChanged, +}) => { + const emitterOptionsChanged = useCallback( + (emitterOptions: EmitterOptions) => { + onOptionsChanged({ + ...options, + options: emitterOptions, + }); + }, + [options] + ); + const library = useTypeSpecLibrary(selectedEmitter); + const linterRuleSet = options.linterRuleSet ?? {}; + const linterRuleSetChanged = useCallback( + (linterRuleSet: LinterRuleSet) => { + onOptionsChanged({ + ...options, + linterRuleSet, + }); + }, + [options] + ); + return ( +
+

Settings

+ <>Emitter: {selectedEmitter} +

Options

+ {library && ( + + )} +

Linter

+ +
+ ); +}; + +function useTypeSpecLibrary(name: string): TypeSpecLibrary | undefined { + const [lib, setLib] = useState>(); + + useEffect(() => { + setLib(undefined); + import(/* @vite-ignore */ name) + .then((module) => { + if (module.$lib === undefined) { + // eslint-disable-next-line no-console + console.error(`Couldn't load library ${name} missing $lib export`); + } + setLib(module.$lib); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error("Failed to load library", name); + }); + }, [name]); + return lib; +} diff --git a/packages/playground/src/react/output-settings.tsx b/packages/playground/src/react/settings/emitter-options-form.tsx similarity index 66% rename from packages/playground/src/react/output-settings.tsx rename to packages/playground/src/react/settings/emitter-options-form.tsx index 2f07b25dff..d9276f9d23 100644 --- a/packages/playground/src/react/output-settings.tsx +++ b/packages/playground/src/react/settings/emitter-options-form.tsx @@ -10,61 +10,15 @@ import { useId, } from "@fluentui/react-components"; import { TypeSpecLibrary } from "@typespec/compiler"; -import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react"; -import { EmitterOptions } from "./types.js"; +import { FunctionComponent, useCallback, useMemo } from "react"; +import { EmitterOptions } from "../types.js"; -export type OutputSettingsProps = { - selectedEmitter: string; - options: EmitterOptions; - optionsChanged: (options: EmitterOptions) => void; -}; - -export const OutputSettings: FunctionComponent = ({ - selectedEmitter, - options, - optionsChanged, -}) => { - const library = useTypeSpecLibrary(selectedEmitter); - return ( -
-

Settings

- <>Emitter: {selectedEmitter} -

Options

- {library && ( - - )} -
- ); -}; - -function useTypeSpecLibrary(name: string): TypeSpecLibrary | undefined { - const [lib, setLib] = useState>(); - - useEffect(() => { - setLib(undefined); - import(/* @vite-ignore */ name) - .then((module) => { - if (module.$lib === undefined) { - // eslint-disable-next-line no-console - console.error(`Couldn't load library ${name} missing $lib export`); - } - setLib(module.$lib); - }) - .catch((e) => { - // eslint-disable-next-line no-console - console.error("Failed to load library", name); - }); - }, [name]); - return lib; -} - -type EmitterOptionsFormProps = { +export interface EmitterOptionsFormProps { library: TypeSpecLibrary; options: EmitterOptions; optionsChanged: (options: EmitterOptions) => void; -}; - -const EmitterOptionsForm: FunctionComponent = ({ +} +export const EmitterOptionsForm: FunctionComponent = ({ library, options, optionsChanged, diff --git a/packages/playground/src/react/settings/linter-form.tsx b/packages/playground/src/react/settings/linter-form.tsx new file mode 100644 index 0000000000..738895820b --- /dev/null +++ b/packages/playground/src/react/settings/linter-form.tsx @@ -0,0 +1,66 @@ +import { Checkbox, CheckboxOnChangeData } from "@fluentui/react-components"; +import { LinterRuleSet, RuleRef } from "@typespec/compiler"; +import { FunctionComponent, useCallback } from "react"; +import { PlaygroundTspLibrary } from "../types.js"; + +export interface LinterFormProps { + libraries: PlaygroundTspLibrary[]; + linterRuleSet: LinterRuleSet; + onLinterRuleSetChanged: (options: LinterRuleSet) => void; +} + +export const LinterForm: FunctionComponent = ({ + libraries, + linterRuleSet, + onLinterRuleSetChanged, +}) => { + const rulesets = libraries.flatMap((lib) => { + return Object.keys(lib.definition?.linter?.ruleSets ?? {}).map( + (x) => `${lib.name}/${x}` + ) as RuleRef[]; + }); + if (rulesets.length === 0) { + return <>No ruleset available; + } + + const handleChange = (ruleSet: RuleRef, checked: boolean) => { + const ruleSets = linterRuleSet.extends ?? []; + + const updatedRuleSets = checked + ? [...ruleSets, ruleSet] + : ruleSets.filter((x) => x !== ruleSet); + + onLinterRuleSetChanged({ + extends: updatedRuleSets, + }); + }; + return ( + <> + {rulesets.map((ruleSet) => { + return ( + + ); + })} + + ); +}; + +interface RuleSetCheckbox { + ruleSet: RuleRef; + checked?: boolean; + onChange(ruleSet: RuleRef, checked: boolean): void; +} +const RuleSetCheckbox = ({ ruleSet, checked, onChange }: RuleSetCheckbox) => { + const handleChange = useCallback( + (_: any, data: CheckboxOnChangeData) => { + onChange(ruleSet, !!data.checked); + }, + [ruleSet, checked, onChange] + ); + return ; +}; diff --git a/packages/playground/src/react/standalone.tsx b/packages/playground/src/react/standalone.tsx index 8b9f595c33..ca7a898e1a 100644 --- a/packages/playground/src/react/standalone.tsx +++ b/packages/playground/src/react/standalone.tsx @@ -5,7 +5,6 @@ import { createBrowserHost } from "../browser-host.js"; import { registerMonacoDefaultWorkers } from "../monaco-worker.js"; import { registerMonacoLanguage } from "../services.js"; import { StateStorage, createUrlStateStorage } from "../state-storage.js"; -import { filterEmitters } from "../utils.js"; import { Playground, PlaygroundProps, PlaygroundSaveData } from "./playground.js"; export interface ReactPlaygroundConfig extends Partial { @@ -14,7 +13,6 @@ export interface ReactPlaygroundConfig extends Partial { export async function createReactPlayground(config: ReactPlaygroundConfig) { const host = await createBrowserHost(config.libraries); - const emitters = await filterEmitters(config.libraries); await registerMonacoLanguage(host); registerMonacoDefaultWorkers(); @@ -23,10 +21,10 @@ export async function createReactPlayground(config: ReactPlaygroundConfig) { const options: PlaygroundProps = { ...config, host, - emitters, + libraries: config.libraries, defaultContent: initialState.content, defaultEmitter: initialState.emitter ?? config.defaultEmitter, - defaultEmitterOptions: initialState.options, + defaultCompilerOptions: initialState.options, defaultSampleName: initialState.sampleName, onSave: (value) => { stateStorage.save(value); diff --git a/packages/playground/src/react/types.ts b/packages/playground/src/react/types.ts index 74187de89e..af70ceb7a5 100644 --- a/packages/playground/src/react/types.ts +++ b/packages/playground/src/react/types.ts @@ -1,4 +1,4 @@ -import { Program } from "@typespec/compiler"; +import { Program, TypeSpecLibrary } from "@typespec/compiler"; import { ReactElement } from "react"; export type CompilationCrashed = { @@ -23,3 +23,9 @@ export interface ViewerProps { filename: string; content: string; } + +export interface PlaygroundTspLibrary { + name: string; + isEmitter: boolean; + definition?: TypeSpecLibrary; +} diff --git a/packages/playground/src/types.ts b/packages/playground/src/types.ts index 647eea5981..9f99c9c28a 100644 --- a/packages/playground/src/types.ts +++ b/packages/playground/src/types.ts @@ -1,5 +1,12 @@ +import { CompilerOptions } from "@typespec/compiler"; + export interface PlaygroundSample { filename: string; preferredEmitter?: string; content: string; + + /** + * Compiler options for the sample. + */ + compilerOptions?: CompilerOptions; } diff --git a/packages/playground/src/utils.ts b/packages/playground/src/utils.ts index 74ee47f1aa..e137870aa3 100644 --- a/packages/playground/src/utils.ts +++ b/packages/playground/src/utils.ts @@ -1,11 +1,16 @@ import { importLibrary } from "./core.js"; +import { PlaygroundTspLibrary } from "./react/types.js"; /** * Filter emitters in given list of libraries - * @param libraries List of libraries that could include emitters + * @param names List of libraries that could include emitters * @returns Names of all the emitters */ -export async function filterEmitters(libraries: string[]): Promise { - const loaded = await Promise.all(libraries.map(async (x) => [x, await importLibrary(x)])); - return loaded.filter(([, x]) => (x as any).$lib?.emitter).map((x: any) => x[0]); +export async function resolveLibraries(names: string[]): Promise { + return await Promise.all( + names.map(async (x) => { + const lib: any = await importLibrary(x); + return { name: x, isEmitter: lib.$lib?.emitter, definition: lib.$lib }; + }) + ); } diff --git a/packages/playground/src/vite/index.ts b/packages/playground/src/vite/index.ts index b547e1b7c2..3bd813c93f 100644 --- a/packages/playground/src/vite/index.ts +++ b/packages/playground/src/vite/index.ts @@ -80,7 +80,13 @@ function playgroundManifestPlugin(config: PlaygroundUserConfig): Plugin { preferredEmitter: ${ config.preferredEmitter ? JSON.stringify(config.preferredEmitter) : "undefined" }, - content: s${index} + content: s${index}, + ${ + config.compilerOptions + ? `compilerOptions: ${JSON.stringify(config.compilerOptions)},` + : "" + } + }, ` ), "}",