Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/playground/cmd/typespec-playground.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import "../dist/cli/cli.js";
13 changes: 10 additions & 3 deletions packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
],
"type": "module",
"main": "dist/src/index.js",
"bin": {
"typespec-playground": "cmd/typespec-playground.js"
},
"exports": {
".": {
"types": "./dist/src/index.d.ts",
Expand Down Expand Up @@ -68,6 +71,7 @@
},
"files": [
"lib/*.tsp",
"cmd/**",
"dist/**",
"!dist/test/**"
],
Expand All @@ -83,17 +87,21 @@
"@typespec/protobuf": "workspace:^",
"@typespec/rest": "workspace:^",
"@typespec/versioning": "workspace:^",
"@vitejs/plugin-react": "catalog:",
"clsx": "catalog:",
"debounce": "catalog:",
"lzutf8": "catalog:",
"monaco-editor": "catalog:",
"picocolors": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-error-boundary": "catalog:",
"swagger-ui-dist": "catalog:",
"vite": "catalog:",
"vscode-languageserver": "catalog:",
"vscode-languageserver-textdocument": "catalog:",
"yaml": "catalog:"
"yaml": "catalog:",
"yargs": "catalog:"
},
"devDependencies": {
"@babel/core": "catalog:",
Expand All @@ -108,16 +116,15 @@
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/swagger-ui-dist": "catalog:",
"@types/yargs": "catalog:",
"@typespec/bundler": "workspace:^",
"@typespec/react-components": "workspace:^",
"@vitejs/plugin-react": "catalog:",
"c8": "catalog:",
"cross-env": "catalog:",
"es-module-shims": "catalog:",
"rimraf": "catalog:",
"storybook": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-checker": "catalog:",
"vite-plugin-dts": "catalog:",
"vitest": "catalog:"
Expand Down
45 changes: 45 additions & 0 deletions packages/playground/src/cli/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable no-console */
import pc from "picocolors";
import { build } from "vite";
import { createPlaygroundViteConfig } from "./create-vite-config.js";
import { resolveLibraries } from "./resolve-libraries.js";

export interface BuildOptions {
readonly emitter: string;
readonly output: string;
readonly libraries?: string[];
}

export async function runBuild(options: BuildOptions): Promise<void> {
const projectRoot = process.cwd();
const allLibraries = resolveLibraries(options.emitter, projectRoot);

// Add any extra libraries specified via CLI
if (options.libraries) {
for (const lib of options.libraries) {
if (!allLibraries.includes(lib)) {
allLibraries.push(lib);
}
}
}

console.log(pc.cyan("TypeSpec Playground Build"));
console.log(pc.dim(`Emitter: ${options.emitter}`));
console.log(pc.dim(`Libraries: ${allLibraries.join(", ")}`));
console.log(pc.dim(`Output: ${options.output}`));
console.log();

const viteConfig = createPlaygroundViteConfig({
defaultEmitter: options.emitter,
libraries: allLibraries,
outputDir: options.output,
});

await build({
...viteConfig,
root: projectRoot,
});

console.log();
console.log(pc.green(`✓ Playground built to ${options.output}`));
}
81 changes: 81 additions & 0 deletions packages/playground/src/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable no-console */
import pc from "picocolors";
import yargs from "yargs";
import { runBuild } from "./build.js";
import { runDev } from "./dev.js";

async function main() {
console.log(pc.cyan("TypeSpec Playground CLI\n"));

await yargs(process.argv.slice(2))
.scriptName("typespec-playground")
.help()
.strict()
.parserConfiguration({
"greedy-arrays": false,
"boolean-negation": false,
})
.option("emitter", {
type: "string",
description: "The emitter package name (e.g. @typespec/openapi3).",
demandOption: true,
})
.option("libraries", {
type: "array",
string: true,
description:
"Additional TypeSpec libraries to include beyond auto-discovered peer dependencies.",
})
.command(
"dev",
"Start a development server with hot-reload for the playground.",
(cmd) => {
return cmd.option("port", {
type: "number",
description: "Port for the dev server.",
default: 5174,
});
},
async (args) => {
await runDev({
emitter: args.emitter,
port: args.port,
libraries: args.libraries,
});
},
)
.command(
"build",
"Create a production build of the playground.",
(cmd) => {
return cmd.option("output", {
type: "string",
description: "Output directory for the build.",
default: "dist/playground",
});
},
async (args) => {
await runBuild({
emitter: args.emitter,
output: args.output,
libraries: args.libraries,
});
},
)
.demandCommand(1, "You must use one of the supported commands.").argv;
}

function internalError(error: unknown): never {
console.error(pc.red("Internal error!"));
console.error("File issue at https://github.com/microsoft/typespec");
console.error();
console.error(error);
process.exit(1);
}

process.on("unhandledRejection", (error: unknown) => {
console.error("Unhandled promise rejection!");
internalError(error);
});

main().catch(internalError);
40 changes: 40 additions & 0 deletions packages/playground/src/cli/create-vite-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { UserConfig } from "vite";
import { definePlaygroundViteConfig } from "../vite/index.js";
import { playgroundAppPlugin } from "./virtual-app-plugin.js";

export interface PlaygroundViteOptions {
/** The default emitter to select in the playground. */
readonly defaultEmitter: string;
/** Full list of TypeSpec libraries to bundle. */
readonly libraries: readonly string[];
/** Title for the playground page. */
readonly title?: string;
/** Output directory for production build. */
readonly outputDir?: string;
}

/**
* Create a full Vite config for the standalone playground CLI.
* Composes the base playground config with the virtual app plugin.
*/
export function createPlaygroundViteConfig(options: PlaygroundViteOptions): UserConfig {
const config = definePlaygroundViteConfig({
defaultEmitter: options.defaultEmitter,
libraries: options.libraries,
});

config.plugins!.push(
playgroundAppPlugin({
title: options.title,
}),
);

if (options.outputDir) {
config.build = {
...config.build,
outDir: options.outputDir,
};
}

return config;
}
47 changes: 47 additions & 0 deletions packages/playground/src/cli/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable no-console */
import pc from "picocolors";
import { createServer } from "vite";
import { createPlaygroundViteConfig } from "./create-vite-config.js";
import { resolveLibraries } from "./resolve-libraries.js";

export interface DevOptions {
readonly emitter: string;
readonly port: number;
readonly libraries?: string[];
}

export async function runDev(options: DevOptions): Promise<void> {
const projectRoot = process.cwd();
const allLibraries = resolveLibraries(options.emitter, projectRoot);

// Add any extra libraries specified via CLI
if (options.libraries) {
for (const lib of options.libraries) {
if (!allLibraries.includes(lib)) {
allLibraries.push(lib);
}
}
}

console.log(pc.cyan("TypeSpec Playground Dev Server"));
console.log(pc.dim(`Emitter: ${options.emitter}`));
console.log(pc.dim(`Libraries: ${allLibraries.join(", ")}`));
console.log();

const viteConfig = createPlaygroundViteConfig({
defaultEmitter: options.emitter,
libraries: allLibraries,
});

const server = await createServer({
...viteConfig,
root: projectRoot,
server: {
...viteConfig.server,
port: options.port,
},
});

await server.listen();
server.printUrls();
}
65 changes: 65 additions & 0 deletions packages/playground/src/cli/resolve-libraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { readFileSync } from "fs";
import { join } from "path";

/**
* Resolve the full set of TypeSpec libraries needed for the playground by
* walking the emitter's peerDependencies. Always includes @typespec/compiler.
*/
export function resolveLibraries(emitterName: string, projectRoot: string): string[] {
const visited = new Set<string>();
const result: string[] = [];

collectDependencies(emitterName, projectRoot, visited, result);

// Ensure @typespec/compiler is always first
if (!result.includes("@typespec/compiler")) {
result.unshift("@typespec/compiler");
} else {
const idx = result.indexOf("@typespec/compiler");
if (idx > 0) {
result.splice(idx, 1);
result.unshift("@typespec/compiler");
}
}

return result;
}

function collectDependencies(
packageName: string,
projectRoot: string,
visited: Set<string>,
result: string[],
): void {
if (visited.has(packageName)) return;
visited.add(packageName);

const pkgJsonPath = resolvePackageJson(packageName, projectRoot);
if (!pkgJsonPath) return;

const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));

// Recurse into peerDependencies first (so dependencies come before dependents)
const peerDeps = pkgJson.peerDependencies ?? {};
for (const dep of Object.keys(peerDeps)) {
if (isTypeSpecPackage(dep)) {
collectDependencies(dep, projectRoot, visited, result);
}
}

result.push(packageName);
}

function resolvePackageJson(packageName: string, projectRoot: string): string | undefined {
const candidate = join(projectRoot, "node_modules", packageName, "package.json");
try {
readFileSync(candidate, "utf-8");
return candidate;
} catch {
return undefined;
}
}

function isTypeSpecPackage(name: string): boolean {
return name.startsWith("@typespec/");
}
Loading
Loading