From 6c60b4ee64180fe33b57b40c5db344b67199b321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 26 Jun 2023 17:34:48 +0200 Subject: [PATCH] feat: integrate the console in the cli (#3021) - Adds `@wingconsole/app` as a depedency to `winglang` - Changes `wing it` so it creates a Console server - Opens the Console URL - Stops referring to simfiles in favor of wingfiles Fixes #3005, fixes #1831, fixes #2617, fixes #723 and fixes #2357. --- .github/workflows/build.yml | 4 ++ apps/wing-playground/package-lock.json | 2 + apps/wing/package-lock.json | 67 +++++++++++++++++++++++++ apps/wing/package.json | 13 ++--- apps/wing/project.json | 2 +- apps/wing/src/cli.ts | 5 +- apps/wing/src/commands/run.test.ts | 68 ++++++++++++++++++++++++-- apps/wing/src/commands/run.ts | 48 +++++++++++++++--- apps/wing/src/util.ts | 20 ++++++++ tools/hangar/src/package.setup.ts | 4 ++ tools/hangar/src/paths.ts | 6 +++ 11 files changed, 219 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b546512191..0dd9d89cd57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,6 +185,8 @@ jobs: - build env: HANGAR_WING_SPEC: "file:${{ github.workspace }}/wing/winglang-${{ needs.build.outputs.version }}.tgz" + HANGAR_WINGCONSOLE_APP_SPEC: "file:${{ github.workspace }}/wingconsoleapp/wingconsole-app-${{ needs.build.outputs.version }}.tgz" + HANGAR_WINGCONSOLE_SERVER_SPEC: "file:${{ github.workspace }}/wingconsoleserver/wingconsole-server-${{ needs.build.outputs.version }}.tgz" HANGAR_WINGCOMPILER_SPEC: "file:${{ github.workspace }}/wingcompiler/winglang-compiler-${{ needs.build.outputs.version }}.tgz" HANGAR_WINGSDK_SPEC: "file:${{ github.workspace }}/wingsdk/winglang-sdk-${{ needs.build.outputs.version }}.tgz" steps: @@ -248,6 +250,8 @@ jobs: runs-on: "${{ matrix.runner }}-latest" env: HANGAR_WING_SPEC: "file:${{ github.workspace }}/wing/winglang-${{ needs.build.outputs.version }}.tgz" + HANGAR_WINGCONSOLE_APP_SPEC: "file:${{ github.workspace }}/wingconsoleapp/wingconsole-app-${{ needs.build.outputs.version }}.tgz" + HANGAR_WINGCONSOLE_SERVER_SPEC: "file:${{ github.workspace }}/wingconsoleserver/wingconsole-server-${{ needs.build.outputs.version }}.tgz" HANGAR_WINGCOMPILER_SPEC: "file:${{ github.workspace }}/wingcompiler/winglang-compiler-${{ needs.build.outputs.version }}.tgz" HANGAR_WINGSDK_SPEC: "file:${{ github.workspace }}/wingsdk/winglang-sdk-${{ needs.build.outputs.version }}.tgz" steps: diff --git a/apps/wing-playground/package-lock.json b/apps/wing-playground/package-lock.json index 9cc41103b68..a0fa7342b22 100644 --- a/apps/wing-playground/package-lock.json +++ b/apps/wing-playground/package-lock.json @@ -152,6 +152,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@wingconsole/app": "file:../wing-console/console/app", "@winglang/compiler": "file:../../libs/wingcompiler", "@winglang/sdk": "file:../../libs/wingsdk", "chalk": "^4.1.2", @@ -3477,6 +3478,7 @@ "@types/node-persist": "^3.1.3", "@types/semver-utils": "^1.1.1", "@types/update-notifier": "^6.0.1", + "@wingconsole/app": "file:../wing-console/console/app", "@winglang/compiler": "file:../../libs/wingcompiler", "@winglang/sdk": "file:../../libs/wingsdk", "bump-pack": "file:../../tools/bump-pack", diff --git a/apps/wing/package-lock.json b/apps/wing/package-lock.json index a8bfabc76b5..8c896eb20bf 100644 --- a/apps/wing/package-lock.json +++ b/apps/wing/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@wingconsole/app": "file:../wing-console/console/app", "@winglang/compiler": "file:../../libs/wingcompiler", "@winglang/sdk": "file:../../libs/wingsdk", "chalk": "^4.1.2", @@ -185,6 +186,39 @@ "typescript": "^5.1.3" } }, + "../wing-console/console/app": { + "name": "@wingconsole/app", + "version": "0.0.0", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@wingconsole/server": "file:../server", + "bump-pack": "file:../../../../tools/bump-pack", + "express": "^4.18.2" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.3", + "@types/express": "^4.17.17", + "@types/react": "^18.2.12", + "@types/react-dom": "^18.2.5", + "@vitejs/plugin-react": "^4.0.0", + "@vitest/coverage-c8": "^0.31.4", + "@wingconsole/error-message": "file:../../tools/error-message", + "@wingconsole/eslint-plugin": "file:../../tools/eslint-plugin", + "@wingconsole/tsconfig": "file:../../tools/tsconfig", + "@wingconsole/ui": "file:../ui", + "autoprefixer": "^10.4.14", + "eslint": "^8.42.0", + "open": "^9.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.3.2", + "tsup": "^6.7.0", + "tsx": "^3.12.7", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vitest": "^0.31.4" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", @@ -728,6 +762,10 @@ "pretty-format": "^27.5.1" } }, + "node_modules/@wingconsole/app": { + "resolved": "../wing-console/console/app", + "link": true + }, "node_modules/@winglang/compiler": { "resolved": "../../libs/wingcompiler", "link": true @@ -3126,6 +3164,35 @@ "pretty-format": "^27.5.1" } }, + "@wingconsole/app": { + "version": "file:../wing-console/console/app", + "requires": { + "@tailwindcss/forms": "^0.5.3", + "@types/express": "^4.17.17", + "@types/react": "^18.2.12", + "@types/react-dom": "^18.2.5", + "@vitejs/plugin-react": "^4.0.0", + "@vitest/coverage-c8": "^0.31.4", + "@wingconsole/error-message": "file:../../tools/error-message", + "@wingconsole/eslint-plugin": "file:../../tools/eslint-plugin", + "@wingconsole/server": "file:../server", + "@wingconsole/tsconfig": "file:../../tools/tsconfig", + "@wingconsole/ui": "file:../ui", + "autoprefixer": "^10.4.14", + "bump-pack": "file:../../../../tools/bump-pack", + "eslint": "^8.42.0", + "express": "^4.18.2", + "open": "^9.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.3.2", + "tsup": "^6.7.0", + "tsx": "^3.12.7", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vitest": "^0.31.4" + } + }, "@winglang/compiler": { "version": "file:../../libs/wingcompiler", "requires": { diff --git a/apps/wing/package.json b/apps/wing/package.json index 83c70f01eea..b3640170f12 100644 --- a/apps/wing/package.json +++ b/apps/wing/package.json @@ -27,6 +27,9 @@ "package": "bump-pack -b" }, "dependencies": { + "@wingconsole/app": "file:../wing-console/console/app", + "@winglang/compiler": "file:../../libs/wingcompiler", + "@winglang/sdk": "file:../../libs/wingsdk", "chalk": "^4.1.2", "codespan-wasm": "0.4.0", "commander": "^10.0.0", @@ -35,9 +38,7 @@ "open": "^8.4.0", "ora": "^5.4.1", "update-notifier": "^6.0.2", - "vscode-languageserver": "^8.0.2", - "@winglang/sdk": "file:../../libs/wingsdk", - "@winglang/compiler": "file:../../libs/wingcompiler" + "vscode-languageserver": "^8.0.2" }, "devDependencies": { "@types/debug": "^4.1.7", @@ -45,13 +46,13 @@ "@types/node-persist": "^3.1.3", "@types/semver-utils": "^1.1.1", "@types/update-notifier": "^6.0.1", + "bump-pack": "file:../../tools/bump-pack", "esbuild": "^0.17.19", "typescript": "^4.9.4", - "vitest": "^0.30.1", - "bump-pack": "file:../../tools/bump-pack" + "vitest": "^0.30.1" }, "volta": { "node": "18.16.0", "npm": "8.19.3" } -} \ No newline at end of file +} diff --git a/apps/wing/project.json b/apps/wing/project.json index 01cde51ef44..dc27b732f55 100644 --- a/apps/wing/project.json +++ b/apps/wing/project.json @@ -1,7 +1,7 @@ { "name": "winglang", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "implicitDependencies": ["wingc", "sdk", "compiler"], + "implicitDependencies": ["wingc", "sdk", "compiler", "console-app"], "targets": { "build": { "dependsOn": ["^build"], diff --git a/apps/wing/src/cli.ts b/apps/wing/src/cli.ts index c28ceb4734e..81811b961ed 100644 --- a/apps/wing/src/cli.ts +++ b/apps/wing/src/cli.ts @@ -49,8 +49,9 @@ async function main() { program .command("run") .alias("it") - .description("Runs a Wing simulator file in the Wing Console") - .argument("[simfile]", ".wsim simulator file") + .description("Runs a Wing program in the Wing Console") + .argument("[entrypoint]", "program .w entrypoint") + .option("-p, --port ", "specify port") .action(run); program.command("lsp").description("Run the Wing language server on stdio").action(run_server); diff --git a/apps/wing/src/commands/run.test.ts b/apps/wing/src/commands/run.test.ts index e6180526015..95b3cd5f6ac 100644 --- a/apps/wing/src/commands/run.test.ts +++ b/apps/wing/src/commands/run.test.ts @@ -1,4 +1,5 @@ import open from "open"; +import { createConsoleApp } from "@wingconsole/app"; import { run } from "./run"; import { mkdtemp } from "fs/promises"; import { join } from "path"; @@ -9,6 +10,16 @@ import { vi, test, expect } from "vitest"; vi.mock("open"); +vi.mock("@wingconsole/app", () => { + return { + createConsoleApp: vi.fn((options?: { requestedPort?: number }) => { + return { + port: options?.requestedPort ?? 3000, + }; + }), + }; +}); + test("wing it runs the only .w file", async () => { const workdir = await mkdtemp(join(tmpdir(), "-wing-it-test")); const prevdir = process.cwd(); @@ -18,7 +29,12 @@ test("wing it runs the only .w file", async () => { writeFileSync("foo.w", "bring cloud;"); await run(); - expect(open).toBeCalledWith("wing-console://" + resolve("foo.w")); + expect(createConsoleApp).toBeCalledWith({ + wingfile: resolve("foo.w"), + requestedPort: undefined, + hostUtils: expect.anything(), + }); + expect(open).toBeCalledWith("http://localhost:3000/"); } finally { process.chdir(prevdir); } @@ -48,7 +64,12 @@ test("wing it with a file runs", async () => { writeFileSync("foo.w", "bring cloud;"); await run("foo.w"); - expect(open).toBeCalledWith("wing-console://" + resolve("foo.w")); + expect(createConsoleApp).toBeCalledWith({ + wingfile: resolve("foo.w"), + requestedPort: undefined, + hostUtils: expect.anything(), + }); + expect(open).toBeCalledWith("http://localhost:3000/"); } finally { process.chdir(prevdir); } @@ -66,7 +87,12 @@ test("wing it with a nested file runs", async () => { writeFileSync(filePath, "bring cloud;"); await run(filePath); - expect(open).toBeCalledWith("wing-console://" + resolve(filePath)); + expect(createConsoleApp).toBeCalledWith({ + wingfile: resolve(filePath), + requestedPort: undefined, + hostUtils: expect.anything(), + }); + expect(open).toBeCalledWith("http://localhost:3000/"); } finally { process.chdir(prevdir); } @@ -85,3 +111,39 @@ test("wing it with an invalid file throws exception", async () => { process.chdir(prevdir); } }); + +test("wing it with a custom port runs", async () => { + const workdir = await mkdtemp(join(tmpdir(), "-wing-it-test")); + const prevdir = process.cwd(); + try { + process.chdir(workdir); + + writeFileSync("foo.w", "bring cloud;"); + + await run("foo.w", { port: "5000" }); + expect(createConsoleApp).toBeCalledWith({ + wingfile: resolve("foo.w"), + requestedPort: 5000, + hostUtils: expect.anything(), + }); + expect(open).toBeCalledWith("http://localhost:5000/"); + } finally { + process.chdir(prevdir); + } +}); + +test("wing it throws when invalid port number is used", async () => { + const workdir = await mkdtemp(join(tmpdir(), "-wing-it-test")); + const prevdir = process.cwd(); + try { + process.chdir(workdir); + + writeFileSync("foo.w", "bring cloud;"); + + await expect(async () => { + await run("foo.w", { port: "not a number" }); + }).rejects.toThrowError('"not a number" is not a number'); + } finally { + process.chdir(prevdir); + } +}); diff --git a/apps/wing/src/commands/run.ts b/apps/wing/src/commands/run.ts index 4240ecc1f61..f4e2135226e 100644 --- a/apps/wing/src/commands/run.ts +++ b/apps/wing/src/commands/run.ts @@ -2,21 +2,53 @@ import { readdirSync, existsSync } from "fs"; import { debug } from "debug"; import { resolve } from "path"; import open from "open"; +import { createConsoleApp } from "@wingconsole/app"; +import { parseNumericString } from "../util"; -export async function run(simfile?: string) { - if (!simfile) { +/** + * Options for the `run` command. + * This is passed from Commander to the `run` function. + */ +export interface RunOptions { + /** + * Preferred port number. + * + * Falls back to a random port number if necessary. + */ + readonly port?: string; +} + +/** + * Runs a Wing program in the Console. + * @param entrypoint The program .w entrypoint. Looks for a .w file in the current directory if not specified. + * @param options Run options. + */ +export async function run(entrypoint?: string, options?: RunOptions) { + if (!entrypoint) { const wingFiles = readdirSync(".").filter((item) => item.endsWith(".w")); if (wingFiles.length !== 1) { throw new Error("Please specify which file you want to run"); } - simfile = wingFiles[0]; + entrypoint = wingFiles[0]; } - if (!existsSync(simfile)) { - throw new Error(simfile + " doesn't exist"); + if (!existsSync(entrypoint)) { + throw new Error(entrypoint + " doesn't exist"); } - simfile = resolve(simfile); - debug("calling wing console protocol with:" + simfile); - await open("wing-console://" + simfile); + entrypoint = resolve(entrypoint); + debug("opening the wing console with:" + entrypoint); + + const { port } = await createConsoleApp({ + wingfile: entrypoint, + requestedPort: parseNumericString(options?.port), + hostUtils: { + async openExternal(url) { + await open(url); + }, + }, + }); + const url = `http://localhost:${port}/`; + await open(url); + console.log(`The Wing Console is running at ${url}`); } diff --git a/apps/wing/src/util.ts b/apps/wing/src/util.ts index 31575c3ca3b..c16b33d43fb 100644 --- a/apps/wing/src/util.ts +++ b/apps/wing/src/util.ts @@ -73,3 +73,23 @@ export async function generateTmpDir(sourcePath: string, ...additionalFiles: str return tempWingFile; } + +/** + * Casts a numeric string to a number. + * + * Returns `undefined` if the string is empty. + * + * @throws If the string is not a number. + */ +export function parseNumericString(text?: string) { + if (!text) { + return undefined; + } + + const number = Number(text); + if (isNaN(number)) { + throw new Error(`"${text}" is not a number`); + } + + return number; +} diff --git a/tools/hangar/src/package.setup.ts b/tools/hangar/src/package.setup.ts index 103e1990ac3..e2d18bb9aa8 100644 --- a/tools/hangar/src/package.setup.ts +++ b/tools/hangar/src/package.setup.ts @@ -7,6 +7,8 @@ import { targetWingSDKSpec, targetWingCompilerSpec, targetWingSpec, + targetWingConsoleAppSpec, + targetWingConsoleServerSpec, tmpDir, wingBin, } from "./paths"; @@ -44,6 +46,8 @@ export default async function () { targetWingSDKSpec, targetWingCompilerSpec, targetWingSpec, + targetWingConsoleAppSpec, + targetWingConsoleServerSpec, ]; const installResult = await execa(npmBin, installArgs, { cwd: tmpDir, diff --git a/tools/hangar/src/paths.ts b/tools/hangar/src/paths.ts index 2002e584de3..f4953228f7e 100644 --- a/tools/hangar/src/paths.ts +++ b/tools/hangar/src/paths.ts @@ -20,6 +20,12 @@ export const snapshotDir = path.join(hangarDir, "__snapshots__"); export const targetWingSpec = process.env.HANGAR_WING_SPEC ?? `file:${path.join(repoRoot, `apps/wing`)}`; +export const targetWingConsoleAppSpec = + process.env.HANGAR_WINGCONSOLE_APP_SPEC ?? + `file:${path.join(repoRoot, `apps/wing-console/console/app`)}`; +export const targetWingConsoleServerSpec = + process.env.HANGAR_WINGCONSOLE_SERVER_SPEC ?? + `file:${path.join(repoRoot, `apps/wing-console/console/server`)}`; export const targetWingCompilerSpec = process.env.HANGAR_WINGCOMPILER_SPEC ?? `file:${path.join(repoRoot, `libs/wingcompiler`)}`;