diff --git a/.dockerignore b/.dockerignore index 8855740d..efc0965a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,13 +31,6 @@ testdata/ .env.local wasm.sh -# Node.js (not needed in the image) -web/node_modules/ - -# Front-end build artifacts not needed by the binary-only image. -frontend-integration/node_modules/ -web/vue-demo/ - # Temporary files tmp/ temp/ diff --git a/.github/workflows/wasm-publish.yaml b/.github/workflows/wasm-publish.yaml index a2f01d8a..5fc1c5cd 100644 --- a/.github/workflows/wasm-publish.yaml +++ b/.github/workflows/wasm-publish.yaml @@ -1,6 +1,7 @@ name: Publish WASM to S3 -# Builds the browser/WASM CLI static site and publishes it to S3 + CloudFront. +# Builds the browser/WASM CLI's static assets (megaport.wasm + wasm_exec.js) and +# publishes them to S3 + CloudFront. # # CONFIGURATION REQUIRED BEFORE MERGE (ESD-1506): # AWS access is provisioned via the shared github_runners_iam Terraform module in @@ -27,10 +28,9 @@ name: Publish WASM to S3 # See the "Check required configuration" step, which fails early with a clear message. # # WHAT GETS PUBLISHED: -# Stable filenames under each prefix: megaport.wasm (brotli, Content-Encoding: br), -# wasm_exec.js, and the rest of the static site. The portal references megaport.wasm -# and wasm_exec.js by URL, so the names are kept stable rather than content-hashed; -# the version/latest prefix provides the cache-busting instead. +# Stable filenames under each prefix: megaport.wasm (brotli, Content-Encoding: br) and +# wasm_exec.js. The portal references both by URL, so the names are kept stable rather +# than content-hashed; the version/latest prefix provides the cache-busting instead. # # PORTAL INTEGRATION: # Point the portal's wasmUrl/wasmExecUrl at the latest/ alias while iterating, then @@ -108,12 +108,6 @@ jobs: go-version-file: 'go.mod' cache: true - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 20 - cache: npm - cache-dependency-path: frontend-integration/package-lock.json - - name: Resolve version label id: version env: @@ -142,7 +136,7 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" echo "Publishing version: $version" - - name: Build static site + - name: Build static assets run: make web-static - name: Compress WASM @@ -151,7 +145,7 @@ jobs: # cmd/wasmcompress writes .br (and .gz) next to the source file (ESD-1268). # CloudFront auto-compression caps at 10 MB; the raw wasm is well over that, # so it must be pre-compressed at origin and served with Content-Encoding: br. - GOWORK=off go run ./cmd/wasmcompress web/vue-demo/megaport.wasm + GOWORK=off go run ./cmd/wasmcompress web/dist/megaport.wasm - name: Compute integrity hashes id: hashes @@ -215,15 +209,14 @@ jobs: PUBLISH_LATEST: ${{ inputs.publish_latest }} run: | set -euo pipefail - src=web/vue-demo + src=web/dist publish() { local dest="s3://${BUCKET}/${PREFIX}/$1" local cache="$2" echo "==> Publishing to ${dest}" - # Sync the static site; the wasm and wasm_exec.js are uploaded separately below - # so we can pin their Content-Type/Content-Encoding instead of relying on - # aws s3 sync's MIME inference. + # The wasm and wasm_exec.js are uploaded separately below so we can pin their + # Content-Type/Content-Encoding instead of relying on aws s3 sync's MIME inference. aws s3 sync "$src/" "$dest/" --delete \ --exclude 'megaport.wasm' \ --exclude 'megaport.wasm.br' \ diff --git a/.gitignore b/.gitignore index cf8b53f2..7a736d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,14 +21,6 @@ web/megaport.wasm *.wasm.br *.wasm.gz -# Node.js / npm -node_modules/ -web/node_modules/ -package-lock.json -web/package-lock.json -# Committed so browser/WASM builds resolve identical dependency versions via npm ci. -!frontend-integration/package-lock.json - # Go build artifacts /megaport-cli megaport-cli diff --git a/Makefile b/Makefile index f4cb7868..02e0b90a 100644 --- a/Makefile +++ b/Makefile @@ -97,10 +97,11 @@ wasm-smoke: wasm wasm-compress: wasm go run ./cmd/wasmcompress web/megaport.wasm -# Build the static browser/WASM site into web/vue-demo/ (for CDN hosting) +# Build the WASM binary + wasm_exec.js loader into web/dist/ (for CDN hosting) web-static: ./scripts/build-web.sh # Clean build artifacts clean: - rm -f megaport-cli cover*.out coverage*.out coverage.html web/megaport.wasm web/megaport.wasm.br web/megaport.wasm.gz web/vue-demo/megaport.wasm web/vue-demo/megaport.wasm.br web/vue-demo/megaport.wasm.gz + rm -f megaport-cli cover*.out coverage*.out coverage.html web/megaport.wasm web/megaport.wasm.br web/megaport.wasm.gz web/wasm_exec.js + rm -rf web/dist diff --git a/WASM_README.md b/WASM_README.md index 2e6c05f5..285563ed 100644 --- a/WASM_README.md +++ b/WASM_README.md @@ -88,9 +88,8 @@ Instead, whenever the API rejects a request with 401/403, whether that's because token expired or because the host revoked it, the failing command's output contains the substring `MEGAPORT_SESSION_EXPIRED`. The host is responsible for watching command output for this marker and, on a match, prompting the user to re-authenticate and -calling `setAuthToken` again with a fresh token. See -[`frontend-integration/types/megaport-wasm.d.ts`](frontend-integration/types/megaport-wasm.d.ts) -for the full type signature and doc comments. +calling `setAuthToken` again with a fresh token. The registration and doc comments +for `setAuthToken` live inline in `internal/wasm/wasm.go`. Separately, the browser-cached OAuth token used by credentials-based logins (`window.tokenManager.getToken(environment)`, checked to avoid re-authenticating on @@ -120,43 +119,102 @@ always returns `{ error: "synchronous execution is not supported; use executeMeg It will be removed in a future release; new integrations should not call it. Full type definitions for the whole JS surface (auth, config file, prompts, telemetry) -live in [`frontend-integration/types/megaport-wasm.d.ts`](frontend-integration/types/megaport-wasm.d.ts). +are documented inline in `internal/wasm/`; there is no separate reference front end in +this repo. Front-end integrators (e.g. the Portal) own their own wrapper around the +`window.executeMegaportCommandAsync` API described above. -## Building +## Interactive Mode -The browser CLI is two pieces: the WASM binary and the Vue front end that hosts it. +Some commands prompt for input (interactive `buy`/`update` flows, confirmations, secrets). +In the browser there is no stdin, so the WASM asks the host page for each value through a +small set of JavaScript functions. Wire these up and run the command through +`executeMegaportCommandAsync` (see [JavaScript API](#javascript-api)), or interactive +commands will never receive a response. -```bash -# WASM binary only (writes web/megaport.wasm) -make wasm +### Host functions -# Full static site: WASM + Vue front end, assembled into web/vue-demo/ -make web-static # or: ./scripts/build-web.sh +The WASM registers these on `window` at startup: + +| Function | Purpose | +|---|---| +| `registerPromptHandler(cb)` | Register a callback the WASM invokes with each prompt request. | +| `submitPromptResponse(id, response)` | Reply to the prompt `id` with the user's input (a string). | +| `cancelPrompt(id)` | Cancel the prompt `id`. A value prompt fails with a "prompt cancelled by user" error; a confirmation is treated as declined. | + +### Prompt request shape + +Your handler receives a single object: + +```js +{ + id: "prompt_1_1700000000000000000", // unique id; echo it back in submit/cancel + message: "Enter port name:", // text to show the user + type: "text", // "text" | "confirm" | "password" | "resource" + resourceType: "port" // set for resource and secret-resource prompts (port, mcr, vxc, ...), else "" +} +``` + +Mask the input when `type === "password"`: render an `` or otherwise +hide the characters. Password prompts and secret-resource prompts (for example VXC/MVE +passwords and pre-shared keys) set this type. Note that some other secret-bearing inputs +(such as partner auth/service/shared keys and MVE registration keys) are currently sent as +`type === "resource"`, so don't rely on the password type alone if you want to mask every +possible secret. + +### Lifecycle + +```js +registerPromptHandler((request) => { + const masked = request.type === 'password'; + showPrompt(request.message, { masked }).then((answer) => { + if (answer === null) { + cancelPrompt(request.id); // user dismissed the prompt + } else { + submitPromptResponse(request.id, answer); + } + }); +}); + +executeMegaportCommandAsync('vxc buy --interactive', (result) => { + console.log(result.output || result.error); +}); +``` + +A prompt left unanswered times out after 10 minutes and the command receives an error. + +### Live output streaming + +By default a command's output arrives once, in the async callback's result. To render +output as it is produced instead, register a handler before running the command: + +```js +registerOutputHandler((chunk) => terminal.write(chunk)); // chunk is a string ``` -`web-static` needs the Go toolchain and Node/npm on `PATH`. It produces a self-contained -**`web/vue-demo/`** directory (Vue build + `megaport.wasm` + `wasm_exec.js`) ready to -publish to a CDN. See [`web/README.md`](web/README.md) for the wasm pre-compression and -cache-header details. +The contract: -## Local Development +- Only narrative output (progress and status messages) streams. Structured document + output (table/JSON/CSV/XML) is never streamed; it arrives once in the completion result. +- If your handler received at least one chunk without throwing, the completion result does + **not** repeat the streamed narrative, so don't render both. +- If the handler throws or no chunk was delivered, streaming is disabled for the rest of + that command and the completion result falls back to the full captured output + (already-streamed chunks may then appear twice). -The front end lives in `frontend-integration/` and has its own Vite dev server. The dev -server serves files from that directory's root and has no `public/` dir, so build the wasm -and copy the loader into `frontend-integration/` first, then start the server: +## Building ```bash -# From the repo root: build the wasm + loader into the dev server's root. -GOOS=js GOARCH=wasm go build -tags js,wasm -o frontend-integration/megaport.wasm . -cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" frontend-integration/wasm_exec.js +# WASM binary only (writes web/megaport.wasm) +make wasm -cd frontend-integration -npm install -npm run dev:demo +# WASM binary + wasm_exec.js loader, assembled into web/dist/ +make web-static # or: ./scripts/build-web.sh ``` -Vite serves `megaport.wasm` with the correct `application/wasm` MIME type and reloads the -front end on change. Rerun the build command above after changing Go code. +`web-static` needs the Go toolchain on `PATH`. It produces a self-contained +**`web/dist/`** directory (`megaport.wasm` + `wasm_exec.js`) ready to publish to a CDN. +See [`web/README.md`](web/README.md) for the wasm pre-compression and cache-header +details. ## Enabling a Module for WASM @@ -230,16 +288,13 @@ aws s3 cp web/megaport.wasm s3://media.megaport.com/portal/megaport-cli/megaport aws s3 cp web/wasm_exec.js s3://media.megaport.com/portal/megaport-cli/wasm_exec.js ``` -For CDN hosting (S3 + CloudFront) of the full static site, sync the assembled -`web/vue-demo/` directory and serve it from the site root: - -```bash -make web-static -aws s3 sync web/vue-demo/ s3://// --delete -``` - -`--delete` prunes stale hashed assets from old builds, so point it at a prefix dedicated -to this site, since it removes anything else under that prefix. +For CDN hosting (S3 + CloudFront), publish via the `.github/workflows/wasm-publish.yaml` +workflow rather than a plain sync. It runs `make web-static`, brotli pre-compresses the +wasm (`cmd/wasmcompress`), then uploads `megaport.wasm` with `Content-Encoding: br` and +pins `Content-Type` on both the wasm and `wasm_exec.js`. It syncs only the remaining +static assets, so a bare `aws s3 sync web/dist/` would instead serve the wasm +uncompressed and let the CDN mis-infer its MIME type, which breaks +`WebAssembly.instantiateStreaming`. ## Troubleshooting diff --git a/cmd/wasmhash/main_test.go b/cmd/wasmhash/main_test.go index f2161110..71bc3a8a 100644 --- a/cmd/wasmhash/main_test.go +++ b/cmd/wasmhash/main_test.go @@ -58,8 +58,8 @@ func TestHashFileDeterministicAndContentSensitive(t *testing.T) { } func TestInsertHash(t *testing.T) { - got := insertHash(filepath.FromSlash("web/vue-demo/megaport.wasm"), "abcd1234") - want := filepath.FromSlash("web/vue-demo/megaport.abcd1234.wasm") + got := insertHash(filepath.FromSlash("web/dist/megaport.wasm"), "abcd1234") + want := filepath.FromSlash("web/dist/megaport.abcd1234.wasm") if got != want { t.Fatalf("insertHash = %q, want %q", got, want) } diff --git a/frontend-integration/.gitignore b/frontend-integration/.gitignore deleted file mode 100644 index 2c6eecc7..00000000 --- a/frontend-integration/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Dependencies -node_modules/ - -# Build output -dist/ -*.local - -# Local dev WASM artifacts (built into this dir for the dev server) -/megaport.wasm -/wasm_exec.js - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Logs -logs/ -*.log -npm-debug.log* - -# Environment -.env -.env.local -.env.*.local diff --git a/frontend-integration/INTEGRATION_GUIDE.md b/frontend-integration/INTEGRATION_GUIDE.md deleted file mode 100644 index 439b04ff..00000000 --- a/frontend-integration/INTEGRATION_GUIDE.md +++ /dev/null @@ -1,508 +0,0 @@ -# Portal Integration Guide - -The Megaport CLI has been compiled to WebAssembly and packaged as Vue 3 components for portal integration. - -**What you get:** - -- Native CLI functionality in the browser -- No backend required -- TypeScript support -- Works with existing portal auth -- Full test coverage - ---- - -## What's Included - -### WebAssembly Binary - -- `megaport.wasm` (~2-5 MB) - Complete CLI compiled to WASM -- `wasm_exec.js` (~15 KB) - Go WASM runtime -- Runs entirely in the browser - -### Vue 3 Components - -``` -frontend-integration/ -├── components/ -│ └── MegaportTerminal.vue # Ready-to-use terminal component -├── composables/ -│ └── useMegaportWASM.ts # Core WASM integration composable -├── types/ -│ └── megaport-wasm.d.ts # TypeScript definitions -├── utils/ -│ └── type-guards.ts # Runtime type validation -└── __tests__/ # Comprehensive test suite -``` - -## Quick Start - -### Step 1: Copy Files to Your Project - -```bash -# In your Megaport Portal project -mkdir -p components/megaport-cli -mkdir -p composables/megaport-cli -mkdir -p types/megaport-cli -mkdir -p public/wasm - -# Copy the integration files -cp frontend-integration/components/* components/megaport-cli/ -cp frontend-integration/composables/* composables/megaport-cli/ -cp frontend-integration/types/* types/megaport-cli/ -cp frontend-integration/utils/* utils/megaport-cli/ - -# Build the WASM artifacts (writes web/megaport.wasm + web/wasm_exec.js) -./wasm.sh - -# Copy WASM files to public directory -cp web/megaport.wasm public/wasm/ -cp web/wasm_exec.js public/wasm/ -``` - -### 2. Install Dependencies - -```bash -npm install xterm @xterm/addon-fit @xterm/addon-web-links -``` - -### 3. Use the Component - -```vue - - - -``` - -## Authentication - -The CLI uses in-memory credentials (no localStorage) and integrates with your existing portal auth. - -### Integration Pattern - -```typescript -// In your auth middleware or store -import { useMegaportWASM } from '~/composables/megaport-cli/useMegaportWASM'; - -// After user logs in via portal -const { setAuth } = useMegaportWASM(); - -onUserLogin((credentials) => { - setAuth( - credentials.accessKey, - credentials.secretKey, - credentials.environment // 'production', 'staging', or 'development' - ); -}); - -// On logout -onUserLogout(() => { - const { clearAuth } = useMegaportWASM(); - clearAuth(); -}); -``` - -### Security - -**Important:** Both Access Key and Secret Key are sensitive: - -- Use `type="password"` for both input fields -- Never log credentials -- Call `clearAuth()` on logout -- Use HTTPS in production - -## Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ Megaport Portal (Vue 3) │ -│ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ Your Portal Components │ │ -│ │ (Dashboard, Resources, Settings, etc.) │ │ -│ └───────────────────┬──────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ MegaportTerminal.vue │ │ -│ │ (Reusable CLI Terminal Component) │ │ -│ └───────────────────┬──────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ useMegaportWASM() Composable │ │ -│ │ - WASM initialization │ │ -│ │ - Command execution │ │ -│ │ - Auth management │ │ -│ │ - Error handling │ │ -│ └───────────────────┬──────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ Browser WebAssembly Runtime │ │ -│ │ ┌────────────────────────────────────────────┐ │ │ -│ │ │ megaport.wasm (Go WASM Binary) │ │ │ -│ │ │ - Browser-compatible subset of the CLI │ │ │ -│ │ │ - Megaport API integration │ │ │ -│ │ │ - Resource modules: ports, vxc, mcr, mve, │ │ │ -│ │ │ locations, partners, servicekeys │ │ │ -│ │ └────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ Megaport API │ - │ (REST endpoints) │ - └──────────────────────┘ -``` - -## Output Streaming - -Command narrative (progress lines, echoes, warnings, validation errors) streams -to the host as the command runs, instead of arriving only when it completes. -Register a handler with `registerOutputHandler(callback)`; the callback is -invoked with each output chunk as it is written. - -```typescript -const { registerOutputHandler } = useMegaportWASM(); - -registerOutputHandler((chunk) => { - // Chunks use `\n` line endings; xterm needs `\r\n`. - terminal.write(chunk.replace(/\n/g, '\r\n')); -}); -``` - -**Completion contract (avoid double-rendering).** When an output handler is -registered: - -- Narrative output is delivered live through the handler and is **not** repeated - in the `execute()` result. -- The result's `output` then carries **only** structured document output - (JSON/CSV/XML/table), or is empty when the command produced only streamed - narrative. -- So render the streamed chunks as they arrive, and render `result.output` - (the structured document) once at completion. Do not render both for the same - content. - -Exception: if the handler throws or delivers nothing, the WASM side disables -streaming for the rest of that command and `result.output` falls back to the -full captured narrative, so already-streamed chunks may appear there too. This -keeps output from being lost when a handler misbehaves. - -When no handler is registered, `result.output` falls back to the full captured -output at completion, preserving the original non-streaming behavior. - -`MegaportTerminal.vue` implements this pattern end-to-end. - -## Integration Examples - -### Admin CLI Page - -```vue - - - - -``` - -### Dashboard Actions - -```vue - - - -``` - -### Resource Actions - -```vue - - - -``` - -## Telemetry - -Track CLI usage: - -```typescript -const { execute } = useMegaportWASM({ - onTelemetry: (event) => { - // Send to your analytics platform - analytics.track(event.type, { - timestamp: event.timestamp, - duration: event.duration, - ...event.metadata, - }); - - // Monitor for errors - if (event.type.endsWith('_error')) { - errorTracking.captureEvent({ - message: event.metadata?.error, - context: { command: event.metadata?.command }, - }); - } - - // Performance monitoring - if (event.duration && event.duration > 5000) { - console.warn(`Slow command: ${event.type} took ${event.duration}ms`); - } - }, -}); -``` - -**Events:** - -- Init: `wasm_init_start`, `wasm_init_success`, `wasm_init_error` -- Commands: `command_execute_start`, `command_execute_success`, `command_execute_error` -- Auth: `auth_set`, `auth_clear` -- UI: `spinner_start`, `spinner_stop` - -## Testing - -The integration includes comprehensive tests you can run: - -```bash -cd frontend-integration -npm test # Run all tests -npm run test:coverage # Run with coverage report -npm run test:ui # Interactive test UI -``` - -Tests cover component lifecycle, WASM initialization, command execution, auth, telemetry, type guards, and error handling. - -## Configuration - -### Nuxt 3 Configuration - -Update your `nuxt.config.ts`: - -```typescript -export default defineNuxtConfig({ - vite: { - optimizeDeps: { - exclude: ['xterm', '@xterm/addon-fit', '@xterm/addon-web-links'], - }, - server: { - fs: { - allow: ['..'], // If WASM files are outside public/ - }, - }, - }, - - // For production builds - nitro: { - compressPublicAssets: true, - publicAssets: [ - { - dir: 'public/wasm', - maxAge: 60 * 60 * 24 * 365, // Cache WASM for 1 year - }, - ], - }, -}); -``` - -### TypeScript Configuration - -Ensure your `tsconfig.json` includes: - -```json -{ - "compilerOptions": { - "types": ["vite/client"], - "moduleResolution": "bundler" - }, - "include": ["types/**/*", "components/**/*", "composables/**/*"] -} -``` - -## Error Handling - -The integration includes robust error handling: - -```typescript -const { execute, error, isReady } = useMegaportWASM({ - maxRetries: 3, // Retry failed init 3 times - retryDelay: 1000, // Start with 1s delay (exponential backoff) - initTimeout: 30000, // 30s timeout for initialization -}); - -// Check for initialization errors -watchEffect(() => { - if (error.value) { - console.error('WASM initialization failed:', error.value); - showErrorNotification({ - title: 'CLI Unavailable', - message: 'The CLI terminal could not be loaded. Please refresh the page.', - }); - } -}); - -// Handle command errors -try { - const result = await execute('ports list'); - if (result.error) { - console.error('Command failed:', result.error); - showErrorNotification({ - title: 'Command Failed', - message: result.error, - }); - } -} catch (err) { - console.error('Execution error:', err); -} -``` - -## Performance - -### 1. Lazy Loading - -Load WASM only when needed: - -```vue - - - -``` - -### Service Worker Caching - -```typescript -// sw.js or your service worker -workbox.precaching.precacheAndRoute([ - { url: '/wasm/megaport.wasm', revision: 'v1.0.0' }, - { url: '/wasm/wasm_exec.js', revision: 'v1.0.0' }, -]); -``` - -### CDN Distribution - -```vue - -``` - -## Troubleshooting - -**WASM fails to load** - -```typescript -// Enable debug mode -const { execute, error } = useMegaportWASM({ debug: true }); - -// Check browser console for detailed logs -// Verify WASM files are accessible: http://localhost:3000/wasm/megaport.wasm -``` - -**Commands return no output** - -```typescript -// Ensure auth is set -const { setAuth, getAuthInfo } = useMegaportWASM(); -setAuth(accessKey, secretKey, 'production'); - -// Check auth status -console.log(getAuthInfo()); -``` - -**TypeScript errors** - -```bash -npm run type-check -# Ensure type definitions are in tsconfig.json: "include": ["types/**/*"] -``` diff --git a/frontend-integration/README.md b/frontend-integration/README.md deleted file mode 100644 index 84bad8b2..00000000 --- a/frontend-integration/README.md +++ /dev/null @@ -1,528 +0,0 @@ -# Megaport CLI WebAssembly - Vue 3 Integration Guide - -## 🎯 Overview - -This package provides Vue 3 + Vite integration for the Megaport CLI WebAssembly module. It's designed specifically for integration into the **Megaport Portal** (Vue 3 + Nuxt 3 + Vite stack). - -## 📦 Package Contents - -``` -frontend-integration/ -├── types/ -│ └── megaport-wasm.d.ts # TypeScript definitions -├── composables/ -│ └── useMegaportWASM.ts # Vue composable for WASM -├── components/ -│ └── MegaportTerminal.vue # Terminal component with xterm.js -├── utils/ -│ └── type-guards.ts # Runtime type validation -├── demo/ -│ ├── App.vue # Demo application -│ └── main.ts # Demo entry point -├── package.json -├── vite.config.ts -├── tsconfig.json -└── README.md (this file) -``` - -## 🚀 Quick Start - -### 1. Installation - -```bash -npm install xterm xterm-addon-fit xterm-addon-web-links -``` - -### 2. Copy WASM Files - -Copy these files to your `public/` directory: - -```bash -# From the CLI build output -cp dist/megaport.wasm public/ -cp dist/wasm_exec.js public/ -``` - -### 3. Basic Usage in Vue 3 - -```vue - - - -``` - -### 4. Using the Terminal Component - -```vue - - - -``` - -## 🏗️ Architecture - -### Direct Mode (Main Thread Execution) - -``` -┌─────────────────────┐ -│ Vue 3 Component │ -└──────────┬──────────┘ - │ - ▼ -┌─────────────────────┐ -│ useMegaportWASM() │ ← Vue Composable -└──────────┬──────────┘ - │ - ▼ -┌─────────────────────┐ -│ wasm_exec.js │ ← Go WASM runtime -│ megaport.wasm │ ← CLI binary -└─────────────────────┘ -``` - -The WASM module runs directly in the main thread with async command execution for non-blocking operation. This provides: - -- Simple integration with minimal overhead -- Direct access to browser APIs -- Reliable authentication handling -- Interactive prompt support - -## 📚 API Reference - -### `useMegaportWASM(config?)` - -Vue composable for WASM integration. - -**Parameters:** - -- `config.wasmPath` (string): Path to megaport.wasm (default: '/megaport.wasm') -- `config.wasmExecPath` (string): Path to wasm_exec.js (default: '/wasm_exec.js') -- `config.debug` (boolean): Enable debug logging (default: false) -- `config.initTimeout` (number): WASM initialization timeout in ms (default: 30000) -- `config.maxRetries` (number): Max retry attempts for initialization (default: 3) -- `config.retryDelay` (number): Base delay between retries in ms (default: 1000) -- `config.onTelemetry` (function): Optional callback for telemetry events - -**Returns:** - -```typescript -{ - isLoading: Ref, // WASM is loading - isReady: Ref, // WASM is ready - error: Ref, // Initialization error - execute: (cmd: string) => Promise, // Execute command - setAuth: (key, secret, env) => void, // Set credentials - clearAuth: () => void, // Clear credentials - getAuthInfo: () => AuthInfo, // Get auth status - resetOutput: () => void, // Reset output buffers - toggleDebug: () => boolean // Toggle debug mode -} -``` - -### Available Commands - -The browser/WASM build registers a subset of the native CLI's modules: `ports`, `vxc`, `mcr`, `mve`, `locations`, `partners`, and `servicekeys`. Other modules (`auth`, `config`, `completion`, `generate-docs`, `version`, `nat-gateway`, `ix`, `users`, `status`, `topology`, `apply`, `product`, `managed-account`, `billing-market`) are not available in the browser. See [`WASM_README.md`](../WASM_README.md#available-commands) for the authoritative list. - -Within each available module, the same subcommands the native CLI provides are exposed (so `partners` is still `list` / `find`, `locations` is still `list` / `get`, etc.). Common examples: - -```bash -# Resource Management -ports list [--output json|table|csv|xml] -vxc list [--output json|table|csv|xml] -mcr list [--output json|table|csv|xml] -mve list [--output json|table|csv|xml] - -# Information -locations list -partners list -servicekeys list - -# Terminal Commands -help # Show help -clear # Clear terminal -``` - -## 🔐 Authentication - -### Browser-Based Auth (Recommended) - -Since WASM runs in the browser, use **localStorage** for credentials: - -```typescript -const { setAuth } = useMegaportWASM(); - -// After user logs in via your auth system -setAuth(accessKey, secretKey, 'staging'); -``` - -### Security Best Practices - -**Important**: Both the Access Key and Secret Key should be treated as sensitive credentials: - -- Use `type="password"` for both Access Key and Secret Key input fields -- Never expose credentials in client-side code or logs -- Clear credentials when users log out using `clearAuth()` -- Consider implementing session timeouts -- Use HTTPS in production to protect credentials in transit - -### Environment Variables - -WASM reads from localStorage keys: - -- `MEGAPORT_ACCESS_KEY` -- `MEGAPORT_SECRET_KEY` -- `MEGAPORT_ENVIRONMENT` - -These are automatically set by `setAuth()`. - -## 🎨 Vite Configuration - -### For Nuxt 3 - -```typescript -// nuxt.config.ts -export default defineNuxtConfig({ - vite: { - optimizeDeps: { - exclude: ['xterm', 'xterm-addon-fit', 'xterm-addon-web-links'], - }, - server: { - fs: { - allow: ['..'], // If WASM files are outside public/ - }, - }, - }, -}); -``` - -### For Vite - -```typescript -// vite.config.ts -export default defineConfig({ - optimizeDeps: { - exclude: ['xterm', 'xterm-addon-fit', 'xterm-addon-web-links'], - }, - server: { - headers: { - 'Cross-Origin-Embedder-Policy': 'require-corp', - 'Cross-Origin-Opener-Policy': 'same-origin', - }, - }, -}); -``` - -## 🧪 Testing the Integration - -### 1. Run Demo Application - -```bash -cd frontend-integration -npm install -npm run dev -``` - -### 2. Test Commands - -Try these commands in the terminal: - -```bash -locations list -help -ports list --output json -``` - -### 3. Verify Output - -- Check browser console for debug logs -- Verify WASM initialization messages -- Test authentication flow -- Confirm API responses - -## ⚡ Performance Considerations - -### WASM File Size - -- `megaport.wasm`: ~2-5 MB -- `wasm_exec.js`: ~15 KB -- First load: 2-5 seconds (includes compilation) -- Subsequent calls: Near-native speed - -### Optimization Tips - -1. **Lazy Load**: Load WASM only when needed - - ```typescript - const showTerminal = ref(false); - // WASM loads when showTerminal becomes true - ``` - -2. **Cache WASM**: Vite/Nuxt will cache WASM files - - ```typescript - // Service Worker caching - workbox.precaching.precacheAndRoute([ - { url: '/megaport.wasm', revision: '1.0.0' }, - ]); - ``` - -3. **Telemetry Tracking**: Monitor performance and errors - - ```typescript - useMegaportWASM({ - onTelemetry: (event) => { - analytics.track(event.type, { - duration: event.duration, - ...event.metadata, - }); - }, - }); - ``` - -4. **Type Safety**: Use runtime type guards - - ```typescript - import { isValidCommand } from './utils/type-guards'; - - if (isValidCommand(userInput)) { - await execute(userInput); - } - ``` - -## 🐛 Troubleshooting - -### WASM Fails to Load - -```javascript -// Check browser console -console.log(window.Go); // Should be defined -console.log(window.executeMegaportCommandAsync); // Should be function -``` - -**Solutions:** - -- Verify WASM files are in `public/` -- Check MIME types: `application/wasm` -- Ensure CORS headers are correct -- Clear browser cache - -### Commands Return No Output - -```javascript -// Check WASM debug info -window.toggleWasmDebug(); // Enable debug -window.dumpBuffers(); // Check buffer contents -``` - -**Solutions:** - -- Verify authentication is set -- Check command syntax -- Enable debug mode -- Review browser console logs - -### TypeScript Errors - -Ensure types are properly configured: - -```json -// tsconfig.json -{ - "compilerOptions": { - "types": ["vite/client"], - "moduleResolution": "bundler" - }, - "include": ["types/megaport-wasm.d.ts"] -} -``` - -## 🆕 New Features - -### Telemetry Support - -Track WASM operations and performance: - -```typescript -const { execute } = useMegaportWASM({ - onTelemetry: (event) => { - console.log(`${event.type}: ${event.duration}ms`, event.metadata); - }, -}); -``` - -Event types: - -- `wasm_init_start` / `wasm_init_success` / `wasm_init_error` -- `command_execute_start` / `command_execute_success` / `command_execute_error` -- `auth_set` / `auth_clear` -- `spinner_start` / `spinner_stop` - -### Runtime Type Guards - -Validate data at runtime: - -```typescript -import { - isValidCommand, - isMegaportCommandResult, - hasWASMFunctions, -} from './utils/type-guards'; - -// Validate commands before execution -if (isValidCommand(userInput)) { - const result = await execute(userInput); - - if (isMegaportCommandResult(result)) { - // Type-safe result handling - } -} -``` - -### Lazy CSS Loading - -xterm.js CSS is now loaded on-demand when the terminal initializes, reducing initial bundle size. - -### Retry Logic - -Automatic retry with exponential backoff for failed WASM initialization: - -```typescript -useMegaportWASM({ - maxRetries: 3, - retryDelay: 1000, // Increases exponentially - initTimeout: 30000, -}); -``` - -## 🌐 Browser Compatibility - -| Browser | Minimum Version | Status | -| ------- | --------------- | --------------- | -| Chrome | 57+ | ✅ Full Support | -| Firefox | 52+ | ✅ Full Support | -| Safari | 11+ | ✅ Full Support | -| Edge | 16+ | ✅ Full Support | - -## 📝 Example Integration Scenarios - -### Scenario 1: Portal Dashboard - -```vue - - - -``` - -### Scenario 2: Admin Console - -```vue - - - -``` - -## 🔗 Integration with Existing Portal Features - -### Authentication - -Use your existing auth system: - -```typescript -// After user logs in -onUserLogin((credentials) => { - const { setAuth } = useMegaportWASM(); - setAuth( - credentials.accessKey, - credentials.secretKey, - credentials.environment - ); -}); -``` - -### State Management (Pinia) - -```typescript -// stores/megaport.ts -import { defineStore } from 'pinia'; -import { useMegaportWASM } from '@/composables/useMegaportWASM'; - -export const useMegaportStore = defineStore('megaport', () => { - const wasm = useMegaportWASM(); - - const listPorts = async () => { - const result = await wasm.execute('ports list --output json'); - return JSON.parse(result.output); - }; - - return { listPorts }; -}); -``` - -### Router Integration - -```typescript -// Lazy load for specific routes -{ - path: '/admin/cli', - component: () => import('@/views/CLITerminal.vue'), - meta: { requiresWASM: true } -} -``` diff --git a/frontend-integration/__tests__/App.test.ts b/frontend-integration/__tests__/App.test.ts deleted file mode 100644 index 071feb39..00000000 --- a/frontend-integration/__tests__/App.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { mount } from '@vue/test-utils'; -import App from '../demo/App.vue'; -import MegaportTerminal from '../components/MegaportTerminal.vue'; - -describe('App.vue', () => { - let wrapper: any; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - if (wrapper) { - wrapper = null; - } - }); - - describe('Authentication Form', () => { - it('should render authentication form when not authenticated', () => { - wrapper = mount(App); - - expect(wrapper.find('.auth-panel').exists()).toBe(true); - expect(wrapper.find('input#accessKey').exists()).toBe(true); - expect(wrapper.find('input#secretKey').exists()).toBe(true); - expect(wrapper.find('select#environment').exists()).toBe(true); - expect(wrapper.find('.btn-primary').text()).toBe('Set Credentials'); - }); - - it('should have default environment as staging', () => { - wrapper = mount(App); - - const select = wrapper.find('select#environment'); - expect((select.element as HTMLSelectElement).value).toBe('staging'); - }); - - it('should update form fields when user types', async () => { - wrapper = mount(App); - - const accessKeyInput = wrapper.find('input#accessKey'); - const secretKeyInput = wrapper.find('input#secretKey'); - const environmentSelect = wrapper.find('select#environment'); - - await accessKeyInput.setValue('test-access-key'); - await secretKeyInput.setValue('test-secret-key'); - await environmentSelect.setValue('production'); - - expect((accessKeyInput.element as HTMLInputElement).value).toBe( - 'test-access-key' - ); - expect((secretKeyInput.element as HTMLInputElement).value).toBe( - 'test-secret-key' - ); - expect((environmentSelect.element as HTMLSelectElement).value).toBe( - 'production' - ); - }); - - it('should call setAuth when form is submitted', async () => { - wrapper = mount(App); - - const accessKeyInput = wrapper.find('input#accessKey'); - const secretKeyInput = wrapper.find('input#secretKey'); - const form = wrapper.find('form'); - - await accessKeyInput.setValue('my-access-key'); - await secretKeyInput.setValue('my-secret-key'); - await form.trigger('submit.prevent'); - - // Auth panel should be hidden after submission - await wrapper.vm.$nextTick(); - expect(wrapper.find('.auth-panel').exists()).toBe(false); - }); - - it('should show terminal section after authentication', async () => { - wrapper = mount(App); - - const accessKeyInput = wrapper.find('input#accessKey'); - const secretKeyInput = wrapper.find('input#secretKey'); - const form = wrapper.find('form'); - - await accessKeyInput.setValue('test-key'); - await secretKeyInput.setValue('test-secret'); - await form.trigger('submit.prevent'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.find('.terminal-section').exists()).toBe(true); - expect(wrapper.findComponent(MegaportTerminal).exists()).toBe(true); - }); - }); - - describe('Quick Actions', () => { - it('should not show quick actions when not authenticated', () => { - wrapper = mount(App); - - expect(wrapper.find('.quick-actions').exists()).toBe(false); - }); - - it('should show quick actions after authentication', async () => { - wrapper = mount(App); - - const form = wrapper.find('form'); - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await form.trigger('submit.prevent'); - - await wrapper.vm.$nextTick(); - - expect(wrapper.find('.quick-actions').exists()).toBe(true); - }); - - it('should render all quick action buttons', async () => { - wrapper = mount(App); - - // Authenticate first - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - const buttons = wrapper.findAll('.btn-action'); - expect(buttons.length).toBe(6); - - expect(buttons[0].text()).toContain('List Ports'); - expect(buttons[1].text()).toContain('List MCRs'); - expect(buttons[2].text()).toContain('List MVEs'); - expect(buttons[3].text()).toContain('List Locations'); - expect(buttons[4].text()).toContain('Help'); - expect(buttons[5].text()).toContain('Clear'); - }); - - it('should have correct commands for each quick action', async () => { - wrapper = mount(App); - - // Authenticate first - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - const buttons = wrapper.findAll('.btn-action'); - - // Check that each button has the correct @click handler - // We can't directly test the command string, but we can verify buttons exist - expect(buttons[0].text()).toContain('List Ports'); - expect(buttons[1].text()).toContain('List MCRs'); - expect(buttons[2].text()).toContain('List MVEs'); - expect(buttons[3].text()).toContain('List Locations'); - }); - - it('should execute commands when quick action buttons are clicked', async () => { - wrapper = mount(App); - - // Authenticate - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - // Get quick action buttons - buttons are shown after auth - const buttons = wrapper.findAll('.btn-action'); - - // Verify we have the buttons - expect(buttons.length).toBe(6); - expect(buttons[0].text()).toContain('List Ports'); - }); - }); - - describe('Clear Authentication', () => { - it('should show clear auth button when authenticated', async () => { - wrapper = mount(App); - - // Authenticate - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - expect(wrapper.find('.btn-secondary').exists()).toBe(true); - expect(wrapper.find('.btn-secondary').text()).toContain('Clear Auth'); - }); - - it('should return to auth form when clear auth is clicked', async () => { - wrapper = mount(App); - - // Authenticate - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - expect(wrapper.find('.auth-panel').exists()).toBe(false); - expect(wrapper.find('.terminal-section').exists()).toBe(true); - - // Clear auth - const clearButton = wrapper.find('.btn-secondary'); - await clearButton.trigger('click'); - await wrapper.vm.$nextTick(); - - expect(wrapper.find('.auth-panel').exists()).toBe(true); - expect(wrapper.find('.terminal-section').exists()).toBe(false); - }); - - it('should reset form fields when auth is cleared', async () => { - wrapper = mount(App); - - // Authenticate - await wrapper.find('input#accessKey').setValue('my-key'); - await wrapper.find('input#secretKey').setValue('my-secret'); - await wrapper.find('select#environment').setValue('production'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - // Clear auth - await wrapper.find('.btn-secondary').trigger('click'); - await wrapper.vm.$nextTick(); - - expect( - (wrapper.find('input#accessKey').element as HTMLInputElement).value - ).toBe(''); - expect( - (wrapper.find('input#secretKey').element as HTMLInputElement).value - ).toBe(''); - expect( - (wrapper.find('select#environment').element as HTMLSelectElement).value - ).toBe('staging'); - }); - }); - - describe('Status Info', () => { - it('should always show status info panel', () => { - wrapper = mount(App); - - expect(wrapper.find('.status-info').exists()).toBe(true); - expect(wrapper.find('.status-info h3').text()).toBe('WASM Status'); - }); - - it('should display loading and ready status', () => { - wrapper = mount(App); - - const statusItems = wrapper.findAll('.status-item'); - expect(statusItems.length).toBeGreaterThanOrEqual(2); - - // Check for Loading and Ready labels - const labels = wrapper.findAll('.status-item .label'); - const labelTexts = labels.map((l: any) => l.text()); - expect(labelTexts).toContain('Loading:'); - expect(labelTexts).toContain('Ready:'); - }); - - it('should show environment after authentication', async () => { - wrapper = mount(App); - - // Authenticate - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('select#environment').setValue('production'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - const statusItems = wrapper.findAll('.status-item'); - const environmentItem = statusItems.find((item: any) => - item.find('.label').text().includes('Environment') - ); - - expect(environmentItem).toBeDefined(); - }); - }); - - describe('Layout and Structure', () => { - it('should render header with correct title', () => { - wrapper = mount(App); - - expect(wrapper.find('.app-header h1').text()).toContain( - 'Megaport CLI WebAssembly Demo' - ); - expect(wrapper.find('.app-header p').text()).toContain( - 'Vue 3 + Vite + WASM Integration' - ); - }); - - it('should render footer with links', () => { - wrapper = mount(App); - - expect(wrapper.find('.app-footer').exists()).toBe(true); - const links = wrapper.findAll('.app-footer a'); - expect(links.length).toBe(2); - expect(links[0].attributes('href')).toBe('https://github.com/megaport'); - expect(links[1].attributes('href')).toBe('https://docs.megaport.com'); - }); - - it('should have proper CSS classes', () => { - wrapper = mount(App); - - expect(wrapper.find('.app-container').exists()).toBe(true); - expect(wrapper.find('.app-header').exists()).toBe(true); - expect(wrapper.find('.app-main').exists()).toBe(true); - expect(wrapper.find('.app-footer').exists()).toBe(true); - }); - }); - - describe('Terminal Integration', () => { - it('should pass correct props to MegaportTerminal', async () => { - wrapper = mount(App); - - // Authenticate to show terminal - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - const terminal = wrapper.findComponent(MegaportTerminal); - expect(terminal.exists()).toBe(true); - expect(terminal.props('wasmPath')).toBe('/megaport.wasm'); - expect(terminal.props('wasmExecPath')).toBe('/wasm_exec.js'); - expect(terminal.props('welcomeMessage')).toContain( - 'Welcome to Megaport CLI' - ); - }); - - it('should include environment in welcome message', async () => { - wrapper = mount(App); - - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('select#environment').setValue('production'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - const terminal = wrapper.findComponent(MegaportTerminal); - expect(terminal.props('welcomeMessage')).toContain( - 'Environment: production' - ); - }); - }); - - describe('Command Execution Order', () => { - it('should execute commands in correct order: ports, mcr, mve, locations', async () => { - wrapper = mount(App); - - // Authenticate - await wrapper.find('input#accessKey').setValue('key'); - await wrapper.find('input#secretKey').setValue('secret'); - await wrapper.find('form').trigger('submit.prevent'); - await wrapper.vm.$nextTick(); - - const buttons = wrapper.findAll('.btn-action'); - - // Verify button order - expect(buttons[0].text()).toContain('List Ports'); - expect(buttons[1].text()).toContain('List MCRs'); - expect(buttons[2].text()).toContain('List MVEs'); - expect(buttons[3].text()).toContain('List Locations'); - }); - }); -}); diff --git a/frontend-integration/__tests__/MegaportTerminal.test.ts b/frontend-integration/__tests__/MegaportTerminal.test.ts deleted file mode 100644 index 493d96c9..00000000 --- a/frontend-integration/__tests__/MegaportTerminal.test.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { mount } from '@vue/test-utils'; -import { ref } from 'vue'; -import { Terminal } from '@xterm/xterm'; -import { FitAddon } from '@xterm/addon-fit'; -import { WebLinksAddon } from '@xterm/addon-web-links'; - -// IMPORTANT: Hoist all mocks to top of file before component import -// Mock xterm with proper constructor that returns instance -const mockTerminalInstance = { - open: vi.fn(), - write: vi.fn(), - writeln: vi.fn(), - clear: vi.fn(), - dispose: vi.fn(), - loadAddon: vi.fn(), - onKey: vi.fn(), - onData: vi.fn((callback: any) => { - // Store callback for testing - mockTerminalInstance._dataCallback = callback; - }), - focus: vi.fn(), - cols: 80, - _dataCallback: null as any, -}; - -vi.mock('@xterm/xterm', () => ({ - Terminal: vi.fn(function (this: any, options: any) { - Object.assign(this, mockTerminalInstance); - // Real xterm.js exposes `cols` as a live property that reflects the - // current size, not a value frozen at construction time. - Object.defineProperty(this, 'cols', { get: () => mockTerminalInstance.cols, configurable: true }); - this.options = options; - }), -})); - -const mockFitAddon = { - fit: vi.fn(), - dispose: vi.fn(), -}; - -vi.mock('@xterm/addon-fit', () => ({ - FitAddon: vi.fn(function (this: any) { - this.fit = mockFitAddon.fit; - this.dispose = mockFitAddon.dispose; - }), -})); - -const mockWebLinksAddon = { - dispose: vi.fn(), -}; - -vi.mock('@xterm/addon-web-links', () => ({ - WebLinksAddon: vi.fn(function (this: any) { - this.dispose = vi.fn(); - }), -})); - -// Mock composable - must be hoisted before component import -const mockComposable = { - isLoading: ref(false), - isReady: ref(true), - error: ref(null), - execute: vi.fn((cmd: string) => - Promise.resolve({ output: `Executed: ${cmd}`, error: '' }) - ), - setAuth: vi.fn(), - clearAuth: vi.fn(), - getAuthInfo: vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: 'staging', - })), - resetOutput: vi.fn(), - toggleDebug: vi.fn(), -}; - -vi.mock('../composables/useMegaportWASM', () => ({ - useMegaportWASM: vi.fn(() => mockComposable), -})); - -// Import component AFTER all mocks are hoisted -import MegaportTerminal from '../components/MegaportTerminal.vue'; - -describe('MegaportTerminal', () => { - beforeEach(() => { - vi.clearAllMocks(); - // Reset mock composable state to default - mockComposable.isLoading.value = false; - mockComposable.isReady.value = true; - mockComposable.error.value = null; - mockComposable.execute.mockClear(); - mockComposable.setAuth.mockClear(); - }); - - describe('Component Mounting', () => { - it('should mount successfully', () => { - const wrapper = mount(MegaportTerminal); - expect(wrapper.exists()).toBe(true); - }); - - it('should accept props', () => { - const wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/custom.wasm', - wasmExecPath: '/custom_exec.js', - welcomeMessage: 'Custom welcome', - }, - }); - - expect(wrapper.props('wasmPath')).toBe('/custom.wasm'); - expect(wrapper.props('wasmExecPath')).toBe('/custom_exec.js'); - expect(wrapper.props('welcomeMessage')).toBe('Custom welcome'); - }); - - it('should use default props', () => { - const wrapper = mount(MegaportTerminal); - - expect(wrapper.props('wasmPath')).toBe('/megaport.wasm'); - expect(wrapper.props('wasmExecPath')).toBe('/wasm_exec.js'); - }); - - it('should accept custom theme', () => { - const wrapper = mount(MegaportTerminal, { - props: { - theme: { - background: '#000000', - foreground: '#ffffff', - cursor: '#ff0000', - }, - }, - }); - - expect(wrapper.props('theme')).toEqual({ - background: '#000000', - foreground: '#ffffff', - cursor: '#ff0000', - }); - }); - }); - - describe('Loading States', () => { - it('should show loading state when WASM is loading', () => { - // Modify mock state for this test - mockComposable.isLoading.value = true; - mockComposable.isReady.value = false; - mockComposable.error.value = null; - - const wrapper = mount(MegaportTerminal); - - expect(wrapper.find('.terminal-loading').exists()).toBe(true); - expect(wrapper.text()).toContain('Loading Megaport CLI'); - }); - - it('should show error state when WASM fails to load', () => { - // Modify mock state for this test - mockComposable.isLoading.value = false; - mockComposable.isReady.value = false; - mockComposable.error.value = new Error('Failed to load WASM'); - - const wrapper = mount(MegaportTerminal); - - expect(wrapper.find('.terminal-error').exists()).toBe(true); - expect(wrapper.text()).toContain('Failed to load Megaport CLI'); - expect(wrapper.text()).toContain('Failed to load WASM'); - }); - - it('should show retry button on error', () => { - // Modify mock state for this test - mockComposable.isLoading.value = false; - mockComposable.isReady.value = false; - mockComposable.error.value = new Error('Network error'); - - const wrapper = mount(MegaportTerminal); - - const retryButton = wrapper.find('.terminal-error button'); - expect(retryButton.exists()).toBe(true); - expect(retryButton.text()).toBe('Retry'); - }); - - it('should show terminal when ready', () => { - // Reset to ready state - mockComposable.isLoading.value = false; - mockComposable.isReady.value = true; - mockComposable.error.value = null; - - const wrapper = mount(MegaportTerminal); - - expect(wrapper.find('.terminal-wrapper').exists()).toBe(true); - expect(wrapper.find('.terminal-loading').exists()).toBe(false); - expect(wrapper.find('.terminal-error').exists()).toBe(false); - }); - }); - - describe('Terminal Initialization', () => { - it('should initialize terminal on mount', async () => { - mount(MegaportTerminal); - - // Wait for terminal initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - expect(vi.mocked(Terminal)).toHaveBeenCalled(); - }); - - it('should load terminal addons', async () => { - mount(MegaportTerminal); - - // Wait for terminal initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Terminal should load addons when initialized - expect(mockTerminalInstance.loadAddon).toHaveBeenCalled(); - }); - - it('should apply custom theme to terminal', async () => { - mount(MegaportTerminal, { - props: { - theme: { - background: '#123456', - foreground: '#abcdef', - cursor: '#ff00ff', - }, - }, - }); - - // Wait for terminal initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - expect(vi.mocked(Terminal)).toHaveBeenCalledWith( - expect.objectContaining({ - theme: expect.objectContaining({ - background: '#123456', - foreground: '#abcdef', - cursor: '#ff00ff', - }), - }) - ); - }); - }); - - describe('Terminal Width Sync', () => { - afterEach(() => { - delete (window as any).setTerminalWidth; - mockTerminalInstance.cols = 80; - }); - - it('should report the terminal width to WASM on init', async () => { - (window as any).setTerminalWidth = vi.fn(); - - const wrapper = mount(MegaportTerminal); - await new Promise((resolve) => setTimeout(resolve, 200)); - - expect(window.setTerminalWidth).toHaveBeenCalledWith(mockTerminalInstance.cols); - wrapper.unmount(); - }); - - it('should not throw when WASM has not exposed setTerminalWidth yet', async () => { - const wrapper = mount(MegaportTerminal); - await new Promise((resolve) => setTimeout(resolve, 200)); - wrapper.unmount(); - }); - - it('should report the terminal width to WASM again on window resize', async () => { - (window as any).setTerminalWidth = vi.fn(); - - const wrapper = mount(MegaportTerminal); - await new Promise((resolve) => setTimeout(resolve, 200)); - vi.mocked(window.setTerminalWidth as any).mockClear(); - - mockTerminalInstance.cols = 40; - window.dispatchEvent(new Event('resize')); - await new Promise((resolve) => setTimeout(resolve, 250)); - - expect(window.setTerminalWidth).toHaveBeenCalledWith(40); - wrapper.unmount(); - }); - }); - - describe('Component API', () => { - it('should expose executeCommand method', async () => { - const wrapper = mount(MegaportTerminal); - - // Access exposed methods via vm - expect(wrapper.vm).toBeDefined(); - }); - - it('should expose clearTerminal method', () => { - const wrapper = mount(MegaportTerminal); - expect(wrapper.vm).toBeDefined(); - }); - - it('should expose focusTerminal method', () => { - const wrapper = mount(MegaportTerminal); - expect(wrapper.vm).toBeDefined(); - }); - }); - - describe('Cleanup', () => { - it('should cleanup terminal on unmount', () => { - const wrapper = mount(MegaportTerminal); - const disposeSpy = vi.fn(); - - // Mock terminal instance - (wrapper.vm as any).terminal = { - dispose: disposeSpy, - }; - - wrapper.unmount(); - - expect(disposeSpy).toHaveBeenCalled(); - }); - - it('should cleanup fit addon on unmount', async () => { - const wrapper = mount(MegaportTerminal); - - // Wait for terminal initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Verify FitAddon was created - expect(vi.mocked(FitAddon)).toHaveBeenCalled(); - - wrapper.unmount(); - - // The fit addon's dispose should be called on unmount - expect(mockFitAddon.dispose).toHaveBeenCalled(); - }); - }); - - describe('Styling', () => { - it('should have terminal container class', () => { - const wrapper = mount(MegaportTerminal); - expect(wrapper.find('.megaport-terminal-container').exists()).toBe(true); - }); - - it('should apply CSS classes correctly', () => { - const wrapper = mount(MegaportTerminal); - - expect(wrapper.classes()).toContain('megaport-terminal-container'); - }); - }); - - describe('Error Recovery', () => { - it('should handle reload on error', async () => { - // Set error state - mockComposable.isLoading.value = false; - mockComposable.isReady.value = false; - mockComposable.error.value = new Error('Test error'); - - const wrapper = mount(MegaportTerminal); - - const retryButton = wrapper.find('.terminal-error button'); - expect(retryButton.exists()).toBe(true); - - // Mock window.location.reload - const reloadSpy = vi.fn(); - Object.defineProperty(window, 'location', { - value: { reload: reloadSpy }, - writable: true, - }); - - // Clicking retry should trigger reload - await retryButton.trigger('click'); - - // Component should attempt to reload - expect(reloadSpy).toHaveBeenCalled(); - }); - }); - - describe('Accessibility', () => { - it('should have semantic HTML structure', () => { - mockComposable.isLoading.value = false; - mockComposable.isReady.value = true; - mockComposable.error.value = null; - - const wrapper = mount(MegaportTerminal); - expect(wrapper.element.tagName).toBe('DIV'); - }); - - it('should provide loading feedback', () => { - mockComposable.isLoading.value = true; - mockComposable.isReady.value = false; - mockComposable.error.value = null; - - const wrapper = mount(MegaportTerminal); - expect(wrapper.text()).toContain('Loading'); - }); - - it('should provide error feedback', () => { - mockComposable.isLoading.value = false; - mockComposable.isReady.value = false; - mockComposable.error.value = new Error('Test error'); - - const wrapper = mount(MegaportTerminal); - expect(wrapper.text()).toContain('Failed to load'); - }); - }); -}); diff --git a/frontend-integration/__tests__/auth.test.ts b/frontend-integration/__tests__/auth.test.ts deleted file mode 100644 index 7b33cc4e..00000000 --- a/frontend-integration/__tests__/auth.test.ts +++ /dev/null @@ -1,600 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { nextTick } from 'vue'; -import { useMegaportWASM } from '../composables/useMegaportWASM'; -import { mount, VueWrapper } from '@vue/test-utils'; - -describe('Authentication Flow', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('setAuthCredentials', () => { - it('should set authentication credentials with all parameters', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth } = useMegaportWASM(); - setAuth('access-key-123', 'secret-key-456', 'staging'); - - expect(mockSetAuth).toHaveBeenCalledWith( - 'access-key-123', - 'secret-key-456', - 'staging' - ); - }); - - it('should handle production environment', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth } = useMegaportWASM(); - setAuth('prod-access', 'prod-secret', 'production'); - - expect(mockSetAuth).toHaveBeenCalledWith( - 'prod-access', - 'prod-secret', - 'production' - ); - }); - - it('should return success response', () => { - const mockSetAuth = vi.fn(() => ({ success: true, message: 'Auth set' })); - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth } = useMegaportWASM(); - setAuth('key', 'secret', 'staging'); - - expect(mockSetAuth).toHaveReturnedWith({ - success: true, - message: 'Auth set', - }); - }); - }); - - describe('clearAuthCredentials', () => { - it('should clear authentication credentials', () => { - const mockClearAuth = vi.fn(() => ({ success: true })); - (window as any).clearAuthCredentials = mockClearAuth; - - const { clearAuth } = useMegaportWASM(); - clearAuth(); - - expect(mockClearAuth).toHaveBeenCalled(); - }); - - it('should clear auth after being set', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - const mockClearAuth = vi.fn(() => ({ success: true })); - (window as any).setAuthCredentials = mockSetAuth; - (window as any).clearAuthCredentials = mockClearAuth; - - const { setAuth, clearAuth } = useMegaportWASM(); - - setAuth('key', 'secret', 'staging'); - expect(mockSetAuth).toHaveBeenCalled(); - - clearAuth(); - expect(mockClearAuth).toHaveBeenCalled(); - }); - }); - - describe('getAuthInfo', () => { - it('should retrieve authentication information', () => { - const mockAuthInfo = { - accessKeySet: true, - accessKeyPreview: 'acc***', - secretKeySet: true, - secretKeyPreview: 'sec***', - environment: 'staging', - }; - (window as any).debugAuthInfo = vi.fn(() => mockAuthInfo); - - const { getAuthInfo } = useMegaportWASM(); - const info = getAuthInfo(); - - expect(info).toEqual(mockAuthInfo); - }); - - it('should show access key is set', () => { - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'test***', - secretKeySet: true, - secretKeyPreview: 'sec***', - environment: 'production', - })); - - const { getAuthInfo } = useMegaportWASM(); - const info = getAuthInfo(); - - expect(info?.accessKeySet).toBe(true); - expect(info?.accessKeyPreview).toBe('test***'); - }); - - it('should show secret key is set', () => { - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'acc***', - secretKeySet: true, - secretKeyPreview: 'my-sec***', - environment: 'staging', - })); - - const { getAuthInfo } = useMegaportWASM(); - const info = getAuthInfo(); - - expect(info?.secretKeySet).toBe(true); - expect(info?.secretKeyPreview).toBe('my-sec***'); - }); - - it('should return current environment', () => { - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'key***', - secretKeySet: true, - secretKeyPreview: 'sec***', - environment: 'production', - })); - - const { getAuthInfo } = useMegaportWASM(); - const info = getAuthInfo(); - - expect(info?.environment).toBe('production'); - }); - - it('should handle unconfigured auth state', () => { - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: '', - })); - - const { getAuthInfo } = useMegaportWASM(); - const info = getAuthInfo(); - - expect(info?.accessKeySet).toBe(false); - expect(info?.secretKeySet).toBe(false); - expect(info?.environment).toBe(''); - }); - }); - - describe('Authentication State Transitions', () => { - it('should transition from unauthenticated to authenticated', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: '', - })); - - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth, getAuthInfo } = useMegaportWASM(); - - let info = getAuthInfo(); - expect(info?.accessKeySet).toBe(false); - - // Simulate auth state change - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'key***', - secretKeySet: true, - secretKeyPreview: 'sec***', - environment: 'staging', - })); - setAuth('key', 'secret', 'staging'); - - info = getAuthInfo(); - expect(info?.accessKeySet).toBe(true); - }); - - it('should transition from authenticated to unauthenticated', () => { - const mockClearAuth = vi.fn(() => ({ success: true })); - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'key***', - secretKeySet: true, - secretKeyPreview: 'sec***', - environment: 'staging', - })); - - (window as any).clearAuthCredentials = mockClearAuth; - - const { clearAuth, getAuthInfo } = useMegaportWASM(); - - let info = getAuthInfo(); - expect(info?.accessKeySet).toBe(true); - - // Simulate auth clear - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: '', - })); - clearAuth(); - - info = getAuthInfo(); - expect(info?.accessKeySet).toBe(false); - }); - - it('should handle re-authentication with different credentials', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'old***', - secretKeySet: true, - secretKeyPreview: 'old***', - environment: 'staging', - })); - - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth, getAuthInfo } = useMegaportWASM(); - - let info = getAuthInfo(); - expect(info?.accessKeyPreview).toBe('old***'); - expect(info?.environment).toBe('staging'); - - // Simulate re-auth with new credentials - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'new***', - secretKeySet: true, - secretKeyPreview: 'new***', - environment: 'production', - })); - setAuth('new-key', 'new-secret', 'production'); - - info = getAuthInfo(); - expect(info?.accessKeyPreview).toBe('new***'); - expect(info?.environment).toBe('production'); - }); - }); - - describe('Authentication Security', () => { - it('should not expose full credentials in preview', () => { - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - accessKeyPreview: 'abc***xyz', - secretKeySet: true, - secretKeyPreview: 'sec***ret', - environment: 'staging', - })); - - const { getAuthInfo } = useMegaportWASM(); - const info = getAuthInfo(); - - expect(info?.accessKeyPreview).toContain('***'); - expect(info?.secretKeyPreview).toContain('***'); - expect(info?.accessKeyPreview).not.toContain('full-access-key'); - expect(info?.secretKeyPreview).not.toContain('full-secret-key'); - }); - - it('should handle auth with password-type inputs', () => { - // This simulates that credentials are entered in password fields - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth } = useMegaportWASM(); - setAuth('sensitive-key', 'sensitive-secret', 'production'); - - expect(mockSetAuth).toHaveBeenCalledWith( - 'sensitive-key', - 'sensitive-secret', - 'production' - ); - }); - }); - - describe('Environment Validation', () => { - it('should accept staging environment', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth } = useMegaportWASM(); - setAuth('key', 'secret', 'staging'); - - expect(mockSetAuth).toHaveBeenCalledWith('key', 'secret', 'staging'); - }); - - it('should accept production environment', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - (window as any).setAuthCredentials = mockSetAuth; - - const { setAuth } = useMegaportWASM(); - setAuth('key', 'secret', 'production'); - - expect(mockSetAuth).toHaveBeenCalledWith('key', 'secret', 'production'); - }); - }); - - describe('Error Handling', () => { - it('should handle missing setAuthCredentials function', () => { - (window as any).setAuthCredentials = undefined; - - const { setAuth } = useMegaportWASM(); - - // Should not throw error - expect(() => { - setAuth('key', 'secret', 'staging'); - }).not.toThrow(); - }); - - it('should handle missing clearAuthCredentials function', () => { - (window as any).clearAuthCredentials = undefined; - - const { clearAuth } = useMegaportWASM(); - - // Should not throw error - expect(() => { - clearAuth(); - }).not.toThrow(); - }); - - it('should handle missing debugAuthInfo', () => { - (window as any).debugAuthInfo = undefined; - - const { getAuthInfo } = useMegaportWASM(); - - // Should not throw error and return undefined - expect(() => { - getAuthInfo(); - }).not.toThrow(); - }); - }); - - describe('Token Authentication (setAuthToken)', () => { - it('should set authentication using portal token with hostname', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production', hostname: 'portal.megaport.com' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('portal-session-token-12345', 'portal.megaport.com'); - - expect(mockSetAuthToken).toHaveBeenCalledWith( - 'portal-session-token-12345', - 'portal.megaport.com' - ); - }); - - it('should work with staging hostname', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'staging', hostname: 'portal-staging.megaport.com' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('staging-token', 'portal-staging.megaport.com'); - - expect(mockSetAuthToken).toHaveBeenCalledWith('staging-token', 'portal-staging.megaport.com'); - }); - - it('should handle JWT-style tokens', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - const jwtToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.test'; - setAuthToken(jwtToken, 'portal.megaport.com'); - - expect(mockSetAuthToken).toHaveBeenCalledWith(jwtToken, 'portal.megaport.com'); - }); - - it('should keep the 2-argument call shape when environment and expiry are omitted', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('token', 'portal.megaport.com'); - - expect(mockSetAuthToken.mock.calls[0]).toEqual(['token', 'portal.megaport.com']); - }); - - it('should forward an explicit environment as the 3rd argument when expiry is omitted', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'qa' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('token', 'portal.megaport.com', 'qa'); - - expect(mockSetAuthToken.mock.calls[0]).toEqual(['token', 'portal.megaport.com', 'qa']); - }); - - it('should forward expiry as the 4th argument, with environment slotted in as undefined, when only expiry is given', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('token', 'portal.megaport.com', undefined, 1700000000000); - - expect(mockSetAuthToken.mock.calls[0]).toEqual([ - 'token', - 'portal.megaport.com', - undefined, - 1700000000000, - ]); - }); - - it('should forward both environment and expiry when both are given', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'qa' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('token', 'portal.megaport.com', 'qa', '2026-07-06T00:00:00Z'); - - expect(mockSetAuthToken.mock.calls[0]).toEqual([ - 'token', - 'portal.megaport.com', - 'qa', - '2026-07-06T00:00:00Z', - ]); - }); - - it('should return success response with environment', () => { - const mockSetAuthToken = vi.fn(() => ({ - success: true, - environment: 'production', - hostname: 'portal.megaport.com', - })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('token', 'portal.megaport.com'); - - expect(mockSetAuthToken).toHaveReturnedWith({ - success: true, - environment: 'production', - hostname: 'portal.megaport.com', - }); - }); - - it('should handle missing setAuthToken function', () => { - (window as any).setAuthToken = undefined; - - const { setAuthToken } = useMegaportWASM(); - - // Should not throw error - expect(() => { - setAuthToken('token', 'portal.megaport.com'); - }).not.toThrow(); - }); - - it('should call debugAuthInfo after setting token', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - const mockDebugAuthInfo = vi.fn(() => ({ - accessTokenSet: true, - accessTokenPreview: 'por***45', - authMethod: 'token', - environment: 'production', - })); - (window as any).setAuthToken = mockSetAuthToken; - (window as any).debugAuthInfo = mockDebugAuthInfo; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('portal-token', 'portal.megaport.com'); - - expect(mockDebugAuthInfo).toHaveBeenCalled(); - }); - - it('should show token preview is masked in auth info', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).setAuthToken = mockSetAuthToken; - (window as any).debugAuthInfo = vi.fn(() => ({ - accessTokenSet: true, - accessTokenPreview: 'tok***123', - authMethod: 'token', - environment: 'production', - })); - - const { setAuthToken, getAuthInfo } = useMegaportWASM(); - setAuthToken('token-123456789', 'portal.megaport.com'); - - const info = getAuthInfo(); - expect(info?.accessTokenPreview).toContain('***'); - expect(info?.accessTokenPreview).not.toBe('token-123456789'); - }); - - it('should indicate token auth method', () => { - (window as any).setAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).debugAuthInfo = vi.fn(() => ({ - accessTokenSet: true, - accessTokenPreview: 'tok***', - authMethod: 'token', - environment: 'production', - })); - - const { setAuthToken, getAuthInfo } = useMegaportWASM(); - setAuthToken('test-token', 'portal.megaport.com'); - - const info = getAuthInfo(); - expect(info?.authMethod).toBe('token'); - }); - - it('should handle error response from setAuthToken', () => { - const mockSetAuthToken = vi.fn(() => ({ - success: false, - error: 'Invalid token format', - })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - - // Should not throw, but log error - expect(() => { - setAuthToken('invalid-token', 'portal.megaport.com'); - }).not.toThrow(); - - expect(mockSetAuthToken).toHaveBeenCalled(); - }); - - it('should map localhost to development environment', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'development', hostname: 'localhost' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('dev-token', 'localhost'); - - expect(mockSetAuthToken).toHaveBeenCalledWith('dev-token', 'localhost'); - }); - - it('should map QA hostname to development environment', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'development', hostname: 'portal-qa.megaport.com' })); - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuthToken } = useMegaportWASM(); - setAuthToken('qa-token', 'portal-qa.megaport.com'); - - expect(mockSetAuthToken).toHaveBeenCalledWith('qa-token', 'portal-qa.megaport.com'); - }); - }); - - describe('Token vs API Key Authentication', () => { - it('should support both token and API key auth methods', () => { - const mockSetAuth = vi.fn(() => ({ success: true })); - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).setAuthCredentials = mockSetAuth; - (window as any).setAuthToken = mockSetAuthToken; - - const { setAuth, setAuthToken } = useMegaportWASM(); - - // Both methods should be available - setAuth('api-key', 'api-secret', 'staging'); - expect(mockSetAuth).toHaveBeenCalled(); - - setAuthToken('portal-token', 'portal.megaport.com'); - expect(mockSetAuthToken).toHaveBeenCalled(); - }); - - it('should show different auth methods in debug info', () => { - const mockSetAuthToken = vi.fn(() => ({ success: true, environment: 'production' })); - (window as any).setAuthToken = mockSetAuthToken; - (window as any).debugAuthInfo = vi.fn(() => ({ - accessTokenSet: true, - authMethod: 'token', - environment: 'production', - })); - - const { setAuthToken, getAuthInfo } = useMegaportWASM(); - setAuthToken('token', 'portal.megaport.com'); - - const info = getAuthInfo(); - expect(info?.authMethod).toBe('token'); - - // Simulate switching to API key - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: true, - secretKeySet: true, - authMethod: 'apikey', - environment: 'staging', - })); - - const info2 = getAuthInfo(); - expect(info2?.authMethod).toBe('apikey'); - }); - }); -}); diff --git a/frontend-integration/__tests__/improvements.test.ts b/frontend-integration/__tests__/improvements.test.ts deleted file mode 100644 index 5aae21d0..00000000 --- a/frontend-integration/__tests__/improvements.test.ts +++ /dev/null @@ -1,1119 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import MegaportTerminal from '../components/MegaportTerminal.vue'; -import App from '../demo/App.vue'; - -/** - * Frontend Improvements and Maintainability Tests - * - * Tests for key features that improve code quality and maintainability: - * - Error handling and resilience - * - WASM initialization with timeout handling - * - Clean debug logging patterns - * - Proper resource cleanup - * - Retry logic for robustness - */ - -describe('Frontend Improvements and Maintainability', () => { - let wrapper: any; - let consoleLogSpy: any; - let consoleWarnSpy: any; - let consoleErrorSpy: any; - - beforeEach(() => { - vi.clearAllMocks(); - // Spy on console methods to verify debug logs are properly controlled - consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - consoleLogSpy.mockRestore(); - consoleWarnSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - - describe('Error Handling and Resilience', () => { - it('should catch and handle component errors in MegaportTerminal', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // Verify component has error capture capability - expect(wrapper.vm).toBeDefined(); - - // Component should have error handling mechanism (onErrorCaptured) - // This is tested by checking that errors don't crash the component - const instance = wrapper.vm; - expect(instance).toBeTruthy(); - }); - - it('should display error UI when WASM fails to load', async () => { - // Mock fetch to fail for WASM loading - global.fetch = vi - .fn() - .mockRejectedValue(new Error('Failed to load WASM')); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // Wait for error state to be set - await new Promise((resolve) => setTimeout(resolve, 100)); - await nextTick(); - - // Check if error is displayed (component may show loading or error state) - // The component should gracefully handle the error - expect(wrapper.vm).toBeDefined(); - }); - - it('should provide retry capability when error occurs', async () => { - // Mock fetch to fail - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 100)); - await nextTick(); - - // Look for reload/retry button in error state - const html = wrapper.html(); - - // Component should either show a retry button or have reload functionality - // Testing that error state is reachable - expect(wrapper.vm).toBeDefined(); - }); - - it('should not crash when terminal operations fail', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // Try to execute command before WASM is ready - should handle gracefully - const executeMethod = wrapper.vm.execute; - - if (executeMethod) { - try { - await executeMethod('test command'); - // Should either succeed or throw gracefully - expect(true).toBe(true); - } catch (error) { - // Error should be caught and handled, not crash the app - expect(error).toBeDefined(); - } - } - }); - - it('should handle errors in App component gracefully', async () => { - wrapper = mount(App); - - // App should render without errors - expect(wrapper.find('.app-container').exists()).toBe(true); - - // Test that app doesn't crash with invalid operations - const authForm = wrapper.find('form'); - expect(authForm.exists()).toBe(true); - }); - }); - - describe('WASM Initialization with Timeout Handling', () => { - it('should have a timeout configuration for WASM initialization', async () => { - // The composable should accept initTimeout configuration - // Default timeout should be 30 seconds (30000ms) - const DEFAULT_TIMEOUT = 30000; - - // This tests that the timeout mechanism exists - // The actual timeout value is checked by examining the composable's config - expect(DEFAULT_TIMEOUT).toBe(30000); - }); - - it('should fail gracefully when WASM loading exceeds timeout', async () => { - // Mock a slow WASM load - global.fetch = vi.fn().mockImplementation( - () => new Promise((resolve) => setTimeout(resolve, 35000)) // Longer than 30s timeout - ); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // Component should handle timeout and show error - // Wait a bit to let initialization attempt - await new Promise((resolve) => setTimeout(resolve, 100)); - await nextTick(); - - expect(wrapper.vm).toBeDefined(); - // Component should be in loading or error state, not crashed - }); - - it('should display timeout error message to user', async () => { - // Mock timeout scenario - global.fetch = vi - .fn() - .mockImplementation( - () => - new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), 100) - ) - ); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - await nextTick(); - - // Check that component handles error state - // Should show error UI or message - const html = wrapper.html(); - expect(wrapper.vm).toBeDefined(); - }); - - it('should allow configuration of custom timeout value', () => { - // Test that custom timeout can be configured - // Default is 30000, but should be configurable - const customTimeout = 60000; - - // This validates the config structure supports timeout - expect(customTimeout).toBeGreaterThan(0); - expect(typeof customTimeout).toBe('number'); - }); - }); - - describe('Clean Debug Logging Patterns', () => { - it('should not log debug messages when debug mode is disabled', async () => { - // Mount with debug disabled (default in production) - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // In production mode, debug logs should be minimal - // The composable uses debug=true by default in the component, but should support debug=false - // Check that debug emoji logs are controlled (not excessive) - const debugLogCalls = consoleLogSpy.mock.calls.filter((call: any[]) => - call.some( - (arg) => - typeof arg === 'string' && - (arg.includes('🚀') || arg.includes('✅') || arg.includes('📦')) - ) - ); - - // In production builds, debug mode should be configurable to disable logs - // For now, verify the mechanism exists (the log helper in composable) - // A production build would set debug: false - // Note: Currently the component uses debug: true, so we allow some logs - expect(debugLogCalls.length).toBeLessThanOrEqual(10); // Reasonable limit for controlled logging - }); - - it('should only log debug messages when debug mode is explicitly enabled', async () => { - // When debug is true, logs are allowed - // When debug is false (default), logs should be suppressed - - // Test the conditional logging behavior - const debugMode = false; // Production default - - if (debugMode) { - expect(consoleLogSpy).toHaveBeenCalled(); - } else { - // In non-debug mode, debug logs should not appear - // This is controlled by the 'log' helper function in the composable - expect(true).toBe(true); // Placeholder - actual test in composable - } - }); - - it('should always log errors regardless of debug mode', async () => { - // Errors should always be logged for production debugging - // Mock an error scenario - global.fetch = vi.fn().mockRejectedValue(new Error('Test error')); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 100)); - await nextTick(); - - // Errors should be logged even in production - // consoleErrorSpy should have been called - expect(consoleErrorSpy).toHaveBeenCalled(); - }); - - it('should strip debug logs in production build', () => { - // Verify that the debug parameter defaults to false - // This ensures production builds don't include debug output - const productionDebugDefault = false; - - expect(productionDebugDefault).toBe(false); - }); - - it('should have conditional debug logging in composable', () => { - // Test that the composable uses conditional logging - // The 'log' helper should only output when debug=true - - const mockDebug = false; - const log = (message: string) => { - if (mockDebug) { - console.log(message); - } - }; - - // Clear previous calls - consoleLogSpy.mockClear(); - - // This should not log - log('Test message'); - - expect(consoleLogSpy).not.toHaveBeenCalled(); - }); - }); - - describe('Proper Resource Cleanup', () => { - it('should clean up terminal resources on unmount', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Verify component is mounted - expect(wrapper.vm).toBeDefined(); - - // Unmount the component - wrapper.unmount(); - - // After unmount, component should be cleaned up - expect(wrapper.vm).toBeDefined(); // vm still exists but should have cleaned up - }); - - it('should terminate worker on unmount', async () => { - // Create a mock worker - const mockWorker = { - terminate: vi.fn(), - postMessage: vi.fn(), - addEventListener: vi.fn(), - }; - - // Mock worker constructor - global.Worker = vi.fn().mockImplementation(() => mockWorker); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Unmount should trigger cleanup - wrapper.unmount(); - - // Worker terminate should have been called if worker was used - // Note: Default is useWorker=false, so this tests the cleanup path - expect(true).toBe(true); // Cleanup function exists - }); - - it('should clear active spinners on unmount', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Unmount - wrapper.unmount(); - - // Active spinners should be cleared - // This is handled by the cleanup function in the composable - expect(wrapper.vm).toBeDefined(); - }); - - it('should clear auth credentials on unmount', async () => { - // Mock window functions - const mockClearAuthCredentials = vi.fn(); - (window as any).clearAuthCredentials = mockClearAuthCredentials; - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Unmount should trigger cleanup - wrapper.unmount(); - - // Auth credentials should be cleared for security - // This prevents credentials from lingering in memory - expect(mockClearAuthCredentials).toHaveBeenCalled(); - - // Cleanup - delete (window as any).clearAuthCredentials; - }); - - it('should dispose terminal addons on unmount', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Terminal and addons should exist - const terminalRef = wrapper.vm.terminal; - const fitAddonRef = wrapper.vm.fitAddon; - - // Unmount - wrapper.unmount(); - - // Dispose should have been called on terminal components - // This prevents memory leaks from xterm.js - expect(wrapper.vm).toBeDefined(); - }); - - it('should remove global window functions on cleanup', async () => { - // Set up global functions - (window as any).wasmStartSpinner = vi.fn(); - (window as any).wasmStopSpinner = vi.fn(); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Unmount should clean up global functions - wrapper.unmount(); - - // Wait for cleanup - await nextTick(); - - // Global functions should be removed - expect((window as any).wasmStartSpinner).toBeUndefined(); - expect((window as any).wasmStopSpinner).toBeUndefined(); - }); - - it('should clear resize timeout on unmount', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await nextTick(); - - // Trigger resize (which sets a timeout) - window.dispatchEvent(new Event('resize')); - - // Unmount should clear pending timeouts - wrapper.unmount(); - - // No way to directly test timeout clearing, but verify unmount doesn't crash - expect(wrapper.vm).toBeDefined(); - }); - }); - - describe('Retry Logic for Robustness', () => { - it('should have retry configuration with maxRetries', () => { - // Default should be 3 retries - const DEFAULT_MAX_RETRIES = 3; - - expect(DEFAULT_MAX_RETRIES).toBe(3); - expect(DEFAULT_MAX_RETRIES).toBeGreaterThan(0); - }); - - it('should have retry configuration with retryDelay', () => { - // Default should be 1000ms (1 second) - const DEFAULT_RETRY_DELAY = 1000; - - expect(DEFAULT_RETRY_DELAY).toBe(1000); - expect(DEFAULT_RETRY_DELAY).toBeGreaterThan(0); - }); - - it('should retry WASM initialization on failure', async () => { - let attemptCount = 0; - - // Mock fetch to fail twice, then succeed - global.fetch = vi.fn().mockImplementation(() => { - attemptCount++; - if (attemptCount < 3) { - return Promise.reject(new Error('Network error')); - } - return Promise.resolve({ - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - }); - }); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // Wait for retries to complete - await new Promise((resolve) => setTimeout(resolve, 500)); - await nextTick(); - - // Should have attempted multiple times - expect(attemptCount).toBeGreaterThan(1); - }); - - it('should use exponential backoff for retries', async () => { - // Test that retry delays increase exponentially - const baseDelay = 1000; - const attempt1Delay = baseDelay * Math.pow(2, 0); // 1000ms - const attempt2Delay = baseDelay * Math.pow(2, 1); // 2000ms - const attempt3Delay = baseDelay * Math.pow(2, 2); // 4000ms - - expect(attempt1Delay).toBe(1000); - expect(attempt2Delay).toBe(2000); - expect(attempt3Delay).toBe(4000); - }); - - it('should fail after max retries exceeded', async () => { - const maxRetries = 3; - let attemptCount = 0; - - // Mock fetch to always fail - global.fetch = vi.fn().mockImplementation(() => { - attemptCount++; - return Promise.reject(new Error('Persistent network error')); - }); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // Wait for all retries to complete - await new Promise((resolve) => setTimeout(resolve, 1000)); - await nextTick(); - - // Should have attempted maxRetries times - expect(attemptCount).toBeGreaterThanOrEqual(1); - - // Component should be in error state - expect(wrapper.vm).toBeDefined(); - }); - - it('should log retry attempts in debug mode', async () => { - let attemptCount = 0; - - // Mock fetch to fail twice - global.fetch = vi.fn().mockImplementation(() => { - attemptCount++; - if (attemptCount < 3) { - return Promise.reject(new Error('Network error')); - } - return Promise.resolve({ - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - }); - }); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 500)); - await nextTick(); - - // In debug mode, retry attempts should be logged - // In production mode (debug=false), only errors should be logged - expect(consoleErrorSpy).toHaveBeenCalled(); - }); - - it('should provide final error message after all retries fail', async () => { - const maxRetries = 3; - - // Mock fetch to always fail - global.fetch = vi.fn().mockRejectedValue(new Error('Test error')); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - await nextTick(); - - // Final error should mention retry count - expect(consoleErrorSpy).toHaveBeenCalled(); - - // Error message should be informative - const errorCalls = consoleErrorSpy.mock.calls; - const hasRetryMessage = errorCalls.some((call: any[]) => - call.some( - (arg) => - typeof arg === 'string' && - (arg.includes('retry') || - arg.includes('retries') || - arg.includes('attempt')) - ) - ); - - expect(hasRetryMessage).toBe(true); - }); - - it('should allow custom retry configuration', () => { - // Test that custom retry config is supported - const customConfig = { - maxRetries: 5, - retryDelay: 2000, - }; - - expect(customConfig.maxRetries).toBe(5); - expect(customConfig.retryDelay).toBe(2000); - expect(customConfig.maxRetries).toBeGreaterThan(0); - expect(customConfig.retryDelay).toBeGreaterThan(0); - }); - - it('should reset state between retry attempts', async () => { - let attemptCount = 0; - - // Mock fetch to fail once then succeed - global.fetch = vi.fn().mockImplementation(() => { - attemptCount++; - if (attemptCount === 1) { - return Promise.reject(new Error('First attempt fails')); - } - return Promise.resolve({ - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - }); - }); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 500)); - await nextTick(); - - // Should have retried and succeeded - expect(attemptCount).toBeGreaterThanOrEqual(1); - expect(wrapper.vm).toBeDefined(); - }); - }); - - describe('Complete Feature Integration', () => { - it('should handle complete failure scenario gracefully', async () => { - // Simulate complete WASM failure - global.fetch = vi.fn().mockRejectedValue(new Error('Complete failure')); - (window as any).Go = undefined; - - wrapper = mount(App); - - await nextTick(); - - // App should still render - expect(wrapper.find('.app-container').exists()).toBe(true); - - // Should show auth form - expect(wrapper.find('.auth-panel').exists()).toBe(true); - }); - - it('should successfully initialize with all critical features', async () => { - // Mock successful WASM load - global.fetch = vi.fn().mockResolvedValue({ - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - }); - - (window as any).Go = vi.fn().mockImplementation(() => ({ - run: vi.fn(), - importObject: {}, - })); - - (window as any).executeMegaportCommandAsync = vi.fn(); - (window as any).clearAuthCredentials = vi.fn(); - - wrapper = mount(App); - - await nextTick(); - - // App should render successfully - expect(wrapper.find('.app-container').exists()).toBe(true); - - // Should have all production features: - // 1. Error boundaries (component doesn't crash) - expect(wrapper.vm).toBeDefined(); - - // 2. Timeout configured (tested via composable defaults) - // 3. Debug logs controlled (tested via spy) - // 4. Cleanup available (tested via unmount) - // 5. Retry logic (tested via multiple attempts) - }); - - it('should clean up all resources on app unmount', async () => { - const mockClearAuth = vi.fn(); - (window as any).clearAuthCredentials = mockClearAuth; - (window as any).wasmStartSpinner = vi.fn(); - (window as any).wasmStopSpinner = vi.fn(); - - wrapper = mount(App); - await nextTick(); - - // Unmount the entire app - wrapper.unmount(); - - // All cleanup should happen - expect(mockClearAuth).toHaveBeenCalled(); - - // Global functions should be cleaned - delete (window as any).clearAuthCredentials; - delete (window as any).wasmStartSpinner; - delete (window as any).wasmStopSpinner; - }); - }); - - describe('Telemetry Hooks', () => { - it('should emit telemetry events when callback is provided', async () => { - const telemetryEvents: any[] = []; - - // Ensure window.Go is available - class MockGo { - run = vi.fn(); - importObject = {}; - } - (window as any).Go = MockGo; - - // Import composable directly to test telemetry - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - const { isReady, execute, error } = useMegaportWASM({ - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - onTelemetry: (event) => { - console.log('Telemetry event received:', event); - telemetryEvents.push(event); - }, - }); - - // Wait for initialization (may succeed or fail in test environment) - await new Promise((resolve) => setTimeout(resolve, 500)); - await nextTick(); - - console.log('Total telemetry events:', telemetryEvents.length); - console.log('isReady:', isReady.value); - console.log('error:', error.value); - - // If no telemetry events at all, the callback mechanism might not be working - // This could happen if onMounted doesn't fire in the test context - // In that case, we should test that the callback is at least defined - if (telemetryEvents.length === 0) { - // Test passes if we can at least verify the telemetry function works - // by calling it directly (testing the mechanism, not the lifecycle) - expect(true).toBe(true); - return; - } - - // If we do get events, validate them - const initEvents = telemetryEvents.filter( - (e) => - e.type === 'wasm_init_start' || - e.type === 'wasm_init_success' || - e.type === 'wasm_init_error' - ); - - expect(initEvents.length).toBeGreaterThan(0); - - // All events should have proper structure - telemetryEvents.forEach((event) => { - expect(event).toHaveProperty('type'); - expect(event).toHaveProperty('timestamp'); - expect(typeof event.timestamp).toBe('number'); - }); - }); - - it('should track command execution duration', async () => { - const telemetryEvents: any[] = []; - - // Ensure window.Go is available - class MockGo { - run = vi.fn(); - importObject = {}; - } - (window as any).Go = MockGo; - - (window as any).executeMegaportCommandAsync = vi.fn((cmd, callback) => { - setTimeout(() => { - callback({ output: 'test', error: '' }); - }, 50); - }); - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - const { execute, isReady } = useMegaportWASM({ - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - onTelemetry: (event) => { - telemetryEvents.push(event); - }, - }); - - // Wait for WASM to be ready - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Skip test if WASM didn't initialize - if (!isReady.value) { - console.warn('WASM not ready, skipping test'); - return; - } - - // Execute a command - await execute('test command'); - - // Should have command execution events with duration - const executeEvents = telemetryEvents.filter( - (e) => e.type === 'command_execute_success' - ); - - if (executeEvents.length > 0) { - expect(executeEvents[0]).toHaveProperty('duration'); - expect(typeof executeEvents[0].duration).toBe('number'); - expect(executeEvents[0].duration).toBeGreaterThan(0); - } - }); - - it('should emit auth telemetry events', async () => { - const telemetryEvents: any[] = []; - - (window as any).setAuthCredentials = vi.fn(() => ({ success: true })); - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - const { setAuth } = useMegaportWASM({ - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - onTelemetry: (event) => { - telemetryEvents.push(event); - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Set auth - setAuth('key', 'secret', 'staging'); - - // Should have auth_set event - const authEvents = telemetryEvents.filter((e) => e.type === 'auth_set'); - expect(authEvents.length).toBeGreaterThan(0); - expect(authEvents[0].metadata).toHaveProperty('environment', 'staging'); - }); - - it('should not emit telemetry when callback not provided', async () => { - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - // No onTelemetry callback - should not throw - const { execute } = useMegaportWASM({ - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Should work without telemetry callback - expect(execute).toBeDefined(); - }); - }); - - describe('Lazy Load xterm CSS', () => { - it('should not load xterm CSS until terminal is initialized', async () => { - // Check that no xterm CSS link exists initially - const initialLinks = document.querySelectorAll('link[href*="xterm.css"]'); - expect(initialLinks.length).toBe(0); - - // Mount terminal component - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - // CSS loading happens when terminal initializes - // In test environment, this may not fully execute due to happy-dom limitations - // but we can verify the component doesn't fail - expect(wrapper.vm).toBeDefined(); - }); - - it('should handle CSS loading errors gracefully', async () => { - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - await nextTick(); - - // Component should handle CSS load failures without crashing - expect(wrapper.vm).toBeDefined(); - }); - - it('should prevent duplicate CSS loading', async () => { - // Create a mock xterm CSS link - const mockLink = document.createElement('link'); - mockLink.rel = 'stylesheet'; - mockLink.href = - 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css'; - document.head.appendChild(mockLink); - - wrapper = mount(MegaportTerminal, { - props: { - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Should detect existing CSS and not add duplicate - const xtermLinks = document.querySelectorAll('link[href*="xterm.css"]'); - - // Cleanup - mockLink.remove(); - - // Component should still work - expect(wrapper.vm).toBeDefined(); - }); - }); - - describe('Type Guards for Runtime Checks', () => { - it('should validate command strings before execution', async () => { - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - (window as any).executeMegaportCommandAsync = vi.fn(); - - const { execute } = useMegaportWASM({ - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Valid command should work - try { - await execute('port list'); - } catch (e) { - // May fail due to test environment, but shouldn't be validation error - } - - // Invalid commands should be rejected - await expect(execute('')).rejects.toThrow('Invalid command'); - await expect(execute(' ')).rejects.toThrow('Invalid command'); - }); - - it('should validate WASM command results', async () => { - // Ensure window.Go is available - class MockGo { - run = vi.fn(); - importObject = {}; - } - (window as any).Go = MockGo; - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - // Mock WASM function that returns invalid result - (window as any).executeMegaportCommandAsync = vi.fn((cmd, callback) => { - callback({ invalid: 'result' }); // Invalid structure - }); - - const { execute, isReady } = useMegaportWASM({ - wasmPath: '/megaport.wasm', - wasmExecPath: '/wasm_exec.js', - }); - - // Wait for WASM to be ready - await new Promise((resolve) => setTimeout(resolve, 200)); - - // Skip test if WASM didn't initialize - if (!isReady.value) { - console.warn('WASM not ready, skipping test'); - return; - } - - // Should reject invalid results - await expect(execute('test')).rejects.toThrow('Invalid command result'); - }); - - it('should use type guards from utility module', async () => { - const { - isMegaportCommandResult, - isMegaportPromptRequest, - hasWASMFunctions, - hasWebAssemblySupport, - isValidCommand, - getErrorMessage, - } = await import('../utils/type-guards'); - - // Test command result validation - expect(isMegaportCommandResult({ output: 'test' })).toBe(true); - expect(isMegaportCommandResult({ error: 'error' })).toBe(true); - expect(isMegaportCommandResult({ output: 'test', error: 'error' })).toBe( - true - ); - expect(isMegaportCommandResult({})).toBe(false); - expect(isMegaportCommandResult({ invalid: 'data' })).toBe(false); - - // Test prompt request validation - expect( - isMegaportPromptRequest({ - id: '123', - message: 'Enter value', - type: 'text', - }) - ).toBe(true); - expect(isMegaportPromptRequest({ id: '123' })).toBe(false); - - // Test WASM functions detection - only the async entrypoint counts; - // the deprecated sync stub alone should not report WASM as ready - delete (window as any).executeMegaportCommandAsync; - (window as any).executeMegaportCommand = () => {}; - expect(hasWASMFunctions(window)).toBe(false); - delete (window as any).executeMegaportCommand; - - (window as any).executeMegaportCommandAsync = () => {}; - expect(hasWASMFunctions(window)).toBe(true); - - // Test WebAssembly support - expect(hasWebAssemblySupport()).toBe(true); - - // Test command validation - expect(isValidCommand('port list')).toBe(true); - expect(isValidCommand('vxc create')).toBe(true); - expect(isValidCommand('')).toBe(false); - expect(isValidCommand(' ')).toBe(false); - expect(isValidCommand('rm -rf /')).toBe(false); - expect(isValidCommand('eval(malicious)')).toBe(false); - - // Test error message extraction - expect(getErrorMessage(new Error('test'))).toBe('test'); - expect(getErrorMessage('string error')).toBe('string error'); - expect(getErrorMessage({ custom: 'error' })).toContain('Object'); - }); - - it('should validate telemetry event types at runtime, including auth_token_set', async () => { - const { isTelemetryEventType } = await import('../utils/type-guards'); - - expect(isTelemetryEventType('auth_set')).toBe(true); - expect(isTelemetryEventType('auth_clear')).toBe(true); - expect(isTelemetryEventType('auth_token_set')).toBe(true); - expect(isTelemetryEventType('not_a_real_event')).toBe(false); - expect(isTelemetryEventType(123)).toBe(false); - }); - - it('should check for dangerous command patterns', async () => { - const { isValidCommand } = await import('../utils/type-guards'); - - // Should block dangerous patterns - expect(isValidCommand('rm -rf /')).toBe(false); - expect(isValidCommand(':(){ :|:& };:')).toBe(false); // fork bomb - expect(isValidCommand('eval(code)')).toBe(false); - expect(isValidCommand('')).toBe(false); - - // Should allow safe commands - expect(isValidCommand('port list')).toBe(true); - expect(isValidCommand('vxc create --name test')).toBe(true); - expect(isValidCommand('location list --output json')).toBe(true); - }); - - it('should validate prompt requests at runtime', async () => { - const { isMegaportPromptRequest } = await import('../utils/type-guards'); - - // Valid prompt request - expect( - isMegaportPromptRequest({ - id: 'prompt-123', - message: 'Enter port name', - type: 'text', - resourceType: 'port', - }) - ).toBe(true); - - // Missing required fields - expect(isMegaportPromptRequest({ id: '123' })).toBe(false); - expect(isMegaportPromptRequest({ message: 'test' })).toBe(false); - expect(isMegaportPromptRequest({ type: 'text' })).toBe(false); - - // Invalid types - expect( - isMegaportPromptRequest({ id: 123, message: 'test', type: 'text' }) - ).toBe(false); - expect( - isMegaportPromptRequest({ id: '123', message: 123, type: 'text' }) - ).toBe(false); - }); - }); -}); diff --git a/frontend-integration/__tests__/integration.test.ts b/frontend-integration/__tests__/integration.test.ts deleted file mode 100644 index ed8032d8..00000000 --- a/frontend-integration/__tests__/integration.test.ts +++ /dev/null @@ -1,549 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { mount } from '@vue/test-utils'; -import { nextTick, ref, defineComponent } from 'vue'; - -/** - * Integration tests for the complete WASM workflow - * Tests the interaction between components, composables, and workers - */ - -// Setup comprehensive mocks - must be synchronous and hoisted -const mockTerminal = { - open: vi.fn(), - write: vi.fn(), - writeln: vi.fn(), - clear: vi.fn(), - dispose: vi.fn(), - loadAddon: vi.fn(), - onKey: vi.fn(), - onData: vi.fn(), - focus: vi.fn(), -}; - -vi.mock('@xterm/xterm', () => ({ - Terminal: vi.fn(function (this: any) { - Object.assign(this, mockTerminal); - }), -})); - -vi.mock('@xterm/addon-fit', () => ({ - FitAddon: vi.fn(function (this: any) { - this.fit = vi.fn(); - this.dispose = vi.fn(); - }), -})); - -vi.mock('@xterm/addon-web-links', () => ({ - WebLinksAddon: vi.fn(function (this: any) { - this.dispose = vi.fn(); - }), -})); - -// Helper to wait for WASM ready state -const waitForReady = async (isReady: any, timeout = 500) => { - const start = Date.now(); - while (!isReady.value && Date.now() - start < timeout) { - await new Promise((resolve) => setTimeout(resolve, 50)); - } - if (!isReady.value) { - throw new Error('WASM did not become ready in time'); - } -}; - -// Helper to create wrapper component for composable testing -const createTestWrapper = (setupFn: () => any) => { - const TestComponent = defineComponent({ - template: '
', - setup: setupFn, - }); - return mount(TestComponent); -}; - -describe('WASM Integration Tests', () => { - beforeEach(() => { - vi.clearAllMocks(); - localStorage.clear(); - }); - - describe('Complete Workflow: Auth + Command Execution', () => { - it('should authenticate and execute commands', async () => { - const mockResult = { output: 'Port list result', error: '' }; - ((global as any).executeMegaportCommandAsync as any).mockImplementation( - (cmd: string, callback: Function) => { - callback(mockResult); - } - ); - - // Import composable - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - // Create wrapper with composable - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { setAuth, execute, isReady } = composableInstance; - - // Wait for WASM to be ready - await waitForReady(isReady); - - // Step 1: Set authentication - setAuth('test-key', 'test-secret', 'staging'); - - await nextTick(); - - // Step 2: Verify auth is set - expect((global as any).setAuthCredentials).toHaveBeenCalledWith( - 'test-key', - 'test-secret', - 'staging' - ); - - // Step 3: Execute command - const result = await execute('port list'); - - // Step 4: Verify result - expect(result.output).toBe('Port list result'); - expect((global as any).executeMegaportCommandAsync).toHaveBeenCalledWith( - 'port list', - expect.any(Function) - ); - - wrapper.unmount(); - }); - - it('should handle multiple sequential commands', async () => { - let commandCount = 0; - ((global as any).executeMegaportCommandAsync as any).mockImplementation( - (cmd: string, callback: Function) => { - commandCount++; - callback({ output: `Result ${commandCount}: ${cmd}`, error: '' }); - } - ); - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { execute, setAuth, isReady } = composableInstance; - await waitForReady(isReady); - - setAuth('key', 'secret', 'staging'); - - const commands = ['port list', 'vxc list', 'location list']; - const results = []; - - for (const cmd of commands) { - const result = await execute(cmd); - results.push(result); - } - - expect(results).toHaveLength(3); - expect(results[0].output).toContain('port list'); - expect(results[1].output).toContain('vxc list'); - expect(results[2].output).toContain('location list'); - - wrapper.unmount(); - }); - }); - - describe('Terminal Component Integration', () => { - it('should integrate terminal with WASM composable', async () => { - const MegaportTerminal = await import( - '../components/MegaportTerminal.vue' - ); - - const wrapper = mount(MegaportTerminal.default, { - props: { - wasmPath: '/test.wasm', - wasmExecPath: '/test_exec.js', - }, - }); - - await nextTick(); - - expect(wrapper.exists()).toBe(true); - expect(wrapper.find('.megaport-terminal-container').exists()).toBe(true); - }); - - it('should handle auth flow in terminal component', async () => { - const MegaportTerminal = await import( - '../components/MegaportTerminal.vue' - ); - - const wrapper = mount(MegaportTerminal.default); - - await nextTick(); - - // Component should initialize WASM - expect(wrapper.vm).toBeDefined(); - }); - }); - - describe('Error Recovery Workflow', () => { - it('should recover from WASM initialization failure', async () => { - // Simulate failure - global.fetch = vi.fn(() => Promise.reject(new Error('Network failure'))); - delete (window as any).Go; - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await new Promise((resolve) => setTimeout(resolve, 250)); - - const { error, isLoading } = composableInstance; - expect(error.value).toBeTruthy(); - expect(isLoading.value).toBe(false); - - wrapper.unmount(); - }); - - it('should handle command execution errors gracefully', async () => { - ((global as any).executeMegaportCommandAsync as any).mockImplementation( - (cmd: string, callback: Function) => { - callback({ output: '', error: 'Command failed' }); - } - ); - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { execute, isReady } = composableInstance; - await waitForReady(isReady); - - const result = await execute('invalid command'); - - expect(result.error).toBe('Command failed'); - expect(result.output).toBe(''); - - wrapper.unmount(); - }); - }); - - describe('State Management Across Components', () => { - it('should share auth state between instances', async () => { - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let instance1: any; - const wrapper1 = createTestWrapper(() => { - instance1 = useMegaportWASM({}); - return instance1; - }); - - let instance2: any; - const wrapper2 = createTestWrapper(() => { - instance2 = useMegaportWASM({}); - return instance2; - }); - - await nextTick(); - - // Set auth in first instance - instance1.setAuth('shared-key', 'shared-secret', 'production'); - - await nextTick(); - - // Check second instance can see it - const authInfo = instance2.getAuthInfo(); - - expect((global as any).setAuthCredentials).toHaveBeenCalled(); - - wrapper1.unmount(); - wrapper2.unmount(); - }); - - it('should handle concurrent command execution', async () => { - const results: any[] = []; - ((global as any).executeMegaportCommandAsync as any).mockImplementation( - (cmd: string, callback: Function) => { - setTimeout(() => { - callback({ output: `Executed: ${cmd}`, error: '' }); - }, Math.random() * 50); - } - ); - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { execute, isReady } = composableInstance; - await waitForReady(isReady); - - // Execute multiple commands concurrently - const promises = [ - execute('command1'), - execute('command2'), - execute('command3'), - ]; - - const allResults = await Promise.all(promises); - - expect(allResults).toHaveLength(3); - allResults.forEach((result) => { - expect(result.output).toContain('Executed:'); - }); - - wrapper.unmount(); - }); - }); - - describe('Performance and Resource Management', () => { - it('should cleanup resources on unmount', async () => { - const MegaportTerminal = await import( - '../components/MegaportTerminal.vue' - ); - - const wrapper = mount(MegaportTerminal.default); - - await nextTick(); - - const disposeSpy = vi.fn(); - (wrapper.vm as any).terminal = { - dispose: disposeSpy, - }; - - wrapper.unmount(); - - expect(disposeSpy).toHaveBeenCalled(); - }); - - it('should reset output buffers', async () => { - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { resetOutput } = composableInstance; - resetOutput(); - - expect((global as any).resetWasmOutput).toHaveBeenCalled(); - - wrapper.unmount(); - }); - - it('should toggle debug mode', async () => { - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - ((global as any).toggleWasmDebug as any).mockReturnValue(true); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { toggleDebug } = composableInstance; - const result = toggleDebug(); - - expect((global as any).toggleWasmDebug).toHaveBeenCalled(); - expect(result).toBe(true); - - wrapper.unmount(); - }); - }); - - describe('Browser Compatibility', () => { - it('should handle missing WebAssembly support', async () => { - const originalWasm = global.WebAssembly; - (global as any).WebAssembly = undefined; - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { error } = composableInstance; - - await new Promise((resolve) => setTimeout(resolve, 200)); - - wrapper.unmount(); - global.WebAssembly = originalWasm; - }); - - it('should handle missing Worker support', async () => { - const originalWorker = global.Worker; - (global as any).Worker = undefined; - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - // Should still work in direct mode - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { isLoading } = composableInstance; - expect(isLoading.value).toBe(true); - - wrapper.unmount(); - global.Worker = originalWorker; - }); - }); - - describe('Real-world Scenarios', () => { - it('should simulate user login and resource listing workflow', async () => { - const mockResponses: Record = { - 'port list --output json': { - output: JSON.stringify([{ id: 1, name: 'Port 1' }]), - error: '', - }, - 'vxc list --output json': { - output: JSON.stringify([{ id: 2, name: 'VXC 1' }]), - error: '', - }, - }; - - ((global as any).executeMegaportCommandAsync as any).mockImplementation( - (cmd: string, callback: Function) => { - callback( - mockResponses[cmd] || { output: '', error: 'Unknown command' } - ); - } - ); - - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM({}); - return composableInstance; - }); - - await nextTick(); - - const { setAuth, execute, getAuthInfo, isReady } = composableInstance; - - // Wait for ready state - await waitForReady(isReady); - - // Mock getAuthInfo to return production environment - const productionAuthInfo = { - accessKeySet: true, - accessKeyPreview: 'user-***', - secretKeySet: true, - secretKeyPreview: '***', - environment: 'production', - }; - ((global as any).debugAuthInfo as any).mockReturnValue( - productionAuthInfo - ); - (window as any).getAuthInfo = vi.fn(() => productionAuthInfo); - - // User logs in - setAuth('user-key', 'user-secret', 'production'); - const auth = getAuthInfo(); - expect(auth?.environment).toBe('production'); - - // User lists ports - const portResult = await execute('port list --output json'); - const ports = JSON.parse(portResult.output || '[]'); - expect(ports).toHaveLength(1); - expect(ports[0].name).toBe('Port 1'); - - // User lists VXCs - const vxcResult = await execute('vxc list --output json'); - const vxcs = JSON.parse(vxcResult.output || '[]'); - expect(vxcs).toHaveLength(1); - expect(vxcs[0].name).toBe('VXC 1'); - - wrapper.unmount(); - }); - - it('should handle session timeout and reauthentication', async () => { - const { useMegaportWASM } = await import( - '../composables/useMegaportWASM' - ); - - let composableInstance: any; - const wrapper = createTestWrapper(() => { - composableInstance = useMegaportWASM(); - return composableInstance; - }); - - await nextTick(); - - const { setAuth, clearAuth, getAuthInfo } = composableInstance; - - // Initial auth - setAuth('key1', 'secret1', 'staging'); - let auth = getAuthInfo(); - expect((global as any).setAuthCredentials).toHaveBeenCalled(); - - // Simulate timeout - clear auth - clearAuth(); - auth = getAuthInfo(); - - // Reauthenticate - setAuth('key2', 'secret2', 'production'); - auth = getAuthInfo(); - expect((global as any).setAuthCredentials).toHaveBeenCalled(); - - wrapper.unmount(); - }); - }); -}); diff --git a/frontend-integration/__tests__/interactive.test.ts b/frontend-integration/__tests__/interactive.test.ts deleted file mode 100644 index 6a800587..00000000 --- a/frontend-integration/__tests__/interactive.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { useMegaportWASM } from '../composables/useMegaportWASM'; - -describe('Interactive Mode', () => { - beforeEach(() => { - vi.clearAllMocks(); - delete (window as any).registerPromptHandler; - delete (window as any).submitPromptResponse; - delete (window as any).cancelPrompt; - }); - - describe('Prompt Handler Registration', () => { - it('should register a custom prompt handler', () => { - const mockRegister = vi.fn(() => true); - (window as any).registerPromptHandler = mockRegister; - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - const result = registerPromptHandler(customHandler); - - expect(result).toBe(true); - expect(mockRegister).toHaveBeenCalledWith(customHandler); - }); - - it('should return false when registerPromptHandler is not available', () => { - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - const result = registerPromptHandler(customHandler); - - expect(result).toBe(false); - }); - - it('should warn when registerPromptHandler is not available', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn'); - const { registerPromptHandler } = useMegaportWASM(); - - registerPromptHandler(vi.fn()); - - // Filter out Vue lifecycle warnings (expected since we're not in a component context) - const relevantWarnings = consoleWarnSpy.mock.calls.filter( - (call: any[]) => - !call.some( - (arg) => - typeof arg === 'string' && - arg.includes('[Vue warn]') && - arg.includes('Lifecycle injection APIs') - ) - ); - - expect(relevantWarnings.length).toBeGreaterThan(0); - expect(relevantWarnings[0][0]).toBe( - 'registerPromptHandler not available - WASM may not be initialized' - ); - }); - - it('should allow registering multiple times', () => { - const mockRegister = vi.fn(() => true); - (window as any).registerPromptHandler = mockRegister; - - const { registerPromptHandler } = useMegaportWASM(); - const handler1 = vi.fn(); - const handler2 = vi.fn(); - - registerPromptHandler(handler1); - registerPromptHandler(handler2); - - expect(mockRegister).toHaveBeenCalledTimes(2); - expect(mockRegister).toHaveBeenNthCalledWith(1, handler1); - expect(mockRegister).toHaveBeenNthCalledWith(2, handler2); - }); - }); - - describe('Prompt Request Handling', () => { - it('should invoke custom handler when prompt is requested', () => { - let registeredHandler: ((request: any) => void) | null = null; - const mockRegister = vi.fn((handler: (request: any) => void) => { - registeredHandler = handler; - return true; - }); - (window as any).registerPromptHandler = mockRegister; - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - // Simulate WASM requesting a prompt - const promptRequest = { - id: 'prompt-1', - message: 'Enter your name:', - defaultValue: '', - }; - - registeredHandler!(promptRequest); - - expect(customHandler).toHaveBeenCalledWith(promptRequest); - }); - - it('should handle prompt requests with default values', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - const promptRequest = { - id: 'prompt-2', - message: 'Enter port name:', - defaultValue: 'my-port', - }; - - registeredHandler!(promptRequest); - - expect(customHandler).toHaveBeenCalledWith( - expect.objectContaining({ - defaultValue: 'my-port', - }) - ); - }); - - it('should handle prompt requests with validation requirements', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - const promptRequest = { - id: 'prompt-3', - message: 'Enter bandwidth (Mbps):', - defaultValue: '1000', - validation: { - required: true, - pattern: '^[0-9]+$', - }, - }; - - registeredHandler!(promptRequest); - - expect(customHandler).toHaveBeenCalledWith( - expect.objectContaining({ - validation: expect.any(Object), - }) - ); - }); - }); - - describe('Prompt Response Submission', () => { - it('should submit prompt response when user provides input', () => { - const mockSubmit = vi.fn(); - (window as any).submitPromptResponse = mockSubmit; - - // Simulate submitting a response - (window as any).submitPromptResponse('prompt-1', 'John Doe'); - - expect(mockSubmit).toHaveBeenCalledWith('prompt-1', 'John Doe'); - }); - - it('should submit empty response when allowed', () => { - const mockSubmit = vi.fn(); - (window as any).submitPromptResponse = mockSubmit; - - (window as any).submitPromptResponse('prompt-2', ''); - - expect(mockSubmit).toHaveBeenCalledWith('prompt-2', ''); - }); - - it('should submit numeric responses', () => { - const mockSubmit = vi.fn(); - (window as any).submitPromptResponse = mockSubmit; - - (window as any).submitPromptResponse('prompt-3', '1000'); - - expect(mockSubmit).toHaveBeenCalledWith('prompt-3', '1000'); - }); - - it('should handle special characters in responses', () => { - const mockSubmit = vi.fn(); - (window as any).submitPromptResponse = mockSubmit; - - (window as any).submitPromptResponse('prompt-4', 'test@example.com'); - - expect(mockSubmit).toHaveBeenCalledWith('prompt-4', 'test@example.com'); - }); - }); - - describe('Prompt Cancellation', () => { - it('should cancel prompt when user cancels', () => { - const mockCancel = vi.fn(); - (window as any).cancelPrompt = mockCancel; - - (window as any).cancelPrompt('prompt-1'); - - expect(mockCancel).toHaveBeenCalledWith('prompt-1'); - }); - - it('should handle cancellation of multiple prompts', () => { - const mockCancel = vi.fn(); - (window as any).cancelPrompt = mockCancel; - - (window as any).cancelPrompt('prompt-1'); - (window as any).cancelPrompt('prompt-2'); - (window as any).cancelPrompt('prompt-3'); - - expect(mockCancel).toHaveBeenCalledTimes(3); - }); - }); - - describe('Interactive Command Flow', () => { - it('should handle complete interactive command flow', async () => { - let registeredHandler: ((request: any) => void) | null = null; - const mockRegister = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - const mockSubmit = vi.fn(); - const mockExecute = vi.fn(() => - Promise.resolve({ output: 'Port created', error: '' }) - ); - - (window as any).registerPromptHandler = mockRegister; - (window as any).submitPromptResponse = mockSubmit; - (window as any).executeMegaportCommandAsync = mockExecute; - - const { registerPromptHandler } = useMegaportWASM(); - - // Register handler that auto-responds - const autoResponseHandler = vi.fn((request: any) => { - if (request.message.includes('name')) { - (window as any).submitPromptResponse(request.id, 'test-port'); - } else if (request.message.includes('bandwidth')) { - (window as any).submitPromptResponse(request.id, '1000'); - } - }); - - registerPromptHandler(autoResponseHandler); - - // Simulate prompt request - const promptRequest = { - id: 'prompt-1', - message: 'Enter port name:', - }; - registeredHandler!(promptRequest); - - expect(autoResponseHandler).toHaveBeenCalledWith(promptRequest); - expect(mockSubmit).toHaveBeenCalledWith('prompt-1', 'test-port'); - }); - - it('should handle interactive command cancellation', () => { - let registeredHandler: ((request: any) => void) | null = null; - const mockRegister = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - const mockCancel = vi.fn(); - - (window as any).registerPromptHandler = mockRegister; - (window as any).cancelPrompt = mockCancel; - - const { registerPromptHandler } = useMegaportWASM(); - - const cancelHandler = vi.fn((request: any) => { - (window as any).cancelPrompt(request.id); - }); - - registerPromptHandler(cancelHandler); - - // Simulate prompt request - const promptRequest = { id: 'prompt-1', message: 'Enter value:' }; - registeredHandler!(promptRequest); - - expect(cancelHandler).toHaveBeenCalledWith(promptRequest); - expect(mockCancel).toHaveBeenCalledWith('prompt-1'); - }); - }); - - describe('Multiple Prompts', () => { - it('should handle multiple sequential prompts', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - (window as any).submitPromptResponse = vi.fn(); - - const { registerPromptHandler } = useMegaportWASM(); - const responseTracker: string[] = []; - - const multiPromptHandler = vi.fn((request: any) => { - responseTracker.push(request.id); - }); - - registerPromptHandler(multiPromptHandler); - - // Simulate multiple prompts - registeredHandler!({ id: 'prompt-1', message: 'Name:' }); - registeredHandler!({ id: 'prompt-2', message: 'Location:' }); - registeredHandler!({ id: 'prompt-3', message: 'Bandwidth:' }); - - expect(multiPromptHandler).toHaveBeenCalledTimes(3); - expect(responseTracker).toEqual(['prompt-1', 'prompt-2', 'prompt-3']); - }); - - it('should handle prompts with different types', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - // Different prompt types - registeredHandler!({ - id: 'prompt-1', - message: 'Text input:', - type: 'text', - }); - registeredHandler!({ - id: 'prompt-2', - message: 'Password:', - type: 'password', - }); - registeredHandler!({ - id: 'prompt-3', - message: 'Confirm (y/n):', - type: 'confirm', - }); - - expect(customHandler).toHaveBeenCalledTimes(3); - }); - }); - - describe('Error Handling', () => { - it('should handle errors in custom prompt handler', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const errorHandler = vi.fn(() => { - throw new Error('Handler error'); - }); - - registerPromptHandler(errorHandler); - - // Should not throw when handler errors - expect(() => { - registeredHandler!({ id: 'prompt-1', message: 'Test:' }); - }).toThrow('Handler error'); - }); - - it('should handle missing submitPromptResponse function', () => { - delete (window as any).submitPromptResponse; - - // Should not throw - expect(() => { - // Attempt to call undefined function would normally error - // This test verifies the application handles this gracefully - if ((window as any).submitPromptResponse) { - (window as any).submitPromptResponse('id', 'value'); - } - }).not.toThrow(); - }); - - it('should handle missing cancelPrompt function', () => { - delete (window as any).cancelPrompt; - - expect(() => { - if ((window as any).cancelPrompt) { - (window as any).cancelPrompt('id'); - } - }).not.toThrow(); - }); - }); - - describe('Prompt Handler Context', () => { - it('should maintain handler context across multiple calls', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - - class PromptManager { - private responses: Map = new Map(); - - handlePrompt = (request: any) => { - this.responses.set(request.id, request.message); - }; - - getResponseCount() { - return this.responses.size; - } - } - - const manager = new PromptManager(); - registerPromptHandler(manager.handlePrompt); - - registeredHandler!({ id: '1', message: 'First' }); - registeredHandler!({ id: '2', message: 'Second' }); - - expect(manager.getResponseCount()).toBe(2); - }); - - it('should allow handler to access external state', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - - const externalState = { promptCount: 0 }; - - const statefulHandler = vi.fn(() => { - externalState.promptCount++; - }); - - registerPromptHandler(statefulHandler); - - registeredHandler!({ id: '1', message: 'Test' }); - registeredHandler!({ id: '2', message: 'Test' }); - registeredHandler!({ id: '3', message: 'Test' }); - - expect(externalState.promptCount).toBe(3); - }); - }); - - describe('Prompt Message Formatting', () => { - it('should handle prompts with HTML entities', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - registeredHandler!({ - id: 'prompt-1', - message: 'Enter value <required>:', - }); - - expect(customHandler).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'Enter value <required>:', - }) - ); - }); - - it('should handle prompts with newlines', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - registeredHandler!({ - id: 'prompt-1', - message: 'Line 1\nLine 2\nLine 3', - }); - - expect(customHandler).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringContaining('\n'), - }) - ); - }); - - it('should handle prompts with unicode characters', () => { - let registeredHandler: ((request: any) => void) | null = null; - (window as any).registerPromptHandler = vi.fn((handler: any) => { - registeredHandler = handler; - return true; - }); - - const { registerPromptHandler } = useMegaportWASM(); - const customHandler = vi.fn(); - - registerPromptHandler(customHandler); - - registeredHandler!({ - id: 'prompt-1', - message: 'Enter value 🚀 📝 ✅:', - }); - - expect(customHandler).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringContaining('🚀'), - }) - ); - }); - }); -}); diff --git a/frontend-integration/__tests__/setup.ts b/frontend-integration/__tests__/setup.ts deleted file mode 100644 index fe01a1d0..00000000 --- a/frontend-integration/__tests__/setup.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { vi, beforeEach, afterEach } from 'vitest'; - -// Proper Go constructor for WASM -class MockGo { - run = vi.fn(); - importObject = {}; -} - -// Setup global test environment -beforeEach(() => { - // Mock Go constructor - must be available before any imports - (window as any).Go = MockGo; - (global as any).Go = MockGo; - - // Mock window WASM functions - (window as any).executeMegaportCommandAsync = vi.fn(); - (window as any).setAuthCredentials = vi.fn(() => ({ success: true })); - (window as any).clearAuthCredentials = vi.fn(() => ({ success: true })); - (window as any).resetWasmOutput = vi.fn(); - (window as any).toggleWasmDebug = vi.fn(() => false); - (window as any).debugAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: 'staging', - })); - (window as any).getAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: 'staging', - })); - - // Mock fetch for WASM loading - synchronous resolution - global.fetch = vi.fn(() => - Promise.resolve({ - ok: true, - arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), - } as Response) - ); - - // Mock WebAssembly - synchronous resolution - global.WebAssembly = { - instantiate: vi.fn(() => - Promise.resolve({ - instance: {}, - module: {}, - }) - ), - instantiateStreaming: vi.fn(), - } as any; - - // Mock Worker constructor - global.Worker = vi.fn(function (this: any, url: string) { - this.url = url; - this.postMessage = vi.fn(); - this.addEventListener = vi.fn(); - this.removeEventListener = vi.fn(); - this.terminate = vi.fn(); - return this; - }) as any; - - // Mock document.createElement for script loading - synchronous Go availability - const originalCreateElement = document.createElement.bind(document); - document.createElement = vi.fn((tag: string) => { - const element = originalCreateElement(tag); - if (tag === 'script') { - // Make Go available immediately instead of async - (window as any).Go = MockGo; - // Trigger onload synchronously in next tick - setTimeout(() => { - if (element.onload) { - element.onload(new Event('load')); - } - }, 0); - } - return element; - }) as any; -}); - -afterEach(() => { - vi.clearAllMocks(); - localStorage.clear(); -}); diff --git a/frontend-integration/__tests__/smoke.test.ts b/frontend-integration/__tests__/smoke.test.ts deleted file mode 100644 index 6e76fe9f..00000000 --- a/frontend-integration/__tests__/smoke.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; - -/** - * Basic smoke tests to ensure the module structure is correct - */ - -describe('Module Exports', () => { - it('should export useMegaportWASM composable', async () => { - const module = await import('../composables/useMegaportWASM'); - expect(module.useMegaportWASM).toBeDefined(); - expect(typeof module.useMegaportWASM).toBe('function'); - }); - - it('should export MegaportTerminal component', async () => { - const module = await import('../components/MegaportTerminal.vue'); - expect(module.default).toBeDefined(); - }); -}); - -describe('Type Definitions', () => { - it('should have proper TypeScript types', () => { - // Type definitions are checked at compile time - // This test just verifies the types are defined - expect(true).toBe(true); - }); -}); - -describe('WASM Functions Mock', () => { - it('should have mocked window functions', () => { - expect((window as any).setAuthCredentials).toBeDefined(); - expect((window as any).clearAuthCredentials).toBeDefined(); - expect((window as any).resetWasmOutput).toBeDefined(); - expect((window as any).toggleWasmDebug).toBeDefined(); - }); - - it('should mock auth operations', () => { - const mockSetAuth = (window as any).setAuthCredentials; - mockSetAuth('test-key', 'test-secret', 'staging'); - expect(mockSetAuth).toHaveBeenCalledWith( - 'test-key', - 'test-secret', - 'staging' - ); - }); - - it('should mock command execution', () => { - const mockExecute = (window as any).executeMegaportCommandAsync; - const callback = vi.fn(); - mockExecute('test command', callback); - expect(mockExecute).toHaveBeenCalledWith('test command', callback); - }); -}); - -describe('Worker Integration', () => { - it('should create worker instance', () => { - const worker = new Worker('test-worker.js'); - expect(worker).toBeDefined(); - expect(worker.postMessage).toBeDefined(); - }); - - it('should handle worker messages', () => { - const worker = new Worker('test-worker.js'); - const message = { type: 'TEST', data: 'test' }; - worker.postMessage(message); - expect(worker.postMessage).toHaveBeenCalledWith(message); - }); -}); - -describe('WebAssembly Support', () => { - it('should have WebAssembly global', () => { - expect(WebAssembly).toBeDefined(); - expect(WebAssembly.instantiate).toBeDefined(); - }); - - it('should mock WASM instantiation', async () => { - const buffer = new ArrayBuffer(8); - const result = await WebAssembly.instantiate(buffer, {}); - expect(result).toHaveProperty('instance'); - expect(result).toHaveProperty('module'); - }); -}); - -describe('Fetch API', () => { - it('should mock fetch for WASM loading', async () => { - const response = await fetch('/test.wasm'); - expect(response.ok).toBe(true); - const buffer = await response.arrayBuffer(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - }); -}); - -describe('LocalStorage', () => { - it('should support localStorage operations', () => { - localStorage.setItem('test', 'value'); - expect(localStorage.getItem('test')).toBe('value'); - - localStorage.removeItem('test'); - expect(localStorage.getItem('test')).toBeNull(); - }); - - it('should clear localStorage', () => { - localStorage.setItem('key1', 'value1'); - localStorage.setItem('key2', 'value2'); - - localStorage.clear(); - - expect(localStorage.getItem('key1')).toBeNull(); - expect(localStorage.getItem('key2')).toBeNull(); - }); -}); diff --git a/frontend-integration/__tests__/spinner.test.ts b/frontend-integration/__tests__/spinner.test.ts deleted file mode 100644 index 1f68768d..00000000 --- a/frontend-integration/__tests__/spinner.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { ref } from 'vue'; - -describe('Spinner Functionality', () => { - let mockActiveSpinners: Map; - let mockWasmStartSpinner: any; - let mockWasmStopSpinner: any; - - beforeEach(() => { - vi.clearAllMocks(); - mockActiveSpinners = new Map(); - - // Mock the window spinner functions - mockWasmStartSpinner = vi.fn((message: string) => { - const spinnerId = `spinner_${Date.now()}_${Math.random()}`; - mockActiveSpinners.set(spinnerId, message); - return spinnerId; - }); - - mockWasmStopSpinner = vi.fn((spinnerId: string) => { - mockActiveSpinners.delete(spinnerId); - }); - - (window as any).wasmStartSpinner = mockWasmStartSpinner; - (window as any).wasmStopSpinner = mockWasmStopSpinner; - }); - - describe('Spinner Registration', () => { - it('should register wasmStartSpinner on window', () => { - expect((window as any).wasmStartSpinner).toBeDefined(); - expect(typeof (window as any).wasmStartSpinner).toBe('function'); - }); - - it('should register wasmStopSpinner on window', () => { - expect((window as any).wasmStopSpinner).toBeDefined(); - expect(typeof (window as any).wasmStopSpinner).toBe('function'); - }); - }); - - describe('Starting Spinners', () => { - it('should start a spinner with a message', () => { - const message = 'Loading data...'; - const spinnerId = mockWasmStartSpinner(message); - - expect(spinnerId).toBeDefined(); - expect(typeof spinnerId).toBe('string'); - expect(mockActiveSpinners.has(spinnerId)).toBe(true); - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - - it('should generate unique spinner IDs', () => { - const id1 = mockWasmStartSpinner('First spinner'); - const id2 = mockWasmStartSpinner('Second spinner'); - const id3 = mockWasmStartSpinner('Third spinner'); - - expect(id1).not.toBe(id2); - expect(id2).not.toBe(id3); - expect(id1).not.toBe(id3); - }); - - it('should support multiple concurrent spinners', () => { - const spinner1 = mockWasmStartSpinner('Logging in...'); - const spinner2 = mockWasmStartSpinner('Fetching data...'); - const spinner3 = mockWasmStartSpinner('Processing...'); - - expect(mockActiveSpinners.size).toBe(3); - expect(mockActiveSpinners.get(spinner1)).toBe('Logging in...'); - expect(mockActiveSpinners.get(spinner2)).toBe('Fetching data...'); - expect(mockActiveSpinners.get(spinner3)).toBe('Processing...'); - }); - - it('should handle special characters in spinner messages', () => { - const message = 'Creating Port pb-test-port-vue...'; - const spinnerId = mockWasmStartSpinner(message); - - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - - it('should handle emoji in spinner messages', () => { - const message = '🔄 Processing request...'; - const spinnerId = mockWasmStartSpinner(message); - - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - - it('should handle long spinner messages', () => { - const message = - 'This is a very long spinner message that might wrap to multiple lines in the UI'; - const spinnerId = mockWasmStartSpinner(message); - - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - }); - - describe('Stopping Spinners', () => { - it('should stop an active spinner', () => { - const spinnerId = mockWasmStartSpinner('Test spinner'); - expect(mockActiveSpinners.has(spinnerId)).toBe(true); - - mockWasmStopSpinner(spinnerId); - expect(mockActiveSpinners.has(spinnerId)).toBe(false); - }); - - it('should stop the correct spinner when multiple are active', () => { - const spinner1 = mockWasmStartSpinner('First'); - const spinner2 = mockWasmStartSpinner('Second'); - const spinner3 = mockWasmStartSpinner('Third'); - - mockWasmStopSpinner(spinner2); - - expect(mockActiveSpinners.has(spinner1)).toBe(true); - expect(mockActiveSpinners.has(spinner2)).toBe(false); - expect(mockActiveSpinners.has(spinner3)).toBe(true); - expect(mockActiveSpinners.size).toBe(2); - }); - - it('should handle stopping non-existent spinner gracefully', () => { - expect(() => { - mockWasmStopSpinner('non-existent-id'); - }).not.toThrow(); - - expect(mockActiveSpinners.size).toBe(0); - }); - - it('should handle stopping the same spinner twice', () => { - const spinnerId = mockWasmStartSpinner('Test'); - mockWasmStopSpinner(spinnerId); - - expect(() => { - mockWasmStopSpinner(spinnerId); - }).not.toThrow(); - }); - - it('should remove all spinners when stopped sequentially', () => { - const spinner1 = mockWasmStartSpinner('First'); - const spinner2 = mockWasmStartSpinner('Second'); - const spinner3 = mockWasmStartSpinner('Third'); - - expect(mockActiveSpinners.size).toBe(3); - - mockWasmStopSpinner(spinner1); - mockWasmStopSpinner(spinner2); - mockWasmStopSpinner(spinner3); - - expect(mockActiveSpinners.size).toBe(0); - }); - }); - - describe('Spinner Lifecycle', () => { - it('should track complete spinner lifecycle', () => { - // Start spinner - const spinnerId = mockWasmStartSpinner('Processing...'); - expect(mockActiveSpinners.size).toBe(1); - - // Spinner should be active - expect(mockActiveSpinners.has(spinnerId)).toBe(true); - - // Stop spinner - mockWasmStopSpinner(spinnerId); - expect(mockActiveSpinners.size).toBe(0); - expect(mockActiveSpinners.has(spinnerId)).toBe(false); - }); - - it('should handle rapid start/stop cycles', () => { - for (let i = 0; i < 10; i++) { - const id = mockWasmStartSpinner(`Iteration ${i}`); - expect(mockActiveSpinners.size).toBe(1); - mockWasmStopSpinner(id); - expect(mockActiveSpinners.size).toBe(0); - } - }); - - it('should maintain order when starting and stopping spinners', () => { - const ids: string[] = []; - - // Start multiple spinners - ids.push(mockWasmStartSpinner('First')); - ids.push(mockWasmStartSpinner('Second')); - ids.push(mockWasmStartSpinner('Third')); - - expect(mockActiveSpinners.size).toBe(3); - - // Stop in reverse order - mockWasmStopSpinner(ids[2]); - expect(mockActiveSpinners.size).toBe(2); - - mockWasmStopSpinner(ids[1]); - expect(mockActiveSpinners.size).toBe(1); - - mockWasmStopSpinner(ids[0]); - expect(mockActiveSpinners.size).toBe(0); - }); - }); - - describe('Spinner State Management', () => { - it('should provide accurate spinner count', () => { - expect(mockActiveSpinners.size).toBe(0); - - mockWasmStartSpinner('One'); - expect(mockActiveSpinners.size).toBe(1); - - mockWasmStartSpinner('Two'); - expect(mockActiveSpinners.size).toBe(2); - - mockWasmStartSpinner('Three'); - expect(mockActiveSpinners.size).toBe(3); - }); - - it('should track spinner messages correctly', () => { - const messages = [ - 'Logging in to Megaport...', - 'Validating Port order...', - 'Creating Port...', - ]; - - const ids = messages.map((msg) => mockWasmStartSpinner(msg)); - - ids.forEach((id, index) => { - expect(mockActiveSpinners.get(id)).toBe(messages[index]); - }); - }); - - it('should update spinner state atomically', () => { - const id1 = mockWasmStartSpinner('First'); - const id2 = mockWasmStartSpinner('Second'); - - expect(mockActiveSpinners.size).toBe(2); - - mockWasmStopSpinner(id1); - - expect(mockActiveSpinners.size).toBe(1); - expect(mockActiveSpinners.has(id1)).toBe(false); - expect(mockActiveSpinners.has(id2)).toBe(true); - }); - }); - - describe('Edge Cases', () => { - it('should handle empty spinner message', () => { - const spinnerId = mockWasmStartSpinner(''); - expect(mockActiveSpinners.get(spinnerId)).toBe(''); - }); - - it('should handle very short spinner messages', () => { - const spinnerId = mockWasmStartSpinner('...'); - expect(mockActiveSpinners.get(spinnerId)).toBe('...'); - }); - - it('should handle whitespace-only messages', () => { - const spinnerId = mockWasmStartSpinner(' '); - expect(mockActiveSpinners.get(spinnerId)).toBe(' '); - }); - - it('should handle messages with newlines', () => { - const message = 'Line 1\nLine 2\nLine 3'; - const spinnerId = mockWasmStartSpinner(message); - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - - it('should handle messages with tabs', () => { - const message = 'Column 1\tColumn 2\tColumn 3'; - const spinnerId = mockWasmStartSpinner(message); - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - - it('should handle Unicode characters', () => { - const message = '日本語 Español Français 中文'; - const spinnerId = mockWasmStartSpinner(message); - expect(mockActiveSpinners.get(spinnerId)).toBe(message); - }); - }); - - describe('Performance', () => { - it('should handle many concurrent spinners', () => { - const spinnerCount = 100; - const ids: string[] = []; - - for (let i = 0; i < spinnerCount; i++) { - ids.push(mockWasmStartSpinner(`Spinner ${i}`)); - } - - expect(mockActiveSpinners.size).toBe(spinnerCount); - - // Stop all spinners - ids.forEach((id) => mockWasmStopSpinner(id)); - expect(mockActiveSpinners.size).toBe(0); - }); - - it('should maintain performance with rapid operations', () => { - const iterations = 1000; - - for (let i = 0; i < iterations; i++) { - const id = mockWasmStartSpinner(`Operation ${i}`); - mockWasmStopSpinner(id); - } - - expect(mockActiveSpinners.size).toBe(0); - }); - }); - - describe('Integration with WASM', () => { - it('should support typical WASM authentication flow', () => { - // Simulate login spinner - const loginId = mockWasmStartSpinner('Logging in to Megaport...'); - expect(mockActiveSpinners.size).toBe(1); - - // Simulate login complete - mockWasmStopSpinner(loginId); - expect(mockActiveSpinners.size).toBe(0); - }); - - it('should support typical WASM command execution flow', () => { - // Start validation spinner - const validateId = mockWasmStartSpinner('Validating Port order...'); - expect(mockActiveSpinners.size).toBe(1); - - mockWasmStopSpinner(validateId); - - // Start creation spinner - const createId = mockWasmStartSpinner('Creating Port...'); - expect(mockActiveSpinners.size).toBe(1); - - mockWasmStopSpinner(createId); - expect(mockActiveSpinners.size).toBe(0); - }); - - it('should support overlapping spinners for parallel operations', () => { - const auth = mockWasmStartSpinner('Authenticating...'); - const fetch1 = mockWasmStartSpinner('Fetching ports...'); - const fetch2 = mockWasmStartSpinner('Fetching locations...'); - - expect(mockActiveSpinners.size).toBe(3); - - // Auth completes first - mockWasmStopSpinner(auth); - expect(mockActiveSpinners.size).toBe(2); - - // Then data fetches - mockWasmStopSpinner(fetch1); - mockWasmStopSpinner(fetch2); - expect(mockActiveSpinners.size).toBe(0); - }); - }); -}); diff --git a/frontend-integration/__tests__/terminal-clear.test.ts b/frontend-integration/__tests__/terminal-clear.test.ts deleted file mode 100644 index 232eb5f6..00000000 --- a/frontend-integration/__tests__/terminal-clear.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -describe('Terminal Clear Functionality', () => { - let mockTerminal: any; - let writeHistory: string[]; - - beforeEach(() => { - writeHistory = []; - - mockTerminal = { - write: vi.fn((text: string) => { - writeHistory.push(text); - }), - clear: vi.fn(), - }; - }); - - describe('Clear Command', () => { - it('should clear the terminal when clear command is executed', () => { - mockTerminal.clear(); - expect(mockTerminal.clear).toHaveBeenCalled(); - }); - - it('should clear the terminal when cls command is executed', () => { - // cls is an alias for clear - mockTerminal.clear(); - expect(mockTerminal.clear).toHaveBeenCalled(); - }); - - it('should move cursor to home position after clear', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); // Home position escape code - - expect(writeHistory).toContain('\x1b[H'); - }); - - it('should write prompt without newline after clear', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - // Should not have \r\n before the prompt - expect(writeHistory[writeHistory.length - 1]).toBe( - '\x1b[32mmegaport>\x1b[0m ' - ); - expect(writeHistory[writeHistory.length - 1]).not.toContain('\r\n'); - }); - }); - - describe('Ctrl+L Shortcut', () => { - it('should clear terminal when Ctrl+L is pressed', () => { - // Simulate Ctrl+L (ASCII code 12) - const ctrlLCode = 12; - - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - - expect(mockTerminal.clear).toHaveBeenCalled(); - expect(writeHistory).toContain('\x1b[H'); - }); - - it('should reset cursor position after Ctrl+L', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory[0]).toBe('\x1b[H'); - expect(writeHistory[1]).toBe('\x1b[32mmegaport>\x1b[0m '); - }); - }); - - describe('Prompt Positioning', () => { - it('should position prompt at left margin after clear', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); // Move to home - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - // Home position escape code should come before prompt - const homeIndex = writeHistory.indexOf('\x1b[H'); - const promptIndex = writeHistory.indexOf('\x1b[32mmegaport>\x1b[0m '); - - expect(homeIndex).toBeGreaterThanOrEqual(0); - expect(promptIndex).toBeGreaterThan(homeIndex); - }); - - it('should not add extra newlines after clear', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - // Check that no writes contain \r\n after clear - const writesAfterHome = writeHistory.slice( - writeHistory.indexOf('\x1b[H') + 1 - ); - const hasNewline = writesAfterHome.some((w) => w.includes('\r\n')); - - expect(hasNewline).toBe(false); - }); - - it('should position cursor at column 0 after clear', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); // Positions at row 1, column 1 - - expect(writeHistory).toContain('\x1b[H'); - }); - }); - - describe('Initial Terminal State', () => { - it('should write welcome message before first prompt', () => { - const welcomeMessage = - 'Welcome to Megaport CLI (WebAssembly)\nType "help" for available commands.\n'; - mockTerminal.write(welcomeMessage); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory[0]).toBe(welcomeMessage); - expect(writeHistory[1]).toBe('\r\n\x1b[32mmegaport>\x1b[0m '); - }); - - it('should add newline before initial prompt', () => { - const welcomeMessage = 'Welcome\n'; - mockTerminal.write(welcomeMessage); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - - // First prompt after welcome should have \r\n - expect(writeHistory[1]).toContain('\r\n'); - }); - - it('should not use justCleared flag for initial prompt', () => { - mockTerminal.write('Welcome\n'); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - - // Should include newline for normal prompt - expect(writeHistory[1]).toContain('\r\n'); - }); - }); - - describe('Multiple Clear Operations', () => { - it('should handle multiple consecutive clears', () => { - // First clear - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(mockTerminal.clear).toHaveBeenCalledTimes(1); - - // Second clear - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(mockTerminal.clear).toHaveBeenCalledTimes(2); - }); - - it('should reset state correctly after each clear', () => { - for (let i = 0; i < 5; i++) { - writeHistory = []; - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory[0]).toBe('\x1b[H'); - expect(writeHistory[1]).toBe('\x1b[32mmegaport>\x1b[0m '); - } - }); - - it('should not accumulate state across clears', () => { - // Clear 1 - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - const firstClearWrites = writeHistory.length; - - // Clear 2 - writeHistory = []; - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory.length).toBe(firstClearWrites); - }); - }); - - describe('Clear After Command Execution', () => { - it('should position prompt correctly after command then clear', () => { - // Execute command - mockTerminal.write('ports list'); - mockTerminal.write('\r\n'); - mockTerminal.write('Output data...\r\n'); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - - writeHistory = []; - - // Then clear - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory[0]).toBe('\x1b[H'); - expect(writeHistory[1]).toBe('\x1b[32mmegaport>\x1b[0m '); - expect(writeHistory[1]).not.toContain('\r\n'); - }); - - it('should clear all previous output', () => { - // Add lots of output - for (let i = 0; i < 100; i++) { - mockTerminal.write(`Line ${i}\r\n`); - } - - // Clear - mockTerminal.clear(); - - expect(mockTerminal.clear).toHaveBeenCalled(); - }); - }); - - describe('Clear During Interactive Command', () => { - it('should be able to clear during interactive input', () => { - // Start interactive command - mockTerminal.write('ports buy --interactive\r\n'); - mockTerminal.write('Enter port name: '); - - // User clears - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(mockTerminal.clear).toHaveBeenCalled(); - expect(writeHistory).toContain('\x1b[H'); - }); - }); - - describe('Escape Sequences', () => { - it('should use correct ANSI escape code for home position', () => { - mockTerminal.write('\x1b[H'); - - expect(writeHistory[0]).toBe('\x1b[H'); - }); - - it('should use correct ANSI escape codes for prompt color', () => { - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory[0]).toContain('\x1b[32m'); // Green color - expect(writeHistory[0]).toContain('\x1b[0m'); // Reset color - }); - - it('should not include carriage return in post-clear prompt', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - const promptWrite = writeHistory[writeHistory.length - 1]; - expect(promptWrite).not.toContain('\r'); - expect(promptWrite).not.toContain('\n'); - }); - }); - - describe('justCleared Flag Behavior', () => { - it('should set justCleared to true after clear', () => { - let justCleared = false; - - mockTerminal.clear(); - justCleared = true; - - expect(justCleared).toBe(true); - }); - - it('should reset justCleared to false after writing prompt', () => { - let justCleared = true; - - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - justCleared = false; - - expect(justCleared).toBe(false); - }); - - it('should not set justCleared for normal prompts', () => { - let justCleared = false; - - // Normal command execution - mockTerminal.write('help\r\n'); - mockTerminal.write('Available commands:\r\n'); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - - expect(justCleared).toBe(false); - }); - - it('should only set justCleared on actual clear operations', () => { - let justCleared = false; - - // Regular operations - mockTerminal.write('test\r\n'); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - expect(justCleared).toBe(false); - - // Clear operation - mockTerminal.clear(); - justCleared = true; - expect(justCleared).toBe(true); - - // After prompt - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - justCleared = false; - expect(justCleared).toBe(false); - }); - }); - - describe('Edge Cases', () => { - it('should handle rapid clear commands', () => { - for (let i = 0; i < 10; i++) { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - } - - expect(mockTerminal.clear).toHaveBeenCalledTimes(10); - }); - - it('should handle clear with no previous output', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(mockTerminal.clear).toHaveBeenCalled(); - expect(writeHistory.length).toBe(2); - }); - - it('should handle clear immediately after terminal initialization', () => { - mockTerminal.write('Welcome\n'); - mockTerminal.write('\r\n\x1b[32mmegaport>\x1b[0m '); - - writeHistory = []; - - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - mockTerminal.write('\x1b[32mmegaport>\x1b[0m '); - - expect(writeHistory[0]).toBe('\x1b[H'); - }); - }); - - describe('Browser Compatibility', () => { - it('should use standard ANSI codes for maximum compatibility', () => { - mockTerminal.write('\x1b[H'); // Home position - mockTerminal.write('\x1b[32m'); // Green color - mockTerminal.write('\x1b[0m'); // Reset - - expect(writeHistory[0]).toBe('\x1b[H'); - expect(writeHistory[1]).toBe('\x1b[32m'); - expect(writeHistory[2]).toBe('\x1b[0m'); - }); - - it('should not use platform-specific clear codes', () => { - mockTerminal.clear(); - mockTerminal.write('\x1b[H'); - - // Should use standard terminal.clear() and \x1b[H - // Not platform-specific codes like \033c or clear screen codes - expect(writeHistory[0]).toBe('\x1b[H'); - }); - }); -}); diff --git a/frontend-integration/__tests__/useMegaportWASM.test.ts b/frontend-integration/__tests__/useMegaportWASM.test.ts deleted file mode 100644 index ea85b727..00000000 --- a/frontend-integration/__tests__/useMegaportWASM.test.ts +++ /dev/null @@ -1,686 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { nextTick, defineComponent } from 'vue'; -import { mount } from '@vue/test-utils'; -import { useMegaportWASM } from '../composables/useMegaportWASM'; - -// Mock Go class -class MockGo { - run = vi.fn(); - importObject = {}; -} - -// Mock WASM functions -const mockExecuteMegaportCommandAsync = vi.fn(); -const mockSetAuthCredentials = vi.fn(() => ({ success: true })); -const mockClearAuthCredentials = vi.fn(() => ({ success: true })); -const mockResetWasmOutput = vi.fn(); -const mockToggleWasmDebug = vi.fn(); -const mockDebugAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: 'staging', -})); -const mockGetAuthInfo = vi.fn(() => ({ - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: 'staging', -})); - -// Helper to create a test wrapper for the composable -const createComposableTestWrapper = (config = {}) => { - let composableInstance: any; - const TestComponent = defineComponent({ - template: '
Test
', - setup() { - composableInstance = useMegaportWASM(config); - return composableInstance; - }, - }); - const wrapper = mount(TestComponent); - return { wrapper, composable: composableInstance }; -}; - -// Helper to wait for WASM to be ready -const waitForReady = async (isReady: any, timeout = 500) => { - const start = Date.now(); - while (!isReady.value && Date.now() - start < timeout) { - await new Promise((resolve) => setTimeout(resolve, 50)); - } - if (!isReady.value) { - throw new Error('WASM did not become ready in time'); - } -}; - -describe('useMegaportWASM', () => { - beforeEach(() => { - // Setup global mocks - (global as any).Go = MockGo; - (global as any).executeMegaportCommandAsync = - mockExecuteMegaportCommandAsync; - (global as any).setAuthCredentials = mockSetAuthCredentials; - (global as any).clearAuthCredentials = mockClearAuthCredentials; - (global as any).resetWasmOutput = mockResetWasmOutput; - (global as any).toggleWasmDebug = mockToggleWasmDebug; - (global as any).debugAuthInfo = mockDebugAuthInfo; - (global as any).getAuthInfo = mockGetAuthInfo; - - (window as any).Go = MockGo; - (window as any).executeMegaportCommandAsync = - mockExecuteMegaportCommandAsync; - (window as any).setAuthCredentials = mockSetAuthCredentials; - (window as any).clearAuthCredentials = mockClearAuthCredentials; - (window as any).debugAuthInfo = mockDebugAuthInfo; - (window as any).getAuthInfo = mockGetAuthInfo; - - // Mock fetch for WASM loading - global.fetch = vi.fn(() => - Promise.resolve({ - arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), - } as Response) - ); - - // Mock WebAssembly - global.WebAssembly = { - instantiate: vi.fn(() => - Promise.resolve({ - instance: {}, - module: {}, - }) - ), - instantiateStreaming: vi.fn(), - } as any; - - // Mock Worker - global.Worker = vi.fn(() => ({ - postMessage: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - terminate: vi.fn(), - })) as any; - - // Clear localStorage - localStorage.clear(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('Initialization', () => { - it('should initialize in loading state', () => { - const { composable } = createComposableTestWrapper(); - const { isLoading, isReady, error } = composable; - - expect(isLoading.value).toBe(true); - expect(isReady.value).toBe(false); - expect(error.value).toBe(null); - }); - - it('should initialize with default config', () => { - const { composable } = createComposableTestWrapper(); - const { isLoading } = composable; - expect(isLoading.value).toBe(true); - }); - - it('should accept custom config', () => { - const { composable } = createComposableTestWrapper({ - wasmPath: '/custom/path.wasm', - wasmExecPath: '/custom/exec.js', - debug: true, - useWorker: false, - }); - const { isLoading } = composable; - - expect(isLoading.value).toBe(true); - }); - - it('should initialize with worker mode when configured', () => { - const { composable } = createComposableTestWrapper({ useWorker: true }); - const { isLoading } = composable; - expect(isLoading.value).toBe(true); - }); - - it('fetches the build-time injected wasm URL when no wasmPath is given', async () => { - (window as any).__MEGAPORT_WASM_URL__ = '/megaport.deadbeef.wasm'; - try { - const { composable } = createComposableTestWrapper(); - await waitForReady(composable.isReady); - expect(global.fetch).toHaveBeenCalledWith('/megaport.deadbeef.wasm'); - } finally { - delete (window as any).__MEGAPORT_WASM_URL__; - } - }); - }); - - describe('Authentication', () => { - it('should set authentication credentials', async () => { - const { composable } = createComposableTestWrapper(); - const { setAuth } = composable; - - setAuth('test-access-key', 'test-secret-key', 'staging'); - - await nextTick(); - - expect(mockSetAuthCredentials).toHaveBeenCalledWith( - 'test-access-key', - 'test-secret-key', - 'staging' - ); - }); - - it('should clear authentication credentials', async () => { - const { composable } = createComposableTestWrapper(); - const { clearAuth } = composable; - - clearAuth(); - - await nextTick(); - - expect(mockClearAuthCredentials).toHaveBeenCalled(); - }); - - it('should get authentication info when configured', () => { - const mockAuthInfo = { - accessKeySet: true, - accessKeyPreview: 'test-key', - secretKeySet: true, - secretKeyPreview: 'test-secret', - environment: 'production', - }; - mockDebugAuthInfo.mockReturnValue(mockAuthInfo); - mockGetAuthInfo.mockReturnValue(mockAuthInfo); - (window as any).getAuthInfo = vi.fn(() => mockAuthInfo); - - const { composable } = createComposableTestWrapper(); - const { getAuthInfo } = composable; - const authInfo = getAuthInfo(); - - expect(authInfo?.accessKeySet).toBe(true); - expect(authInfo?.secretKeySet).toBe(true); - expect(authInfo?.environment).toBe('production'); - }); - - it('should detect unconfigured auth', () => { - const emptyAuthInfo = { - accessKeySet: false, - accessKeyPreview: '', - secretKeySet: false, - secretKeyPreview: '', - environment: '', - }; - mockDebugAuthInfo.mockReturnValue(emptyAuthInfo); - mockGetAuthInfo.mockReturnValue(emptyAuthInfo); - (window as any).getAuthInfo = vi.fn(() => emptyAuthInfo); - - const { composable } = createComposableTestWrapper(); - const { getAuthInfo } = composable; - const authInfo = getAuthInfo(); - - expect(authInfo?.accessKeySet).toBe(false); - expect(authInfo?.secretKeySet).toBe(false); - }); - }); - - describe('Command Execution', () => { - it('should execute commands successfully', async () => { - const mockResult = { - output: 'Command output', - error: '', - }; - - mockExecuteMegaportCommandAsync.mockImplementation( - (cmd: string, callback: Function) => { - callback(mockResult); - } - ); - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { execute, isReady } = composable; - - // Wait for actual ready state - await waitForReady(isReady); - - const result = await execute('port list'); - - expect(result.output).toBe('Command output'); - expect(result.error).toBe(''); - - wrapper.unmount(); - }); - - it('should handle command errors', async () => { - const mockResult = { - output: '', - error: 'Command failed', - }; - - mockExecuteMegaportCommandAsync.mockImplementation( - (cmd: string, callback: Function) => { - callback(mockResult); - } - ); - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { execute, isReady } = composable; - await waitForReady(isReady); - - const result = await execute('invalid command'); - - expect(result.output).toBe(''); - expect(result.error).toBe('Command failed'); - - wrapper.unmount(); - }); - - it('should reject when WASM is not ready', async () => { - mockExecuteMegaportCommandAsync.mockImplementation(() => { - throw new Error('WASM not initialized'); - }); - - const { composable } = createComposableTestWrapper(); - const { execute } = composable; - - await expect(execute('port list')).rejects.toThrow(); - }); - - it('should handle multiple concurrent commands', async () => { - mockExecuteMegaportCommandAsync.mockImplementation( - (cmd: string, callback: Function) => { - setTimeout(() => { - callback({ output: `Result for: ${cmd}`, error: '' }); - }, 50); - } - ); - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { execute, isReady } = composable; - await waitForReady(isReady); - - const results = await Promise.all([ - execute('port list'), - execute('vxc list'), - execute('location list'), - ]); - - expect(results).toHaveLength(3); - results.forEach((result) => { - expect(result.output).toContain('Result for:'); - }); - - wrapper.unmount(); - }); - }); - - describe('Output Management', () => { - it('should reset output buffers', () => { - const { composable } = createComposableTestWrapper(); - const { resetOutput } = composable; - - resetOutput(); - - expect(mockResetWasmOutput).toHaveBeenCalled(); - }); - - it('should handle missing reset function gracefully', () => { - (global as any).resetWasmOutput = undefined; - - const { composable } = createComposableTestWrapper(); - const { resetOutput } = composable; - - expect(() => resetOutput()).not.toThrow(); - }); - }); - - describe('Debug Mode', () => { - it('should toggle debug mode', () => { - mockToggleWasmDebug.mockReturnValue(true); - - const { composable } = createComposableTestWrapper(); - const { toggleDebug } = composable; - - const enabled = toggleDebug(); - - expect(mockToggleWasmDebug).toHaveBeenCalled(); - expect(enabled).toBe(true); - }); - - it('should handle missing debug function gracefully', () => { - (global as any).toggleWasmDebug = undefined; - - const { composable } = createComposableTestWrapper(); - const { toggleDebug } = composable; - - expect(() => toggleDebug()).not.toThrow(); - }); - - it('should initialize with debug mode when configured', () => { - const { composable } = createComposableTestWrapper({ debug: true }); - const { toggleDebug } = composable; - - // Debug mode should be enabled during initialization - expect(toggleDebug).toBeDefined(); - }); - }); - - describe('Error Handling', () => { - it('should handle WASM loading errors', async () => { - global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))); - delete (window as any).Go; - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - - await new Promise((resolve) => setTimeout(resolve, 250)); - - const { error, isLoading } = composable; - expect(error.value).toBeTruthy(); - expect(isLoading.value).toBe(false); - - wrapper.unmount(); - }); - - it('should handle missing wasm_exec.js script', async () => { - // Mock script loading failure - const originalCreateElement = document.createElement.bind(document); - document.createElement = vi.fn((tag: string) => { - const element = originalCreateElement(tag); - if (tag === 'script') { - setTimeout(() => { - element.onerror?.(new Event('error')); - }, 10); - } - return element; - }) as any; - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { error } = composable; - - await new Promise((resolve) => setTimeout(resolve, 100)); - - document.createElement = originalCreateElement; - - wrapper.unmount(); - }); - - it('should handle WebAssembly instantiation errors', async () => { - global.WebAssembly.instantiate = vi.fn(() => - Promise.reject(new Error('Invalid WASM')) - ); - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - - await new Promise((resolve) => setTimeout(resolve, 250)); - - const { error, isLoading } = composable; - expect(error.value).toBeTruthy(); - expect(isLoading.value).toBe(false); - - wrapper.unmount(); - }); - }); - - describe('Worker Mode', () => { - it('should create worker when useWorker is true', () => { - const { composable } = createComposableTestWrapper({ useWorker: true }); - const { isLoading } = composable; - - // Worker mode falls back to direct mode, so Worker is not called - expect(isLoading.value).toBe(true); - }); - - it('should send INIT message to worker', async () => { - // Worker mode not fully implemented - falls back to direct mode - const { composable } = createComposableTestWrapper({ useWorker: true }); - const { isLoading } = composable; - - await nextTick(); - - expect(isLoading.value).toBe(true); - }); - - it('should handle worker errors', async () => { - // Worker mode falls back to direct - test direct mode error instead - global.fetch = vi.fn(() => Promise.reject(new Error('Init failed'))); - delete (window as any).Go; - - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: true, - }); - - await new Promise((resolve) => setTimeout(resolve, 250)); - - const { error } = composable; - expect(error.value).toBeTruthy(); - - wrapper.unmount(); - }); - }); - - describe('Reactivity', () => { - it('should expose reactive refs', () => { - const { composable, wrapper } = createComposableTestWrapper(); - const { isLoading, isReady, error } = composable; - - expect(isLoading.value).toBeDefined(); - expect(isReady.value).toBeDefined(); - expect(error.value).toBeDefined(); - - wrapper.unmount(); - }); - - it('should update loading state', async () => { - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { isLoading, isReady } = composable; - - expect(isLoading.value).toBe(true); - expect(isReady.value).toBe(false); - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 300)); - - // After initialization, loading should be false - // (Implementation may vary based on actual WASM loading) - - wrapper.unmount(); - }); - }); - - describe('Cleanup', () => { - it('should provide methods for cleanup', () => { - const { composable, wrapper } = createComposableTestWrapper(); - const { resetOutput, clearAuth } = composable; - - expect(resetOutput).toBeDefined(); - expect(clearAuth).toBeDefined(); - - wrapper.unmount(); - }); - - it('should clear auth on cleanup', () => { - const { composable, wrapper } = createComposableTestWrapper(); - const { clearAuth } = composable; - - clearAuth(); - - expect(mockClearAuthCredentials).toHaveBeenCalled(); - - wrapper.unmount(); - }); - }); - - describe('Spinner Functionality', () => { - beforeEach(async () => { - // Clear window spinner functions - delete (window as any).wasmStartSpinner; - delete (window as any).wasmStopSpinner; - // Small delay to ensure cleanup is complete - await new Promise((resolve) => setTimeout(resolve, 10)); - }); - - it('should expose activeSpinners state', () => { - const { composable, wrapper } = createComposableTestWrapper(); - const { activeSpinners } = composable; - - expect(activeSpinners).toBeDefined(); - expect(activeSpinners.value).toBeInstanceOf(Map); - expect(activeSpinners.value.size).toBe(0); - - wrapper.unmount(); - }); - - it('should register wasmStartSpinner on window', async () => { - const { wrapper } = createComposableTestWrapper({ useWorker: false }); - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - expect((window as any).wasmStartSpinner).toBeDefined(); - expect(typeof (window as any).wasmStartSpinner).toBe('function'); - - wrapper.unmount(); - }); - - it('should register wasmStopSpinner on window', async () => { - const { wrapper } = createComposableTestWrapper({ useWorker: false }); - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - expect((window as any).wasmStopSpinner).toBeDefined(); - expect(typeof (window as any).wasmStopSpinner).toBe('function'); - - wrapper.unmount(); - }); - - it('should add spinner when wasmStartSpinner is called', async () => { - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { activeSpinners } = composable; - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - const spinnerId = (window as any).wasmStartSpinner?.( - 'Test spinner message' - ); - - expect(spinnerId).toBeDefined(); - expect(activeSpinners.value.size).toBe(1); - expect(activeSpinners.value.get(spinnerId)).toBe('Test spinner message'); - - wrapper.unmount(); - }); - - it('should remove spinner when wasmStopSpinner is called', async () => { - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { activeSpinners } = composable; - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - const spinnerId = (window as any).wasmStartSpinner?.( - 'Test spinner message' - ); - expect(activeSpinners.value.size).toBe(1); - - (window as any).wasmStopSpinner?.(spinnerId); - expect(activeSpinners.value.size).toBe(0); - - wrapper.unmount(); - }); - - it('should handle multiple concurrent spinners', async () => { - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { activeSpinners } = composable; - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - const spinner1 = (window as any).wasmStartSpinner?.('Spinner 1'); - const spinner2 = (window as any).wasmStartSpinner?.('Spinner 2'); - const spinner3 = (window as any).wasmStartSpinner?.('Spinner 3'); - - expect(activeSpinners.value.size).toBe(3); - expect(activeSpinners.value.get(spinner1)).toBe('Spinner 1'); - expect(activeSpinners.value.get(spinner2)).toBe('Spinner 2'); - expect(activeSpinners.value.get(spinner3)).toBe('Spinner 3'); - - (window as any).wasmStopSpinner?.(spinner2); - expect(activeSpinners.value.size).toBe(2); - expect(activeSpinners.value.has(spinner1)).toBe(true); - expect(activeSpinners.value.has(spinner2)).toBe(false); - expect(activeSpinners.value.has(spinner3)).toBe(true); - - wrapper.unmount(); - }); - - it('should generate unique spinner IDs', async () => { - const { wrapper } = createComposableTestWrapper({ useWorker: false }); - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - const ids: string[] = []; - for (let i = 0; i < 10; i++) { - const id = (window as any).wasmStartSpinner?.(`Spinner ${i}`); - ids.push(id); - } - - // All IDs should be unique - const uniqueIds = new Set(ids); - expect(uniqueIds.size).toBe(10); - - wrapper.unmount(); - }); - - it('should track spinner messages correctly', async () => { - const { composable, wrapper } = createComposableTestWrapper({ - useWorker: false, - }); - const { activeSpinners } = composable; - - // Wait for initialization - await new Promise((resolve) => setTimeout(resolve, 200)); - - const messages = [ - 'Logging in to Megaport...', - 'Validating Port order...', - 'Creating Port pb-test-port-vue...', - ]; - - const ids = messages.map((msg) => - (window as any).wasmStartSpinner?.(msg) - ); - - expect(activeSpinners.value.size).toBe(3); - - ids.forEach((id, index) => { - expect(activeSpinners.value.get(id)).toBe(messages[index]); - }); - - wrapper.unmount(); - }); - }); -}); diff --git a/frontend-integration/__tests__/wasmUrl.test.ts b/frontend-integration/__tests__/wasmUrl.test.ts deleted file mode 100644 index 345d0919..00000000 --- a/frontend-integration/__tests__/wasmUrl.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, it, expect, afterEach } from 'vitest'; -import { resolveWasmUrl, DEFAULT_WASM_PATH } from '@/utils/wasmUrl'; - -describe('resolveWasmUrl', () => { - afterEach(() => { - delete window.__MEGAPORT_WASM_URL__; - }); - - it('falls back to the fixed path when no global is injected', () => { - expect(resolveWasmUrl()).toBe(DEFAULT_WASM_PATH); - expect(DEFAULT_WASM_PATH).toBe('/megaport.wasm'); - }); - - it('prefers the build-time-injected content-hashed URL', () => { - window.__MEGAPORT_WASM_URL__ = '/megaport.5f560da7.wasm'; - expect(resolveWasmUrl()).toBe('/megaport.5f560da7.wasm'); - }); - - it('ignores an empty injected value and falls back', () => { - window.__MEGAPORT_WASM_URL__ = ''; - expect(resolveWasmUrl()).toBe(DEFAULT_WASM_PATH); - }); -}); diff --git a/frontend-integration/components/MegaportTerminal.vue b/frontend-integration/components/MegaportTerminal.vue deleted file mode 100644 index 74c6b21c..00000000 --- a/frontend-integration/components/MegaportTerminal.vue +++ /dev/null @@ -1,866 +0,0 @@ -/** * Vue 3 Component: Megaport CLI Terminal * Provides an interactive terminal -interface using xterm.js */ - - - - - - diff --git a/frontend-integration/composables/useMegaportWASM.ts b/frontend-integration/composables/useMegaportWASM.ts deleted file mode 100644 index ce183448..00000000 --- a/frontend-integration/composables/useMegaportWASM.ts +++ /dev/null @@ -1,662 +0,0 @@ -/** - * Vue 3 Composable for Megaport CLI WASM Integration - * Handles WASM loading, initialization, and command execution - */ - -import { ref, onMounted, onUnmounted, readonly, triggerRef } from 'vue'; -import type { Ref } from 'vue'; -import { WASM_CONFIG } from '../constants/megaportWASM'; -import { - isMegaportCommandResult, - isMegaportPromptRequest, - hasWASMFunctions, - hasWebAssemblySupport, - isValidCommand, - getErrorMessage, -} from '../utils/type-guards'; -import { resolveWasmUrl } from '../utils/wasmUrl'; - -interface MegaportCommandResult { - output?: string; - error?: string; -} - -interface MegaportWASMConfig { - wasmPath?: string; - wasmExecPath?: string; - debug?: boolean; - initTimeout?: number; // Timeout for WASM initialization in ms - maxRetries?: number; // Maximum retry attempts for failed initialization - retryDelay?: number; // Base delay between retries in ms - onTelemetry?: ( - event: import('../types/megaport-wasm').TelemetryEvent - ) => void; // Telemetry callback -} - -export function useMegaportWASM(config: MegaportWASMConfig = {}) { - const { - wasmPath = resolveWasmUrl(), - wasmExecPath = '/wasm_exec.js', - debug = false, - initTimeout = WASM_CONFIG.INIT_TIMEOUT, - maxRetries = WASM_CONFIG.MAX_RETRIES, - retryDelay = WASM_CONFIG.RETRY_DELAY, - onTelemetry, - } = config; - - // State - const isLoading: Ref = ref(true); - const isReady: Ref = ref(false); - const error: Ref = ref(null); - const activeSpinners: Ref> = ref(new Map()); - - // Counter for unique spinner IDs - let spinnerCounter = 0; - - /** - * Conditional logging helper - only logs when debug mode is enabled - */ - const log = (message: string, ...args: any[]) => { - if (debug) { - console.log(message, ...args); - } - }; - - const warn = (message: string, ...args: any[]) => { - // Warnings should always be shown, regardless of debug mode - console.warn(message, ...args); - }; - - /** - * Emit telemetry event if callback is provided - */ - const emitTelemetry = ( - type: import('../types/megaport-wasm').TelemetryEventType, - metadata?: Record, - duration?: number - ) => { - if (onTelemetry) { - onTelemetry({ - type, - timestamp: Date.now(), - duration, - metadata, - }); - } - }; - - /** - * Load the wasm_exec.js script - */ - const loadWasmExec = (): Promise => { - return new Promise((resolve, reject) => { - if (window.Go) { - resolve(); - return; - } - - const script = document.createElement('script'); - script.src = wasmExecPath; - script.onload = () => resolve(); - script.onerror = () => reject(new Error('Failed to load wasm_exec.js')); - document.head.appendChild(script); - }); - }; - - /** - * Setup global spinner functions for WASM - */ - const setupSpinnerFunctions = (): void => { - // Global spinner start function - (window as any).wasmStartSpinner = (message: string): string => { - const spinnerId = `spinner_${Date.now()}_${spinnerCounter++}`; - // Mutate the Map directly and trigger reactivity manually - activeSpinners.value.set(spinnerId, message); - triggerRef(activeSpinners); - - log(`🔄 Spinner started: ${spinnerId} - ${message}`); - emitTelemetry('spinner_start', { spinnerId, message }); - - return spinnerId; - }; - - // Global spinner stop function - (window as any).wasmStopSpinner = (spinnerId: string): void => { - const message = activeSpinners.value.get(spinnerId); - // Mutate the Map directly and trigger reactivity manually - activeSpinners.value.delete(spinnerId); - triggerRef(activeSpinners); - - if (message) { - log(`⏹️ Spinner stopped: ${spinnerId} - ${message}`); - } - emitTelemetry('spinner_stop', { spinnerId, message }); - }; - - log('✅ Spinner functions registered on window'); - }; - - /** - * Initialize WASM directly in main thread with timeout - * Better for development and simpler integration - */ - const initDirect = async (): Promise => { - const startTime = Date.now(); - emitTelemetry('wasm_init_start', { mode: 'direct' }); - - // Verify WebAssembly support - if (!hasWebAssemblySupport()) { - const err = new Error('WebAssembly is not supported in this browser'); - error.value = err; - isLoading.value = false; - emitTelemetry( - 'wasm_init_error', - { - mode: 'direct', - error: err.message, - }, - Date.now() - startTime - ); - throw err; - } - - // Create timeout promise - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error(`WASM initialization timeout after ${initTimeout}ms`)); - }, initTimeout); - }); - - // Create initialization promise - const initPromise = async () => { - try { - // Setup spinner functions first - setupSpinnerFunctions(); - - // Load wasm_exec.js - await loadWasmExec(); - - if (!window.Go) { - throw new Error('Go WASM runtime not loaded'); - } - - // Initialize Go runtime - const go = new window.Go(); - - // Fetch and instantiate WASM - const response = await fetch(wasmPath); - const buffer = await response.arrayBuffer(); - const result = await WebAssembly.instantiate(buffer, go.importObject); - - // Run the Go program - go.run(result.instance); - - // Wait a bit for initialization - await new Promise((resolve) => - setTimeout(resolve, WASM_CONFIG.INIT_STABILIZATION_DELAY) - ); - - // Verify functions are available - if (!hasWASMFunctions(window)) { - throw new Error('WASM functions not exposed'); - } - - // Register prompt handler for interactive mode - if (window.registerPromptHandler) { - window.registerPromptHandler((promptRequest: any) => { - // Validate prompt request with type guard - if (!isMegaportPromptRequest(promptRequest)) { - warn('⚠️ Invalid prompt request received:', promptRequest); - return; - } - - log('📝 Prompt requested:', promptRequest); - - // Note: The default handler does nothing - applications MUST register - // their own prompt handler for interactive mode to work properly. - // This prevents unwanted browser prompt() dialogs. - // See MegaportTerminal.vue for an example of inline terminal prompts. - warn( - '⚠️ No custom prompt handler registered. Interactive commands require ' + - 'a prompt handler. Use registerPromptHandler() to provide one.' - ); - }); - - log( - '✅ Default prompt handler registered (does nothing - override required)' - ); - } - - isReady.value = true; - isLoading.value = false; - - const duration = Date.now() - startTime; - emitTelemetry('wasm_init_success', { mode: 'direct' }, duration); - - log('✅ Megaport WASM ready (direct mode)'); - log('Available functions:', { - executeMegaportCommandAsync: - typeof window.executeMegaportCommandAsync, - debugAuthInfo: typeof window.debugAuthInfo, - }); - } catch (err) { - error.value = err as Error; - isLoading.value = false; - const duration = Date.now() - startTime; - emitTelemetry( - 'wasm_init_error', - { - mode: 'direct', - error: (err as Error).message, - }, - duration - ); - throw err; - } - }; - - // Race initialization against timeout - try { - await Promise.race([initPromise(), timeoutPromise]); - } catch (err) { - error.value = err as Error; - isLoading.value = false; - const duration = Date.now() - startTime; - emitTelemetry( - 'wasm_init_error', - { - mode: 'direct', - error: (err as Error).message, - }, - duration - ); - throw err; - } - }; - - /** - * Execute a CLI command - */ - const execute = async (command: string): Promise => { - // Validate command with type guard - if (!isValidCommand(command)) { - const error = - 'Invalid command: must be a non-empty string without dangerous patterns'; - emitTelemetry('command_execute_error', { command, error }, 0); - throw new Error(error); - } - - if (!isReady.value) { - throw new Error('WASM not ready'); - } - - const startTime = Date.now(); - emitTelemetry('command_execute_start', { command }); - - return new Promise((resolve, reject) => { - try { - log(`🚀 Executing command: ${command}`); - - if (typeof window.executeMegaportCommandAsync !== 'function') { - const duration = Date.now() - startTime; - emitTelemetry( - 'command_execute_error', - { - command, - error: 'executeMegaportCommandAsync is not available on window', - }, - duration - ); - reject(new Error('executeMegaportCommandAsync is not available on window')); - return; - } - - window.executeMegaportCommandAsync(command, (result) => { - const duration = Date.now() - startTime; - - // Validate result with type guard - if (!isMegaportCommandResult(result)) { - const error = 'Invalid command result received from WASM'; - warn('⚠️ Invalid result:', result); - emitTelemetry( - 'command_execute_error', - { command, error }, - duration - ); - reject(new Error(error)); - return; - } - - log('📦 Command result:', result); - - if (result.error) { - emitTelemetry( - 'command_execute_error', - { - command, - error: result.error, - }, - duration - ); - } else { - emitTelemetry('command_execute_success', { command }, duration); - } - - resolve(result); - }); - } catch (err) { - const duration = Date.now() - startTime; - emitTelemetry( - 'command_execute_error', - { - command, - error: getErrorMessage(err), - }, - duration - ); - reject(err); - } - }); - }; - - /** - * Set authentication credentials (secure, in-memory only) - * - * Available WASM functions: - * - executeMegaportCommandAsync: 'function' - Asynchronous command execution with callback - * - debugAuthInfo: 'function' - Get current auth state for debugging - * - setAuthCredentials: 'function' - Secure in-memory credential storage - * - clearAuthCredentials: 'function' - Clear credentials from memory - * - * This function uses setAuthCredentials to store credentials securely in-memory. - * Credentials are NOT persisted and will be cleared on page refresh. - * This prevents XSS attacks that could steal credentials from localStorage. - */ - const setAuth = ( - accessKey: string, - secretKey: string, - environment = 'staging' - ): void => { - // Use secure in-memory credential storage - if (window.setAuthCredentials) { - const result = window.setAuthCredentials( - accessKey, - secretKey, - environment - ); - - log('🔑 Auth credentials set securely (in-memory only)'); - emitTelemetry('auth_set', { environment, success: result?.success }); - - if (result && !result.success) { - console.error('Failed to set credentials:', result.error); // Always log errors - } - if (window.debugAuthInfo) { - log('Auth info:', window.debugAuthInfo()); - } - } else { - console.error( - '❌ setAuthCredentials function not available. WASM may not be initialized.' - ); // Always log errors - emitTelemetry('auth_set', { environment, success: false }); - } - }; - - /** - * Clear authentication credentials from memory - */ - const clearAuth = (): void => { - if (window.clearAuthCredentials) { - window.clearAuthCredentials(); - log('🔓 Auth credentials cleared from memory'); - emitTelemetry('auth_clear', {}); - } else { - console.error( - '❌ clearAuthCredentials function not available. WASM may not be initialized.' - ); // Always log errors - } - }; - - /** - * Set authentication using an existing token from the portal session - * This bypasses the OAuth flow - no API Key/Secret needed! - * Use this when the portal already has a valid session token. - * - * @param token - The access token from the portal session - * @param hostname - The current hostname (e.g., window.location.hostname) - used to determine environment - * @param environment - Optional explicit environment override; see `window.setAuthToken` - * @param expiry - Optional real expiry of `token`, as epoch milliseconds or an RFC3339 string. - * Stored and echoed back by the CLI, but not checked proactively; omit it if unknown. Whenever - * the API actually rejects a request with 401/403, command output contains the marker - * `"MEGAPORT_SESSION_EXPIRED"` - watch for it and call `setAuthToken` again to re-authenticate. - */ - const setAuthToken = ( - token: string, - hostname?: string, - environment?: string, - expiry?: number | string - ): void => { - // SSR-safe: only access window.location in browser context - const actualHostname = hostname ?? (typeof window !== 'undefined' ? window.location.hostname : 'localhost'); - if (window.setAuthToken) { - // Only pass environment/expiry through when actually supplied, so calls - // that don't use them keep the historical two-argument call shape. - const args: [string, string, string?, (number | string)?] = [token, actualHostname]; - if (environment !== undefined || expiry !== undefined) { - args.push(environment); - } - if (expiry !== undefined) { - args.push(expiry); - } - const result = window.setAuthToken(...args); - - log(`🔑 External token set (bypassing OAuth flow) - hostname: ${actualHostname}, environment: ${result?.environment}`); - emitTelemetry('auth_token_set', { - hostname: actualHostname, - environment: result?.environment, - success: result?.success, - }); - - if (result && !result.success) { - console.error('Failed to set token:', result.error); - } - if (window.debugAuthInfo) { - log('Auth info:', window.debugAuthInfo()); - } - } else { - console.error( - '❌ setAuthToken function not available. WASM may not be initialized.' - ); - emitTelemetry('auth_token_set', { hostname: actualHostname, success: false }); - } - }; - - /** - * Get authentication status - */ - const getAuthInfo = () => { - if (window.debugAuthInfo) { - return window.debugAuthInfo(); - } - return null; - }; - - /** - * Reset output buffers - */ - const resetOutput = (): void => { - if (window.resetWasmOutput) { - window.resetWasmOutput(); - } - }; - - /** - * Toggle debug mode - */ - const toggleDebug = (): boolean => { - if (window.toggleWasmDebug) { - return window.toggleWasmDebug(); - } - return false; - }; - - /** - * Register a custom prompt handler for interactive commands - * This allows applications to provide their own UI for prompts - * instead of using the default browser prompt() - * - * @param callback - Function to handle prompt requests - * @returns true if registered successfully - * - * @example - * ```typescript - * registerPromptHandler((request) => { - * // Show custom UI for the prompt - * showCustomPrompt(request.message).then(response => { - * if (response) { - * window.submitPromptResponse(request.id, response); - * } else { - * window.cancelPrompt(request.id); - * } - * }); - * }); - * ``` - */ - const registerPromptHandler = (callback: (request: any) => void): boolean => { - if (window.registerPromptHandler) { - return window.registerPromptHandler(callback); - } - warn('registerPromptHandler not available - WASM may not be initialized'); - return false; - }; - - /** - * Register a handler for live command output. - * - * The handler is invoked with each chunk of narrative output (progress lines, - * echoes, warnings, validation errors) as the command writes it, so the - * terminal can render output as it streams instead of waiting for completion. - * - * Contract: when the handler delivers output normally, the narrative is - * delivered here and is NOT repeated in the `execute()` result. The result's - * `output` then holds only structured document output (JSON/CSV/XML/table), - * or is empty when the command produced only streamed narrative. Do not render - * both. If the handler throws (or delivers nothing), the WASM side falls back - * to returning the full captured output in `result.output`, so already-streamed - * chunks may appear there too. - * - * Chunks use `\n` line endings; xterm hosts should translate to `\r\n`. - * - * @param callback - Function called with each output chunk - * @returns true if registered successfully - */ - const registerOutputHandler = (callback: (chunk: string) => void): boolean => { - if (window.registerOutputHandler) { - return window.registerOutputHandler(callback); - } - warn('registerOutputHandler not available - WASM may not be initialized'); - return false; - }; - - /** - * Initialize WASM with retry logic - * Attempts initialization multiple times with exponential backoff - */ - const initWithRetry = async (): Promise => { - let lastError: Error | null = null; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - log(`Attempt ${attempt}/${maxRetries}: Initializing Megaport WASM...`); - - await initDirect(); - - log(`✅ WASM initialized successfully on attempt ${attempt}`); - return; // Success! - } catch (err) { - lastError = err as Error; - console.error( - `❌ WASM initialization attempt ${attempt}/${maxRetries} failed:`, - err - ); - - if (attempt < maxRetries) { - // Calculate exponential backoff delay - const delay = retryDelay * Math.pow(2, attempt - 1); - log(`Retrying in ${delay}ms...`); - - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - } - - // All retries failed - const finalError = new Error( - `WASM initialization failed after ${maxRetries} attempts. Last error: ${lastError?.message}` - ); - error.value = finalError; - isLoading.value = false; - throw finalError; - }; - - // Initialize on mount - onMounted(async () => { - try { - await initWithRetry(); - } catch (err) { - console.error( - 'Failed to initialize Megaport WASM after all retries:', - err - ); // Always log final failure - error.value = err as Error; - } - }); - - /** - * Cleanup function - clears state and auth - */ - const cleanup = () => { - log('🧹 Cleaning up WASM resources'); - - // Clear active spinners - activeSpinners.value.clear(); - - // Clear auth credentials from memory for security - if (window.clearAuthCredentials) { - window.clearAuthCredentials(); - } - - // Remove global spinner functions - if (typeof window !== 'undefined') { - delete (window as any).wasmStartSpinner; - delete (window as any).wasmStopSpinner; - } - - log('Cleanup complete'); - }; - - // Cleanup on unmount - onUnmounted(() => { - cleanup(); - }); - - return { - // State (readonly refs) - isLoading: readonly(isLoading), - isReady: readonly(isReady), - error: readonly(error), - activeSpinners: readonly(activeSpinners), - - // Methods - execute, - setAuth, - setAuthToken, // For portal token integration (bypasses OAuth) - clearAuth, - getAuthInfo, - resetOutput, - toggleDebug, - registerPromptHandler, - registerOutputHandler, // For live streamed command output - cleanup, // Expose cleanup for manual cleanup if needed - }; -} diff --git a/frontend-integration/constants/megaportWASM.ts b/frontend-integration/constants/megaportWASM.ts deleted file mode 100644 index 0d754982..00000000 --- a/frontend-integration/constants/megaportWASM.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Megaport WASM Configuration Constants - * Centralized configuration for WASM initialization and terminal behavior - */ - -/** - * WASM Initialization Configuration - */ -export const WASM_CONFIG = { - /** Maximum time to wait for WASM initialization (milliseconds) */ - INIT_TIMEOUT: 30000, - - /** Delay after initialization to allow WASM to stabilize (milliseconds) */ - INIT_STABILIZATION_DELAY: 100, - - /** Maximum number of retry attempts for failed operations */ - MAX_RETRIES: 3, - - /** Initial delay between retry attempts (milliseconds) */ - RETRY_DELAY: 1000, - - /** Interval for checking WASM ready state (milliseconds) */ - READY_CHECK_INTERVAL: 100, - - /** Maximum time to wait for WASM ready state (milliseconds) */ - READY_TIMEOUT: 30000, -} as const; - -/** - * Terminal Configuration - */ -export const TERMINAL_CONFIG = { - /** Font size for terminal display (pixels) */ - FONT_SIZE: 14, - - /** Font family for terminal display */ - FONT_FAMILY: 'Menlo, Monaco, "Courier New", monospace', - - /** Delay for debouncing terminal resize events (milliseconds) */ - RESIZE_DEBOUNCE_DELAY: 150, - - /** Maximum number of commands to keep in history */ - MAX_HISTORY_SIZE: 100, -} as const; - -/** - * Type exports for better IDE support - */ -export type WASMConfig = typeof WASM_CONFIG; -export type TerminalConfig = typeof TERMINAL_CONFIG; diff --git a/frontend-integration/demo/App.vue b/frontend-integration/demo/App.vue deleted file mode 100644 index 972f1d7b..00000000 --- a/frontend-integration/demo/App.vue +++ /dev/null @@ -1,514 +0,0 @@ -/** * Demo Application: Megaport CLI WASM with Vue 3 */ - - - - - - diff --git a/frontend-integration/demo/main.ts b/frontend-integration/demo/main.ts deleted file mode 100644 index b0788dd0..00000000 --- a/frontend-integration/demo/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createApp } from 'vue'; -import App from './App.vue'; - -const app = createApp(App); -app.mount('#app'); diff --git a/frontend-integration/index.html b/frontend-integration/index.html deleted file mode 100644 index 8be6c408..00000000 --- a/frontend-integration/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Megaport CLI WASM - Vue 3 Demo - - - -
- - - - \ No newline at end of file diff --git a/frontend-integration/index.ts b/frontend-integration/index.ts deleted file mode 100644 index 0c38570d..00000000 --- a/frontend-integration/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Main entry point for the Megaport CLI WASM Vue 3 integration package - -export { useMegaportWASM } from './composables/useMegaportWASM'; -export { default as MegaportTerminal } from './components/MegaportTerminal.vue'; -export type * from './types/megaport-wasm.d'; diff --git a/frontend-integration/package-lock.json b/frontend-integration/package-lock.json deleted file mode 100644 index 25f10ff1..00000000 --- a/frontend-integration/package-lock.json +++ /dev/null @@ -1,2500 +0,0 @@ -{ - "name": "@megaport/cli-wasm-vue3", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@megaport/cli-wasm-vue3", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0", - "@xterm/xterm": "^5.5.0", - "vue": "^3.3.0" - }, - "devDependencies": { - "@types/node": "^20.10.0", - "@vitejs/plugin-vue": "^6.0.7", - "@vitest/ui": "^4.1.3", - "@vue/test-utils": "^2.4.6", - "happy-dom": "^20.0.10", - "typescript": "^5.3.0", - "vite": "^8.0.16", - "vitest": "^4.1.3", - "vue-tsc": "^2.0.0" - }, - "engines": { - "node": ">=20", - "npm": ">=9" - }, - "peerDependencies": { - "vue": "^3.3.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/whatwg-mimetype": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", - "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.8", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.8.tgz", - "integrity": "sha512-RUS2ZU2TsduVrI+9c12uTNaKrNUTsm6yFt3fueEUB9iKvyC2UP83F+sqIz00HQIah4UOL1TMoDAki8K0NjGvsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.8", - "fflate": "^0.8.2", - "flatted": "^3.4.2", - "pathe": "^2.0.3", - "sirv": "^3.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.8" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", - "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.15" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", - "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", - "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.15", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", - "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/shared": "3.5.35", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", - "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.35", - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", - "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/compiler-core": "3.5.35", - "@vue/compiler-dom": "3.5.35", - "@vue/compiler-ssr": "3.5.35", - "@vue/shared": "3.5.35", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.15", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", - "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.35", - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/@vue/language-core": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", - "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.15", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^1.0.3", - "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", - "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", - "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.35", - "@vue/shared": "3.5.35" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", - "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.35", - "@vue/runtime-core": "3.5.35", - "@vue/shared": "3.5.35", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", - "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.35", - "@vue/shared": "3.5.35" - }, - "peerDependencies": { - "vue": "3.5.35" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", - "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", - "license": "MIT" - }, - "node_modules/@vue/test-utils": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.11.tgz", - "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-beautify": "^1.14.9", - "vue-component-type-helpers": "^3.0.0" - }, - "peerDependencies": { - "@vue/compiler-dom": "3.x", - "@vue/server-renderer": "3.x", - "vue": "3.x" - }, - "peerDependenciesMeta": { - "@vue/server-renderer": { - "optional": true - } - } - }, - "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } - }, - "node_modules/@xterm/addon-web-links": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", - "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } - }, - "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/alien-signals": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", - "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/buffer-image-size": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", - "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/editorconfig": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", - "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "^9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/happy-dom": { - "version": "20.10.1", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.10.1.tgz", - "integrity": "sha512-awPoqPjx8CgjapJllyDlgzgVHjBExcitKK5ZJkxwhQJyQpHFkyS2bEcqCm7IeW20cQvuCI0cz2Ifq79CJKqtiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": ">=20.0.0", - "@types/whatwg-mimetype": "^3.0.2", - "@types/ws": "^8.18.1", - "buffer-image-size": "^0.6.4", - "entities": "^7.0.1", - "whatwg-mimetype": "^3.0.0", - "ws": "^8.18.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/js-beautify": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", - "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.4.2", - "js-cookie": "^3.0.5", - "nopt": "^7.2.1" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-cookie": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", - "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, - "license": "ISC" - }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/semver": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", - "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vue": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", - "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.35", - "@vue/compiler-sfc": "3.5.35", - "@vue/runtime-dom": "3.5.35", - "@vue/server-renderer": "3.5.35", - "@vue/shared": "3.5.35" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-component-type-helpers": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.3.tgz", - "integrity": "sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==", - "dev": true, - "license": "MIT" - }, - "node_modules/vue-tsc": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", - "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "2.4.15", - "@vue/language-core": "2.2.12" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/frontend-integration/package.json b/frontend-integration/package.json deleted file mode 100644 index 48b1e135..00000000 --- a/frontend-integration/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@megaport/cli-wasm-vue3", - "version": "1.0.0", - "description": "Vue 3 integration package for Megaport CLI WebAssembly", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./composables": { - "import": "./dist/composables/index.js", - "types": "./dist/composables/index.d.ts" - }, - "./components": { - "import": "./dist/components/index.js", - "types": "./dist/components/index.d.ts" - } - }, - "files": [ - "dist", - "README.md" - ], - "scripts": { - "build": "vite build && vue-tsc --declaration --emitDeclarationOnly", - "build:demo": "vite build --config vite.demo.config.ts", - "dev": "vite", - "dev:demo": "vite --config vite.demo.config.ts", - "preview": "vite preview", - "type-check": "vue-tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage" - }, - "engines": { - "node": ">=20", - "npm": ">=9" - }, - "peerDependencies": { - "vue": "^3.3.0" - }, - "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0", - "@xterm/xterm": "^5.5.0", - "vue": "^3.3.0" - }, - "devDependencies": { - "@types/node": "^20.10.0", - "@vitejs/plugin-vue": "^6.0.7", - "@vitest/ui": "^4.1.3", - "@vue/test-utils": "^2.4.6", - "happy-dom": "^20.0.10", - "typescript": "^5.3.0", - "vite": "^8.0.16", - "vitest": "^4.1.3", - "vue-tsc": "^2.0.0" - }, - "keywords": [ - "megaport", - "cli", - "wasm", - "webassembly", - "vue3", - "vite", - "terminal" - ], - "author": "Megaport", - "license": "MIT" -} diff --git a/frontend-integration/tsconfig.json b/frontend-integration/tsconfig.json deleted file mode 100644 index 1ef977eb..00000000 --- a/frontend-integration/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"], - "jsx": "preserve", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "allowImportingTsExtensions": true, - "noEmit": true, - "strict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noFallthroughCasesInSwitch": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"] - }, - "types": ["vite/client", "node"] - }, - "include": ["vite-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.vue", "**/*.d.ts"], - "exclude": ["node_modules", "dist", "vite.config.ts"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/frontend-integration/tsconfig.node.json b/frontend-integration/tsconfig.node.json deleted file mode 100644 index 42872c59..00000000 --- a/frontend-integration/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/frontend-integration/types/megaport-wasm.d.ts b/frontend-integration/types/megaport-wasm.d.ts deleted file mode 100644 index 89041d63..00000000 --- a/frontend-integration/types/megaport-wasm.d.ts +++ /dev/null @@ -1,364 +0,0 @@ -/** - * Megaport CLI WebAssembly Module - TypeScript Definitions - * For Vue 3 + Vite Integration - */ - -export interface MegaportCommandResult { - /** - * Captured stdout/stderr text from the command. When the token set via - * `setAuthToken` was rejected by the API (401/403), this contains the - * substring `"MEGAPORT_SESSION_EXPIRED"` — command failures are printed - * text, not a distinct error channel, so the host must scan `output` for - * the marker rather than relying on a separate field. On a match, prompt - * for re-authentication and call `setAuthToken` again. - */ - output?: string; - error?: string; -} - -export interface MegaportAuthInfo { - accessKeySet: boolean; - accessKeyPreview: string; - secretKeySet: boolean; - secretKeyPreview: string; - accessTokenSet: boolean; - accessTokenPreview: string; - environment: string; - apiURL: string; - authMethod: 'token' | 'apikey' | 'none'; -} - -export interface MegaportBufferDump { - stdout: string; - stderr: string; - direct: string; -} - -/** - * Prompt request from WASM for interactive commands - */ -export interface MegaportPromptRequest { - id: string; - message: string; - type: string; // "text", "confirm", "resource" - resourceType?: string; // for resource prompts -} - -/** - * Telemetry event types for tracking WASM operations - */ -export type TelemetryEventType = - | 'wasm_init_start' - | 'wasm_init_success' - | 'wasm_init_error' - | 'command_execute_start' - | 'command_execute_success' - | 'command_execute_error' - | 'auth_set' - | 'auth_token_set' - | 'auth_clear' - | 'spinner_start' - | 'spinner_stop' - | 'prompt_requested' - | 'prompt_submitted' - | 'prompt_cancelled'; - -/** - * Telemetry event data - */ -export interface TelemetryEvent { - type: TelemetryEventType; - timestamp: number; - duration?: number; // milliseconds - metadata?: Record; -} - -/** - * Telemetry callback function - */ -export type TelemetryCallback = (event: TelemetryEvent) => void; - -/** - * Global WASM interface exposed by the Megaport CLI - * Available after WASM module initialization - */ -export interface MegaportWASM { - /** - * Deprecated stub kept for one release as a soft landing for hosts that - * still detect or call it. It no longer executes commands and always - * returns an immediate error result. - * @param command - Ignored - * @returns An error result pointing to executeMegaportCommandAsync - * @deprecated Use executeMegaportCommandAsync instead; this function does not run commands - */ - executeMegaportCommand(command: string): MegaportCommandResult; - - /** - * Execute a CLI command asynchronously (RECOMMENDED) - * @param command - Full command string (e.g., "port list --output json") - * @param callback - Callback function to receive the result - */ - executeMegaportCommandAsync( - command: string, - callback: (result: MegaportCommandResult) => void - ): void; - - /** - * Read a config file from localStorage - * @param filename - Name of the file to read - */ - readConfigFile(filename: string): { content?: string; error?: string }; - - /** - * Write a config file to localStorage - * @param filename - Name of the file to write - * @param content - Content to write - */ - writeConfigFile(filename: string, content: string): { success: boolean }; - - /** - * Get authentication information - */ - debugAuthInfo(): MegaportAuthInfo; - - /** - * Save data to localStorage - */ - saveToLocalStorage(key: string, value: string): boolean; - - /** - * Load data from localStorage - */ - loadFromLocalStorage(key: string): string; - - /** - * Set authentication credentials securely (in-memory only, recommended) - * Stores credentials in Go environment variables and JavaScript global - * Does NOT use localStorage to prevent XSS attacks - * @param accessKey - Megaport API access key - * @param secretKey - Megaport API secret key - * @param environment - Environment (production, staging, development) - * @returns Result object with success status - */ - setAuthCredentials( - accessKey: string, - secretKey: string, - environment: string - ): { success: boolean; error?: string }; - - /** - * Set authentication using an existing token from the portal session, - * bypassing the OAuth flow. Use this when the host page already holds a - * valid Megaport access token (typically via SSO into the portal). - * - * ## Environment resolution - * - * The environment is resolved in this order: - * - * 1. The explicit `environment` argument, if non-empty. - * 2. The environment derived from `hostname` per the Megaport conventions: - * - `megaport.com`, `www.megaport.com`, and any `.megaport.com` - * (single-word app, no hyphens) → `"production"`. - * - `-.megaport.com` → `` (env may contain further - * hyphens, so `api-mpone-dev.megaport.com` resolves to `"mpone-dev"`). - * - * If neither yields a value (e.g. `hostname` is `"localhost"`, a private IP, - * or a non-Megaport domain), **the call fails**. The function never - * silently falls back to production. - * - * ## API URL - * - * The API URL is always built from the resolved environment: - * - `"production"` → `https://api.megaport.com/`. - * - anything else → `https://api-.megaport.com/`. - * - * ## Validation - * - * The explicit `environment` argument must match `/^[a-z0-9-]+$/` — any - * other value (containing `/`, `.`, `@`, `:`, uppercase, etc.) is rejected - * with an error to prevent hostname injection into the API URL. - * - * ## Expiry and session handling - * - * Pass the token's real expiry as `expiry` when the host knows it (epoch - * milliseconds, or an RFC3339 string). This is optional and backward - * compatible: omitting it (or passing `0`/an unparseable value) is fine. - * The CLI stores `expiry` and echoes it back in the result, but does not - * check it proactively — there is no background timer or pre-request - * check that watches it. The CLI also does not refresh tokens on this - * path, since there are no credentials to refresh with. The only actual - * signal is reactive: whenever the API rejects a request with 401/403 - * (whether because the token expired or for any other reason), that - * command's output contains the marker `"MEGAPORT_SESSION_EXPIRED"` (see - * {@link MegaportCommandResult.output}). The host should treat that as a - * signal to prompt for re-authentication and call `setAuthToken` again. - * - * @param token - The access token from the portal session - * @param hostname - The current hostname, e.g. `window.location.hostname` - * @param environment - Optional explicit environment override; supersedes the hostname-derived value. Useful when `hostname` is `"localhost"` or a non-portal host, or when the portal needs to talk to a specific backend regardless of where it's served - * @param expiry - Optional real expiry of `token`, as epoch milliseconds (number) or an RFC3339 string. Stored and echoed back, but not checked proactively (see above). Omit, or pass `0`/an unparseable value, if the expiry is unknown - * @returns On success: `{ success: true, environment, hostname, apiURL, expiry? }` where `environment` is the resolved env name (e.g. `"qa"`), `apiURL` is the matching `api-.megaport.com/` URL, and `expiry` (RFC3339 string) echoes back the resolved expiry if one was set. On failure: `{ success: false, error }` with a human-readable message; the caller should surface the message to guide the user - * - * @example - * // Portal served from a recognised host — no override needed. - * setAuthToken(token, window.location.hostname); - * - * @example - * // Local development against the qa backend. - * setAuthToken(token, window.location.hostname, "qa"); - * - * @example - * // Record the token's known expiry for display; the CLI still only - * // reacts to an actual 401/403 from the API, not to this timestamp. - * setAuthToken(token, window.location.hostname, undefined, Date.now() + 60 * 60 * 1000); - */ - setAuthToken( - token: string, - hostname: string, - environment?: string, - expiry?: number | string - ): { success: boolean; error?: string; environment?: string; hostname?: string; apiURL?: string; expiry?: string }; - - /** - * Clear authentication credentials from memory - * @returns Result object with success status - */ - clearAuthCredentials(): { success: boolean }; - - /** - * Reset WASM output buffers - */ - resetWasmOutput(): boolean; - - /** - * Get current WASM output - */ - getWasmOutput(): string; - - /** - * Toggle WASM debug mode - */ - toggleWasmDebug(): boolean; - - /** - * Dump all buffer contents for debugging - */ - dumpBuffers(): MegaportBufferDump; - - /** - * Check if WASM debug mode is enabled - */ - wasmDebug(): boolean; - - /** - * Log location command debug information - */ - logLocationCommand(message: string): void; - - /** - * Register a prompt handler for interactive commands - * @param callback - Function to call when user input is needed - */ - registerPromptHandler( - callback: (request: MegaportPromptRequest) => void - ): boolean; - - /** - * Register a handler for live command output. - * - * The callback is invoked with each chunk of narrative output as the command - * writes it. When a handler is registered the narrative is streamed here and - * is not repeated in the command result (see MegaportCommandResult.output). - * Chunks use `\n` line endings. - * - * @param callback - Function called with each output chunk - */ - registerOutputHandler(callback: (chunk: string) => void): boolean; - - /** - * Submit a response to a pending prompt - * @param id - Prompt ID - * @param response - User's response - */ - submitPromptResponse(id: string, response: string): void; - - /** - * Cancel a pending prompt - * @param id - Prompt ID - */ - cancelPrompt(id: string): void; - - /** - * Get list of pending prompts (for debugging) - */ - getPendingPrompts(): MegaportPromptRequest[]; - - /** - * Tell the WASM table renderer the host terminal's viewport width, in - * columns, so table output scales to it instead of a fixed layout. - * Call on terminal init and again on every resize (after the fit addon - * recalculates `terminal.cols`). - * @param cols - Terminal width in columns - */ - setTerminalWidth(cols: number): { success: boolean; error?: string }; -} - -/** - * Go WASM runtime - */ -export interface GoWASM { - run(instance: WebAssembly.Instance): void; - importObject: WebAssembly.Imports; - _exitPromise?: Promise; - _resolveExitPromise?: () => void; - _pendingEvent?: { id: number; this: any; args: any[] }; -} - -declare global { - interface Window { - executeMegaportCommand?: (command: string) => MegaportCommandResult; - executeMegaportCommandAsync?: ( - command: string, - callback: (result: MegaportCommandResult) => void - ) => void; - readConfigFile?: (filename: string) => { content?: string; error?: string }; - writeConfigFile?: ( - filename: string, - content: string - ) => { success: boolean }; - debugAuthInfo?: () => MegaportAuthInfo; - saveToLocalStorage?: (key: string, value: string) => boolean; - loadFromLocalStorage?: (key: string) => string; - setAuthCredentials?: ( - accessKey: string, - secretKey: string, - environment: string - ) => { success: boolean; error?: string }; - setAuthToken?: ( - token: string, - hostname: string, - environment?: string, - expiry?: number | string - ) => { success: boolean; error?: string; environment?: string; hostname?: string; apiURL?: string; expiry?: string }; - clearAuthCredentials?: () => { success: boolean }; - resetWasmOutput?: () => boolean; - getWasmOutput?: () => string; - toggleWasmDebug?: () => boolean; - dumpBuffers?: () => MegaportBufferDump; - wasmDebug?: () => boolean; - logLocationCommand?: (message: string) => void; - registerPromptHandler?: ( - callback: (request: MegaportPromptRequest) => void - ) => boolean; - registerOutputHandler?: (callback: (chunk: string) => void) => boolean; - submitPromptResponse?: (id: string, response: string) => void; - cancelPrompt?: (id: string) => void; - getPendingPrompts?: () => MegaportPromptRequest[]; - setTerminalWidth?: (cols: number) => { success: boolean; error?: string }; - Go?: new () => GoWASM; - // Content-hashed wasm URL injected into index.html at build time (ESD-1272). - __MEGAPORT_WASM_URL__?: string; - } -} - -export {}; diff --git a/frontend-integration/utils/type-guards.ts b/frontend-integration/utils/type-guards.ts deleted file mode 100644 index 0d0a6080..00000000 --- a/frontend-integration/utils/type-guards.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Runtime type guards for WASM integration - * Provides safe runtime checks for TypeScript types - */ - -import type { - MegaportCommandResult, - MegaportAuthInfo, - MegaportPromptRequest, - TelemetryEvent, - TelemetryEventType, -} from '../types/megaport-wasm'; - -/** - * Type guard for MegaportCommandResult - */ -export function isMegaportCommandResult( - value: unknown -): value is MegaportCommandResult { - if (typeof value !== 'object' || value === null) { - return false; - } - - const result = value as Partial; - - // Must have at least one of output or error - if (!('output' in result) && !('error' in result)) { - return false; - } - - // If present, output must be string or undefined - if ( - 'output' in result && - result.output !== undefined && - typeof result.output !== 'string' - ) { - return false; - } - - // If present, error must be string or undefined - if ( - 'error' in result && - result.error !== undefined && - typeof result.error !== 'string' - ) { - return false; - } - - return true; -} - -/** - * Type guard for MegaportAuthInfo - */ -export function isMegaportAuthInfo(value: unknown): value is MegaportAuthInfo { - if (typeof value !== 'object' || value === null) { - return false; - } - - const info = value as Partial; - - return ( - typeof info.accessKeySet === 'boolean' && - typeof info.accessKeyPreview === 'string' && - typeof info.secretKeySet === 'boolean' && - typeof info.secretKeyPreview === 'string' && - typeof info.environment === 'string' - ); -} - -/** - * Type guard for MegaportPromptRequest - */ -export function isMegaportPromptRequest( - value: unknown -): value is MegaportPromptRequest { - if (typeof value !== 'object' || value === null) { - return false; - } - - const request = value as Partial; - - if ( - typeof request.id !== 'string' || - typeof request.message !== 'string' || - typeof request.type !== 'string' - ) { - return false; - } - - // resourceType is optional - if ( - 'resourceType' in request && - request.resourceType !== undefined && - typeof request.resourceType !== 'string' - ) { - return false; - } - - return true; -} - -/** - * Type guard for TelemetryEventType - */ -export function isTelemetryEventType( - value: unknown -): value is TelemetryEventType { - const validTypes: TelemetryEventType[] = [ - 'wasm_init_start', - 'wasm_init_success', - 'wasm_init_error', - 'command_execute_start', - 'command_execute_success', - 'command_execute_error', - 'auth_set', - 'auth_clear', - 'auth_token_set', - 'spinner_start', - 'spinner_stop', - 'prompt_requested', - 'prompt_submitted', - 'prompt_cancelled', - ]; - - return ( - typeof value === 'string' && - validTypes.includes(value as TelemetryEventType) - ); -} - -/** - * Type guard for TelemetryEvent - */ -export function isTelemetryEvent(value: unknown): value is TelemetryEvent { - if (typeof value !== 'object' || value === null) { - return false; - } - - const event = value as Partial; - - if (!isTelemetryEventType(event.type)) { - return false; - } - - if (typeof event.timestamp !== 'number') { - return false; - } - - // duration is optional - if ( - 'duration' in event && - event.duration !== undefined && - typeof event.duration !== 'number' - ) { - return false; - } - - // metadata is optional - if ( - 'metadata' in event && - event.metadata !== undefined && - (typeof event.metadata !== 'object' || event.metadata === null) - ) { - return false; - } - - return true; -} - -/** - * Checks whether Window exposes the WASM async entrypoint. Only the async - * entrypoint is checked: executeMegaportCommand is a deprecated stub that no - * longer executes commands, so its presence alone doesn't mean WASM is ready. - */ -export function hasWASMFunctions(win: Window): boolean { - return typeof win.executeMegaportCommandAsync === 'function'; -} - -/** - * Type guard for Worker support - */ -export function hasWorkerSupport(): boolean { - return typeof Worker !== 'undefined'; -} - -/** - * Type guard for WebAssembly support - */ -export function hasWebAssemblySupport(): boolean { - return ( - typeof WebAssembly !== 'undefined' && - typeof WebAssembly.instantiate === 'function' - ); -} - -/** - * Type guard for string - */ -export function isString(value: unknown): value is string { - return typeof value === 'string'; -} - -/** - * Type guard for non-empty string - */ -export function isNonEmptyString(value: unknown): value is string { - return typeof value === 'string' && value.trim().length > 0; -} - -/** - * Type guard for Error object - */ -export function isError(value: unknown): value is Error { - return value instanceof Error; -} - -/** - * Safe error message extraction - */ -export function getErrorMessage(error: unknown): string { - if (isError(error)) { - return error.message; - } - if (isString(error)) { - return error; - } - return String(error); -} - -/** - * Type guard for object with specific key - */ -export function hasKey( - obj: unknown, - key: K -): obj is Record { - return typeof obj === 'object' && obj !== null && key in obj; -} - -/** - * Type guard for callable function - */ -export function isFunction(value: unknown): value is Function { - return typeof value === 'function'; -} - -/** - * Safe callback invocation with type checking - */ -export function safeInvoke( - fn: unknown, - ...args: T -): R | undefined { - if (isFunction(fn)) { - try { - return fn(...args) as R; - } catch (error) { - console.error('Error invoking function:', getErrorMessage(error)); - return undefined; - } - } - return undefined; -} - -/** - * Validate and sanitize command string - */ -export function isValidCommand(command: unknown): command is string { - if (!isNonEmptyString(command)) { - return false; - } - - // Basic command validation - should not contain dangerous patterns - const dangerousPatterns = [ - /rm\s+-rf\s+\//i, // Dangerous rm command - /:\(\)\{/i, // Fork bomb pattern - /eval\(/i, // eval() call - / - - - - -
- - - \ No newline at end of file diff --git a/web/vue-demo/wasm_exec.js b/web/vue-demo/wasm_exec.js deleted file mode 100644 index d71af9e9..00000000 --- a/web/vue-demo/wasm_exec.js +++ /dev/null @@ -1,575 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -"use strict"; - -(() => { - const enosys = () => { - const err = new Error("not implemented"); - err.code = "ENOSYS"; - return err; - }; - - if (!globalThis.fs) { - let outputBuf = ""; - globalThis.fs = { - constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused - writeSync(fd, buf) { - outputBuf += decoder.decode(buf); - const nl = outputBuf.lastIndexOf("\n"); - if (nl != -1) { - console.log(outputBuf.substring(0, nl)); - outputBuf = outputBuf.substring(nl + 1); - } - return buf.length; - }, - write(fd, buf, offset, length, position, callback) { - if (offset !== 0 || length !== buf.length || position !== null) { - callback(enosys()); - return; - } - const n = this.writeSync(fd, buf); - callback(null, n); - }, - chmod(path, mode, callback) { callback(enosys()); }, - chown(path, uid, gid, callback) { callback(enosys()); }, - close(fd, callback) { callback(enosys()); }, - fchmod(fd, mode, callback) { callback(enosys()); }, - fchown(fd, uid, gid, callback) { callback(enosys()); }, - fstat(fd, callback) { callback(enosys()); }, - fsync(fd, callback) { callback(null); }, - ftruncate(fd, length, callback) { callback(enosys()); }, - lchown(path, uid, gid, callback) { callback(enosys()); }, - link(path, link, callback) { callback(enosys()); }, - lstat(path, callback) { callback(enosys()); }, - mkdir(path, perm, callback) { callback(enosys()); }, - open(path, flags, mode, callback) { callback(enosys()); }, - read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, - readdir(path, callback) { callback(enosys()); }, - readlink(path, callback) { callback(enosys()); }, - rename(from, to, callback) { callback(enosys()); }, - rmdir(path, callback) { callback(enosys()); }, - stat(path, callback) { callback(enosys()); }, - symlink(path, link, callback) { callback(enosys()); }, - truncate(path, length, callback) { callback(enosys()); }, - unlink(path, callback) { callback(enosys()); }, - utimes(path, atime, mtime, callback) { callback(enosys()); }, - }; - } - - if (!globalThis.process) { - globalThis.process = { - getuid() { return -1; }, - getgid() { return -1; }, - geteuid() { return -1; }, - getegid() { return -1; }, - getgroups() { throw enosys(); }, - pid: -1, - ppid: -1, - umask() { throw enosys(); }, - cwd() { throw enosys(); }, - chdir() { throw enosys(); }, - } - } - - if (!globalThis.path) { - globalThis.path = { - resolve(...pathSegments) { - return pathSegments.join("/"); - } - } - } - - if (!globalThis.crypto) { - throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); - } - - if (!globalThis.performance) { - throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); - } - - if (!globalThis.TextEncoder) { - throw new Error("globalThis.TextEncoder is not available, polyfill required"); - } - - if (!globalThis.TextDecoder) { - throw new Error("globalThis.TextDecoder is not available, polyfill required"); - } - - const encoder = new TextEncoder("utf-8"); - const decoder = new TextDecoder("utf-8"); - - globalThis.Go = class { - constructor() { - this.argv = ["js"]; - this.env = {}; - this.exit = (code) => { - if (code !== 0) { - console.warn("exit code:", code); - } - }; - this._exitPromise = new Promise((resolve) => { - this._resolveExitPromise = resolve; - }); - this._pendingEvent = null; - this._scheduledTimeouts = new Map(); - this._nextCallbackTimeoutID = 1; - - const setInt64 = (addr, v) => { - this.mem.setUint32(addr + 0, v, true); - this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); - } - - const setInt32 = (addr, v) => { - this.mem.setUint32(addr + 0, v, true); - } - - const getInt64 = (addr) => { - const low = this.mem.getUint32(addr + 0, true); - const high = this.mem.getInt32(addr + 4, true); - return low + high * 4294967296; - } - - const loadValue = (addr) => { - const f = this.mem.getFloat64(addr, true); - if (f === 0) { - return undefined; - } - if (!isNaN(f)) { - return f; - } - - const id = this.mem.getUint32(addr, true); - return this._values[id]; - } - - const storeValue = (addr, v) => { - const nanHead = 0x7FF80000; - - if (typeof v === "number" && v !== 0) { - if (isNaN(v)) { - this.mem.setUint32(addr + 4, nanHead, true); - this.mem.setUint32(addr, 0, true); - return; - } - this.mem.setFloat64(addr, v, true); - return; - } - - if (v === undefined) { - this.mem.setFloat64(addr, 0, true); - return; - } - - let id = this._ids.get(v); - if (id === undefined) { - id = this._idPool.pop(); - if (id === undefined) { - id = this._values.length; - } - this._values[id] = v; - this._goRefCounts[id] = 0; - this._ids.set(v, id); - } - this._goRefCounts[id]++; - let typeFlag = 0; - switch (typeof v) { - case "object": - if (v !== null) { - typeFlag = 1; - } - break; - case "string": - typeFlag = 2; - break; - case "symbol": - typeFlag = 3; - break; - case "function": - typeFlag = 4; - break; - } - this.mem.setUint32(addr + 4, nanHead | typeFlag, true); - this.mem.setUint32(addr, id, true); - } - - const loadSlice = (addr) => { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - return new Uint8Array(this._inst.exports.mem.buffer, array, len); - } - - const loadSliceOfValues = (addr) => { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - const a = new Array(len); - for (let i = 0; i < len; i++) { - a[i] = loadValue(array + i * 8); - } - return a; - } - - const loadString = (addr) => { - const saddr = getInt64(addr + 0); - const len = getInt64(addr + 8); - return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); - } - - const testCallExport = (a, b) => { - this._inst.exports.testExport0(); - return this._inst.exports.testExport(a, b); - } - - const timeOrigin = Date.now() - performance.now(); - this.importObject = { - _gotest: { - add: (a, b) => a + b, - callExport: testCallExport, - }, - gojs: { - // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) - // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported - // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). - // This changes the SP, thus we have to update the SP used by the imported function. - - // func wasmExit(code int32) - "runtime.wasmExit": (sp) => { - sp >>>= 0; - const code = this.mem.getInt32(sp + 8, true); - this.exited = true; - delete this._inst; - delete this._values; - delete this._goRefCounts; - delete this._ids; - delete this._idPool; - this.exit(code); - }, - - // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) - "runtime.wasmWrite": (sp) => { - sp >>>= 0; - const fd = getInt64(sp + 8); - const p = getInt64(sp + 16); - const n = this.mem.getInt32(sp + 24, true); - fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); - }, - - // func resetMemoryDataView() - "runtime.resetMemoryDataView": (sp) => { - sp >>>= 0; - this.mem = new DataView(this._inst.exports.mem.buffer); - }, - - // func nanotime1() int64 - "runtime.nanotime1": (sp) => { - sp >>>= 0; - setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); - }, - - // func walltime() (sec int64, nsec int32) - "runtime.walltime": (sp) => { - sp >>>= 0; - const msec = (new Date).getTime(); - setInt64(sp + 8, msec / 1000); - this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); - }, - - // func scheduleTimeoutEvent(delay int64) int32 - "runtime.scheduleTimeoutEvent": (sp) => { - sp >>>= 0; - const id = this._nextCallbackTimeoutID; - this._nextCallbackTimeoutID++; - this._scheduledTimeouts.set(id, setTimeout( - () => { - this._resume(); - while (this._scheduledTimeouts.has(id)) { - // for some reason Go failed to register the timeout event, log and try again - // (temporary workaround for https://github.com/golang/go/issues/28975) - console.warn("scheduleTimeoutEvent: missed timeout event"); - this._resume(); - } - }, - getInt64(sp + 8), - )); - this.mem.setInt32(sp + 16, id, true); - }, - - // func clearTimeoutEvent(id int32) - "runtime.clearTimeoutEvent": (sp) => { - sp >>>= 0; - const id = this.mem.getInt32(sp + 8, true); - clearTimeout(this._scheduledTimeouts.get(id)); - this._scheduledTimeouts.delete(id); - }, - - // func getRandomData(r []byte) - "runtime.getRandomData": (sp) => { - sp >>>= 0; - crypto.getRandomValues(loadSlice(sp + 8)); - }, - - // func finalizeRef(v ref) - "syscall/js.finalizeRef": (sp) => { - sp >>>= 0; - const id = this.mem.getUint32(sp + 8, true); - this._goRefCounts[id]--; - if (this._goRefCounts[id] === 0) { - const v = this._values[id]; - this._values[id] = null; - this._ids.delete(v); - this._idPool.push(id); - } - }, - - // func stringVal(value string) ref - "syscall/js.stringVal": (sp) => { - sp >>>= 0; - storeValue(sp + 24, loadString(sp + 8)); - }, - - // func valueGet(v ref, p string) ref - "syscall/js.valueGet": (sp) => { - sp >>>= 0; - const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 32, result); - }, - - // func valueSet(v ref, p string, x ref) - "syscall/js.valueSet": (sp) => { - sp >>>= 0; - Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); - }, - - // func valueDelete(v ref, p string) - "syscall/js.valueDelete": (sp) => { - sp >>>= 0; - Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); - }, - - // func valueIndex(v ref, i int) ref - "syscall/js.valueIndex": (sp) => { - sp >>>= 0; - storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); - }, - - // valueSetIndex(v ref, i int, x ref) - "syscall/js.valueSetIndex": (sp) => { - sp >>>= 0; - Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); - }, - - // func valueCall(v ref, m string, args []ref) (ref, bool) - "syscall/js.valueCall": (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const m = Reflect.get(v, loadString(sp + 16)); - const args = loadSliceOfValues(sp + 32); - const result = Reflect.apply(m, v, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 56, result); - this.mem.setUint8(sp + 64, 1); - } catch (err) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 56, err); - this.mem.setUint8(sp + 64, 0); - } - }, - - // func valueInvoke(v ref, args []ref) (ref, bool) - "syscall/js.valueInvoke": (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const args = loadSliceOfValues(sp + 16); - const result = Reflect.apply(v, undefined, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, result); - this.mem.setUint8(sp + 48, 1); - } catch (err) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, err); - this.mem.setUint8(sp + 48, 0); - } - }, - - // func valueNew(v ref, args []ref) (ref, bool) - "syscall/js.valueNew": (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const args = loadSliceOfValues(sp + 16); - const result = Reflect.construct(v, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, result); - this.mem.setUint8(sp + 48, 1); - } catch (err) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, err); - this.mem.setUint8(sp + 48, 0); - } - }, - - // func valueLength(v ref) int - "syscall/js.valueLength": (sp) => { - sp >>>= 0; - setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); - }, - - // valuePrepareString(v ref) (ref, int) - "syscall/js.valuePrepareString": (sp) => { - sp >>>= 0; - const str = encoder.encode(String(loadValue(sp + 8))); - storeValue(sp + 16, str); - setInt64(sp + 24, str.length); - }, - - // valueLoadString(v ref, b []byte) - "syscall/js.valueLoadString": (sp) => { - sp >>>= 0; - const str = loadValue(sp + 8); - loadSlice(sp + 16).set(str); - }, - - // func valueInstanceOf(v ref, t ref) bool - "syscall/js.valueInstanceOf": (sp) => { - sp >>>= 0; - this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); - }, - - // func copyBytesToGo(dst []byte, src ref) (int, bool) - "syscall/js.copyBytesToGo": (sp) => { - sp >>>= 0; - const dst = loadSlice(sp + 8); - const src = loadValue(sp + 32); - if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { - this.mem.setUint8(sp + 48, 0); - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(sp + 40, toCopy.length); - this.mem.setUint8(sp + 48, 1); - }, - - // func copyBytesToJS(dst ref, src []byte) (int, bool) - "syscall/js.copyBytesToJS": (sp) => { - sp >>>= 0; - const dst = loadValue(sp + 8); - const src = loadSlice(sp + 16); - if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { - this.mem.setUint8(sp + 48, 0); - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(sp + 40, toCopy.length); - this.mem.setUint8(sp + 48, 1); - }, - - "debug": (value) => { - console.log(value); - }, - } - }; - } - - async run(instance) { - if (!(instance instanceof WebAssembly.Instance)) { - throw new Error("Go.run: WebAssembly.Instance expected"); - } - this._inst = instance; - this.mem = new DataView(this._inst.exports.mem.buffer); - this._values = [ // JS values that Go currently has references to, indexed by reference id - NaN, - 0, - null, - true, - false, - globalThis, - this, - ]; - this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id - this._ids = new Map([ // mapping from JS values to reference ids - [0, 1], - [null, 2], - [true, 3], - [false, 4], - [globalThis, 5], - [this, 6], - ]); - this._idPool = []; // unused ids that have been garbage collected - this.exited = false; // whether the Go program has exited - - // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. - let offset = 4096; - - const strPtr = (str) => { - const ptr = offset; - const bytes = encoder.encode(str + "\0"); - new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); - offset += bytes.length; - if (offset % 8 !== 0) { - offset += 8 - (offset % 8); - } - return ptr; - }; - - const argc = this.argv.length; - - const argvPtrs = []; - this.argv.forEach((arg) => { - argvPtrs.push(strPtr(arg)); - }); - argvPtrs.push(0); - - const keys = Object.keys(this.env).sort(); - keys.forEach((key) => { - argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); - }); - argvPtrs.push(0); - - const argv = offset; - argvPtrs.forEach((ptr) => { - this.mem.setUint32(offset, ptr, true); - this.mem.setUint32(offset + 4, 0, true); - offset += 8; - }); - - // The linker guarantees global data starts from at least wasmMinDataAddr. - // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. - const wasmMinDataAddr = 4096 + 8192; - if (offset >= wasmMinDataAddr) { - throw new Error("total length of command line and environment variables exceeds limit"); - } - - this._inst.exports.run(argc, argv); - if (this.exited) { - this._resolveExitPromise(); - } - await this._exitPromise; - } - - _resume() { - if (this.exited) { - throw new Error("Go program has already exited"); - } - this._inst.exports.resume(); - if (this.exited) { - this._resolveExitPromise(); - } - } - - _makeFuncWrapper(id) { - const go = this; - return function () { - const event = { id: id, this: this, args: arguments }; - go._pendingEvent = event; - go._resume(); - return event.result; - }; - } - } -})();