diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3426e63c..5766a412 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,6 +84,15 @@ jobs: name: vscode-escript-native.win32-x64 path: native/build/Release/vscode-escript-native.win32-x64.node + # Kept out of the .vsix (see .vscodeignore) but retained here so a crash + # dump from a released build can be resolved to a function and line. + - name: Upload Debug Symbols + uses: actions/upload-artifact@v4 + with: + name: vscode-escript-native.win32-x64.symbols + path: native/build/Release/vscode-escript-native.win32-x64.pdb + if-no-files-found: warn + mac: name: Build and Test [Mac] runs-on: macos-latest diff --git a/.vscode/launch.json b/.vscode/launch.json index 9ef961f3..d9cb39e7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,11 +8,8 @@ "name": "Launch Client", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}"], - "outFiles": ["${workspaceRoot}/client/out/**/*.js"], - "preLaunchTask": { - "type": "npm", - "script": "watch" - } + "outFiles": ["${workspaceRoot}/dist/**/*.js"], + "preLaunchTask": "watch-all" }, { "name": "Attach to Language Server", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 070d88eb..7924c956 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -17,10 +17,28 @@ "type": "npm", "script": "watch", "isBackground": true, - "group": { - "kind": "build", - "isDefault": true + "presentation": { + "panel": "dedicated", + "reveal": "never" }, + "problemMatcher": { + "owner": "esbuild", + "fileLocation": "relative", + "pattern": { + "regexp": "^✘ \\[ERROR\\] (.*)$", + "message": 1 + }, + "background": { + "activeBegins": true, + "beginsPattern": "\\[watch\\] build started", + "endsPattern": "\\[watch\\] build finished" + } + } + }, + { + "type": "npm", + "script": "watch:tsc", + "isBackground": true, "presentation": { "panel": "dedicated", "reveal": "never" @@ -28,6 +46,18 @@ "problemMatcher": [ "$tsc-watch" ] + }, + { + "label": "watch-all", + "dependsOn": [ + "npm: watch", + "npm: watch:tsc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index c9ebedf8..e2a9a4be 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,30 +1,47 @@ +# Sources, build inputs and test material. .vscode/** +.github/** +.gitignore doc-assets/**/* +docs/** **/*.ts **/*.map -**/coverage **/*.d.ts -.gitignore +**/coverage **/tsconfig.json **/tsconfig.base.json -.github/** +**/package-lock.json +**/*.tsbuildinfo +.gitmodules +native/jest.config.js client/test/** -client/node_modules/** -!client/node_modules/vscode-jsonrpc/** -!client/node_modules/vscode-languageclient/** -!client/node_modules/vscode-languageserver-protocol/** -!client/node_modules/vscode-languageserver-types/** -!client/node_modules/{minimatch,brace-expansion,concat-map,balanced-match}/** -!client/node_modules/{semver,lru-cache,yallist}/** server/test/** native/test/** client/jest.e2e.config.js server/jest.config.js +jest.config.js +eslint.config.js +esbuild.mjs +grammars/*.yml + +# The client and server ship as the two bundles in dist/. Everything the old +# unbundled layout needed -- tsc output next to three near-duplicate copies of +# the vscode-languageserver trees -- is dead weight now, so no negation rules +# are needed to carve exceptions out of node_modules. +client/out/** +server/out/** +native/out/** +node_modules/** +client/node_modules/** +server/node_modules/** +native/node_modules/** + +# Native addon: ship the built binary, not the sources, the CMake tree or the +# ~40 MB PDB that Release builds now emit. native/.clang-format native/CMakeLists.txt native/cpp/** native/prebuilds/** -native/build/** -!native/build/Release/** native/polserver/** -native/node_modules/** +native/build/** +!native/build/Release/*.node diff --git a/README.md b/README.md index 2d02ab6b..bc36f423 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ https://github.com/polserver/vscode-escript/issues. - Run the launch config. - In the new Visual Studio Code [Extension Development Host], open a POL distro folder that contains `pol.cfg` and `scripts/ecompile.cfg`. -- Check the "ECompile Language Server" channel in the Output pane. +- Check the "EScript Language Server" channel in the Output pane. ## NVIM LSP integration Run `npm install && npm build` in this folder. This installs all necessary diff --git a/client/src/activatePolDebug.ts b/client/src/activatePolDebug.ts index df1c4ab5..36b0d960 100644 --- a/client/src/activatePolDebug.ts +++ b/client/src/activatePolDebug.ts @@ -116,7 +116,9 @@ class PolDebugConfigurationProvider implements vscode.DebugConfigurationProvider config.pid = selectionPid; } } catch (e) { - vscode.window.showErrorMessage(`Could not get process list from debug server: ${e.message}`).then(_ => { }); + const detail = e instanceof Error ? (e.stack ?? e.message) : String(e); + console.error(`Could not get process list from debug server: ${detail}`); + vscode.window.showErrorMessage(`Could not get process list from debug server: ${e instanceof Error ? e.message : String(e)}`).then(_ => { }); return undefined; } finally { try { diff --git a/client/src/extension.ts b/client/src/extension.ts index f2480181..be8e55bb 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -4,32 +4,57 @@ * ------------------------------------------------------------------------------------------ */ import * as path from 'path'; -import { workspace, ExtensionContext } from 'vscode'; +import { workspace, ExtensionContext, Uri, Disposable } from 'vscode'; import { activatePolDebug } from './activatePolDebug'; import { LanguageClient, LanguageClientOptions, ServerOptions, - State, TransportKind } from 'vscode-languageclient/node'; -let client: LanguageClient; +let client: LanguageClient | undefined; -export function activate(context: ExtensionContext) { +/** + * Whether any workspace folder is a POL distribution. + * + * This is the same test the server applies in `onInitialize`: without both files + * it never calls `workspace.open()`, so `compilercfg` is never read and no + * analysis can work. Checking here as well means a workspace the server would + * have rejected does not get a forked Node process and a 2.6 MB native addon + * loaded into it first. + * + * It matters because the extension activates more widely than it can serve: + * `contributes.languages` binds `escriptcfg` to `.cfg`, so opening a `setup.cfg` + * in an unrelated project activates this extension. + */ +async function isPolWorkspace(): Promise { + for (const folder of workspace.workspaceFolders ?? []) { + try { + await workspace.fs.stat(Uri.joinPath(folder.uri, 'pol.cfg')); + await workspace.fs.stat(Uri.joinPath(folder.uri, 'scripts', 'ecompile.cfg')); + return true; + } catch { + // Not a POL distribution; try the next folder. + } + } + return false; +} + +function startClient(context: ExtensionContext): LanguageClient { // The server is implemented in node - let serverModule = context.asAbsolutePath( - path.join('server', 'out', 'index.js') + const serverModule = context.asAbsolutePath( + path.join('dist', 'server.js') ); // The debug options for the server // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging - let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; + const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; const args = [`--storageUri=${context.storageUri}`]; // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used - let serverOptions: ServerOptions = { + const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc, args }, debug: { module: serverModule, @@ -40,7 +65,7 @@ export function activate(context: ExtensionContext) { }; // Options to control the language client - let clientOptions: LanguageClientOptions = { + const clientOptions: LanguageClientOptions = { // Register the server for plain text documents documentSelector: [{ scheme: 'file', language: 'escript' }], synchronize: { @@ -52,8 +77,7 @@ export function activate(context: ExtensionContext) { } }; - // Create the language client and start the client. - client = new LanguageClient( + const started = new LanguageClient( 'escript', 'EScript Language Server', serverOptions, @@ -61,15 +85,56 @@ export function activate(context: ExtensionContext) { ); // Start the client. This will also launch the server - client.start(); + started.start(); + return started; +} + +export async function activate(context: ExtensionContext) { + // Cheap and unconditional: registering the debug providers costs nothing and + // must happen before a 'pol' session is resolved. activatePolDebug(context); workspace.onDidChangeConfiguration(e => { - client.sendNotification('didChangeConfiguration', { + // Without this check every settings change in the window -- including + // unrelated extensions' -- pushes a config notification, which can + // restart the workspace cache build. + if (!e.affectsConfiguration('escript')) { + return; + } + client?.sendNotification('didChangeConfiguration', { configuration: workspace.getConfiguration('escript') }); + }, undefined, context.subscriptions); + + const startIfPol = async () => { + if (client || !await isPolWorkspace()) { + return; + } + // Checked again: the activation-time call and a folder event can both + // pass the first check before either probe resolves. + if (client) { + return; + } + client = startClient(context); + }; + + // A POL distribution can be added to the window after activation, so the + // check has to be repeated rather than made once at startup. + let folderWatch: Disposable | undefined = workspace.onDidChangeWorkspaceFolders(async () => { + await startIfPol(); + if (client) { + folderWatch?.dispose(); + folderWatch = undefined; + } }); + context.subscriptions.push({ dispose: () => folderWatch?.dispose() }); + + await startIfPol(); + if (client) { + folderWatch.dispose(); + folderWatch = undefined; + } } export function deactivate(): Thenable | undefined { diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100644 index 00000000..fd8975f2 --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,53 @@ +/** + * Bundles the extension host client and the language server. + * + * Before this, both shipped as raw `tsc` output next to their own + * `node_modules`, so the `.vsix` carried three near-identical copies of the + * `vscode-jsonrpc` / `vscode-languageserver-*` trees -- ~750 files and ~4.3 MB + * to support ~67 KB of project code -- and the server paid for resolving and + * parsing all of it on every boot. + * + * Two things are deliberately left external: + * + * - `vscode`, which is injected by the extension host and has no package. + * - `*.node`, the compiled addon. `native/src/index.ts` locates it at runtime + * through a computed `require()`, which esbuild leaves alone; the roots it + * searches account for this file's bundled location. + */ +import { build, context } from 'esbuild'; + +const watch = process.argv.includes('--watch'); +const production = !watch; + +/** @type {import('esbuild').BuildOptions} */ +const common = { + bundle: true, + format: 'cjs', + platform: 'node', + target: 'node18', + external: ['vscode', '*.node'], + minify: false, + sourcemap: production ? false : 'inline', + logLevel: 'info' +}; + +const entries = [ + { + ...common, + entryPoints: ['client/src/extension.ts'], + outfile: 'dist/extension.js' + }, + { + ...common, + entryPoints: ['server/src/index.ts'], + outfile: 'dist/server.js' + } +]; + +if (watch) { + // The Extension Development Host loads `dist/`, so the debug launch's watch + // task has to rebuild it; `tsc -b -w` alone only refreshes `*/out`. + await Promise.all(entries.map(async options => (await context(options)).watch())); +} else { + await Promise.all(entries.map(options => build(options))); +} diff --git a/eslint.config.js b/eslint.config.js index 3becb138..9435603c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -27,6 +27,8 @@ module.exports = [ 'native/node_modules/**', 'native/polserver/**', 'native/out/**', + // Generated bundles, not sources. + 'dist/**', 'coverage', ] } diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 8aabb825..cee04cfb 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -53,6 +53,10 @@ target_link_libraries(${PROJECT_NAME} PUBLIC ) target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8 + # Without this, node-addon-api's callback wrapper catches only Napi::Error. + # Any other std::exception escaping an N-API method reaches std::terminate() + # and aborts the language server with no message, stack, or log entry. + NODE_ADDON_API_CPP_EXCEPTIONS_ALL ) if(MSVC) @@ -60,6 +64,18 @@ if(MSVC) $<$:/MT> #---------| $<$:/MTd> #---|-- Statically link the runtime libraries $<$:/MT> #--| + + # Emit debug symbols for optimized builds as well. Without a PDB, a crash + # dump from a released extension cannot be resolved to a function or line, + # which is why native aborts have been unattributable. + $<$:/Zi> + ) + target_link_options(${PROJECT_NAME} PRIVATE + # /DEBUG produces the PDB but disables these two by default; re-enabling + # them keeps the shipped binary equivalent to a plain Release build. + $<$:/DEBUG> + $<$:/OPT:REF> + $<$:/OPT:ICF> ) endif() @@ -72,3 +88,35 @@ target_link_libraries(${PROJECT_NAME} PRIVATE plib libtinyxml ) + +# Link-time optimization. +# +# bscript, plib, libtinyxml and the addon itself all link statically into a +# single .node, so the whole compiler is visible to the optimizer at once -- +# exactly the shape LTO is good at, and the remaining codegen lever now that +# Release already builds at /O2 /Ob2 /DNDEBUG. +# +# IPO is set on the polserver targets from here rather than inside the submodule +# so the vendored tree stays untouched. antlr is an ExternalProject with its own +# build and is not covered by this. +# +# /GL coexists with the /Zi + /DEBUG + /OPT:REF + /OPT:ICF above: the PDB is +# still emitted, so a released crash dump stays symbolisable. Verify that after +# changing anything here -- an unsymbolisable Release build is the exact problem +# that setup was added to fix. +option(ESCRIPT_LTO "Enable link-time optimization for Release builds" ON) + +if(ESCRIPT_LTO) + include(CheckIPOSupported) + check_ipo_supported(RESULT ESCRIPT_IPO_SUPPORTED OUTPUT ESCRIPT_IPO_ERROR) + if(ESCRIPT_IPO_SUPPORTED) + foreach(LTO_TARGET ${PROJECT_NAME} bscript plib libtinyxml) + if(TARGET ${LTO_TARGET}) + set_property(TARGET ${LTO_TARGET} PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + endif() + endforeach() + message("** Link-time optimization enabled for Release") + else() + message(WARNING "** Link-time optimization unavailable: ${ESCRIPT_IPO_ERROR}") + endif() +endif() diff --git a/native/cpp/compiler/CompletionBuilder.cc b/native/cpp/compiler/CompletionBuilder.cc index ab6a279e..c3e41028 100644 --- a/native/cpp/compiler/CompletionBuilder.cc +++ b/native/cpp/compiler/CompletionBuilder.cc @@ -1,5 +1,7 @@ #include "CompletionBuilder.h" +#include "TokenLength.h" + #include #include "bscript/compiler/ast/ClassDeclaration.h" @@ -94,9 +96,18 @@ std::vector CompletionBuilder::context() } } + // Past the line we want: tokens are in source order, so nothing further can + // match. The scan has to start from the top -- it accumulates the enclosing + // class/function/enum state above -- but it never needed to run to the end + // of the file. + if ( token->getLine() > position.line_number ) + { + break; + } + if ( token->getLine() == position.line_number && token->getCharPositionInLine() + 1 <= position.character_column && - token->getCharPositionInLine() + 1 + token->getText().length() >= + token->getCharPositionInLine() + 1 + token_length( token ) >= position.character_column ) { result = token; diff --git a/native/cpp/compiler/HoverBuilder.cc b/native/cpp/compiler/HoverBuilder.cc index 79e0d5ae..cac08e49 100644 --- a/native/cpp/compiler/HoverBuilder.cc +++ b/native/cpp/compiler/HoverBuilder.cc @@ -301,7 +301,6 @@ HoverResult& HoverBuilder::append_comment( const SourceLocation& source_location gExtensionConfiguration.showModuleFunctionComments ) { auto itr = workspace.builder_workspace.source_files.find( pathname ); - auto tokens = workspace.source->get_all_tokens(); if ( itr != workspace.builder_workspace.source_files.end() ) { auto sf = itr->second; diff --git a/native/cpp/compiler/SemanticContextBuilder.h b/native/cpp/compiler/SemanticContextBuilder.h index 6816c689..a665c636 100644 --- a/native/cpp/compiler/SemanticContextBuilder.h +++ b/native/cpp/compiler/SemanticContextBuilder.h @@ -1,6 +1,7 @@ #ifndef VSCODEESCRIPT_SEMANTICCONTEXTBUILDER_H #define VSCODEESCRIPT_SEMANTICCONTEXTBUILDER_H +#include "TokenLength.h" #include "bscript/compiler/ast/ClassDeclaration.h" #include "bscript/compiler/ast/ConstDeclaration.h" #include "bscript/compiler/ast/Expression.h" @@ -824,7 +825,7 @@ bool SemanticContextBuilder::contains( antlr4::Token* sym ) { if ( sym ) { - auto length = sym->getText().length(); + auto length = token_length( sym ); auto line_number = sym->getLine(); auto character_column = sym->getCharPositionInLine() + 1; if ( line_number == position.line_number && character_column <= position.character_column && @@ -906,9 +907,24 @@ antlrcpp::Any SemanticContextBuilder::visitChildren( antlr4::tree::ParseTree* { if ( auto* ctx = dynamic_cast( child ) ) { - Pol::Bscript::Compiler::Range range( *ctx ); - if ( range.contains( position ) ) + // A rule context spans all of its children, so if the position is outside + // this one it is outside everything below it. Descending anyway meant + // every hover, definition, completion, signature help and references + // request walked the file's entire parse tree -- with a dynamic_cast per + // node -- to collect what is only ever a single root-to-leaf chain. + // + // The guard is on the stop token, not just on contains(): an incomplete + // rule has no stop token, calculate_end_position() gives it end {0,0}, and + // contains() is then false for every position. That is routine while the + // user is mid-keystroke, so such a subtree has to be descended into or + // completion would break on exactly the half-typed code it serves. + if ( ctx->getStart() && ctx->getStop() ) { + Pol::Bscript::Compiler::Range range( *ctx ); + if ( !range.contains( position ) ) + { + continue; + } nodes.push_back( ctx ); } } diff --git a/native/cpp/compiler/TokenLength.h b/native/cpp/compiler/TokenLength.h new file mode 100644 index 00000000..3b7726ab --- /dev/null +++ b/native/cpp/compiler/TokenLength.h @@ -0,0 +1,45 @@ +#ifndef VSCODEESCRIPT_TOKENLENGTH_H +#define VSCODEESCRIPT_TOKENLENGTH_H + +#include +#include + +namespace VSCodeEscript::CompilerExt +{ +/** + * Length of a token in characters, without materializing its text. + * + * The position tests in the builders only ever needed `getText().length()`, but + * `CommonToken::getText()` copies a substring out of the input stream, so asking + * for a length allocated a `std::string` per token examined. Those tests run + * across every token in the file on hover, completion and signature help. + * + * Deliberately only two virtual calls. An earlier version also fetched the + * input stream and its size to decide whether the indices were usable, and + * benchmarked no faster than `getText()`: a token short enough for the + * string's small-buffer optimisation never reaches the heap, so the allocation + * this was meant to avoid mostly was not happening, while the extra virtual + * dispatch was. + * + * For any token spanning real input the text is the inclusive interval + * `[start, stop]`. A zero-width token -- EOF above all -- carries + * `stop == start - 1`, which would wrap on the unsigned subtraction, so those + * fall through to `getText()` and match its "" exactly. + */ +inline std::size_t token_length( const antlr4::Token* token ) +{ + if ( !token ) + return 0; + + auto start = token->getStartIndex(); + auto stop = token->getStopIndex(); + + if ( stop >= start ) + return stop - start + 1; + + return token->getText().length(); +} + +} // namespace VSCodeEscript::CompilerExt + +#endif // VSCODEESCRIPT_TOKENLENGTH_H diff --git a/native/cpp/misc/XmlDocParser.cc b/native/cpp/misc/XmlDocParser.cc index b0689f1e..731aca24 100644 --- a/native/cpp/misc/XmlDocParser.cc +++ b/native/cpp/misc/XmlDocParser.cc @@ -70,7 +70,7 @@ std::unique_ptr XmlDocParser::parse_function( const std::string& f { auto* paramName = child->Attribute( "name" ); auto* paramValue = child->Attribute( "value" ); - if ( paramValue != nullptr && paramValue != nullptr ) + if ( paramName != nullptr && paramValue != nullptr ) { parsed->parameters.push_back( XmlDocFunctionParameter{ paramName, paramValue } ); } diff --git a/native/cpp/napi/LSPDocument.cc b/native/cpp/napi/LSPDocument.cc index d0625752..b993940e 100644 --- a/native/cpp/napi/LSPDocument.cc +++ b/native/cpp/napi/LSPDocument.cc @@ -16,7 +16,9 @@ #include "bscript/compiler/model/CompilerWorkspace.h" #include "bscript/compilercfg.h" #include "clib/strutil.h" +#include #include +#include using namespace Pol::Bscript; @@ -131,10 +133,28 @@ Napi::Function LSPDocument::GetClass( Napi::Env env ) LSPDocument::InstanceMethod( "buildReferences", &LSPDocument::BuildReferences ), LSPDocument::InstanceMethod( "toFormattedString", &LSPDocument::ToFormattedString ), LSPDocument::InstanceMethod( "symbols", &LSPDocument::Symbols ), + LSPDocument::InstanceMethod( "release", &LSPDocument::Release ), LSPDocument::InstanceMethod( "dependents", &LSPDocument::Dependents ) } ); } +// Frees the syntax tree and diagnostics while keeping `referenced_by`, which +// the workspace-wide reference index still needs. +// +// LSPWorkspace's document cache holds a strong reference to every LSPDocument +// it ever creates and only clears on reopen, so without this the syntax tree of +// every file the user has opened stays resident for the life of the server. +// Reopening the document simply analyzes it again. +Napi::Value LSPDocument::Release( const Napi::CallbackInfo& info ) +{ + compiler_workspace.reset(); + if ( reporter ) + { + reporter->diagnostics.clear(); + } + return info.Env().Undefined(); +} + Napi::Value LSPDocument::Analyze( const Napi::CallbackInfo& info ) { auto env = info.Env(); @@ -148,14 +168,28 @@ Napi::Value LSPDocument::Analyze( const Napi::CallbackInfo& info ) compiler_workspace.reset(); auto* lsp_workspace = LSPWorkspace::Unwrap( workspace.Value() ); + + // Analyzing an .em/.inc means that file's own contents may have just + // changed, and get_contents() serves unsaved editor text. Its cached parse + // tree would otherwise be reused and the edit silently ignored. Analyzing a + // .src -- the common case -- leaves the cache intact, which is the point. + if ( type != LSPDocumentType::SRC ) + lsp_workspace->clear_parse_tree_caches(); + auto compiler = lsp_workspace->make_compiler(); if ( type == LSPDocumentType::INC || gExtensionConfiguration.referenceAllFunctions ) { compiler->set_include_compile_mode(); } - bool continue_on_error = - info.Length() > 0 && info[0].IsBoolean() ? info[0].As().Value() : true; + // Falls back to the configured setting rather than hardcoding `true`. + // buildReferences() is called with no argument for every script in the + // workspace cache build, so a hardcoded default silently overrode a user + // who had set escript.continueAnalysisOnError to false precisely to avoid + // the compiler crashes that option is documented to cause. + bool continue_on_error = info.Length() > 0 && info[0].IsBoolean() + ? info[0].As().Value() + : gExtensionConfiguration.continueAnalysisOnError; compiler_workspace = compiler->analyze( pathname_, *report, type == LSPDocumentType::EM, continue_on_error ); @@ -165,6 +199,10 @@ Napi::Value LSPDocument::Analyze( const Napi::CallbackInfo& info ) build_references( *compiler_workspace ); } + // load() inserts into the parse-tree caches without bound; this trims them + // back to the size configured from ecompile.cfg. + lsp_workspace->prune_parse_tree_caches(); + return env.Undefined(); } catch ( const std::exception& ex ) @@ -182,8 +220,11 @@ Napi::Value LSPDocument::Diagnostics( const Napi::CallbackInfo& info ) { auto env = info.Env(); + // Indexed Set() rather than push(): this runs for the document *and* every + // open document that includes it, on every diagnostics pull, and a push() per + // item costs a property lookup plus a JS call each. auto results = Napi::Array::New( env ); - auto push = results.Get( "push" ).As(); + uint32_t index = 0; for ( const auto& diagnostic : reporter->diagnostics ) { @@ -223,38 +264,62 @@ Napi::Value LSPDocument::Diagnostics( const Napi::CallbackInfo& info ) env, diagnostic.severity == Compiler::Diagnostic::Severity::Error ? 1 : 2 ); diag["message"] = Napi::String::New( env, diagnostic.message ); - push.Call( results, { diag } ); + results.Set( index++, diag ); } return results; } +// Returns tokens as a flat Uint32Array of 5 values each (line, character, +// length, type, modifiers), already ordered by position. +// +// The previous shape was one JS array per token built with `push` calls, which +// on a few-thousand-token file meant tens of thousands of N-API calls plus a JS +// sort of that many arrays -- on every edit. Writing straight into the typed +// array's backing store avoids per-element crossings entirely. Napi::Value LSPDocument::Tokens( const Napi::CallbackInfo& info ) { auto env = info.Env(); - auto results = Napi::Array::New( env ); - auto push = results.Get( "push" ).As(); + if ( !compiler_workspace ) + { + return Napi::Uint32Array::New( env, 0 ); + } - if ( compiler_workspace ) + const auto& tokens = compiler_workspace->tokens; + + std::vector ordered; + ordered.reserve( tokens.size() ); + for ( const auto& token : tokens ) { - for ( const auto& token : compiler_workspace->tokens ) - { - auto semTok = Napi::Array::New( env ); - push.Call( semTok, { Napi::Number::New( env, token.line_number - 1 ) } ); - push.Call( semTok, { Napi::Number::New( env, token.character_column - 1 ) } ); - push.Call( semTok, { Napi::Number::New( env, token.length ) } ); - push.Call( semTok, { Napi::Number::New( env, static_cast( token.type ) ) } ); - - int modifiers = 0; - for ( auto const& modifier : token.modifiers ) - { - modifiers += ( 1 << static_cast( modifier ) ); - } - push.Call( semTok, { Napi::Number::New( env, modifiers ) } ); + ordered.push_back( &token ); + } + + std::sort( ordered.begin(), ordered.end(), + []( const Compiler::SemanticToken* a, const Compiler::SemanticToken* b ) + { + if ( a->line_number != b->line_number ) + return a->line_number < b->line_number; + return a->character_column < b->character_column; + } ); + + auto results = Napi::Uint32Array::New( env, ordered.size() * 5 ); + uint32_t* data = results.Data(); + + size_t index = 0; + for ( const auto* token : ordered ) + { + data[index++] = static_cast( token->line_number - 1 ); + data[index++] = static_cast( token->character_column - 1 ); + data[index++] = static_cast( token->length ); + data[index++] = static_cast( token->type ); - push.Call( results, { semTok } ); + uint32_t modifiers = 0; + for ( auto const& modifier : token->modifiers ) + { + modifiers |= ( 1u << static_cast( modifier ) ); } + data[index++] = modifiers; } return results; @@ -264,15 +329,18 @@ Napi::Value LSPDocument::Dependents( const Napi::CallbackInfo& info ) { auto env = info.Env(); - auto results = Napi::Array::New( env ); - auto push = results.Get( "push" ).As(); + if ( !compiler_workspace ) + { + return Napi::Array::New( env, 0 ); + } - if ( compiler_workspace ) + const auto& identifiers = compiler_workspace->referenced_source_file_identifiers; + auto results = Napi::Array::New( env, identifiers.size() ); + + uint32_t index = 0; + for ( const auto& sourceId : identifiers ) { - for ( const auto& sourceId : compiler_workspace->referenced_source_file_identifiers ) - { - push.Call( results, { Napi::String::New( env, sourceId->pathname ) } ); - } + results.Set( index++, Napi::String::New( env, sourceId->pathname ) ); } return results; @@ -432,10 +500,11 @@ Napi::Value LSPDocument::References( const Napi::CallbackInfo& info ) auto references = finder.context(); if ( references.has_value() ) { - auto results = Napi::Array::New( env ); - auto push = results.Get( "push" ).As(); + const auto& locations = references.value(); + auto results = Napi::Array::New( env, locations.size() ); - for ( const auto& location : references.value() ) + uint32_t index = 0; + for ( const auto& location : locations ) { const auto& locationRange = location.range; auto result = Napi::Object::New( env ); @@ -452,7 +521,7 @@ Napi::Value LSPDocument::References( const Napi::CallbackInfo& info ) result["range"] = range; result["fsPath"] = location.pathname; - push.Call( results, { result } ); + results.Set( index++, result ); } return results; @@ -471,36 +540,37 @@ Napi::Value LSPDocument::Completion( const Napi::CallbackInfo& info ) .ThrowAsJavaScriptException(); } - auto results = Napi::Array::New( env ); - auto push = results.Get( "push" ).As(); + if ( !compiler_workspace ) + { + return Napi::Array::New( env, 0 ); + } - if ( compiler_workspace ) + auto position = info[0].As(); + auto line = position.Get( "line" ); + auto character = position.Get( "character" ); + if ( !line.IsNumber() || !character.IsNumber() ) { - auto position = info[0].As(); - auto line = position.Get( "line" ); - auto character = position.Get( "character" ); - if ( !line.IsNumber() || !character.IsNumber() ) - { - Napi::TypeError::New( env, Napi::String::New( env, "Invalid arguments" ) ) - .ThrowAsJavaScriptException(); - } - Compiler::Position pos{ - static_cast( line.As().Int32Value() ), - static_cast( character.As().Int32Value() ) }; + Napi::TypeError::New( env, Napi::String::New( env, "Invalid arguments" ) ) + .ThrowAsJavaScriptException(); + } + Compiler::Position pos{ static_cast( line.As().Int32Value() ), + static_cast( character.As().Int32Value() ) }; - CompilerExt::CompletionBuilder finder( *compiler_workspace, pos ); - auto definition = finder.context(); - for ( const auto& completionItem : definition ) + CompilerExt::CompletionBuilder finder( *compiler_workspace, pos ); + auto completionItems = finder.context(); + + auto results = Napi::Array::New( env, completionItems.size() ); + uint32_t index = 0; + for ( const auto& completionItem : completionItems ) + { + auto result = Napi::Object::New( env ); + result["label"] = completionItem.label; + if ( completionItem.kind.has_value() ) { - auto result = Napi::Object::New( env ); - result["label"] = completionItem.label; - if ( completionItem.kind.has_value() ) - { - result["kind"] = - Napi::Number::New( env, static_cast( completionItem.kind.value() ) ); - } - push.Call( results, { result } ); + result["kind"] = + Napi::Number::New( env, static_cast( completionItem.kind.value() ) ); } + results.Set( index++, result ); } return results; } @@ -596,8 +666,14 @@ Napi::Value LSPDocument::BuildReferences( const Napi::CallbackInfo& info ) } else { + // Report into the local reporter, not the member. `local_reporter` was + // created and then ignored, so the workspace cache build -- which runs this + // for every script in the distribution -- appended every diagnostic of every + // file into that document's persistent reporter, which nothing ever clears. + // Unbounded memory, and diagnostics() would answer with stale results for a + // document the user had never opened. auto local_reporter = std::make_unique(); - auto local_report = std::make_unique( *reporter ); + auto local_report = std::make_unique( *local_reporter ); auto* lsp_workspace = LSPWorkspace::Unwrap( workspace.Value() ); auto compiler = lsp_workspace->make_compiler(); @@ -606,14 +682,27 @@ Napi::Value LSPDocument::BuildReferences( const Napi::CallbackInfo& info ) compiler->set_include_compile_mode(); } - bool continue_on_error = - info.Length() > 0 && info[0].IsBoolean() ? info[0].As().Value() : true; + // Falls back to the configured setting rather than hardcoding `true`. + // buildReferences() is called with no argument for every script in the + // workspace cache build, so a hardcoded default silently overrode a user + // who had set escript.continueAnalysisOnError to false precisely to avoid + // the compiler crashes that option is documented to cause. + bool continue_on_error = info.Length() > 0 && info[0].IsBoolean() + ? info[0].As().Value() + : gExtensionConfiguration.continueAnalysisOnError; if ( auto local_compiler_workspace = compiler->analyze( pathname_, *local_report, type == LSPDocumentType::EM, continue_on_error ) ) { build_references( *local_compiler_workspace ); } + + // Analyze() prunes; this path did not. load() inserts into the em/inc caches + // without bound and keep_some() is the only thing that enforces the size + // configured from ecompile.cfg, so the one path that touches every file in + // the workspace grew both caches to the whole include set and never trimmed. + // ecompile calls keep_some() after every file for the same reason. + lsp_workspace->prune_parse_tree_caches(); } return env.Undefined(); diff --git a/native/cpp/napi/LSPDocument.h b/native/cpp/napi/LSPDocument.h index 92c3e97b..7cd2b34b 100644 --- a/native/cpp/napi/LSPDocument.h +++ b/native/cpp/napi/LSPDocument.h @@ -44,6 +44,7 @@ class LSPDocument : public Napi::ObjectWrap Napi::Value BuildReferences( const Napi::CallbackInfo& ); Napi::Value ToFormattedString( const Napi::CallbackInfo& ); Napi::Value Symbols( const Napi::CallbackInfo& ); + Napi::Value Release( const Napi::CallbackInfo& ); std::unique_ptr reporter; diff --git a/native/cpp/napi/LSPWorkspace.cc b/native/cpp/napi/LSPWorkspace.cc index 6af971b7..822b949e 100644 --- a/native/cpp/napi/LSPWorkspace.cc +++ b/native/cpp/napi/LSPWorkspace.cc @@ -10,6 +10,7 @@ #include "plib/pkg.h" #include "plib/systemstate.h" +#include #include #include #include @@ -63,36 +64,86 @@ Napi::Function LSPWorkspace::GetClass( Napi::Env env ) LSPWorkspace::InstanceMethod( "getConfigValue", &LSPWorkspace::GetConfigValue ), LSPWorkspace::InstanceAccessor( "workspaceRoot", &LSPWorkspace::GetWorkspaceRoot, nullptr ), LSPWorkspace::InstanceAccessor( "scripts", &LSPWorkspace::AutoCompiledScripts, nullptr ), - LSPWorkspace::InstanceMethod( "cacheScripts", &LSPWorkspace::CacheCompiledScripts ), LSPWorkspace::InstanceMethod( "getDocument", &LSPWorkspace::GetDocument ), + LSPWorkspace::InstanceMethod( "clearParseTreeCache", &LSPWorkspace::ClearParseTreeCache ), + LSPWorkspace::InstanceAccessor( "profile", &LSPWorkspace::GetProfile, nullptr ), LSPWorkspace::InstanceAccessor( "autoCompiledScripts", &LSPWorkspace::AutoCompiledScripts, nullptr ) } ); } +// Collects script and include files beneath `basedir`. +// +// Every filesystem call below deliberately uses the non-throwing std::error_code +// overload. This walk runs during startup indexing across an entire POL tree, +// where a permission-denied directory, a directory junction, a dangling symlink, +// a file removed mid-walk, or a path exceeding MAX_PATH are all routine. The +// throwing overloads would raise std::filesystem_error out of an N-API method, +// which terminates the language server process. Unreadable entries are reported +// on stderr (piped to the "EScript Language Server" output channel) and skipped. void recurse_collect( const fs::path& basedir, std::set* files_src, std::set* files_inc ) { - if ( !fs::is_directory( basedir ) ) - return; std::error_code ec; - for ( auto dir_itr = fs::recursive_directory_iterator( basedir, ec ); - dir_itr != fs::recursive_directory_iterator(); ++dir_itr ) + + if ( !fs::is_directory( basedir, ec ) || ec ) + return; + + auto dir_itr = fs::recursive_directory_iterator( + basedir, fs::directory_options::skip_permission_denied, ec ); + if ( ec ) + { + fprintf( stderr, "[escript-lsp] Skipping unreadable directory '%s': %s\n", + basedir.string().c_str(), ec.message().c_str() ); + return; + } + + const auto end = fs::recursive_directory_iterator(); + while ( dir_itr != end ) { - if ( auto fn = dir_itr->path().filename().string(); !fn.empty() && *fn.begin() == '.' ) + const auto& path = dir_itr->path(); + + if ( auto fn = path.filename().string(); !fn.empty() && *fn.begin() == '.' ) { - if ( dir_itr->is_directory() ) + if ( dir_itr->is_directory( ec ) && !ec ) dir_itr.disable_recursion_pending(); - continue; + ec.clear(); + } + else if ( dir_itr->is_regular_file( ec ) && !ec ) + { + const auto ext = path.extension(); + const bool is_inc = !ext.compare( ".inc" ); + const bool is_src = !ext.compare( ".src" ) || !ext.compare( ".hsr" ) || + ( compilercfg.CompileAspPages && !ext.compare( ".asp" ) ); + + if ( is_inc || is_src ) + { + auto canonical = fs::canonical( path, ec ); + if ( ec ) + { + fprintf( stderr, "[escript-lsp] Skipping unresolvable file '%s': %s\n", + path.string().c_str(), ec.message().c_str() ); + ec.clear(); + } + else + { + ( is_inc ? files_inc : files_src )->insert( canonical.string() ); + } + } + } + else + { + ec.clear(); + } + + dir_itr.increment( ec ); + if ( ec ) + { + // Do not retry: a failing increment may not advance, which would spin. + fprintf( stderr, "[escript-lsp] Stopping directory walk under '%s': %s\n", + basedir.string().c_str(), ec.message().c_str() ); + break; } - else if ( !dir_itr->is_regular_file() ) - continue; - const auto ext = dir_itr->path().extension(); - if ( !ext.compare( ".inc" ) ) - files_inc->insert( fs::canonical( dir_itr->path() ).string() ); - else if ( !ext.compare( ".src" ) || !ext.compare( ".hsr" ) || - ( compilercfg.CompileAspPages && !ext.compare( ".asp" ) ) ) - files_src->insert( fs::canonical( dir_itr->path() ).string() ); } } @@ -139,66 +190,53 @@ LSPDocument* LSPWorkspace::create_or_get_from_cache( const std::string& path ) return LSPDocument::Unwrap( document ); } -Napi::Value LSPWorkspace::CacheCompiledScripts( const Napi::CallbackInfo& info ) + +Napi::Value LSPWorkspace::AutoCompiledScripts( const Napi::CallbackInfo& info ) { - auto env = info.Env(); - if ( info.Length() < 1 || !info[0].IsFunction() ) + if ( !CompiledScripts.IsEmpty() ) { - Napi::TypeError::New( env, Napi::String::New( env, "Invalid arguments" ) ) - .ThrowAsJavaScriptException(); - return Napi::Value(); + return CompiledScripts.Value(); } - std::set files; + auto env = info.Env(); - recurse_collect( fs::path( compilercfg.PolScriptRoot ), &files, &files ); - for ( const auto& pkg : Pol::Plib::systemstate.packages ) - recurse_collect( fs::path( pkg->dir() ), &files, &files ); + // This runs during startup indexing over the whole POL tree. An unhandled + // exception here previously terminated the language server with no output. + try + { + std::set files; - auto LSPWorkspace_ctor = env.GetInstanceData>() - ->Value() - .Get( "LSPDocument" ) - .As(); + recurse_collect( fs::path( compilercfg.PolScriptRoot ), &files, &files ); + for ( const auto& pkg : Pol::Plib::systemstate.packages ) + recurse_collect( fs::path( pkg->dir() ), &files, &files ); - for ( const auto& path : files ) - { - if ( _cache.find( path ) == _cache.end() ) + auto results = Napi::Array::New( env, files.size() ); + + uint32_t index = 0; + for ( const auto& path : files ) { - auto document = LSPWorkspace_ctor.New( { Value(), Napi::String::New( env, path ) } ); - _cache[path] = Persistent( document ); - document.Get( "analyze" ).As().Call( document, {} ); + results.Set( index++, Napi::String::New( env, path ) ); } - } - - return env.Undefined(); -} - -Napi::Value LSPWorkspace::AutoCompiledScripts( const Napi::CallbackInfo& info ) -{ - if ( !CompiledScripts.IsEmpty() ) + results.Freeze(); + CompiledScripts.Reset( results ); + return results; + } + catch ( const Napi::Error& ) { - return CompiledScripts.Value(); + throw; } - - std::set files; - - recurse_collect( fs::path( compilercfg.PolScriptRoot ), &files, &files ); - for ( const auto& pkg : Pol::Plib::systemstate.packages ) - recurse_collect( fs::path( pkg->dir() ), &files, &files ); - - auto env = info.Env(); - auto results = Napi::Array::New( env ); - auto push = results.Get( "push" ).As(); - - for ( const auto& path : files ) + catch ( const std::exception& ex ) + { + Napi::Error::New( env, std::string( "Error collecting workspace scripts: " ) + ex.what() ) + .ThrowAsJavaScriptException(); + } + catch ( ... ) { - push.Call( results, { Napi::String::New( env, path ) } ); + Napi::Error::New( env, "Unknown error collecting workspace scripts" ) + .ThrowAsJavaScriptException(); } - - results.Freeze(); - CompiledScripts.Reset( results ); - return results; + return Napi::Value(); } Napi::Value LSPWorkspace::Open( const Napi::CallbackInfo& info ) @@ -227,6 +265,9 @@ Napi::Value LSPWorkspace::Open( const Napi::CallbackInfo& info ) make_absolute( packageRoot ); } + configure_parse_tree_caches(); + clear_parse_tree_caches(); + CompiledScripts.Reset(); _cache.clear(); Pol::Plib::systemstate.packages.clear(); @@ -310,8 +351,11 @@ Napi::Value LSPWorkspace::Reopen( const Napi::CallbackInfo& info ) has_changes = true; } + configure_parse_tree_caches(); + if ( has_changes ) { + clear_parse_tree_caches(); CompiledScripts.Reset(); _cache.clear(); Pol::Plib::systemstate.packages.clear(); @@ -353,11 +397,22 @@ void LSPWorkspace::make_absolute( std::string& path ) std::string LSPWorkspace::get_contents( const std::string& pathname ) const { auto value = GetContents.Call( Value(), { Napi::String::New( Env(), pathname ) } ); - if ( !value.IsString() ) + + // A string means the file is open in an editor and this is its buffer, unsaved + // edits included -- which is the whole reason contents are fetched through JS. + if ( value.IsString() ) { - throw std::runtime_error( "Could not get contents of file" ); + return value.As().Utf8Value(); } - return value.As().Utf8Value(); + + // Anything else means "not open in an editor". Reading it here instead of + // having JS answer with readFileSync drops an N-API round trip and two string + // copies per file -- on the path walked by every include of every analysis and + // by every file of the workspace index build, almost none of which are open. + // + // Delegating to the base implementation rather than opening the file here + // keeps the LSP reading files exactly the way ecompile does. + return SourceFileLoader::get_contents( pathname ); } std::optional LSPWorkspace::get_xml_doc_path( const std::string& moduleEmFile ) const @@ -382,6 +437,92 @@ std::unique_ptr LSPWorkspace::make_compiler() profile ); } +void LSPWorkspace::configure_parse_tree_caches() +{ + // ecompile.cfg holds these as ints; a negative value would wrap to a huge + // unsigned limit and make the cache unbounded. + auto clamp = []( int size ) { return size > 0 ? static_cast( size ) : 0u; }; + + em_parse_tree_cache.configure( clamp( compilercfg.EmParseTreeCacheSize ) ); + inc_parse_tree_cache.configure( clamp( compilercfg.IncParseTreeCacheSize ) ); +} + +void LSPWorkspace::prune_parse_tree_caches() +{ + em_parse_tree_cache.keep_some(); + inc_parse_tree_cache.keep_some(); +} + +void LSPWorkspace::clear_parse_tree_caches() +{ + em_parse_tree_cache.clear(); + inc_parse_tree_cache.clear(); +} + +Napi::Value LSPWorkspace::ClearParseTreeCache( const Napi::CallbackInfo& info ) +{ + clear_parse_tree_caches(); + return info.Env().Undefined(); +} + +// Exposes the compiler's existing counters, which were previously collected but +// never readable from JS. +// +// All of Profile's fields are surfaced, not just the parse/cache subset. Now that +// parsing is cached, the interesting costs are the phases that still re-run on +// every analyze -- `optimize`, `analyze`, `tokenize` -- and the AST rebuild that +// happens even on a parse-tree cache hit (`ast*Micros`). +// +// Nothing resets these: they are cumulative for the life of the workspace, so a +// caller has to diff two snapshots rather than read absolutes. +// +// Caveat inherited from the compiler: `astSrcMicros` and `astIncMicros` are +// decremented by nested include time, so they read as self-time and can be +// transiently negative. +Napi::Value LSPWorkspace::GetProfile( const Napi::CallbackInfo& info ) +{ + auto env = info.Env(); + auto result = Napi::Object::New( env ); + + auto set = [&]( const char* name, auto value ) + { result[name] = Napi::Number::New( env, static_cast( value ) ); }; + + set( "buildWorkspaceMicros", profile.build_workspace_micros.load() ); + set( "registerConstDeclarationsMicros", profile.register_const_declarations_micros.load() ); + set( "optimizeMicros", profile.optimize_micros.load() ); + set( "disambiguateMicros", profile.disambiguate_micros.load() ); + set( "analyzeMicros", profile.analyze_micros.load() ); + set( "tokenizeMicros", profile.tokenize_micros.load() ); + set( "codegenMicros", profile.codegen_micros.load() ); + set( "pruneCacheSelectMicros", profile.prune_cache_select_micros.load() ); + set( "pruneCacheDeleteMicros", profile.prune_cache_delete_micros.load() ); + + // An ANTLR ambiguity report means the SLL parse bailed and the file was + // re-parsed in full LL mode -- roughly twice the parse cost for that file. + set( "ambiguities", profile.ambiguities.load() ); + + set( "parseEmCount", profile.parse_em_count.load() ); + set( "parseIncCount", profile.parse_inc_count.load() ); + set( "parseSrcCount", profile.parse_src_count.load() ); + + set( "loadEmMicros", profile.load_em_micros.load() ); + set( "parseEmMicros", profile.parse_em_micros.load() ); + set( "astEmMicros", profile.ast_em_micros.load() ); + + set( "parseIncMicros", profile.parse_inc_micros.load() ); + set( "astIncMicros", profile.ast_inc_micros.load() ); + + set( "parseSrcMicros", profile.parse_src_micros.load() ); + set( "astSrcMicros", profile.ast_src_micros.load() ); + + set( "astResolveFunctionsMicros", profile.ast_resolve_functions_micros.load() ); + + set( "cacheHits", profile.cache_hits.load() ); + set( "cacheMisses", profile.cache_misses.load() ); + + return result; +} + Napi::Value LSPWorkspace::GetWorkspaceRoot( const Napi::CallbackInfo& info ) { return Napi::String::New( info.Env(), _workspaceRoot.generic_string() ); @@ -421,4 +562,4 @@ Napi::Value LSPWorkspace::GetConfigValue( const Napi::CallbackInfo& info ) return Napi::Value(); } -} // namespace VSCodeEscript \ No newline at end of file +} // namespace VSCodeEscript diff --git a/native/cpp/napi/LSPWorkspace.h b/native/cpp/napi/LSPWorkspace.h index 07e602a4..76fed56b 100644 --- a/native/cpp/napi/LSPWorkspace.h +++ b/native/cpp/napi/LSPWorkspace.h @@ -33,8 +33,9 @@ class LSPWorkspace : public Napi::ObjectWrap, Napi::Value GetConfigValue( const Napi::CallbackInfo& ); Napi::Value GetWorkspaceRoot( const Napi::CallbackInfo& ); Napi::Value AutoCompiledScripts( const Napi::CallbackInfo& ); - Napi::Value CacheCompiledScripts( const Napi::CallbackInfo& ); Napi::Value GetDocument( const Napi::CallbackInfo& ); + Napi::Value ClearParseTreeCache( const Napi::CallbackInfo& ); + Napi::Value GetProfile( const Napi::CallbackInfo& ); std::string get_contents( const std::string& pathname ) const override; @@ -44,6 +45,19 @@ class LSPWorkspace : public Napi::ObjectWrap, LSPDocument* create_or_get_from_cache( const std::string& pathname ); + // Sizes the .em/.inc parse-tree caches from ecompile.cfg. Until this is + // called they keep zero entries, which makes SourceFileCache::load() bypass + // the cache entirely and reparse every included file on every analyze(). + void configure_parse_tree_caches(); + + // Trims both caches back to their configured size. load() inserts without + // bound, so this is what actually enforces the limit. + void prune_parse_tree_caches(); + + // Drops every cached parse tree. Required whenever an .inc/.em file changes, + // because contents may come from an unsaved editor buffer. + void clear_parse_tree_caches(); + private: void make_absolute( std::string& path ); diff --git a/native/src/index.ts b/native/src/index.ts index e945d4bd..9e2074d0 100644 --- a/native/src/index.ts +++ b/native/src/index.ts @@ -20,13 +20,70 @@ export type SignatureHelp = { } export type LSPWorkspaceConfig = { - getContents: (pathname: string) => string; + /** + * Contents of an open document, or `undefined` when it is not open in an + * editor -- in which case the addon reads the file itself. Returning the + * file's contents from here anyway is still correct, just slower. + */ + getContents: (pathname: string) => string | undefined; getXmlDocPath?: (moduleEmFile: string) => string | null; } +/** + * Counters collected by the compiler, cumulative for the life of the workspace. + * Nothing resets them, so a caller measuring one operation must diff two + * snapshots rather than read absolutes. + * + * `cacheHits`/`cacheMisses` and the `parse*Count` fields show whether include + * files are being reused across analyses or reparsed every time. Now that + * parsing is cached, the phases that still re-run on every analyze -- + * `optimizeMicros`, `analyzeMicros`, `tokenizeMicros` -- and the AST rebuild + * that happens even on a cache hit (`ast*Micros`) are where the remaining time + * goes. + * + * Two readings need care: + * - `astSrcMicros` and `astIncMicros` are decremented by nested include time, + * so they are self-time and can be transiently negative. + * - `ambiguities` counts ANTLR ambiguity reports. Each one means the SLL parse + * bailed and the file was re-parsed in full LL mode, roughly doubling its + * parse cost. + */ +export type CompilerProfile = { + cacheHits: number; + cacheMisses: number; + ambiguities: number; + parseEmCount: number; + parseIncCount: number; + parseSrcCount: number; + buildWorkspaceMicros: number; + registerConstDeclarationsMicros: number; + optimizeMicros: number; + disambiguateMicros: number; + analyzeMicros: number; + tokenizeMicros: number; + codegenMicros: number; + pruneCacheSelectMicros: number; + pruneCacheDeleteMicros: number; + loadEmMicros: number; + parseEmMicros: number; + astEmMicros: number; + parseIncMicros: number; + astIncMicros: number; + parseSrcMicros: number; + astSrcMicros: number; + astResolveFunctionsMicros: number; +} + export interface LSPWorkspace { new(config: LSPWorkspaceConfig): LSPWorkspace; workspaceRoot: string; + /** Snapshot of the compiler's counters. Read-only. */ + readonly profile: CompilerProfile; + /** + * Discards cached `.em`/`.inc` parse trees. Must be called whenever such a + * file changes, since contents may come from an unsaved editor buffer. + */ + clearParseTreeCache(): void; open(workspaceRoot: string): void; reopen(): boolean; // `true` if folder changes occurred in scripts/ecompile.cfg getConfigValue(key: 'PackageRoot'): Array; @@ -34,13 +91,24 @@ export interface LSPWorkspace { scripts: { inc: string[], src: string[] }; autoCompiledScripts: readonly string[]; getDocument(pathname: string): LSPDocument; - cacheScripts(...args: any[]): void; updateCache: typeof updateCache; + /** + * Drops the memoized workspace reference index so the next `updateCache()` + * rebuilds it instead of answering from the previous run. Call whenever + * `reopen()` reports configuration changes. + */ + invalidateReferenceCache(): void; } export interface LSPDocument { new(workspace: LSPWorkspace, pathname: string): LSPDocument; analyze(continueOnError?: boolean): void; + /** + * Frees this document's syntax tree and diagnostics, keeping the reference + * index. Call when the editor closes the document; a later `analyze()` + * restores it. + */ + release(): void; dependents(): string[]; diagnostics(): Diagnostic[]; hover(position: Position): string | undefined; @@ -49,7 +117,11 @@ export interface LSPDocument { references(position: Position): { range: Range, fsPath: string }[] | undefined; signatureHelp(position: Position): SignatureHelp | undefined; toFormattedString(options?: Partial>, formatRange?: Range): string; // throws - tokens(): [line: number, startChar: number, length: number, tokenType: number, tokenModifiers: number][]; + /** + * Semantic tokens as a flat array of 5 values each -- line, startChar, + * length, tokenType, tokenModifiers -- ordered by position. + */ + tokens(): Uint32Array; toStringTree(): string | undefined; buildReferences(): undefined; references(position: Position): Location[] | undefined; @@ -79,16 +151,31 @@ export interface EscriptVscodeNative { const baseFilename = `vscode-escript-native.${process.platform}-${process.arch}.node`; -const tries = [ - ...(process.platform === 'darwin' ? [ - [__dirname, '..', 'build', 'Debug', `vscode-escript-native.darwin-universal.node`], - [__dirname, '..', 'build', 'RelWithDebInfo', `vscode-escript-native.darwin-universal.node`], - [__dirname, '..', 'build', 'Release', `vscode-escript-native.darwin-universal.node`], - ] : []), - [__dirname, '..', 'build', 'Debug', baseFilename], - [__dirname, '..', 'build', 'RelWithDebInfo', baseFilename], - [__dirname, '..', 'build', 'Release', baseFilename], -].map(segment => resolve(...segment)); +/** + * Directories that may contain the `build/` tree holding the compiled addon. + * + * Unbundled -- tests, and the old extension layout -- this file runs from + * `native/out`, so `build/` is one level up. Bundled it runs from the bundle's + * own directory: `dist/` in the packaged extension, `server/out` if bundled in + * place. Each is listed rather than derived, so a layout change fails loudly in + * the error below instead of silently resolving to nothing. + */ +const roots = [ + resolve(__dirname, '..'), + resolve(__dirname, '..', 'native'), + resolve(__dirname, '..', '..', 'native') +]; + +const configurations = ['Debug', 'RelWithDebInfo', 'Release']; + +const filenames = [ + ...(process.platform === 'darwin' ? ['vscode-escript-native.darwin-universal.node'] : []), + baseFilename +]; + +const tries = roots.flatMap(root => + filenames.flatMap(name => + configurations.map(configuration => resolve(root, 'build', configuration, name)))); const filename = tries.find(filepath => existsSync(filepath)); @@ -100,37 +187,99 @@ if (!filename) { export type UpdateCacheProgressCallback = (progress: { count: number, total: number }) => void; export const native = require(filename) as EscriptVscodeNative; native.LSPWorkspace.prototype.updateCache = updateCache; +native.LSPWorkspace.prototype.invalidateReferenceCache = invalidateReferenceCache; + +type UpdateCacheTask = { + promise: Promise; + progresses: UpdateCacheProgressCallback[]; + signals: AbortSignal[]; +}; + +const updateCacheMap = new WeakMap(); -const updateCacheMap = new WeakMap, progresses: UpdateCacheProgressCallback[], signals: AbortSignal[] }>(); +/** + * How long the index build may hold the event loop before yielding, in + * milliseconds. + * + * It used to yield once per file, so a 1240-script distribution spent 1240 + * macrotask turns purely to let a pending request through -- far more often than + * a request actually arrives. A time budget keeps the server just as responsive + * while paying the scheduler once per budget instead of once per file. + */ +const UPDATE_CACHE_YIELD_MS = 5; + +/** + * Discards the memoized reference-index build. + * + * A completed build deliberately leaves its entry in `updateCacheMap`: that is + * what makes every later `textDocument/references` free rather than an N-file + * recompile. But `reopen()` clears the native document cache when the + * ecompile.cfg folders change, and the memo outlives it -- the caller then asks + * for `updateCache()`, the already-resolved promise answers immediately, and the + * index is silently never rebuilt for the new configuration. + * + * This cannot wrap `reopen()` itself: methods defined through N-API's + * DefineClass land on the prototype as non-writable, so assigning over one + * throws. `updateCache` gets away with it only because it is a new property. + */ +function invalidateReferenceCache(this: LSPWorkspace): void { + updateCacheMap.delete(this); +} function updateCache(this: LSPWorkspace, progress?: UpdateCacheProgressCallback, signal?: AbortSignal) { + // Progress reporters and abort signals belong to the caller that supplied + // them; once the build settles they are done with, and holding them would + // pin the reporter objects for the life of the workspace. + // + // Scoped to the task that owns them: a task superseded by invalidation + // settles *after* its replacement has already registered, and must not + // clear the replacement's reporters on its way out. + const settleFor = (owner: UpdateCacheTask) => (completed: boolean) => { + if (updateCacheMap.get(this) === owner) { + owner.progresses.length = 0; + owner.signals.length = 0; + } + return completed; + }; + const existing = updateCacheMap.get(this); if (existing) { - const { promise, progresses } = existing; if (progress) { - progresses.push(progress); + existing.progresses.push(progress); } - return promise.then((completed) => { - const existing = updateCacheMap.get(this); - if (existing) { - existing.progresses.length = 0; - existing.signals.length = 0; - } - return completed; - }); + if (signal) { + existing.signals.push(signal); + } + return existing.promise.then(settleFor(existing)); } - const update = async () => { + const update = async (): Promise => { const { autoCompiledScripts } = this; let count = 0; const total = autoCompiledScripts.length; + + // Hand control back before the first compile, so this call returns to + // its caller as promptly as it did when every file yielded. + await new Promise(resolve => setImmediate(resolve)); + let lastYield = Date.now(); + for (const p of autoCompiledScripts) { - await new Promise(resolve => setImmediate(resolve)); - const existing = updateCacheMap.get(this); - const canceled = Boolean(existing?.signals.some(signal => signal.aborted)); + if (Date.now() - lastYield >= UPDATE_CACHE_YIELD_MS) { + await new Promise(resolve => setImmediate(resolve)); + lastYield = Date.now(); + } + + const current = updateCacheMap.get(this); + + // A reopen() with folder changes replaced or dropped this task. A + // build started against the old configuration must not keep writing + // into the new one. + if (current !== task) { + return false; + } - if (canceled) { - // Delete from the map, so a new call to updateCache() creates a new task. + if (current.signals.some(aborter => aborter.aborted)) { + // Drop the entry so a later updateCache() starts a fresh task. updateCacheMap.delete(this); return false; } @@ -138,26 +287,24 @@ function updateCache(this: LSPWorkspace, progress?: UpdateCacheProgressCallback, try { this.getDocument(p).buildReferences(); } catch (e) { - // Should never happen - console.error(`Failed to process ${p}: ${e}`); + console.error(`Failed to process ${p}: ${e instanceof Error ? e.stack ?? e.message : String(e)}`); } ++count; - if (existing) { - existing.progresses.forEach(progress => progress({ count, total })); - } + current.progresses.forEach(report => report({ count, total })); } return true; }; - const promise = update(); - updateCacheMap.set(this, { promise, progresses: progress ? [progress] : [], signals: signal ? [signal] : [] }); - - return promise.then((completed) => { - const existing = updateCacheMap.get(this); - if (existing) { - existing.progresses.length = 0; - existing.signals.length = 0; - } - return completed; - }); + + // Registered before the task starts: with a time budget the first files are + // compiled before any yield, and the loop checks its own registration. + const task: UpdateCacheTask = { + promise: undefined as unknown as Promise, + progresses: progress ? [progress] : [], + signals: signal ? [signal] : [] + }; + updateCacheMap.set(this, task); + task.promise = update(); + + return task.promise.then(settleFor(task)); } diff --git a/native/test/benchmark.spec.ts b/native/test/benchmark.spec.ts new file mode 100644 index 00000000..6211abac --- /dev/null +++ b/native/test/benchmark.spec.ts @@ -0,0 +1,229 @@ +import { resolve } from 'path'; +import { readFileSync } from 'fs'; +import { native, type CompilerProfile } from '../src/index'; + +const { LSPWorkspace } = native; + +/** + * Performance harness, not a correctness test: it asserts almost nothing and + * exists to produce comparable numbers before and after a change. + * + * Gated behind JEST_RUN_LONG_TESTS (`npm run test-all`) like the other slow + * suites, since it deliberately runs each operation hundreds of times. + */ +// describe.skip rather than a no-op: every test here is gated, so a no-op would +// leave the suite with no tests at all and Jest fails the file outright. +const describeLongTest = process.env['JEST_RUN_LONG_TESTS'] ? describe : describe.skip; + +const dir = resolve(__dirname); + +/** + * Fields worth reporting, in the order they occur in a compile. The rest of + * CompilerProfile is still collected; these are the ones that move. + */ +const REPORTED: ReadonlyArray = [ + 'buildWorkspaceMicros', + 'parseSrcMicros', + 'astSrcMicros', + 'parseIncMicros', + 'astIncMicros', + 'parseEmMicros', + 'astEmMicros', + 'astResolveFunctionsMicros', + 'optimizeMicros', + 'disambiguateMicros', + 'analyzeMicros', + 'tokenizeMicros', + 'cacheHits', + 'cacheMisses', + 'ambiguities' +]; + +function delta(before: CompilerProfile, after: CompilerProfile): string { + return REPORTED + .map(key => { + // Null-coalesced so the harness can also run against a build whose + // GetProfile predates these counters. + const value = (after[key] ?? 0) - (before[key] ?? 0); + return key.endsWith('Micros') + ? `${key.slice(0, -'Micros'.length)}=${(value / 1000).toFixed(1)}ms` + : `${key}=${value}`; + }) + .join(' '); +} + +/** Wall-clock milliseconds per iteration. */ +function time(iterations: number, fn: (iteration: number) => void): number { + const startedAt = process.hrtime.bigint(); + for (let i = 0; i < iterations; i++) { + fn(i); + } + return Number(process.hrtime.bigint() - startedAt) / 1e6 / iterations; +} + +describeLongTest('Benchmarks', () => { + // Deep enough to exercise the include and module chains, which is where the + // parse-tree caches and the AST rebuild on top of them show up. + const source = `use basicio; +use os; +include "testutil"; + +function helper( a, b := 2 ) + var total := a + b; + foreach entry in ( { 1, 2, 3 } ) + total := total + entry; + endforeach + return total; +endfunction + +program bench( args ) + var result := helper( 1 ); + Print( result ); + Print( args ); +endprogram +`; + + let text = source; + const src = resolve(dir, 'benchmark-probe.src'); + + const workspace = new LSPWorkspace({ + getContents: (pathname: string) => pathname === src ? text : readFileSync(pathname, 'utf-8') + }); + + const report = (label: string, perOp: number, before: CompilerProfile, after: CompilerProfile) => { + console.log(`\n ${label}: ${perOp.toFixed(3)} ms/op\n ${delta(before, after)}`); + }; + + beforeAll(() => { + workspace.open(dir); + }); + + it('analyze', () => { + const document = workspace.getDocument(src); + document.analyze(); + + const before = { ...workspace.profile }; + // Vary the text so nothing can memoize the root file, the way an editing + // session does. + const perOp = time(200, i => { + text = source.replace('var result := helper( 1 );', `var result := helper( ${i} );`); + document.analyze(); + }); + const after = { ...workspace.profile }; + + report('analyze', perOp, before, after); + expect(perOp).toBeGreaterThan(0); + }); + + // The small fixture above cannot show the parse-tree walk cost: with ~17 + // lines there is nothing to skip. This one is sized like a real script, and + // probes near the end of it, which is where a walk that visits every node + // costs the most and a walk pruned by position costs the same as anywhere. + it('per-request on a large file', () => { + const functions = 200; + const lines: string[] = ['use basicio;', 'include "testutil";', '']; + + for (let i = 0; i < functions; i++) { + lines.push( + `function generated_${i}( a, b := ${i} )`, + ` var total_${i} := a + b;`, + ` foreach entry in ( { 1, 2, 3 } )`, + ` total_${i} := total_${i} + entry;`, + ` endforeach`, + ` return total_${i};`, + `endfunction`, + '' + ); + } + + lines.push('program big( args )', ' var result := generated_0( 1 );'); + // The probe line: last call in the program, far from the root. + const probeLine = lines.push(` result := generated_${functions - 1}( result );`); + lines.push(' Print( result );', ' Print( args );', 'endprogram', ''); + + let bigText = lines.join('\n'); + + const bigSrc = resolve(dir, 'benchmark-big.src'); + const bigWorkspace = new LSPWorkspace({ + getContents: (pathname: string) => pathname === bigSrc ? bigText : readFileSync(pathname, 'utf-8') + }); + bigWorkspace.open(dir); + + const document = bigWorkspace.getDocument(bigSrc); + document.analyze(); + + // ` result := generated_199( result );` -- inside the callee name. + const position = { line: probeLine, character: 20 }; + expect(document.hover(position)).toBeDefined(); + expect(document.definition(position)).toBeDefined(); + + const before = { ...bigWorkspace.profile }; + const results = { + analyze: time(20, () => { document.analyze(); }), + hover: time(200, () => { document.hover(position); }), + definition: time(200, () => { document.definition(position); }), + completion: time(200, () => { document.completion(position); }), + tokens: time(200, () => { document.tokens(); }), + symbols: time(200, () => { document.symbols(); }) + }; + const after = { ...bigWorkspace.profile }; + + console.log(` + large file (${lines.length} lines, ${functions} functions):`); + for (const [name, perOp] of Object.entries(results)) { + console.log(` ${name.padEnd(15)} ${perOp.toFixed(4)} ms/op`); + } + console.log(` ${delta(before, after)}`); + + for (const perOp of Object.values(results)) { + expect(perOp).toBeGreaterThan(0); + } + }); + + it('hover, definition, completion, signatureHelp, tokens, symbols', () => { + text = source; + const document = workspace.getDocument(src); + document.analyze(); + + // Inside the `helper` call on line 14, columns 19-24. Both are 1-based + // here: the addon takes the position the way connection.ts hands it + // over, already offset from LSP's 0-based coordinates. A position that + // misses a symbol makes every builder bail immediately and the whole + // benchmark measure nothing. + const position = { line: 14, character: 21 }; + + // Signature help wants a caret inside the argument list, not on the + // callee, and resolves nothing at column 21. + const signaturePosition = { line: 14, character: 27 }; + // A position that misses makes every builder bail immediately, which + // would quietly turn this into a benchmark of nothing. + expect(document.hover(position)).toBeDefined(); + expect(document.definition(position)).toBeDefined(); + expect(document.signatureHelp(signaturePosition)).toBeDefined(); + + const before = { ...workspace.profile }; + + const results = { + hover: time(500, () => { document.hover(position); }), + definition: time(500, () => { document.definition(position); }), + completion: time(500, () => { document.completion(position); }), + signatureHelp: time(500, () => { document.signatureHelp(signaturePosition); }), + tokens: time(500, () => { document.tokens(); }), + symbols: time(500, () => { document.symbols(); }) + }; + + const after = { ...workspace.profile }; + + // These read an already-analyzed tree, so the compiler counters should + // barely move -- if they do, something is re-compiling per request. + console.log('\n per-request (no re-analysis):'); + for (const [name, perOp] of Object.entries(results)) { + console.log(` ${name.padEnd(15)} ${perOp.toFixed(4)} ms/op`); + } + console.log(` ${delta(before, after)}`); + + for (const perOp of Object.values(results)) { + expect(perOp).toBeGreaterThan(0); + } + }); +}); diff --git a/native/test/native.spec.ts b/native/test/native.spec.ts index 4163ce7d..76cc7b74 100644 --- a/native/test/native.spec.ts +++ b/native/test/native.spec.ts @@ -3,7 +3,7 @@ import { readFileSync, readdirSync } from 'fs'; import { LSPDocument, LSPWorkspace, native } from '../src/index'; import { inspect } from 'util'; import { F_OK } from 'constants'; -import { writeFile, access, mkdir, readFile } from 'fs/promises'; +import { writeFile, access, mkdir, readFile, rm } from 'fs/promises'; import { dirname, join } from 'path'; import type { Range, Position } from 'vscode-languageclient/node'; @@ -1138,7 +1138,7 @@ describe('Tokens - SRC', () => { it('Can get regexp tokens', () => { const tokens = getTokens('/[a-z]+/i'); - expect(tokens).toEqual([[0, 0, 9, 20 /* regexp */, 0]]); + expect(Array.from(tokens)).toEqual([0, 0, 9, 20 /* regexp */, 0]); }); }); @@ -2150,3 +2150,140 @@ describe('Formatter', () => { }); } }); + +describe('XML doc robustness', () => { + // Regression: XmlDocParser tested `paramValue` twice and never `paramName`, + // so a element without a name attribute constructed a + // std::string from nullptr. That is undefined behaviour, and it was + // reachable from an ordinary hover over a module function. + const brokenDocDir = resolve(__dirname, 'xmldoc-malformed'); + const brokenDoc = join(brokenDocDir, 'basicioem.xml'); + + let document: LSPDocument; + let text: string; + + beforeAll(async () => { + await mkdir(brokenDocDir, { recursive: true }); + await writeFile(brokenDoc, ` + + + Print(anything, console_color:="") + + + Prints anything to the console. + + +`, 'utf-8'); + + const src = 'in-memory-file.src'; + const workspace = new LSPWorkspace({ + getContents: (pathname: string) => pathname === src ? text : readFileSync(pathname, 'utf-8'), + getXmlDocPath(moduleEmFile: string) { + return extname(moduleEmFile).toLowerCase() === '.em' ? brokenDoc : null; + } + }); + workspace.open(dir); + + document = new LSPDocument(workspace, src); + }); + + afterAll(async () => { + await rm(brokenDocDir, { recursive: true, force: true }); + }); + + it('Survives a parameter element with no name attribute', () => { + text = 'use basicio; Print( 1 );'; + document.analyze(); + + const hover = document.hover({ line: 1, character: 15 }); + + toBeDefined(hover); + expect(hover).toContain('Print'); + // Parsing continued past the malformed element rather than aborting. + expect(hover).toContain('Prints anything to the console.'); + }); +}); + +describe('Parse tree cache', () => { + // The .em/.inc caches were constructed but never configure()d, so + // SourceFileCache::load() bypassed them and every analyze() reparsed the + // whole include and module chain. + it('Reuses parsed includes across analyses of a .src', () => { + let text = 'use basicio; include "testutil"; Print( 1 );'; + + const src = resolve(dir, 'cache-probe.src'); + const workspace = new LSPWorkspace({ + getContents: (pathname: string) => pathname === src ? text : readFileSync(pathname, 'utf-8') + }); + workspace.open(dir); + + const document = workspace.getDocument(src); + + document.analyze(); + const before = { ...workspace.profile }; + + for (let i = 0; i < 5; i++) { + text = `use basicio; include "testutil"; Print( ${i} );`; + document.analyze(); + } + + const after = { ...workspace.profile }; + + expect(after.cacheHits - before.cacheHits).toBeGreaterThan(0); + expect(after.cacheMisses - before.cacheMisses).toEqual(0); + }); + + it('Re-reads an .em whose contents changed', () => { + // Analyzing an .em/.inc must drop the cached tree, since contents can + // come from an unsaved editor buffer. + let text: string; + let calls = 0; + + const workspace = new LSPWorkspace({ + getContents: () => { + calls++; + return text; + } + }); + workspace.open(dir); + + const document = workspace.getDocument('basicio.em'); + + text = 'Print(anything);'; + document.analyze(); + expect(document.diagnostics()).toHaveLength(0); + + text = 'if (1) endif'; + document.analyze(); + expect(document.diagnostics()).toHaveLength(1); + + expect(calls).toEqual(2); + }); +}); + +describe('Document release', () => { + // LSPWorkspace caches every document it creates and only clears on reopen, + // so a closed editor tab used to leave its syntax tree resident forever. + it('Drops the syntax tree and can rebuild it', () => { + let text = 'program foo() var x := 1; Print( x ); endprogram'; + + const src = resolve(dir, 'release-probe.src'); + const workspace = new LSPWorkspace({ + getContents: (pathname: string) => pathname === src ? text : readFileSync(pathname, 'utf-8') + }); + workspace.open(dir); + + const document = workspace.getDocument(src); + + document.analyze(); + expect(document.symbols()?.length).toBeGreaterThan(0); + + document.release(); + expect(document.symbols() ?? []).toHaveLength(0); + expect(document.diagnostics()).toHaveLength(0); + + // Reopening the document analyzes it again. + document.analyze(); + expect(document.symbols()?.length).toBeGreaterThan(0); + }); +}); diff --git a/package-lock.json b/package-lock.json index eb5d1f50..d729581c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,13 +18,13 @@ "@types/node": "^22.14.1", "@typescript-eslint/parser": "^8.29.1", "cross-env": "^7.0.3", + "esbuild": "^0.28.2", "eslint": "^9.24.0", "jest": "^29.7.0", "js-yaml": "^4.1.0", "mock-fs": "^5.5.0", "rimraf": "^6.0.1", "ts-jest": "^29.3.2", - "ts-loader": "^9.5.2", "typescript": "^5.8.3" }, "engines": { @@ -500,6 +500,448 @@ "dev": true, "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -1258,18 +1700,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -1397,30 +1827,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1638,221 +2044,29 @@ "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" + "@typescript-eslint/types": "8.29.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", - "peer": true + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/acorn": { "version": "8.14.0", @@ -1893,17 +2107,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2221,16 +2424,6 @@ "node": ">=10" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -2304,14 +2497,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2498,20 +2683,6 @@ "dev": true, "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2522,12 +2693,47 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, - "peer": true + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } }, "node_modules/escalade": { "version": "3.2.0", @@ -2613,20 +2819,6 @@ } } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -2768,16 +2960,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2788,16 +2970,6 @@ "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3149,14 +3321,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4211,16 +4375,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4322,29 +4476,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4399,13 +4530,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4822,17 +4946,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -5015,48 +5128,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -5069,17 +5140,6 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5132,18 +5192,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5301,104 +5349,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5509,37 +5459,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5720,79 +5639,6 @@ "makeerror": "1.0.12" } }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6266,6 +6112,188 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "dev": true, + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -6799,17 +6827,6 @@ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, - "@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -6917,28 +6934,6 @@ "@babel/types": "^7.20.7" } }, - "@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "peer": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "peer": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -7116,181 +7111,6 @@ } } }, - "@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "peer": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "peer": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "peer": true - }, - "@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "peer": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "peer": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "peer": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "peer": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "peer": true, - "requires": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "peer": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "peer": true - }, "acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -7316,14 +7136,6 @@ "uri-js": "^4.2.2" } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peer": true, - "requires": {} - }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -7534,13 +7346,6 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "peer": true - }, "ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -7591,13 +7396,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7718,16 +7516,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7737,12 +7525,39 @@ "is-arrayish": "^0.2.1" } }, - "es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", - "dev": true, - "peer": true + "esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } }, "escalade": { "version": "3.2.0", @@ -7823,17 +7638,6 @@ } } }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "peer": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -7899,26 +7703,12 @@ } } }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "peer": true - }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "peer": true - }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -8172,13 +7962,6 @@ "is-glob": "^4.0.3" } }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "peer": true - }, "globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -8932,13 +8715,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "peer": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9015,23 +8791,6 @@ "picomatch": "^2.3.1" } }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "peer": true - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "peer": true, - "requires": { - "mime-db": "1.52.0" - } - }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -9071,13 +8830,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9349,16 +9101,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "peer": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -9470,40 +9212,11 @@ "queue-microtask": "^1.2.2" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "peer": true - }, - "schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "peer": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, "semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" }, - "serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "peer": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9543,17 +9256,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "peer": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -9660,63 +9362,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, - "terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "dependencies": { - "jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9776,27 +9421,6 @@ } } }, - "ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "dependencies": { - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true - } - } - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9916,56 +9540,6 @@ "makeerror": "1.0.12" } }, - "watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "peer": true, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", - "dev": true, - "peer": true, - "requires": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - } - }, - "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "peer": true - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index cebe1552..b72b9646 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,14 @@ "vscode": "^1.78.0" }, "activationEvents": [ - "onDebug" + "onDebugResolve:pol", + "onDebugDynamicConfigurations:pol" ], "repository": { "type": "git", "url": "https://github.com/polserver/vscode-escript" }, - "main": "./client/out/extension.js", + "main": "./dist/extension.js", "contributes": { "languages": [ { @@ -91,6 +92,16 @@ "type": "boolean", "default": false, "markdownDescription": "By default, the compiler will only include functions that have been called when analyzing sources. If `true`, all functions will be analyzed, regardless if they are used." + }, + "escript.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the EScript language server. Set to 'verbose' to record which request the server was handling when a problem occurred." } } }, @@ -237,9 +248,11 @@ "scripts": { "vscode:prepublish": "npm run clean && npm run build", "postinstall": "cd client && npm install && cd ../native && npm install && cd ../server && npm install", - "watch": "npm run grammar && tsc -b -w", - "build": "npm run grammar && tsc -b && cd native && npm run build", - "clean": "rimraf client/out server/out grammars/escript.json", + "watch": "npm run grammar && node esbuild.mjs --watch", + "watch:tsc": "tsc -b -w", + "bundle": "node esbuild.mjs", + "build": "npm run grammar && tsc -b && node esbuild.mjs && cd native && npm run build", + "clean": "rimraf client/out server/out dist grammars/escript.json", "e2e": "cd client && npm test", "test": "jest --coverage --detectOpenHandles --runInBand --testTimeout=15000", "test-all": "cross-env JEST_RUN_LONG_TESTS=1 jest --coverage --detectOpenHandles --runInBand --testTimeout=15000", @@ -255,13 +268,13 @@ "@types/node": "^22.14.1", "@typescript-eslint/parser": "^8.29.1", "cross-env": "^7.0.3", + "esbuild": "^0.28.2", "eslint": "^9.24.0", "jest": "^29.7.0", "js-yaml": "^4.1.0", "mock-fs": "^5.5.0", "rimraf": "^6.0.1", "ts-jest": "^29.3.2", - "ts-loader": "^9.5.2", "typescript": "^5.8.3" } } diff --git a/server/package-lock.json b/server/package-lock.json index 7b3ae075..4ee00e4a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,15 +9,12 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "async": "^3.2.6", "vscode-escript-native": "file:../native", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.1.0" }, - "devDependencies": { - "@types/async": "^3.2.24" - }, + "devDependencies": {}, "engines": { "node": "*" } @@ -34,18 +31,6 @@ "node": "*" } }, - "node_modules/@types/async": { - "version": "3.2.24", - "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.24.tgz", - "integrity": "sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==", - "dev": true - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/vscode-escript-native": { "resolved": "../native", "link": true @@ -101,17 +86,6 @@ } }, "dependencies": { - "@types/async": { - "version": "3.2.24", - "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.24.tgz", - "integrity": "sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==", - "dev": true - }, - "async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, "vscode-escript-native": { "version": "file:../native", "requires": { diff --git a/server/package.json b/server/package.json index 5e4bdceb..b6a5d958 100644 --- a/server/package.json +++ b/server/package.json @@ -17,14 +17,11 @@ "url": "https://github.com/polserver/vscode-escript" }, "dependencies": { - "async": "^3.2.6", "vscode-escript-native": "file:../native", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.1.0" }, "scripts": {}, - "devDependencies": { - "@types/async": "^3.2.24" - } + "devDependencies": {} } diff --git a/server/src/index.ts b/server/src/index.ts index c481fb7a..c6c7d9d9 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,4 +1,4 @@ -import { LSPServer } from './server/connection'; +import { installCrashHandlers, formatError } from './misc/Diagnostics'; import { parseArgs } from 'node:util'; @@ -19,5 +19,20 @@ const options = { storageFsPath: URI.parse(String(storageUri)).fsPath }; +installCrashHandlers(options.storageFsPath); + console.log(`Escript Language Server started [pid ${process.pid}]`); -new LSPServer(options).listen(); + +// `./server/connection` loads the native addon at its top level, and a static +// `import` of it would be hoisted above installCrashHandlers() -- leaving the +// single most likely startup failure completely unreported. Requiring it here +// keeps the ordering real. +try { + const { LSPServer } = require('./server/connection') as typeof import('./server/connection'); + new LSPServer(options).listen(); +} catch (e) { + process.stderr.write( + `[escript-lsp] Failed to start the language server: ${formatError(e)}\n` + ); + process.exit(1); +} diff --git a/server/src/misc/Diagnostics.ts b/server/src/misc/Diagnostics.ts new file mode 100644 index 00000000..f65eba75 --- /dev/null +++ b/server/src/misc/Diagnostics.ts @@ -0,0 +1,126 @@ +/** + * Crash attribution helpers. + * + * The compiler runs synchronously on the server's only thread, so a native + * failure takes the whole process down before any handler can report it. What + * survives is whatever was written before the call started -- hence the + * breadcrumb: the operation and document in flight are recorded up front, and + * the crash handlers below print them on the way out. + */ + +import { mkdirSync } from 'fs'; + +export type BreadcrumbPosition = { + line: number; + character: number; +}; + +type Breadcrumb = { + operation: string; + fsPath?: string; + position?: BreadcrumbPosition; + startedAt: number; +}; + +/** + * At most one native call is ever in flight, so a single slot suffices. + */ +let current: Breadcrumb | undefined; + +/** + * Retained after completion so a crash that happens just *after* a native call + * (during marshalling, or in a later microtask) still names the likely culprit. + */ +let previous: Breadcrumb | undefined; + +export function beginOperation(operation: string, fsPath?: string, position?: BreadcrumbPosition): void { + current = { operation, fsPath, position, startedAt: Date.now() }; +} + +export function endOperation(): void { + if (current) { + previous = current; + current = undefined; + } +} + +function describe(breadcrumb: Breadcrumb | undefined, label: string): string | undefined { + if (!breadcrumb) { + return undefined; + } + const { operation, fsPath, position, startedAt } = breadcrumb; + const where = fsPath ? ` ${fsPath}` : ''; + const at = position ? `:${position.line}:${position.character}` : ''; + return `${label}: ${operation}${where}${at} (${Date.now() - startedAt}ms ago)`; +} + +/** + * Human-readable description of what the server was doing, for crash reports. + */ +export function describeBreadcrumbs(): string { + const parts = [ + describe(current, 'in flight'), + describe(previous, 'previous') + ].filter((part): part is string => typeof part === 'string'); + + return parts.length ? parts.join('; ') : 'no operation recorded'; +} + +/** + * Renders an unknown thrown value with its stack. Several call sites previously + * interpolated errors into template strings, which calls `toString()` and drops + * the stack -- exactly the information a crash report needs. + */ +export function formatError(e: unknown): string { + if (e instanceof Error) { + return e.stack ?? `${e.name}: ${e.message}`; + } + return String(e); +} + +/** + * Installs last-resort handlers. Without these, an unhandled rejection kills the + * server silently on Node 15+, and a fatal error produces no diagnostic at all. + * + * Writes go straight to stderr rather than through the LSP connection: by the + * time these run the connection may already be gone, and the client pipes the + * server's stderr into its output channel regardless. + */ +export function installCrashHandlers(storageFsPath: string): void { + const report = (kind: string, error: unknown) => { + process.stderr.write( + `[escript-lsp] ${kind}\n` + + `[escript-lsp] ${describeBreadcrumbs()}\n` + + `[escript-lsp] ${formatError(error)}\n` + ); + }; + + process.on('uncaughtException', (error) => { + report('Uncaught exception; the language server will exit.', error); + process.exit(1); + }); + + process.on('unhandledRejection', (reason) => { + // Reported rather than rethrown: an unhandled rejection in a background + // task (such as the workspace cache build) should not take down a server + // that is otherwise serving requests fine. + report('Unhandled promise rejection.', reason); + }); + + // Node's diagnostic report captures a *native* stack on fatal errors, which + // is the only way to see where an abort inside the addon came from. + try { + // Created here rather than waiting for onInitialize: the window this is + // for includes loading the addon, and Node will not write a report into + // a directory that does not exist yet. + mkdirSync(storageFsPath, { recursive: true }); + process.report.directory = storageFsPath; + process.report.filename = ''; + process.report.reportOnFatalError = true; + process.stderr.write( + `[escript-lsp] Fatal-error diagnostic reports will be written to ${storageFsPath}\n` + ); + } catch (e) { + process.stderr.write(`[escript-lsp] Could not enable diagnostic reports: ${formatError(e)}\n`); + } +} diff --git a/server/src/server/connection.ts b/server/src/server/connection.ts index e7becd4d..20970653 100644 --- a/server/src/server/connection.ts +++ b/server/src/server/connection.ts @@ -1,7 +1,6 @@ -import { createConnection, TextDocuments, TextDocumentChangeEvent, ProposedFeatures, InitializeParams, DocumentSymbolParams, TextDocumentSyncKind, InitializeResult, SemanticTokensParams, SemanticTokensBuilder, SemanticTokens, Hover, HoverParams, MarkupContent, DefinitionParams, Location, CompletionParams, CompletionItem, SignatureHelpParams, SignatureHelp, ReferenceParams, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, DocumentUri, FullDocumentDiagnosticReport, DocumentFormattingParams, TextEdit, DocumentRangeFormattingParams, FormattingOptions, Range, DidChangeWatchedFilesParams, FileChangeType, DocumentSymbol } from 'vscode-languageserver/node'; +import { CancellationToken, ResponseError, LSPErrorCodes, createConnection, TextDocuments, TextDocumentChangeEvent, ProposedFeatures, InitializeParams, DocumentSymbolParams, TextDocumentSyncKind, InitializeResult, SemanticTokensParams, SemanticTokensBuilder, SemanticTokens, Hover, HoverParams, MarkupContent, DefinitionParams, Location, CompletionParams, CompletionItem, SignatureHelpParams, SignatureHelp, ReferenceParams, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, DocumentUri, FullDocumentDiagnosticReport, DocumentFormattingParams, TextEdit, DocumentRangeFormattingParams, FormattingOptions, Range, DidChangeWatchedFilesParams, FileChangeType, DocumentSymbol } from 'vscode-languageserver/node'; import { Position, TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; -import { readFileSync } from 'fs'; import { access, mkdir } from 'fs/promises'; import { join } from 'path'; import { F_OK } from 'constants'; @@ -10,14 +9,27 @@ import DocsDownloader from '../workspace/DocsDownloader'; // vsce does not support symlinks // import { escript } from 'vscode-escript-native'; const { native } = require('../../../native/out/index') as typeof import('vscode-escript-native'); -import type { ExtensionConfiguration } from 'vscode-escript-native'; +import type { CompilerProfile, ExtensionConfiguration } from 'vscode-escript-native'; import { deepEquals } from '../misc/Utils'; +import { beginOperation, endOperation, formatError, type BreadcrumbPosition } from '../misc/Diagnostics'; const { LSPWorkspace, LSPDocument, ExtensionConfiguration } = native; type LSPServerOptions = { storageFsPath: string; } +/** + * Files whose parse trees the compiler caches. Editing one of these invalidates + * the cache; editing a `.src` does not, which is the common case and where the + * caching actually pays off. + */ +const CACHED_PARSE_TREE_EXTENSIONS = ['.inc', '.em']; + +function isCachedParseTreeFile(fsPath: string): boolean { + const lowered = fsPath.toLowerCase(); + return CACHED_PARSE_TREE_EXTENSIONS.some(extension => lowered.endsWith(extension)); +} + export interface DidChangeConfigurationParams { configuration: ExtensionConfiguration } @@ -32,6 +44,31 @@ export class LSPServer { private workspace: typeof LSPWorkspace; public static options: Readonly; private sources: Map = new Map(); + + /** + * Documents whose analysis is out of date. Editing marks; the first request + * that actually needs the syntax tree pays for it. + * + * Analysis used to run eagerly on every change notification, and pull + * diagnostics then re-ran it, so a single keystroke cost at least two full + * compiles of the document and of every open file including it. + */ + private dirty: Set = new Set(); + + /** + * fsPath -> files that document includes, refreshed after each analysis. + * Cached so that marking dependents dirty stays in JS instead of calling + * across N-API for every open document on every keystroke. + */ + private dependsOn: Map = new Map(); + + /** + * Set when an include or module buffer changes, and honored by the next + * analysis of *any* document: whichever one is analyzed first -- the edited + * file or a `.src` including it -- must not reuse the pre-edit tree. Kept as + * a flag so typing in a `.inc` costs no N-API call per keystroke. + */ + private parseTreeCacheStale = false; private downloader: DocsDownloader; private configuration: ExtensionConfiguration | undefined; private updateCacheAbortController: AbortController | undefined; @@ -56,6 +93,10 @@ export class LSPServer { this.connection.onCompletion(this.onCompletion); this.connection.onSignatureHelp(this.onSignatureHelp); this.connection.onNotification('didChangeConfiguration', this.onDidChangeConfiguration); + // No client UI drives this: it exists so a scripted LSP client -- which + // is how this server's performance work gets measured -- can read the + // counters at an arbitrary point in a session. + this.connection.onNotification('escript/logProfile', () => this.logProfile('on demand')); this.connection.onReferences(this.onReferences); this.connection.languages.diagnostics.on(this.onDocumentDiagnostics); this.connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles); @@ -64,14 +105,12 @@ export class LSPServer { this.documents.listen(this.connection); this.downloader = new DocsDownloader(LSPServer.options.storageFsPath); this.workspace = new LSPWorkspace({ - getContents: (pathname) => { - const uri = URI.file(pathname).toString(); - const text = this.documents.get(uri)?.getText(); - if (typeof text !== 'undefined') { - return text; - } - return readFileSync(pathname, 'utf-8'); - }, + // Answers only for documents open in an editor, where the buffer + // may hold unsaved edits the file does not. Everything else -- + // every include pulled in by an analysis, every file of the + // workspace index build -- is read by the addon directly, which + // saves an N-API round trip and two string copies per file. + getContents: (pathname) => this.documents.get(URI.file(pathname).toString())?.getText(), getXmlDocPath: this.downloader.getXmlDocPath.bind(this.downloader) }); } @@ -80,16 +119,131 @@ export class LSPServer { this.connection.listen(); } + /** + * Logging goes through the LSP connection so it is timestamped and + * level-tagged in the client's output channel. The fallback covers the + * window before the connection is listening. + */ + private log(message: string): void { + try { + this.connection.console.log(message); + } catch { + console.log(message); + } + } + + private logError(message: string, e?: unknown): void { + const text = e === undefined ? message : `${message}: ${formatError(e)}`; + try { + this.connection.console.error(text); + } catch { + console.error(text); + } + } + + private logWarn(message: string): void { + try { + this.connection.console.warn(message); + } catch { + console.warn(message); + } + } + + /** + * Renders the compiler's counters for the output channel. + * + * `*Micros` fields are reported as milliseconds, and zero-valued entries are + * dropped so the line stays readable. The counters are cumulative and are + * never reset, so anything measuring a single operation has to pass the + * snapshot it took beforehand as `since`. + * + * Two of them lie if read naively: `astSrcMicros` and `astIncMicros` are + * decremented by nested include time, so they are self-time and can come out + * negative. `ambiguities` counts SLL parse failures, each of which cost a + * full re-parse in LL mode. + */ + private formatProfile(profile: CompilerProfile, since?: CompilerProfile): string { + const parts = Object.entries(profile) + .map(([key, value]) => { + const delta = since ? value - (since[key as keyof CompilerProfile] ?? 0) : value; + return [key, delta] as const; + }) + .filter(([, delta]) => delta !== 0) + .map(([key, delta]) => key.endsWith('Micros') + ? `${key.slice(0, -'Micros'.length)}=${(delta / 1000).toFixed(1)}ms` + : `${key}=${delta}`); + + return parts.length ? parts.join(' ') : '(no change)'; + } + + /** + * Snapshot of the compiler's counters, or `undefined` if the workspace was + * never opened. + */ + private profileSnapshot(): CompilerProfile | undefined { + return this.guard('workspace/profile', () => this.workspace.profile, undefined); + } + + private logProfile(label: string, since?: CompilerProfile): void { + const profile = this.profileSnapshot(); + if (profile) { + this.log(`Compiler profile [${label}]: ${this.formatProfile(profile, since)}`); + } + } + + /** + * Runs a native call with a crash breadcrumb set, and degrades a failure to + * `fallback` rather than letting it escape the handler. + * + * Every method on `LSPDocument`/`LSPWorkspace` is a synchronous call into the + * compiler. Before this, most handlers invoked them bare: a native failure + * either rejected the request or, for anything the addon could not convert to + * a JS error, took the whole server process down with it. + */ + private guard(operation: string, fn: () => T, fallback: T, fsPath?: string, position?: BreadcrumbPosition): T { + beginOperation(operation, fsPath, position); + try { + return fn(); + } catch (ex) { + this.logError(`${operation} failed`, ex); + return fallback; + } finally { + endOperation(); + } + } + + /** + * Aborts a request the client has already given up on. + * + * VSCode cancels superseded requests as the user keeps typing: the hover + * from two keystrokes ago, a completion list the editor has since + * dismissed. Every one of them still cost a full compile, because + * `ensureAnalyzed()` sits behind almost every handler below. + * + * This answers with the protocol's `RequestCancelled` rather than an empty + * result on purpose. Several of these requests drive persistent editor + * state -- semantic tokens are the file's highlighting, diagnostics are the + * Problems panel -- and an empty success would blank it. An error response + * leaves the client holding what it already had. + */ + private throwIfCancelled(token?: CancellationToken): void { + if (token?.isCancellationRequested) { + throw new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled'); + } + } + private onInitialize = async (params: InitializeParams): Promise => { - console.log('Got initialization params', params.capabilities); + // Indented: the client capabilities blob is several KB and unreadable + // as a single line in the output channel. + this.log(`Got initialization params ${JSON.stringify(params.capabilities, null, 2)}`); const workspaceFolders = params.workspaceFolders ?? []; const initializationOptions: InitializationOptions = params.initializationOptions; try { await mkdir(LSPServer.options.storageFsPath, { recursive: true }); } catch (ex) { - console.error(`Could not create storage directory '${LSPServer.options.storageFsPath}': ${ex} `); + this.logError(`Could not create storage directory '${LSPServer.options.storageFsPath}'`, ex); } let found = false; @@ -101,25 +255,30 @@ export class LSPServer { try { await access(polCfg, F_OK); await access(ecompileCfg, F_OK); - this.workspace.open(fsPath); - console.log(`Successfully read ${ecompileCfg}. Loading cache...`); + beginOperation('workspace/open', fsPath); + try { + this.workspace.open(fsPath); + } finally { + endOperation(); + } + this.log(`Successfully read ${ecompileCfg}. Loading cache...`); found = true; } catch (e) { - console.error(`Error reading ${ecompileCfg}`, e); + this.logError(`Error reading ${ecompileCfg}`, e); } } if (found) { this.onDidChangeConfiguration(initializationOptions); } else { - console.log(`Could not find pol.cfg;scripts/ecompile.cfg in [${workspaceFolders.map(x => x.uri).join(', ')}]`); + this.log(`Could not find pol.cfg;scripts/ecompile.cfg in [${workspaceFolders.map(x => x.uri).join(', ')}]`); } try { ExtensionConfiguration.setFromObject(initializationOptions?.configuration ?? {}); } catch (e) { - console.error('Error setting native configuration:', e); + this.logError('Error setting native configuration', e); } this.hasDiagnosticRelatedInformationCapability = Boolean(params.capabilities.textDocument?.publishDiagnostics?.relatedInformation); @@ -161,126 +320,245 @@ export class LSPServer { return result; }; + /** + * Marks a document, plus every open document that includes it, as needing + * re-analysis. + */ + private markDirty(fsPath: string): void { + this.dirty.add(fsPath); + + for (const dependeePathname of this.sources.keys()) { + if (dependeePathname === fsPath || this.dirty.has(dependeePathname)) { + continue; + } + if ((this.dependsOn.get(dependeePathname) ?? []).includes(fsPath)) { + this.dirty.add(dependeePathname); + } + } + } + + /** + * Brings a document's analysis up to date if it is stale. Safe to call + * before any request that reads the syntax tree; a no-op when clean. + */ + private ensureAnalyzed(fsPath: string, document: typeof LSPDocument): void { + if (!this.dirty.has(fsPath)) { + return; + } + + if (this.parseTreeCacheStale) { + this.workspace.clearParseTreeCache(); + this.parseTreeCacheStale = false; + } + + // analyze() drops the old syntax tree before compiling, so the document + // stays dirty until it succeeds: a throw here must not leave an empty + // tree that later requests treat as up to date. + document.analyze(this.configuration?.continueAnalysisOnError); + this.dependsOn.set(fsPath, document.dependents()); + this.dirty.delete(fsPath); + } + private onDidOpen = async (e: TextDocumentChangeEvent) => { const { fsPath } = URI.parse(e.document.uri); - this.sources.set(fsPath, this.workspace.getDocument(fsPath)); + const document = this.guard('textDocument/didOpen', () => this.workspace.getDocument(fsPath), undefined, fsPath); + if (document) { + this.sources.set(fsPath, document); + this.markDirty(fsPath); + } }; private onDidClose = async (e: TextDocumentChangeEvent) => { const { uri } = e.document; const { fsPath } = URI.parse(uri); + const document = this.sources.get(fsPath); + + // Closing discards any unsaved edits, so contents fall back to disk: + // whatever was analyzed from the buffer -- its cached parse tree, and + // every open document including it -- is now stale. + this.markDirty(fsPath); + if (isCachedParseTreeFile(fsPath)) { + this.parseTreeCacheStale = true; + } + this.sources.delete(fsPath); + this.dirty.delete(fsPath); + this.dependsOn.delete(fsPath); + + // The native workspace caches documents indefinitely, so the syntax + // tree has to be handed back explicitly or it outlives the editor tab. + if (document) { + this.guard('textDocument/didClose', () => document.release(), undefined, fsPath); + } }; private onDidChangeContent = async (e: TextDocumentChangeEvent) => { const { uri } = e.document; const { fsPath } = URI.parse(uri); - const document = this.sources.get(fsPath); - try { - if (!document) { - throw new Error('Document not opened'); - } - document.analyze(this.configuration?.continueAnalysisOnError); - } catch (ex) { - console.error(ex); + if (!this.sources.has(fsPath)) { + this.logError(`textDocument/didChange for a document that was never opened: ${fsPath}`); + return; + } + + // Deliberately does no work here: the next request that needs the tree + // triggers exactly one analysis. + if (isCachedParseTreeFile(fsPath)) { + this.parseTreeCacheStale = true; } + this.markDirty(fsPath); }; - private onDocumentDiagnostics = async (e: DocumentDiagnosticParams): Promise => { + private onDocumentDiagnostics = async (e: DocumentDiagnosticParams, token?: CancellationToken): Promise => { + this.throwIfCancelled(token); + const { uri } = e.textDocument; const { fsPath } = URI.parse(uri); - const document = this.sources.get(fsPath) ?? this.workspace.getDocument(fsPath); - this.sources.set(fsPath, document); - const diagnostics = document.diagnostics(); - - const relatedDocuments: {[uri: DocumentUri]: FullDocumentDiagnosticReport} = {}; - - for (const [dependeePathname, dependeeDoc] of this.sources.entries()) { - if (dependeePathname !== fsPath && dependeeDoc.dependents().includes(fsPath)) { - const uri = URI.file(dependeePathname).toString(); - dependeeDoc.analyze(); - const diagnostics = dependeeDoc.diagnostics(); - relatedDocuments[uri] = { - kind: DocumentDiagnosticReportKind.Full, - items: diagnostics - }; - } - } - return { + const empty: DocumentDiagnosticReport = { kind: DocumentDiagnosticReportKind.Full, - items: diagnostics, - relatedDocuments + items: [] }; + + return this.guard('textDocument/diagnostic', () => { + let document = this.sources.get(fsPath); + if (!document) { + document = this.workspace.getDocument(fsPath); + this.sources.set(fsPath, document); + this.markDirty(fsPath); + } + this.ensureAnalyzed(fsPath, document); + const diagnostics = document.diagnostics(); + + const relatedDocuments: {[uri: DocumentUri]: FullDocumentDiagnosticReport} = {}; + + for (const [dependeePathname, dependeeDoc] of this.sources.entries()) { + // Uses the cached dependency list rather than calling across + // N-API for every open document, and re-analyzes only those the + // change actually invalidated. + if (dependeePathname !== fsPath && (this.dependsOn.get(dependeePathname) ?? []).includes(fsPath)) { + const dependeeUri = URI.file(dependeePathname).toString(); + this.ensureAnalyzed(dependeePathname, dependeeDoc); + relatedDocuments[dependeeUri] = { + kind: DocumentDiagnosticReportKind.Full, + items: dependeeDoc.diagnostics() + }; + } + } + return { + kind: DocumentDiagnosticReportKind.Full, + items: diagnostics, + relatedDocuments + }; + }, empty, fsPath); }; private onDidChangeWatchedFiles = (e: DidChangeWatchedFilesParams) => { const ecompileCfg = join(this.workspace.workspaceRoot, 'scripts', 'ecompile.cfg'); const shouldReopen = e.changes.some(change => change.type === FileChangeType.Changed && URI.parse(change.uri).fsPath === ecompileCfg); + // An include or module file changed on disk (git checkout, external + // edit): anything cached from it is now stale. + const changedIncludes = e.changes + .map(change => URI.parse(change.uri).fsPath) + .filter(isCachedParseTreeFile); + + if (changedIncludes.length) { + this.guard('workspace/invalidateParseTreeCache', () => this.workspace.clearParseTreeCache(), undefined); + // Open documents including these files now hold stale analyses. + changedIncludes.forEach(changed => this.markDirty(changed)); + } + if (shouldReopen) { - const hasChanges = this.workspace.reopen(); - if (hasChanges && this.configuration?.disableWorkspaceReferences === false) { - this.updateCache(); + const hasChanges = this.guard('workspace/didChangeWatchedFiles', () => this.workspace.reopen(), false, ecompileCfg); + if (hasChanges) { + // reopen() drops the native document cache, but the reference + // index build is memoized in JS and a finished one answers + // instantly -- which is exactly what keeps repeat + // textDocument/references cheap. Unless it is dropped here the + // index is never rebuilt for the new configuration. Done + // regardless of the setting, so that re-enabling workspace + // references later does not resurrect a stale index. + this.guard('workspace/invalidateReferenceCache', () => this.workspace.invalidateReferenceCache(), undefined); + + // Every retained analysis was built against the old + // configuration. + this.parseTreeCacheStale = true; + for (const pathname of this.sources.keys()) { + this.dirty.add(pathname); + } + + if (this.configuration?.disableWorkspaceReferences === false) { + this.updateCache(); + } } } }; - private onDocumentSymbol = (params: DocumentSymbolParams): DocumentSymbol[] | null => { + private onDocumentSymbol = (params: DocumentSymbolParams, token?: CancellationToken): DocumentSymbol[] | null => { + this.throwIfCancelled(token); + const { fsPath } = URI.parse(params.textDocument.uri); const document = this.sources.get(fsPath); - return document?.symbols() ?? null; + return this.guard('textDocument/documentSymbol', () => { + if (!document) { + return null; + } + this.ensureAnalyzed(fsPath, document); + return document.symbols() ?? null; + }, null, fsPath); }; - private onSemanticTokens = async (params: SemanticTokensParams): Promise => { + private onSemanticTokens = async (params: SemanticTokensParams, token?: CancellationToken): Promise => { + this.throwIfCancelled(token); + const builder = new SemanticTokensBuilder(); const { fsPath } = URI.parse(params.textDocument.uri); const document = this.sources.get(fsPath); - try { - if (!document) { - throw new Error('Document not opened'); - } - const tokens = document.tokens(); + if (!document) { + this.logError(`textDocument/semanticTokens for a document that was never opened: ${fsPath}`); + return builder.build(); + } - const sorted = tokens.sort((tokInfo1, tokInfo2) => { - const line = tokInfo1[0] - tokInfo2[0]; - if (line === 0) { - return tokInfo1[1] - tokInfo2[1]; - } - return line; - }); + this.guard('textDocument/semanticTokens', () => { + this.ensureAnalyzed(fsPath, document); + // Already sorted by position on the native side. + const tokens = document.tokens(); - for (const token of sorted) { - builder.push(...token); + for (let i = 0; i + 4 < tokens.length; i += 5) { + builder.push(tokens[i], tokens[i + 1], tokens[i + 2], tokens[i + 3], tokens[i + 4]); } - } catch (ex) { - console.error(ex); - } + }, undefined, fsPath); + return builder.build(); }; - private onHover = (params: HoverParams): Hover | null => { + private onHover = (params: HoverParams, token?: CancellationToken): Hover | null => { + this.throwIfCancelled(token); + const { fsPath } = URI.parse(params.textDocument.uri); const { position: { line, character } } = params; const position: Position = { line: line + 1, character: character + 1 }; const document = this.sources.get(fsPath); - if (document) { - const hover = document.hover(position); - if (hover) { - const contents: MarkupContent = { - value: hover, - kind: 'markdown' - }; - - return { - contents - }; + return this.guard('textDocument/hover', () => { + if (document) { + this.ensureAnalyzed(fsPath, document); + const hover = document.hover(position); + if (hover) { + const contents: MarkupContent = { + value: hover, + kind: 'markdown' + }; + + return { + contents + }; + } } - } - return null; + return null; + }, null, fsPath, position); }; private onDocumentRangeFormatting = async (params: DocumentRangeFormattingParams): Promise => { @@ -293,43 +571,61 @@ export class LSPServer { return this.getFormattedTextEdit(uri, options); }; - private onDefinition = async (params: DefinitionParams): Promise => { + private onDefinition = async (params: DefinitionParams, token?: CancellationToken): Promise => { + this.throwIfCancelled(token); + const { fsPath } = URI.parse(params.textDocument.uri); const { position: { line, character } } = params; const position: Position = { line: line + 1, character: character + 1 }; const document = this.sources.get(fsPath); - if (document) { - const definition = document.definition(position); - if (definition) { - return { - range: definition.range, - uri: URI.file(definition.fsPath).toString() - }; + return this.guard('textDocument/definition', () => { + if (document) { + this.ensureAnalyzed(fsPath, document); + const definition = document.definition(position); + if (definition) { + return { + range: definition.range, + uri: URI.file(definition.fsPath).toString() + }; + } } - } - return null; + return null; + }, null, fsPath, position); }; - private onCompletion = async (params: CompletionParams): Promise => { + private onCompletion = async (params: CompletionParams, token?: CancellationToken): Promise => { + this.throwIfCancelled(token); + const { fsPath } = URI.parse(params.textDocument.uri); const { position: { line, character } } = params; const position: Position = { line: line + 1, character: character + 1 }; const document = this.sources.get(fsPath); - if (document) { - const completion = document.completion(position); - if (completion) { - return completion; + return this.guard('textDocument/completion', () => { + if (document) { + this.ensureAnalyzed(fsPath, document); + const completion = document.completion(position); + if (completion) { + return completion; + } } - } - return null; + return null; + }, null, fsPath, position); }; - private onSignatureHelp = async (params: SignatureHelpParams): Promise => { + private onSignatureHelp = async (params: SignatureHelpParams, token?: CancellationToken): Promise => { + this.throwIfCancelled(token); + const { fsPath } = URI.parse(params.textDocument.uri); const { position: { line, character } } = params; const position: Position = { line: line + 1, character: character + 1 }; const document = this.sources.get(fsPath); - return document?.signatureHelp(position) ?? null; + return this.guard('textDocument/signatureHelp', () => { + if (!document) { + return null; + } + this.ensureAnalyzed(fsPath, document); + return document.signatureHelp(position) ?? null; + }, null, fsPath, position); }; private onDidChangeConfiguration = (params: DidChangeConfigurationParams): void => { @@ -337,18 +633,18 @@ export class LSPServer { return; } - console.log('ExtensionConfiguration changed:', params); + this.log(`ExtensionConfiguration changed: ${JSON.stringify(params.configuration)}`); this.configuration = params.configuration; try { ExtensionConfiguration.setFromObject(params.configuration ?? {}); } catch (e) { - console.error('Error setting native configuration:', e); + this.logError('Error setting native configuration', e); } if (this.downloader.commitId === '' || params.configuration.polCommitId !== this.downloader.commitId) { this.downloader.start(this.workspace.workspaceRoot, this.workspace.getConfigValue('ModuleDirectory'), params.configuration.polCommitId).catch(e => { - console.warn(`Could not download polserver documentation: ${e?.message ?? e}`); + this.logWarn(`Could not download polserver documentation: ${formatError(e)}`); }); } @@ -366,39 +662,47 @@ export class LSPServer { const position: Position = { line: line + 1, character: character + 1 }; if (this.configuration?.disableWorkspaceReferences === false) { - const serverInitiatedReporter = await this.connection.window.createWorkDoneProgress(); + try { + const serverInitiatedReporter = await this.connection.window.createWorkDoneProgress(); - serverInitiatedReporter.begin('References', undefined, undefined, true); + serverInitiatedReporter.begin('References', undefined, undefined, true); - const controller = new AbortController(); - serverInitiatedReporter.token.onCancellationRequested(() => { - controller.abort(); - }); + const controller = new AbortController(); + serverInitiatedReporter.token.onCancellationRequested(() => { + controller.abort(); + }); - const loaded = await this.workspace.updateCache(({ count, total }) => { - serverInitiatedReporter.report(100 * count / total, `Waiting for workspace cache...`); - }, controller.signal); + const loaded = await this.workspace.updateCache(({ count, total }) => { + serverInitiatedReporter.report(100 * count / total, `Waiting for workspace cache...`); + }, controller.signal); - serverInitiatedReporter.done(); - if (!loaded) { + serverInitiatedReporter.done(); + if (!loaded) { + return undefined; + } + } catch (ex) { + this.logError('Building the workspace reference cache failed', ex); return undefined; } } const document = this.sources.get(fsPath); - if (document) { - const references = document.references(position); - if (references) { - if (params.context.includeDeclaration) { - const definition = document.definition(position, { nameOnly: true }); - if (definition) { - references.unshift(definition); + return this.guard('textDocument/references', () => { + if (document) { + this.ensureAnalyzed(fsPath, document); + const references = document.references(position); + if (references) { + if (params.context.includeDeclaration) { + const definition = document.definition(position, { nameOnly: true }); + if (definition) { + references.unshift(definition); + } } + return references.map(x => ({ ...x, uri: URI.file(x.fsPath).toString() })); } - return references.map(x => ({ ...x, uri: URI.file(x.fsPath).toString() })); } - } - return null; + return null; + }, null, fsPath, position); }; private updateCache() { @@ -411,14 +715,26 @@ export class LSPServer { // Must do next tick because `window/workDoneProgress/create` will not be registered yet. process.nextTick(async () => { - const serverInitiatedReporter = await this.connection.window.createWorkDoneProgress(); - serverInitiatedReporter.begin('Workspace Cache'); - this.workspace.updateCache(({ count, total }) => { - serverInitiatedReporter.report(100 * count / total, `Reading files ${count}/${total}`); - }, updateCacheAbortController.signal).then(() => { - serverInitiatedReporter.done(); - console.log(`Cache loaded.`); - }); + // Every await and every native call below is inside this try: an + // unhandled rejection here used to terminate the server outright, + // and it runs during startup indexing where it is least visible. + try { + const serverInitiatedReporter = await this.connection.window.createWorkDoneProgress(); + serverInitiatedReporter.begin('Workspace Cache'); + const before = this.profileSnapshot(); + const startedAt = Date.now(); + try { + await this.workspace.updateCache(({ count, total }) => { + serverInitiatedReporter.report(100 * count / total, `Reading files ${count}/${total}`); + }, updateCacheAbortController.signal); + this.log(`Cache loaded in ${Date.now() - startedAt}ms.`); + this.logProfile('workspace cache build', before); + } finally { + serverInitiatedReporter.done(); + } + } catch (ex) { + this.logError('Building the workspace cache failed', ex); + } }); } @@ -436,12 +752,16 @@ export class LSPServer { return null; } - const formatted = range ? + const formatted = this.guard('textDocument/formatting', () => range ? document.toFormattedString(options, { start: { line: range.start.line + 1, character: range.start.character }, end: { line: range.end.line + 1, character: range.end.character } }) : - document.toFormattedString(options); + document.toFormattedString(options), undefined, fsPath); + + if (typeof formatted === 'undefined') { + return null; + } const originalText = textDocument.getText(); diff --git a/server/src/workspace/DocsDownloader.ts b/server/src/workspace/DocsDownloader.ts index 94204f88..180dad0c 100644 --- a/server/src/workspace/DocsDownloader.ts +++ b/server/src/workspace/DocsDownloader.ts @@ -6,9 +6,38 @@ import { readdir, mkdir, access } from 'fs/promises'; import { createWriteStream, unlink } from 'fs'; import * as http from 'http'; import * as https from 'https'; -import { eachLimit } from 'async'; import { F_OK } from 'constants'; +/** + * Runs `worker` over `items`, at most `limit` of them in flight, and rejects + * with the first failure without starting anything further. + * + * Replaces `async`'s `eachLimit`, whose package is ~225 KB of JavaScript parsed + * during server boot for this single call site -- one that only runs when a POL + * workspace was found and the documentation for that revision is not already on + * disk. + */ +async function eachLimit(items: readonly T[], limit: number, worker: (item: T) => Promise): Promise { + let next = 0; + let failure: unknown; + + const runners = Array.from({ length: Math.min(limit, items.length) }, async () => { + while (next < items.length && failure === undefined) { + try { + await worker(items[next++]); + } catch (e) { + failure ??= e; + } + } + }); + + await Promise.all(runners); + + if (failure !== undefined) { + throw failure; + } +} + export default class DocsDownloader { private static readonly POL_REV_REGEX = /\(Rev. ([0-9a-fA-F]{9})\)/; public commitId: string | null = null; @@ -39,11 +68,7 @@ export default class DocsDownloader { if (commitId) { const _commitId = commitId; const modules = await this.availableModules(moduleDirectory); - await eachLimit(modules, 5, (moduleName, cb) => { - this.downloadDoc(_commitId, moduleName) - .then(_ => cb()) - .catch(e => cb(e)); - }); + await eachLimit(modules, 5, moduleName => this.downloadDoc(_commitId, moduleName)); } }