diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14baec19eb..418d4ce579 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -736,10 +736,9 @@ jobs: - name: "Install Node dependencies" run: npm ci working-directory: playground - - name: "Build Ruff playground" - run: npm run dev:build --workspace ruff-playground + - name: "Build playgrounds" + run: npm run dev:wasm working-directory: playground - # Requires a build for ruff_wasm to exist - name: "Run TypeScript checks" run: npm run check working-directory: playground diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index e901efe1ae..fbcd3019a6 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -38,12 +38,12 @@ jobs: - name: "Install Node dependencies" run: npm ci working-directory: playground - - name: "Build Ruff playground" - run: npm run build --workspace ruff-playground - working-directory: playground - name: "Run TypeScript checks" run: npm run check working-directory: playground + - name: "Build Ruff playground" + run: npm run build --workspace ruff-playground + working-directory: playground - name: "Deploy to Cloudflare Pages" if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 diff --git a/Cargo.lock b/Cargo.lock index 1ba625e2ea..c36cf789f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2644,6 +2644,8 @@ dependencies = [ "ruff_db", "ruff_notebook", "ruff_python_ast", + "ruff_source_file", + "ruff_text_size", "wasm-bindgen", "wasm-bindgen-test", ] diff --git a/crates/red_knot_wasm/Cargo.toml b/crates/red_knot_wasm/Cargo.toml index 413dc2021b..50d8b7b871 100644 --- a/crates/red_knot_wasm/Cargo.toml +++ b/crates/red_knot_wasm/Cargo.toml @@ -22,8 +22,10 @@ default = ["console_error_panic_hook"] red_knot_project = { workspace = true, default-features = false, features = ["deflate"] } ruff_db = { workspace = true, default-features = false, features = [] } -ruff_python_ast = { workspace = true } ruff_notebook = { workspace = true } +ruff_python_ast = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } console_error_panic_hook = { workspace = true, optional = true } console_log = { workspace = true } diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/red_knot_wasm/src/lib.rs index e7e4203e27..865c161411 100644 --- a/crates/red_knot_wasm/src/lib.rs +++ b/crates/red_knot_wasm/src/lib.rs @@ -1,20 +1,23 @@ use std::any::Any; -use js_sys::Error; -use wasm_bindgen::prelude::*; - +use js_sys::{Error, JsString}; use red_knot_project::metadata::options::{EnvironmentOptions, Options}; use red_knot_project::metadata::value::RangedValue; +use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use red_knot_project::ProjectMetadata; use red_knot_project::{Db, ProjectDatabase}; use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait}; use ruff_db::files::{system_path_to_file, File}; +use ruff_db::source::{line_index, source_text}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; use ruff_db::system::{ CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System, SystemPath, SystemPathBuf, SystemVirtualPath, }; +use ruff_db::Upcast; use ruff_notebook::Notebook; +use ruff_source_file::SourceLocation; +use wasm_bindgen::prelude::*; #[wasm_bindgen(start)] pub fn run() { @@ -36,6 +39,7 @@ pub fn run() { pub struct Workspace { db: ProjectDatabase, system: WasmSystem, + options: Options, } #[wasm_bindgen] @@ -47,34 +51,49 @@ impl Workspace { let mut workspace = ProjectMetadata::discover(SystemPath::new(root), &system).map_err(into_error)?; - workspace.apply_cli_options(Options { + let options = Options { environment: Some(EnvironmentOptions { python_version: Some(RangedValue::cli(settings.python_version.into())), ..EnvironmentOptions::default() }), ..Options::default() - }); + }; + + workspace.apply_cli_options(options.clone()); let db = ProjectDatabase::new(workspace, system.clone()).map_err(into_error)?; - Ok(Self { db, system }) + Ok(Self { + db, + system, + options, + }) } #[wasm_bindgen(js_name = "openFile")] pub fn open_file(&mut self, path: &str, contents: &str) -> Result { + let path = SystemPath::new(path); + self.system .fs .write_file_all(path, contents) .map_err(into_error)?; + self.db.apply_changes( + vec![ChangeEvent::Created { + path: path.to_path_buf(), + kind: CreatedKind::File, + }], + Some(&self.options), + ); + let file = system_path_to_file(&self.db, path).expect("File to exist"); - file.sync(&mut self.db); self.db.project().open_file(&mut self.db, file); Ok(FileHandle { file, - path: SystemPath::new(path).to_path_buf(), + path: path.to_path_buf(), }) } @@ -89,7 +108,19 @@ impl Workspace { .write_file(&file_id.path, contents) .map_err(into_error)?; - file_id.file.sync(&mut self.db); + self.db.apply_changes( + vec![ + ChangeEvent::Changed { + path: file_id.path.to_path_buf(), + kind: ChangedKind::FileContent, + }, + ChangeEvent::Changed { + path: file_id.path.to_path_buf(), + kind: ChangedKind::FileMetadata, + }, + ], + Some(&self.options), + ); Ok(()) } @@ -104,32 +135,30 @@ impl Workspace { .remove_file(&file_id.path) .map_err(into_error)?; - file.sync(&mut self.db); + self.db.apply_changes( + vec![ChangeEvent::Deleted { + path: file_id.path.to_path_buf(), + kind: DeletedKind::File, + }], + Some(&self.options), + ); Ok(()) } /// Checks a single file. #[wasm_bindgen(js_name = "checkFile")] - pub fn check_file(&self, file_id: &FileHandle) -> Result, Error> { + pub fn check_file(&self, file_id: &FileHandle) -> Result, Error> { let result = self.db.check_file(file_id.file).map_err(into_error)?; - let display_config = DisplayDiagnosticConfig::default().color(false); - Ok(result - .into_iter() - .map(|diagnostic| diagnostic.display(&self.db, &display_config).to_string()) - .collect()) + Ok(result.into_iter().map(Diagnostic::wrap).collect()) } /// Checks all open files - pub fn check(&self) -> Result, Error> { + pub fn check(&self) -> Result, Error> { let result = self.db.check().map_err(into_error)?; - let display_config = DisplayDiagnosticConfig::default().color(false); - Ok(result - .into_iter() - .map(|diagnostic| diagnostic.display(&self.db, &display_config).to_string()) - .collect()) + Ok(result.into_iter().map(Diagnostic::wrap).collect()) } /// Returns the parsed AST for `path` @@ -185,6 +214,124 @@ impl Settings { } } +#[wasm_bindgen] +pub struct Diagnostic { + #[wasm_bindgen(readonly)] + inner: Box, +} + +#[wasm_bindgen] +impl Diagnostic { + fn wrap(diagnostic: Box) -> Self { + Self { inner: diagnostic } + } + + #[wasm_bindgen] + pub fn message(&self) -> JsString { + JsString::from(&*self.inner.message()) + } + + #[wasm_bindgen] + pub fn id(&self) -> JsString { + JsString::from(self.inner.id().to_string()) + } + + #[wasm_bindgen] + pub fn severity(&self) -> Severity { + Severity::from(self.inner.severity()) + } + + #[wasm_bindgen] + pub fn text_range(&self) -> Option { + self.inner + .span() + .and_then(|span| Some(TextRange::from(span.range()?))) + } + + #[wasm_bindgen] + pub fn to_range(&self, workspace: &Workspace) -> Option { + self.inner.span().and_then(|span| { + let line_index = line_index(workspace.db.upcast(), span.file()); + let source = source_text(workspace.db.upcast(), span.file()); + let text_range = span.range()?; + + Some(Range { + start: line_index + .source_location(text_range.start(), &source) + .into(), + end: line_index.source_location(text_range.end(), &source).into(), + }) + }) + } + + #[wasm_bindgen] + pub fn display(&self, workspace: &Workspace) -> JsString { + let config = DisplayDiagnosticConfig::default().color(false); + self.inner + .display(workspace.db.upcast(), &config) + .to_string() + .into() + } +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Range { + pub start: Position, + pub end: Position, +} + +impl From for Position { + fn from(location: SourceLocation) -> Self { + Self { + line: location.row.to_zero_indexed(), + character: location.column.to_zero_indexed(), + } + } +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Position { + pub line: usize, + pub character: usize, +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Hash, PartialEq, Eq)] +pub enum Severity { + Info, + Warning, + Error, + Fatal, +} + +impl From for Severity { + fn from(value: ruff_db::diagnostic::Severity) -> Self { + match value { + ruff_db::diagnostic::Severity::Info => Self::Info, + ruff_db::diagnostic::Severity::Warning => Self::Warning, + ruff_db::diagnostic::Severity::Error => Self::Error, + ruff_db::diagnostic::Severity::Fatal => Self::Fatal, + } + } +} + +#[wasm_bindgen] +pub struct TextRange { + pub start: u32, + pub end: u32, +} + +impl From for TextRange { + fn from(value: ruff_text_size::TextRange) -> Self { + Self { + start: value.start().into(), + end: value.end().into(), + } + } +} + #[wasm_bindgen] #[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum PythonVersion { diff --git a/crates/red_knot_wasm/tests/api.rs b/crates/red_knot_wasm/tests/api.rs index 437f9b8988..9f20a6dc14 100644 --- a/crates/red_knot_wasm/tests/api.rs +++ b/crates/red_knot_wasm/tests/api.rs @@ -2,7 +2,7 @@ use wasm_bindgen_test::wasm_bindgen_test; -use red_knot_wasm::{PythonVersion, Settings, Workspace}; +use red_knot_wasm::{Position, PythonVersion, Settings, Workspace}; #[wasm_bindgen_test] fn check() { @@ -17,17 +17,17 @@ fn check() { let result = workspace.check().expect("Check to succeed"); + assert_eq!(result.len(), 1); + + let diagnostic = &result[0]; + + assert_eq!(diagnostic.id(), "lint:unresolved-import"); assert_eq!( - result, - vec![ - "\ -error: lint:unresolved-import - --> /test.py:1:8 - | -1 | import random22 - | ^^^^^^^^ Cannot resolve import `random22` - | -", - ], + diagnostic.to_range(&workspace).unwrap().start, + Position { + line: 0, + character: 7 + } ); + assert_eq!(diagnostic.message(), "Cannot resolve import `random22`"); } diff --git a/playground/.prettierignore b/playground/.prettierignore index 67682a03dd..ac9e43b7a3 100644 --- a/playground/.prettierignore +++ b/playground/.prettierignore @@ -1,3 +1,5 @@ **.md ruff/dist ruff/ruff_wasm +knot/dist +knot/red_knot_wasm \ No newline at end of file diff --git a/playground/README.md b/playground/README.md index 8259121456..efe28b7557 100644 --- a/playground/README.md +++ b/playground/README.md @@ -4,12 +4,16 @@ In-browser playground for Ruff. Available [https://play.ruff.rs/](https://play.r ## Getting started -Install the NPM dependencies with `npm install`, and run, and run the development server with `npm start`. -You may need to restart the server after making changes to Ruff to re-build the WASM module. +Install the NPM dependencies with `npm install`, and run, and run the development server with +`npm start --workspace ruff-playground` or `npm start --workspace knot-playground`. +You may need to restart the server after making changes to Ruff or Red Knot to re-build the WASM +module. -To run the datastore, which is based on [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/), +To run the datastore, which is based +on [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/), install the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/), -then run `npx wrangler dev --local` from the `./playground/api` directory. Note that the datastore is +then run `npx wrangler dev --local` from the `./playground/api` directory. Note that the datastore +is only required to generate shareable URLs for code snippets. The development datastore does not require Cloudflare authentication or login, but in turn only persists data locally. @@ -20,8 +24,17 @@ The playground is implemented as a single-page React application powered by [Monaco](https://github.com/microsoft/monaco-editor). The playground stores state in `localStorage`, but supports persisting code snippets to -a persistent datastore based on [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/) -and exposed via a [Cloudflare Worker](https://developers.cloudflare.com/workers/learning/how-workers-works/). +a persistent datastore based +on [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/) +and exposed via +a [Cloudflare Worker](https://developers.cloudflare.com/workers/learning/how-workers-works/). The playground design is originally based on [Tailwind Play](https://play.tailwindcss.com/), with additional inspiration from the [Biome Playground](https://biomejs.dev/playground/). + +## Known issues + +### Stack overflows + +If you see stack overflows in the playground, build the WASM module in release mode: +`npm run --workspace knot-playground build:wasm`. diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index dff3b5daf5..5d4a5076ca 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -16,20 +16,36 @@ "@cloudflare/workers-types": "^4.20230801.0", "miniflare": "^3.20230801.1", "typescript": "^5.1.6", - "wrangler": "3.111.0" + "wrangler": "^4.1.0" } }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", - "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "mime": "^3.0.0" }, "engines": { - "node": ">=16.13" + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } } }, "node_modules/@cloudflare/workerd-darwin-64": { @@ -118,9 +134,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20250224.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250224.0.tgz", - "integrity": "sha512-j6ZwQ5G2moQRaEtGI2u5TBQhVXv/XwOS5jfBAheZHcpCM07zm8j0i8jZHHLq/6VA8e6VRjKohOyj5j6tZ1KHLQ==", + "version": "4.20250317.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250317.0.tgz", + "integrity": "sha512-ud1x5D1ksDIdx35jsx6wG9G8a/SLeg7kfGv/c732umLYn7I+DZ7TdKTvM1LaWTLzp+yYGyXZOrInJywfUU8bVw==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -147,378 +163,429 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild-plugins/node-globals-polyfill": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", - "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", - "dev": true, - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild-plugins/node-modules-polyfill": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", - "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^4.0.0", - "rollup-plugin-node-polyfills": "^0.2.1" - }, - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@fastify/busboy": { @@ -1082,13 +1149,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -1136,60 +1196,46 @@ } }, "node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, "node_modules/execa": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", @@ -1224,6 +1270,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/exsolve": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz", + "integrity": "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==", + "dev": true, + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1305,15 +1358,6 @@ "node": ">=6" } }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1324,6 +1368,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -1368,26 +1413,6 @@ "node": ">=16.13" } }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", - "dev": true, - "license": "MIT" - }, "node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -1434,9 +1459,9 @@ } }, "node_modules/ohash": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", - "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "dev": true, "license": "MIT" }, @@ -1475,9 +1500,9 @@ "license": "MIT" }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -1492,54 +1517,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.0.tgz", - "integrity": "sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.3", - "pathe": "^1.1.2" - } - }, "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, - "node_modules/rollup-plugin-inject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", - "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1", - "magic-string": "^0.25.3", - "rollup-pluginutils": "^2.8.1" - } - }, - "node_modules/rollup-plugin-node-polyfills": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", - "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", - "dev": true, - "dependencies": { - "rollup-plugin-inject": "^3.0.0" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -1637,13 +1620,6 @@ "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true - }, "node_modules/stacktracey": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", @@ -1718,16 +1694,16 @@ } }, "node_modules/unenv": { - "version": "2.0.0-rc.1", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.1.tgz", - "integrity": "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==", + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", "dev": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", - "mlly": "^1.7.4", - "ohash": "^1.1.4", - "pathe": "^1.1.2", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", "ufo": "^1.5.4" } }, @@ -1791,35 +1767,34 @@ } }, "node_modules/wrangler": { - "version": "3.111.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.111.0.tgz", - "integrity": "sha512-3j/Wq5aj/sCQRSmkjBLxbkIH7LCx0h2UnaxmhOplDjJmZty10lGRs/jGgaG/M/GEsDg5TJ7UHvBh3hSldgjfKg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.1.0.tgz", + "integrity": "sha512-HcQZ2YappySGipEDEdbjMq01g3v+mv+xZYZSzwPTmRsoTfnbL5yteObshcK1JX9jdx7Qw23Ywd/4BPa1JyKIUQ==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { - "@cloudflare/kv-asset-handler": "0.3.4", - "@esbuild-plugins/node-globals-polyfill": "0.2.3", - "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "@cloudflare/kv-asset-handler": "0.4.0", + "@cloudflare/unenv-preset": "2.0.2", "blake3-wasm": "2.1.5", - "esbuild": "0.17.19", - "miniflare": "3.20250214.1", + "esbuild": "0.24.2", + "miniflare": "4.20250317.0", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.1", - "workerd": "1.20250214.0" + "unenv": "2.0.0-rc.14", + "workerd": "1.20250317.0" }, "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" }, "engines": { - "node": ">=16.17.0" + "node": ">=18.0.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250214.0" + "@cloudflare/workers-types": "^4.20250317.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -1827,6 +1802,138 @@ } } }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250317.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250317.0.tgz", + "integrity": "sha512-ZnpF+MP/azHJ7sUOW9Ut/5pqeijsEOSmRUpONDXImv/DiHgtCd2BA/He11srp8nG2XeWav3jk+Ob84NKrrXXHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250317.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250317.0.tgz", + "integrity": "sha512-ypn2/SIK7LAouYx5oB0NNhzb3h+ZdXtDh94VCcsNV81xAVdDXKp6xvTnqY8CWjGfuKWJocbRZVZvU+Lquhuujg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250317.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250317.0.tgz", + "integrity": "sha512-KfAHN9VHF2NxGjDjj7udLAatZ72GIg4xmN9r2AZ6N1/hsGDlbn+NbVkSJtWjpXBcCoWYxQqtAdpHyO4eb7nIvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250317.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250317.0.tgz", + "integrity": "sha512-o7a3poQ4vzw553xGudUWm8yGsfdRWSGxqDEdYyuzT5k3z4qjsYMGsZgW9Yw8x3f1SSpPgYpdLlc8IKg9n7eukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250317.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250317.0.tgz", + "integrity": "sha512-tfDSioKY5OKP0nZ7Mkc6bLcwY2fIrROwoq2WjekQ62x91KRbKCJwjkOSvyFJYbshDATK90GutYoblqV80e34jw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "4.20250317.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250317.0.tgz", + "integrity": "sha512-fCyFTa3G41Vyo24QUZD5xgdm+6RMKT6VC3vk9Usmr+Pwf/15HcH1AVLPVgzmJaJosWVb8r4S0HQ9a/+bmmZx0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250317.0", + "ws": "8.18.0", + "youch": "3.2.3", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20250317.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250317.0.tgz", + "integrity": "sha512-m+aqA4RS/jsIaml0KuTi96UBlkx1vC0mcLClGKPFNPiMStK75hVQxUhupXEMI4knHtb/vgNQyPFMKAJtxW5c6w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250317.0", + "@cloudflare/workerd-darwin-arm64": "1.20250317.0", + "@cloudflare/workerd-linux-64": "1.20250317.0", + "@cloudflare/workerd-linux-arm64": "1.20250317.0", + "@cloudflare/workerd-windows-64": "1.20250317.0" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", diff --git a/playground/api/package.json b/playground/api/package.json index ec5e4fd2bf..87d9ab8709 100644 --- a/playground/api/package.json +++ b/playground/api/package.json @@ -5,7 +5,7 @@ "@cloudflare/workers-types": "^4.20230801.0", "miniflare": "^3.20230801.1", "typescript": "^5.1.6", - "wrangler": "3.111.0" + "wrangler": "^4.1.0" }, "private": true, "scripts": { diff --git a/playground/api/src/index.ts b/playground/api/src/index.ts index 2fbd4eaebf..42b863166c 100644 --- a/playground/api/src/index.ts +++ b/playground/api/src/index.ts @@ -41,6 +41,10 @@ export default { const headers = DEV ? DEVELOPMENT_HEADERS : PRODUCTION_HEADERS; + if (!DEV && request.url.startsWith("https://playknot.ruff.rs")) { + headers["Access-Control-Allow-Origin"] = "https://playknot.ruff.rs"; + } + switch (request.method) { case "GET": { // Ex) `https://api.astral-1ad.workers.dev/` @@ -55,7 +59,7 @@ export default { } const playground = await PLAYGROUND.get(key); - if (playground === null) { + if (playground == null) { return new Response("Not Found", { status: 404, headers, diff --git a/playground/eslint.config.mjs b/playground/eslint.config.mjs index 53d569e863..5e0c342582 100644 --- a/playground/eslint.config.mjs +++ b/playground/eslint.config.mjs @@ -29,6 +29,7 @@ export default tseslint.config( "@typescript-eslint/no-explicit-any": "off", // Handled by typescript. It doesn't support shared? "import/no-unresolved": "off", + "no-console": "error", }, }, { diff --git a/playground/knot/index.html b/playground/knot/index.html new file mode 100644 index 0000000000..368ba81eb4 --- /dev/null +++ b/playground/knot/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + Playground | Red Knot + + + + + + + + + + + + + +
+ + + diff --git a/playground/knot/package.json b/playground/knot/package.json new file mode 100644 index 0000000000..01ee926cfd --- /dev/null +++ b/playground/knot/package.json @@ -0,0 +1,35 @@ +{ + "name": "knot-playground", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "prebuild": "npm run build:wasm", + "build": "vite build", + "build:wasm": "wasm-pack build ../../crates/red_knot_wasm --target web --out-dir ../../playground/knot/red_knot_wasm", + "dev:wasm": "wasm-pack build ../../crates/red_knot_wasm --dev --target web --out-dir ../../playground/knot/red_knot_wasm", + "predev:build": "npm run dev:wasm", + "dev:build": "vite build", + "prestart": "npm run dev:wasm", + "start": "vite", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "classnames": "^2.5.1", + "lz-string": "^1.5.0", + "monaco-editor": "^0.52.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-resizable-panels": "^2.1.7", + "red_knot_wasm": "file:red_knot_wasm", + "shared": "0.0.0", + "smol-toml": "^1.3.1" + }, + "overrides": { + "@monaco-editor/react": { + "react": "$react", + "react-dom": "$react-dom" + } + } +} diff --git a/playground/knot/public/Astral.png b/playground/knot/public/Astral.png new file mode 100644 index 0000000000..012dd96ec2 Binary files /dev/null and b/playground/knot/public/Astral.png differ diff --git a/playground/knot/public/apple-touch-icon.png b/playground/knot/public/apple-touch-icon.png new file mode 100644 index 0000000000..eae28581c3 Binary files /dev/null and b/playground/knot/public/apple-touch-icon.png differ diff --git a/playground/knot/public/favicon-16x16.png b/playground/knot/public/favicon-16x16.png new file mode 100644 index 0000000000..00eee0db92 Binary files /dev/null and b/playground/knot/public/favicon-16x16.png differ diff --git a/playground/knot/public/favicon-32x32.png b/playground/knot/public/favicon-32x32.png new file mode 100644 index 0000000000..bc5e2fd1bc Binary files /dev/null and b/playground/knot/public/favicon-32x32.png differ diff --git a/playground/knot/public/favicon.ico b/playground/knot/public/favicon.ico new file mode 100644 index 0000000000..8103f77074 Binary files /dev/null and b/playground/knot/public/favicon.ico differ diff --git a/playground/knot/src/Editor/Chrome.tsx b/playground/knot/src/Editor/Chrome.tsx new file mode 100644 index 0000000000..acf6ff65b3 --- /dev/null +++ b/playground/knot/src/Editor/Chrome.tsx @@ -0,0 +1,587 @@ +import { + useCallback, + useDeferredValue, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; +import { + Header, + useTheme, + setupMonaco, + ErrorMessage, + HorizontalResizeHandle, + VerticalResizeHandle, +} from "shared"; +import initRedKnot, { + Diagnostic, + FileHandle, + Settings, + PythonVersion, + Workspace, +} from "red_knot_wasm"; +import { loader } from "@monaco-editor/react"; +import { Panel, PanelGroup } from "react-resizable-panels"; +import { Files } from "./Files"; +import { persist, persistLocal, restore } from "./persist"; +import SecondarySideBar from "./SecondarySideBar"; +import Editor from "./Editor"; +import SecondaryPanel, { + SecondaryPanelResult, + SecondaryTool, +} from "./SecondaryPanel"; +import Diagnostics from "./Diagnostics"; +import { editor } from "monaco-editor"; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; + +interface CheckResult { + diagnostics: Diagnostic[]; + error: string | null; + secondary: SecondaryPanelResult; +} + +export default function Chrome() { + const initPromise = useRef>(null); + const [workspace, setWorkspace] = useState(null); + const [files, dispatchFiles] = useReducer(filesReducer, { + index: [], + contents: Object.create(null), + handles: Object.create(null), + nextId: 0, + revision: 0, + selected: null, + }); + const [secondaryTool, setSecondaryTool] = useState( + null, + ); + + const editorRef = useRef(null); + const [version, setVersion] = useState(""); + const [theme, setTheme] = useTheme(); + + usePersistLocally(files); + + const handleShare = useCallback(() => { + const serialized = serializeFiles(files); + + if (serialized != null) { + persist(serialized).catch((error) => { + // eslint-disable-next-line no-console + console.error("Failed to share playground", error); + }); + } + }, [files]); + + if (initPromise.current == null) { + initPromise.current = startPlayground() + .then(({ version, workspace: fetchedWorkspace }) => { + const settings = new Settings(PythonVersion.Py312); + const workspace = new Workspace("/", settings); + setVersion(version); + setWorkspace(workspace); + + for (const [name, content] of Object.entries(fetchedWorkspace.files)) { + const handle = workspace.openFile(name, content); + dispatchFiles({ type: "add", handle, name, content }); + } + + dispatchFiles({ + type: "selectFileByName", + name: fetchedWorkspace.current, + }); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error("Failed to initialize playground.", error); + }); + } + + const handleSourceChanged = useCallback( + (source: string) => { + if (files.selected == null) { + return; + } + + dispatchFiles({ + type: "change", + id: files.selected, + content: source, + }); + }, + [files.selected], + ); + + const handleFileClicked = useCallback( + (file: FileId) => { + if (workspace != null && files.selected != null) { + workspace.updateFile( + files.handles[files.selected], + files.contents[files.selected], + ); + } + + dispatchFiles({ type: "selectFile", id: file }); + }, + [workspace, files.contents, files.handles, files.selected], + ); + + const handleFileAdded = useCallback( + (name: string) => { + if (workspace == null) { + return; + } + + if (files.selected != null) { + workspace.updateFile( + files.handles[files.selected], + files.contents[files.selected], + ); + } + + const handle = workspace.openFile(name, ""); + dispatchFiles({ type: "add", name, handle, content: "" }); + }, + [workspace, files.handles, files.contents, files.selected], + ); + + const handleFileRemoved = useCallback( + (file: FileId) => { + if (workspace != null) { + workspace.closeFile(files.handles[file]); + } + + dispatchFiles({ type: "remove", id: file }); + }, + [workspace, files.handles], + ); + + const handleFileRenamed = useCallback( + (file: FileId, newName: string) => { + if (workspace == null) { + return; + } + + workspace.closeFile(files.handles[file]); + const newHandle = workspace.openFile(newName, files.contents[file]); + + editorRef.current?.focus(); + + dispatchFiles({ type: "rename", id: file, to: newName, newHandle }); + }, + [workspace, files.handles, files.contents], + ); + + const handleSecondaryToolSelected = useCallback( + (tool: SecondaryTool | null) => { + setSecondaryTool((secondaryTool) => { + if (tool === secondaryTool) { + return null; + } + + return tool; + }); + }, + [], + ); + + const handleEditorMount = useCallback((editor: IStandaloneCodeEditor) => { + editorRef.current = editor; + }, []); + + const handleGoTo = useCallback((line: number, column: number) => { + const editor = editorRef.current; + + if (editor == null) { + return; + } + + const range = { + startLineNumber: line, + startColumn: column, + endLineNumber: line, + endColumn: column, + }; + editor.revealRange(range); + editor.setSelection(range); + }, []); + + const checkResult = useCheckResult(files, workspace, secondaryTool); + + return ( +
+
+ + {workspace != null && files.selected != null ? ( + <> + + + + + + + + + + + + + + {secondaryTool != null && ( + <> + + + + + + )} + + + + ) : null} + + {checkResult.error ? ( +
+ {checkResult.error} +
+ ) : null} +
+ ); +} + +// Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state. +async function startPlayground(): Promise<{ + version: string; + workspace: { files: { [name: string]: string }; current: string }; +}> { + await initRedKnot(); + const monaco = await loader.init(); + + setupMonaco(monaco); + + const restored = await restore(); + + const workspace = restored ?? { + files: { "main.py": "import os" }, + current: "main.py", + }; + + return { + version: "0.0.0", + workspace, + }; +} + +/** + * Persists the files to local storage. This is done deferred to avoid too frequent writes. + */ +function usePersistLocally(files: FilesState): void { + const deferredFiles = useDeferredValue(files); + + useEffect(() => { + const serialized = serializeFiles(deferredFiles); + if (serialized != null) { + persistLocal(serialized); + } + }, [deferredFiles]); +} + +function useCheckResult( + files: FilesState, + workspace: Workspace | null, + secondaryTool: SecondaryTool | null, +): CheckResult { + const deferredContent = useDeferredValue( + files.selected == null ? null : files.contents[files.selected], + ); + + return useMemo(() => { + if ( + workspace == null || + files.selected == null || + deferredContent == null + ) { + return { + diagnostics: [], + error: null, + secondary: null, + }; + } + + const currentHandle = files.handles[files.selected]; + // Update the workspace content but use the deferred value to avoid too frequent updates. + workspace.updateFile(currentHandle, deferredContent); + + try { + const diagnostics = workspace.checkFile(currentHandle); + + let secondary: SecondaryPanelResult = null; + + try { + switch (secondaryTool) { + case "AST": + secondary = { + status: "ok", + content: workspace.parsed(currentHandle), + }; + break; + + case "Tokens": + secondary = { + status: "ok", + content: workspace.tokens(currentHandle), + }; + break; + } + } catch (error: unknown) { + secondary = { + status: "error", + error: error instanceof Error ? error.message : error + "", + }; + } + + return { + diagnostics, + error: null, + secondary, + }; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + + return { + diagnostics: [], + error: (e as Error).message, + secondary: null, + }; + } + }, [ + deferredContent, + workspace, + files.selected, + files.handles, + secondaryTool, + ]); +} + +export type FileId = number; + +interface FilesState { + /** + * The currently selected file that is shown in the editor. + */ + selected: FileId | null; + + /** + * The files in display order (ordering is sensitive) + */ + index: ReadonlyArray<{ id: FileId; name: string }>; + + /** + * The database file handles by file id. + */ + handles: Readonly<{ [id: FileId]: FileHandle }>; + + /** + * The content per file indexed by file id. + */ + contents: Readonly<{ [id: FileId]: string }>; + + /** + * The revision. Gets incremented everytime files changes. + */ + revision: number; + nextId: FileId; +} + +type FileAction = + | { + type: "add"; + handle: FileHandle; + /// The file name + name: string; + content: string; + } + | { + type: "change"; + id: FileId; + content: string; + } + | { type: "rename"; id: FileId; to: string; newHandle: FileHandle } + | { + type: "remove"; + id: FileId; + } + | { type: "selectFile"; id: FileId } + | { type: "selectFileByName"; name: string }; + +function filesReducer( + state: Readonly, + action: FileAction, +): FilesState { + switch (action.type) { + case "add": { + const { handle, name, content } = action; + const id = state.nextId; + return { + ...state, + selected: id, + index: [...state.index, { id, name }], + handles: { ...state.handles, [id]: handle }, + contents: { ...state.contents, [id]: content }, + nextId: state.nextId + 1, + revision: state.revision + 1, + }; + } + + case "change": { + const { id, content } = action; + return { + ...state, + contents: { ...state.contents, [id]: content }, + revision: state.revision + 1, + }; + } + + case "remove": { + const { id } = action; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [id]: _content, ...contents } = state.contents; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [id]: _handle, ...handles } = state.handles; + + let selected = state.selected; + + if (state.selected === id) { + const index = state.index.findIndex((file) => file.id === id); + + selected = + index > 0 ? state.index[index - 1].id : state.index[index + 1].id; + } + + return { + ...state, + selected, + index: state.index.filter((file) => file.id !== id), + contents, + handles, + revision: state.revision + 1, + }; + } + case "rename": { + const { id, to, newHandle } = action; + + const index = state.index.findIndex((file) => file.id === id); + const newIndex = [...state.index]; + newIndex.splice(index, 1, { id, name: to }); + + return { + ...state, + index: newIndex, + handles: { ...state.handles, [id]: newHandle }, + }; + } + + case "selectFile": { + const { id } = action; + + return { + ...state, + selected: id, + }; + } + + case "selectFileByName": { + const { name } = action; + + const selected = + state.index.find((file) => file.name === name)?.id ?? null; + + return { + ...state, + selected, + }; + } + } +} + +function serializeFiles(files: FilesState): { + files: { [name: string]: string }; + current: string; +} | null { + const serializedFiles = Object.create(null); + let selected = null; + + for (const { id, name } of files.index) { + serializedFiles[name] = files.contents[id]; + + if (files.selected === id) { + selected = name; + } + } + + if (selected == null) { + return null; + } + + return { files: serializedFiles, current: selected }; +} diff --git a/playground/knot/src/Editor/Diagnostics.tsx b/playground/knot/src/Editor/Diagnostics.tsx new file mode 100644 index 0000000000..13e63c71b2 --- /dev/null +++ b/playground/knot/src/Editor/Diagnostics.tsx @@ -0,0 +1,99 @@ +import { Diagnostic, Workspace } from "red_knot_wasm"; +import classNames from "classnames"; +import { Theme } from "shared"; +import { useMemo } from "react"; + +interface Props { + diagnostics: Diagnostic[]; + workspace: Workspace; + theme: Theme; + + onGoTo(line: number, column: number): void; +} + +export default function Diagnostics({ + diagnostics: unsorted, + workspace, + theme, + onGoTo, +}: Props) { + const diagnostics = useMemo(() => { + const sorted = [...unsorted]; + sorted.sort((a, b) => { + return (a.text_range()?.start ?? 0) - (b.text_range()?.start ?? 0); + }); + + return sorted; + }, [unsorted]); + + return ( +
+
+ File diagnostics ({diagnostics.length}) +
+ +
+ +
+
+ ); +} + +function Items({ + diagnostics, + onGoTo, + workspace, +}: { + diagnostics: Array; + workspace: Workspace; + onGoTo(line: number, column: number): void; +}) { + if (diagnostics.length === 0) { + return ( +
+ Everything is looking good! +
+ ); + } + + return ( +
    + {diagnostics.map((diagnostic, index) => { + const position = diagnostic.to_range(workspace); + const start = position?.start; + const id = diagnostic.id(); + + const startLine = (start?.line ?? 0) + 1; + const startColumn = (start?.character ?? 0) + 1; + + return ( +
  • + +
  • + ); + })} +
+ ); +} diff --git a/playground/knot/src/Editor/Editor.tsx b/playground/knot/src/Editor/Editor.tsx new file mode 100644 index 0000000000..d2dfaff4c7 --- /dev/null +++ b/playground/knot/src/Editor/Editor.tsx @@ -0,0 +1,134 @@ +/** + * Editor for the Python source code. + */ + +import Moncao, { Monaco, OnMount } from "@monaco-editor/react"; +import { editor, MarkerSeverity } from "monaco-editor"; +import { useCallback, useEffect, useRef } from "react"; +import { Theme } from "shared"; +import { Diagnostic, Severity, Workspace } from "red_knot_wasm"; + +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; + +type Props = { + visible: boolean; + source: string; + diagnostics: Diagnostic[]; + theme: Theme; + workspace: Workspace; + onChange(content: string): void; + onMount(editor: IStandaloneCodeEditor): void; +}; + +type MonacoEditorState = { + monaco: Monaco; +}; + +export default function Editor({ + visible, + source, + theme, + diagnostics, + workspace, + onChange, + onMount, +}: Props) { + const monacoRef = useRef(null); + + // Update the diagnostics in the editor. + useEffect(() => { + const editorState = monacoRef.current; + + if (editorState == null) { + return; + } + + updateMarkers(editorState.monaco, workspace, diagnostics); + }, [workspace, diagnostics]); + + const handleChange = useCallback( + (value: string | undefined) => { + onChange(value ?? ""); + }, + [onChange], + ); + + const handleMount: OnMount = useCallback( + (editor, instance) => { + updateMarkers(instance, workspace, diagnostics); + + monacoRef.current = { + monaco: instance, + }; + + onMount(editor); + }, + + [onMount, workspace, diagnostics], + ); + + return ( + + ); +} + +function updateMarkers( + monaco: Monaco, + workspace: Workspace, + diagnostics: Array, +) { + const editor = monaco.editor; + const model = editor?.getModels()[0]; + + if (!model) { + return; + } + + editor.setModelMarkers( + model, + "owner", + diagnostics.map((diagnostic) => { + const mapSeverity = (severity: Severity) => { + switch (severity) { + case Severity.Info: + return MarkerSeverity.Info; + case Severity.Warning: + return MarkerSeverity.Warning; + case Severity.Error: + return MarkerSeverity.Error; + case Severity.Fatal: + return MarkerSeverity.Error; + } + }; + + const range = diagnostic.to_range(workspace); + + return { + code: diagnostic.id(), + startLineNumber: (range?.start?.line ?? 0) + 1, + startColumn: (range?.start?.character ?? 0) + 1, + endLineNumber: (range?.end?.line ?? 0) + 1, + endColumn: (range?.end?.character ?? 0) + 1, + message: diagnostic.message(), + severity: mapSeverity(diagnostic.severity()), + tags: [], + }; + }), + ); +} diff --git a/playground/knot/src/Editor/Files.tsx b/playground/knot/src/Editor/Files.tsx new file mode 100644 index 0000000000..56c1411ebf --- /dev/null +++ b/playground/knot/src/Editor/Files.tsx @@ -0,0 +1,180 @@ +import { FileId } from "./Chrome"; +import { Icons, Theme } from "shared"; +import classNames from "classnames"; +import { useState } from "react"; + +export interface Props { + // The file names + files: ReadonlyArray<{ id: FileId; name: string }>; + theme: Theme; + selected: FileId; + + onAdd(name: string): void; + + onRemove(id: FileId): void; + + onSelected(id: FileId): void; + + onRename(id: FileId, newName: string): void; +} + +export function Files({ + files, + selected, + theme, + onAdd, + onRemove, + onRename, + onSelected, +}: Props) { + const handleAdd = () => { + let index: number | null = null; + let fileName = "module.py"; + + while (files.some(({ name }) => name === fileName)) { + index = (index ?? 0) + 1; + fileName = `module${index}.py`; + } + + onAdd(fileName); + }; + + const lastFile = files.length === 1; + + return ( +
    + {files.map(({ id, name }) => ( + + onSelected(id)} + onRenamed={(newName) => { + if (!files.some(({ name }) => name === newName)) { + onRename(id, newName); + } + }} + /> + + + + ))} + + + +
+ ); +} + +interface ListItemProps { + selected: boolean; + children: React.ReactNode; + theme: Theme; +} + +function ListItem({ children, selected, theme }: ListItemProps) { + const activeBorderColor = + theme === "light" ? "border-galaxy" : "border-radiate"; + + return ( +
  • + {children} +
  • + ); +} + +interface FileEntryProps { + selected: boolean; + name: string; + + onClicked(): void; + + onRenamed(name: string): void; +} + +function FileEntry({ name, onClicked, onRenamed, selected }: FileEntryProps) { + const [newName, setNewName] = useState(null); + + if (!selected && newName != null) { + setNewName(null); + } + + const handleRenamed = (newName: string) => { + setNewName(null); + if (name !== newName) { + onRenamed(newName); + } + }; + + return ( + + ); +} diff --git a/playground/knot/src/Editor/SecondaryPanel.tsx b/playground/knot/src/Editor/SecondaryPanel.tsx new file mode 100644 index 0000000000..14f8b133a9 --- /dev/null +++ b/playground/knot/src/Editor/SecondaryPanel.tsx @@ -0,0 +1,78 @@ +import MonacoEditor from "@monaco-editor/react"; +import { Theme } from "shared"; + +export enum SecondaryTool { + "AST" = "AST", + "Tokens" = "Tokens", +} + +export type SecondaryPanelResult = + | null + | { status: "ok"; content: string } + | { status: "error"; error: string }; + +export interface SecondaryPanelProps { + tool: SecondaryTool; + result: SecondaryPanelResult; + theme: Theme; +} + +export default function SecondaryPanel({ + tool, + result, + theme, +}: SecondaryPanelProps) { + return ( +
    +
    + +
    +
    + ); +} + +function Content({ + tool, + result, + theme, +}: { + tool: SecondaryTool; + result: SecondaryPanelResult; + theme: Theme; +}) { + if (result == null) { + return ""; + } else { + let language; + switch (result.status) { + case "ok": + switch (tool) { + case "AST": + language = "RustPythonAst"; + break; + + case "Tokens": + language = "RustPythonTokens"; + break; + } + + return ( + + ); + case "error": + return {result.error}; + } + } +} diff --git a/playground/knot/src/Editor/SecondarySideBar.tsx b/playground/knot/src/Editor/SecondarySideBar.tsx new file mode 100644 index 0000000000..af82c8b545 --- /dev/null +++ b/playground/knot/src/Editor/SecondarySideBar.tsx @@ -0,0 +1,31 @@ +import { Icons, SideBar, SideBarEntry } from "shared"; +import { SecondaryTool } from "./SecondaryPanel"; + +interface Props { + selected: SecondaryTool | null; + onSelected(tool: SecondaryTool): void; +} + +export default function SecondarySideBar({ selected, onSelected }: Props) { + return ( + + onSelected(SecondaryTool.AST)} + > + + + + onSelected(SecondaryTool.Tokens)} + > + + + + ); +} diff --git a/playground/knot/src/Editor/api.ts b/playground/knot/src/Editor/api.ts new file mode 100644 index 0000000000..e039a57f1f --- /dev/null +++ b/playground/knot/src/Editor/api.ts @@ -0,0 +1,38 @@ +const API_URL = import.meta.env.PROD + ? "https://api.astral-1ad.workers.dev" + : "http://0.0.0.0:8787"; + +export type Playground = { + files: { [name: string]: string }; + /// the name of the current file + current: string; +}; + +/** + * Fetch a playground by ID. + */ +export async function fetchPlayground(id: string): Promise { + const response = await fetch(`${API_URL}/${encodeURIComponent(id)}`); + + if (!response.ok) { + throw new Error(`Failed to fetch playground ${id}: ${response.status}`); + } + + return await response.json(); +} + +/** + * Save a playground and return its ID. + */ +export async function savePlayground(playground: Playground): Promise { + const response = await fetch(API_URL, { + method: "POST", + body: JSON.stringify(playground), + }); + + if (!response.ok) { + throw new Error(`Failed to save playground: ${response.status}`); + } + + return await response.text(); +} diff --git a/playground/knot/src/Editor/persist.ts b/playground/knot/src/Editor/persist.ts new file mode 100644 index 0000000000..2adc5f3fe9 --- /dev/null +++ b/playground/knot/src/Editor/persist.ts @@ -0,0 +1,54 @@ +import { fetchPlayground, savePlayground } from "./api"; + +interface Workspace { + files: { [name: string]: string }; + + // Name of the current file + current: string; +} + +/** + * Persist the configuration to a URL. + */ +export async function persist(workspace: Workspace): Promise { + const id = await savePlayground(workspace); + + await navigator.clipboard.writeText( + `${window.location.origin}/${encodeURIComponent(id)}`, + ); +} + +/** + * Restore the workspace by fetching the data for the ID specified in the URL + * or by restoring from local storage. + */ +export async function restore(): Promise { + // URLs stored in the database, like: + // https://play.ruff.rs/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed + const id = window.location.pathname.slice(1); + if (id !== "") { + const playground = await fetchPlayground(id); + if (playground == null) { + return null; + } + + return playground; + } + + // If no URL is present, restore from local storage. + return restoreLocal(); +} + +export function persistLocal(workspace: Workspace) { + localStorage.setItem("workspace", JSON.stringify(workspace)); +} + +function restoreLocal(): Workspace | null { + const workspace = localStorage.getItem("workspace"); + + if (workspace == null) { + return null; + } else { + return JSON.parse(workspace); + } +} diff --git a/playground/knot/src/index.css b/playground/knot/src/index.css new file mode 100644 index 0000000000..170dfc3a33 --- /dev/null +++ b/playground/knot/src/index.css @@ -0,0 +1,123 @@ +@import "tailwindcss"; +@source "../../shared/"; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --color-ayu-accent: #ffac2f; + + --color-ayu-background: #f8f9fa; + --color-ayu-background-dark: #0b0e14; + + --color-black: #261230; + --color-white: #ffffff; + --color-radiate: #d7ff64; + --color-flare: #6340ac; + --color-rock: #78876e; + --color-galaxy: #261230; + --color-space: #30173d; + --color-comet: #6f5d6f; + --color-cosmic: #de5fe9; + --color-sun: #ffac2f; + --color-electron: #46ebe1; + --color-aurora: #46eb74; + --color-constellation: #5f6de9; + --color-neutron: #cff3cf; + --color-proton: #f6afbc; + --color-nebula: #cdcbfb; + --color-supernova: #f1aff6; + --color-starlight: #f4f4f1; + --color-lunar: #fbf2fc; + --color-asteroid: #e3cee3; + --color-crater: #f0dfdf; + + --font-heading: + Alliance Platt, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, + Arial, monospace, Apple Color Emoji, Segoe UI Emoji; + --font-body: + Alliance Text, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, + sans-serif, Apple Color Emoji, Segoe UI Emoji; + --font-mono: Roboto Mono; + + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; +} + +* { + box-sizing: border-box; +} + +body, +html, +#root { + margin: 0; + height: 100%; + width: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.shadow-copied { + --tw-shadow: 0 0 0 1px var(--color-white), inset 0 0 0 1px var(--color-white); + --tw-shadow-colored: + 0 0 0 1px var(--tw-shadow-color), inset 0 0 0 1px var(--tw-shadow-color); + + box-shadow: + var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), + var(--tw-shadow); +} + +@font-face { + font-family: "Alliance Text"; + src: + url("https://static.astral.sh/fonts/Alliance-TextRegular.woff2") + format("woff2"), + url("https://static.astral.sh/fonts/Alliance-TextRegular.woff") + format("woff"); + font-weight: normal; + font-style: normal; + font-display: block; +} + +@font-face { + font-family: "Alliance Text"; + src: + url("https://static.astral.sh/fonts/Alliance-TextMedium.woff2") + format("woff2"), + url("https://static.astral.sh/fonts/Alliance-TextMedium.woff") + format("woff"); + font-weight: 500; + font-style: normal; + font-display: block; +} + +@font-face { + font-family: "Alliance Platt"; + src: + url("https://static.astral.sh/fonts/Alliance-PlattMedium.woff2") + format("woff2"), + url("https://static.astral.sh/fonts/Alliance-PlattMedium.woff") + format("woff"); + font-weight: 500; + font-style: normal; + font-display: block; +} + +@font-face { + font-family: "Alliance Platt"; + src: + url("https://static.astral.sh/fonts/Alliance-PlattRegular.woff2") + format("woff2"), + url("https://static.astral.sh/fonts/Alliance-PlattRegular.woff") + format("woff"); + font-weight: normal; + font-style: normal; + font-display: block; +} diff --git a/playground/knot/src/main.tsx b/playground/knot/src/main.tsx new file mode 100644 index 0000000000..fbe0181a4d --- /dev/null +++ b/playground/knot/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import Chrome from "./Editor/Chrome"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/playground/knot/src/third-party.d.ts b/playground/knot/src/third-party.d.ts new file mode 100644 index 0000000000..9ce038a9ca --- /dev/null +++ b/playground/knot/src/third-party.d.ts @@ -0,0 +1,6 @@ +declare module "lz-string" { + function decompressFromEncodedURIComponent( + input: string | null, + ): string | null; + function compressToEncodedURIComponent(input: string | null): string; +} diff --git a/playground/knot/src/vite-env.d.ts b/playground/knot/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/playground/knot/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/playground/knot/vite.config.ts b/playground/knot/vite.config.ts new file mode 100644 index 0000000000..4667d4047f --- /dev/null +++ b/playground/knot/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react-swc"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); diff --git a/playground/package-lock.json b/playground/package-lock.json index 3049ec7b20..433cb31374 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -8,6 +8,7 @@ "name": "playground", "version": "0.0.0", "workspaces": [ + "knot", "ruff", "shared" ], @@ -29,6 +30,26 @@ "wasm-pack": "^0.13.1" } }, + "knot": { + "name": "knot-playground", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "classnames": "^2.5.1", + "lz-string": "^1.5.0", + "monaco-editor": "^0.52.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-resizable-panels": "^2.1.7", + "red_knot_wasm": "file:red_knot_wasm", + "shared": "0.0.0", + "smol-toml": "^1.3.1" + } + }, + "knot/red_knot_wasm": { + "version": "0.0.0", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -3947,6 +3968,10 @@ "json-buffer": "3.0.1" } }, + "node_modules/knot-playground": { + "resolved": "knot", + "link": true + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4808,6 +4833,10 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/red_knot_wasm": { + "resolved": "knot/red_knot_wasm", + "link": true + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", diff --git a/playground/package.json b/playground/package.json index d3468fdf87..82a74c906c 100644 --- a/playground/package.json +++ b/playground/package.json @@ -4,13 +4,16 @@ "version": "0.0.0", "type": "module", "scripts": { - "check": "npm run lint && npm run tsc", + "check": "npm run dev:wasm && npm run lint && npm run tsc", + "dev:wasm": "npm run dev:wasm --workspace knot-playground && npm run dev:wasm --workspace ruff-playground", + "dev:build": "npm run dev:build --workspace knot-playground && npm run dev:build --workspace ruff-playground", "fmt": "prettier --cache -w .", "fmt:check": "prettier --cache --check .", - "lint": "eslint --cache --ext .ts,.tsx ruff/src", + "lint": "eslint --cache --ext .ts,.tsx ruff/src knot/src", "tsc": "tsc" }, "workspaces": [ + "knot", "ruff", "shared" ], diff --git a/playground/ruff/src/Editor/Chrome.tsx b/playground/ruff/src/Editor/Chrome.tsx index d972213346..cdadce001a 100644 --- a/playground/ruff/src/Editor/Chrome.tsx +++ b/playground/ruff/src/Editor/Chrome.tsx @@ -21,6 +21,7 @@ export default function Chrome() { } persist(settings, pythonSource).catch((error) => + // eslint-disable-next-line no-console console.error(`Failed to share playground: ${error}`), ); }, [pythonSource, settings]); @@ -34,6 +35,7 @@ export default function Chrome() { setRevision(1); }) .catch((error) => { + // eslint-disable-next-line no-console console.error("Failed to initialize playground.", error); }); } @@ -75,6 +77,7 @@ export default function Chrome() {
    setTab(tool)} selected={tab} /> - + void; onShare?: () => void; @@ -42,19 +44,7 @@ export default function Header({ )} >
    - - - +
    {version ? ( @@ -78,3 +68,70 @@ function Divider() {
    ); } + +function Logo({ + name, + className, +}: { + name: "ruff" | "astral"; + className: string; +}) { + switch (name) { + case "ruff": + return ( + + + + ); + case "astral": + return ( + + + + + + + + + ); + } +} diff --git a/playground/shared/src/Icons.tsx b/playground/shared/src/Icons.tsx index df96338d4f..989bc51d08 100644 --- a/playground/shared/src/Icons.tsx +++ b/playground/shared/src/Icons.tsx @@ -140,3 +140,63 @@ export function Comments() { ); } + +export function Add() { + return ( + + + + ); +} + +export function Close() { + return ( + + + + ); +} + +// https://github.com/material-extensions/vscode-material-icon-theme/blob/main/icons/python.svg +// or use https://github.com/vscode-icons/vscode-icons/blob/master/icons/file_type_python.svg?short_path=677f216 +export function Python({ + height = 24, + width = 24, +}: { + height?: number; + width?: number; +}) { + return ( + + + + + ); +} diff --git a/playground/ruff/src/Editor/SideBar.tsx b/playground/shared/src/SideBar.tsx similarity index 100% rename from playground/ruff/src/Editor/SideBar.tsx rename to playground/shared/src/SideBar.tsx diff --git a/playground/shared/src/index.ts b/playground/shared/src/index.ts index 5bf052d782..29bd886823 100644 --- a/playground/shared/src/index.ts +++ b/playground/shared/src/index.ts @@ -7,3 +7,8 @@ export { default as ShareButton } from "./ShareButton"; export { type Theme, useTheme } from "./theme"; export { HorizontalResizeHandle, VerticalResizeHandle } from "./ResizeHandle"; export { setupMonaco } from "./setupMonaco"; +export { + default as SideBar, + SideBarEntry, + type SideBarEntryProps, +} from "./SideBar"; diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 820a022dea..d539e17861 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -16,6 +16,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["ruff/src"], + "include": ["ruff/src", "knot/src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/playground/tsconfig.node.json b/playground/tsconfig.node.json index 9d16ed34a0..c4f5d4847b 100644 --- a/playground/tsconfig.node.json +++ b/playground/tsconfig.node.json @@ -5,5 +5,5 @@ "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, - "include": ["ruff/vite.config.ts"] + "include": ["ruff/vite.config.ts", "knot/vite.config.ts"] }