diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..ba6de87c22 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,95 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json + +language: "en-US" + +reviews: + profile: "chill" # community repo — keep it welcoming + request_changes_workflow: false + high_level_summary: true + poem: false # serious OSS platform + in_progress_fortune: false # noise + review_status: false + review_details: false + commit_status: true + collapse_walkthrough: true + changed_files_summary: true + sequence_diagrams: false # overkill for package-level PRs + estimate_code_review_effort: true + assess_linked_issues: true + related_issues: true + related_prs: true + suggested_labels: true + auto_apply_labels: false + suggested_reviewers: true + auto_assign_reviewers: false + + # Exclude generated, build, and Meteor-internal files + path_filters: + - "!**/node_modules/**" + - "!**/.meteor/**" + - "!**/bundle/**" + - "!**/programs/**" + - "!**/*.min.js" + - "!**/cordova-build/**" + - "!**/package-lock.json" + + path_instructions: + - path: "packages/**" + instructions: > + This is a core Meteor Atmosphere package. Focus on API backwards + compatibility, DDP/reactivity correctness, and client/server split. + Avoid nitpicking style — the codebase has legacy patterns. + - path: "tools/**" + instructions: > + This is the Meteor build tool (Isobuild). Be thorough about + correctness, edge cases, and performance in the CLI/build pipeline. + - path: "npm-packages/**" + instructions: > + These are npm packages published from the Meteor monorepo. + Check for correct exports, peer dependency handling, and Node.js compatibility. + - path: "v3-docs/**" + instructions: > + Documentation for Meteor v3. Check for accuracy, clarity, and + correct code examples. Grammar and spelling matter here. + - path: "scripts/**" + instructions: > + Build and CI scripts. Focus on correctness, portability, and + error handling. + + auto_review: + enabled: true + drafts: false + auto_incremental_review: true + auto_pause_after_reviewed_commits: 3 + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + base_branches: [] + + finishing_touches: + docstrings: + enabled: false # legacy JS — too much noise across 100s of packages + unit_tests: + enabled: true + simplify: + enabled: false + + tools: + shellcheck: + enabled: true # ✅ they have .sh scripts in /scripts + markdownlint: + enabled: true # ✅ heavy docs contribution + languagetool: + enabled: true # ✅ useful for international doc contributors + level: "default" + disabled_categories: + - "TYPOGRAPHY" # too nitpicky for code comments + ruff: + enabled: false # ❌ not a Python project + biome: + enabled: false # ❌ they use ESLint already (.eslintignore exists) + ast-grep: + essential_rules: true + +chat: + auto_reply: true diff --git a/.github/skills/ai-context/SKILL.md b/.github/skills/ai-context/SKILL.md new file mode 100644 index 0000000000..c51a968344 --- /dev/null +++ b/.github/skills/ai-context/SKILL.md @@ -0,0 +1,111 @@ +--- +name: ai-context +description: Use when creating, updating, or maintaining AI documentation files (AGENTS.md, CLAUDE.md, skills). Covers file structure, conventions, and guidelines for evolving AI context. +--- + +# AI Context Documentation + +How to write and maintain the structured documentation that AI coding assistants consume. + +## File Hierarchy + +``` +AGENTS.md # Root context — always loaded by agents +CLAUDE.md # Required for Claude Code (loads AGENTS.md) +.github/skills//SKILL.md # On-demand detailed context +packages//AGENTS.md # Package-specific context +/AGENTS.md # Folder-specific context +``` + +## Root Files + +### AGENTS.md + +Always loaded on every interaction. Keep it **minimal** to save tokens. + +Must contain: +- One-line project description +- Essential commands (run, test, build) +- Repository structure overview (top-level dirs only) +- Skills index table linking to each `SKILL.md` +- Key entry points for common tasks + +Must **not** contain: +- Detailed explanations (put those in skills) +- Code examples longer than one line +- Duplicated content from skills + +### CLAUDE.md + +Required because Claude Code doesn't load `AGENTS.md` natively. It bridges Claude Code into the same context system. Contents: + +```markdown +Read [AGENTS.md](AGENTS.md) before starting any task. + +## Skills + +Load these for detailed context on specific topics: + +| Skill | When to use | +|-------|-------------| +| [](.github/skills//SKILL.md) | | +``` + +Keep in sync with the skills table in `AGENTS.md`. + +## Skills + +### Creating a Skill + +1. Create `.github/skills//SKILL.md` +2. Add YAML frontmatter with `name` and `description` +3. Add an entry to the skills table in both `AGENTS.md` and `CLAUDE.md` + +### SKILL.md Format + +```markdown +--- +name: +description: +--- + +# + +<One-line summary.> + +## <Sections organized by task> +``` + +### Writing Guidelines + +- **Frontmatter `description`**: Write it as a trigger — what task or question should cause an agent to load this skill +- **Be concise**: Use tables over prose, code snippets over explanations +- **Be specific**: File paths, command names, function signatures — not vague descriptions +- **No duplication**: If info exists in another skill, reference it instead of repeating +- **Actionable structure**: Organize by what the agent needs to *do*, not by architecture + +## Package & Folder Context + +Add `AGENTS.md` inside a package or folder when: +- The directory has non-obvious conventions agents keep getting wrong +- There are local commands, patterns, or gotchas not covered by root docs + +Keep these files very short — a few lines of context is often enough. + +## When to Update + +| Trigger | Action | +|---------|--------| +| Agent repeatedly asks about a topic | Create a new skill | +| Agent gets something wrong despite docs | Refine the relevant skill | +| New package/directory with unique patterns | Add a local `AGENTS.md` | +| Architecture or tooling changes | Update affected skills | +| Skill grows too large | Split into multiple skills | +| Skills table changes | Update both `AGENTS.md` and `CLAUDE.md` | + +## Principles + +1. **Token budget**: Root files stay small; details go in skills +2. **Load on demand**: Skills are only read when relevant to the task +3. **Living docs**: Update when patterns change — stale docs are worse than none +4. **Cross-platform**: `AGENTS.md` + `.github/skills/` is the shared convention; `CLAUDE.md` bridges Claude Code which doesn't load `AGENTS.md` natively diff --git a/.github/skills/codebase/SKILL.md b/.github/skills/codebase/SKILL.md new file mode 100644 index 0000000000..32ba7872fe --- /dev/null +++ b/.github/skills/codebase/SKILL.md @@ -0,0 +1,110 @@ +--- +name: codebase +description: Use when understanding the build system, modifying CLI commands, working with isobuild, or navigating the tools/ directory. Covers build pipeline flow and file locations. +--- + +# Codebase + +Meteor's build system (Isobuild) and CLI structure. + +## Overview + +Meteor is a full-stack JavaScript platform with: +- **Core packages** in `/packages` +- **Build system (Isobuild)** in `/tools/isobuild` +- **CLI tool** in `/tools/cli` +- **Real-time data layer** via DDP +- **Mobile support** via Cordova + +## Build Pipeline + +1. **CLI** (`tools/cli/main.js`) → parses commands +2. **Project Context** (`project-context.js`) → resolves packages, dependencies +3. **Isobuild** (`tools/isobuild/`) + - Bundler (`bundler.js`) → orchestrates build + - Compiler (`compiler.js`) → compiles packages + - Linker (`linker.js`) → wraps modules + - Build plugins (Babel, TypeScript, CSS) +4. **Output** → `star.json`, programs +5. **Runners** (`tools/runners/`) → run-app.js, run-mongo.js, run-hmr.js +6. **Live App** → DDP Server ↔ Minimongo ↔ UI + +## Directory Structure + +``` +tools/ +├── cli/ # Command-line interface +├── isobuild/ # Build system core +├── packaging/ # Package management +├── runners/ # App execution engines +├── fs/ # File system utilities +├── cordova/ # Mobile/Cordova support +├── static-assets/ # Project templates +└── project-context.js # Dependency resolution +``` + +## CLI (`tools/cli/`) + +| File | Description | +|------|-------------| +| `main.js` | Entry point, command dispatcher | +| `commands.js` | Main command implementations | +| `commands-packages.js` | Package management commands | +| `commands-cordova.js` | Cordova/mobile commands | + +**Commands:** `meteor create`, `run`, `build`, `deploy`, `add/remove`, `mongo`, `shell` + +## Isobuild (`tools/isobuild/`) + +| File | Description | +|------|-------------| +| `bundler.js` | High-level bundling orchestration | +| `compiler.js` | Package compilation | +| `linker.js` | Module wrapping and linking | +| `import-scanner.ts` | Import statement parsing | +| `compiler-plugin.js` | Compiler plugin API | +| `isopack.js` | Package format handling | + +## Runners (`tools/runners/`) + +| File | Description | +|------|-------------| +| `run-app.js` | Web application runner | +| `run-mongo.js` | MongoDB server runner | +| `run-hmr.js` | Hot module reload runner | +| `run-all.js` | Multi-runner orchestration | + +## Build Targets + +| Target | Description | +|--------|-------------| +| `web.browser` | Modern browsers | +| `web.browser.legacy` | Legacy browsers (IE11) | +| `web.cordova` | Cordova mobile apps | +| `server` | Node.js server | + +## Package Relationships + +- `tools-core` → rspack, future integrations +- `accounts-base` → all accounts-* packages +- `ddp-server` + `ddp-client` → realtime communication +- `mongo` → minimongo (client-side) +- `webapp` → all HTTP handling + +## Project Templates + +Via `meteor create --<template>`: `react`, `vue`, `svelte`, `angular`, `blaze`, `typescript`, `tailwind`, `solid`, `apollo`, `minimal`, `bare`, `full` + +## Environment Variables + +| Variable | Purpose | +|----------|---------| +| `METEOR_PROFILE` | Build profiling | +| `METEOR_PACKAGE_DIRS` | Additional package paths | +| `METEOR_DEBUG_BUILD` | Verbose build output | + +## Troubleshooting + +- **Package not found:** Check `package.js` name, run `meteor reset` +- **Build plugin not running:** Check `archMatching`, file extensions +- **npm issues:** Clear `.meteor/local/`, run `meteor npm install` diff --git a/.github/skills/conventions/SKILL.md b/.github/skills/conventions/SKILL.md new file mode 100644 index 0000000000..1767100ca5 --- /dev/null +++ b/.github/skills/conventions/SKILL.md @@ -0,0 +1,188 @@ +--- +name: conventions +description: Use when writing new packages, adding CLI commands, creating build plugins, or following Meteor code patterns. Covers package.js structure, file naming, and common code patterns. +--- + +# Code Conventions + +Package structure, file naming, and code patterns for the Meteor codebase. + +## Package Structure + +Every Meteor package follows this structure: + +``` +packages/my-package/ +├── package.js # Package manifest (name, version, dependencies, exports) +├── my-package.js # Main implementation (or split by concern) +├── my-package-server.js # Server-only code (optional) +├── my-package-client.js # Client-only code (optional) +├── my-package-tests.js # Tests (loaded via api.addFiles in test mode) +└── README.md # Documentation (optional) +``` + +## Package.js Anatomy + +```javascript +Package.describe({ + name: 'my-package', + version: '1.0.0', + summary: 'Brief description', + git: 'https://github.com/meteor/meteor.git', + documentation: 'README.md' +}); + +Package.onUse(function(api) { + api.versionsFrom(['3.0']); // Minimum Meteor version + + api.use([ + 'ecmascript', // ES2015+ support + 'mongo', // MongoDB integration + 'tracker' // Reactivity (client) + ]); + + api.use('accounts-base', { weak: true }); // Optional dependency + + api.mainModule('my-package-server.js', 'server'); + api.mainModule('my-package-client.js', 'client'); + + api.export('MyPackage'); // Global export +}); + +Package.onTest(function(api) { + api.use(['tinytest', 'my-package']); + api.addFiles('my-package-tests.js'); +}); + +Npm.depends({ + 'lodash': '4.17.21' // npm dependencies +}); +``` + +## File Naming Conventions + +| Pattern | Purpose | +|---------|---------| +| `*-server.js` | Server-only code | +| `*-client.js` | Client-only code | +| `*-common.js` | Shared code | +| `*-tests.js` | Test files | +| `*.d.ts` | TypeScript declarations | + +## Common Patterns + +### Adding a New Core Package + +1. Create directory in `/packages/my-package/` +2. Add `package.js` with proper dependencies +3. Implement functionality with proper exports +4. Add tests in `*-tests.js` +5. Update version numbers if needed + +### Modifying Build System + +Key files to understand: +- `/tools/isobuild/bundler.js` - High-level bundling +- `/tools/isobuild/compiler.js` - Package compilation +- `/tools/project-context.js` - Dependency resolution +- `/tools/cli/commands.js` - CLI command handlers + +### Adding CLI Commands + +Edit `/tools/cli/commands.js` or create new command file: + +```javascript +main.registerCommand({ + name: 'my-command', + options: { + 'option-name': { type: String, short: 'o' } + }, + catalogRefresh: new catalog.Refresh.Never() +}, function(options) { + // Implementation +}); +``` + +### WebApp Middleware Pattern + +```javascript +import { WebApp } from 'meteor/webapp'; + +// Add middleware before Meteor's default handlers +WebApp.rawConnectHandlers.use('/api', (req, res, next) => { + // Runs before authentication + next(); +}); + +// Add middleware after authentication +WebApp.connectHandlers.use('/api', (req, res, next) => { + // req.userId available if authenticated + next(); +}); +``` + +### Build Plugin Pattern + +```javascript +// In package.js +Package.registerBuildPlugin({ + name: 'compile-my-files', + use: ['ecmascript', 'caching-compiler'], + sources: ['plugin.js'], + npmDependencies: { 'my-compiler': '1.0.0' } +}); + +// In plugin.js +Plugin.registerCompiler({ + extensions: ['myext'], + archMatching: 'web' +}, () => new MyCompiler()); + +class MyCompiler extends CachingCompiler { + getCacheKey(inputFile) { + return inputFile.getSourceHash(); + } + + compileOneFile(inputFile) { + const source = inputFile.getContentsAsString(); + const compiled = transform(source); + inputFile.addJavaScript({ + data: compiled, + path: inputFile.getPathInPackage() + '.js' + }); + } +} +``` + +### Using tools-core in Packages + +```javascript +// In package.js +api.use('tools-core'); + +// In implementation +import { + logProgress, + checkNpmDependencyExists, + getMeteorAppConfig, + spawnProcess +} from 'meteor/tools-core'; + +// Check and install dependencies +if (!checkNpmDependencyExists('@rspack/core')) { + installNpmDependency(['@rspack/core@^1.7.1']); +} + +// Spawn external process +const proc = spawnProcess('npx', ['rspack', 'build'], { + cwd: getMeteorAppDir(), + onStdout: (data) => logProgress(data) +}); +``` + +## Version Patterns + +Meteor uses `X.Y.Z-rcN.M` versioning where: +- `X.Y.Z` - Semantic version +- `rcN` - Release candidate number +- `M` - Package-specific revision diff --git a/.github/skills/e2e-coverage/SKILL.md b/.github/skills/e2e-coverage/SKILL.md new file mode 100644 index 0000000000..c43b00ba2e --- /dev/null +++ b/.github/skills/e2e-coverage/SKILL.md @@ -0,0 +1,50 @@ +--- +name: e2e-coverage +description: Use when adding, modifying, or reviewing E2E test apps/skeletons to keep the test coverage report up to date. +--- + +# E2E Test Coverage Report + +Guidelines for maintaining `dev/modern-tools/rspack/E2E_COVERAGE.md` — a single-page report of what every E2E app and skeleton tests. + +## When to Update + +| Trigger | Action | +|---------|--------| +| New app added to `apps/` | Add a subsection under **Apps** with a coverage table | +| New skeleton added to `skeleton.test.js` | Add a row to the **Skeletons** table | +| New npm package imported for compatibility testing | Add an entry under **NPM Package Compatibility** with the package name, file, and reason | +| New custom assertion added to a test file | Add a row to that app's coverage table | +| New feature tested across multiple apps | Add a row to the **Feature Coverage Matrix** | +| App or skeleton removed | Remove its entries from all sections | + +## Report Structure + +The report has five sections, in this order: + +1. **Test Lifecycle** — the phases every app/skeleton goes through (init, run, prod, test, test once, build) and what default assertions apply +2. **Apps** — one subsection per `apps/<name>/` with a short description and a `| What is covered | Phase |` table +3. **Skeletons** — single table with one row per skeleton (`| Skeleton | Port | Language | Extra coverage |`) +4. **NPM Package Compatibility** — grouped by app, each entry has the package name, file path, and why it's included (ESM-only, native bindings, subpath exports, etc.) +5. **Feature Coverage Matrix** — cross-reference table (`| Feature | Apps | Skeletons |`) showing where each capability is tested + +## How to Gather Information + +For each app or skeleton, check these sources: + +| Source | What to look for | +|--------|-----------------| +| `<name>.test.js` | Test helper used, options (`env`, `configFile`, `buildDir`, `testFullApp`, `checkBundleFilePaths`), all `customAssertions` callbacks and what they assert | +| `skeleton.test.js` | The `testMeteorSkeleton({ skeletonName: '<name>' })` block for that skeleton | +| `apps/<name>/server/main.js` | npm imports with comments explaining why (ESM-only, native bindings, etc.) | +| `apps/<name>/imports/` | Shared code with special imports (`node:` protocol, JSX packages) | +| `apps/<name>/rspack.config.*` | Custom config features (`compileWithRspack`, `compileWithMeteor`, `disablePlugins`, custom rules) | +| `apps/<name>/package.json` | Dependencies that exist solely for compatibility testing | + +## Writing Guidelines + +- Keep descriptions short — one line per table row +- Use the phase names from the lifecycle table: Init, Run, Prod, Test, Build, All +- For npm packages, always state the **reason** (what module format issue it validates) +- Don't duplicate info between the per-app table and the feature matrix — the app table has detail, the matrix has the cross-reference +- When an env var is set in a test file, note it as `All (env prefix)` in the phase column diff --git a/.github/skills/modern-tools/SKILL.md b/.github/skills/modern-tools/SKILL.md new file mode 100644 index 0000000000..cfcacdb09a --- /dev/null +++ b/.github/skills/modern-tools/SKILL.md @@ -0,0 +1,246 @@ +--- +name: modern-tools +description: Use when working with tools-core utilities, rspack integration, or modern tooling. Covers logging, npm management, process spawning, git helpers, and Meteor app configuration APIs. +--- + +# Modern Tools + +Utility packages for modern tooling, bundler integrations, and native solutions. + +## tools-core (`/packages/tools-core`) + +Central utility package providing helpers for npm, logging, process management, and Meteor configuration. This is the foundation for modern tool integrations. + +### Logging Module (`lib/log.js`) + +```javascript +import { logProgress, logError, logInfo, logSuccess } from 'meteor/tools-core'; + +logProgress('Building application...'); // Blue +logSuccess('Build complete'); // Green +logError('Build failed'); // Red +logInfo('Using Rspack bundler'); // Purple +``` + +Respects `METEOR_DISABLE_COLORS` environment variable. + +### NPM Management Module (`lib/npm.js`) + +| Function | Description | +|----------|-------------| +| `getNodeBinaryPath(binaryName)` | Gets path to Node binaries (npm, npx, node) | +| `checkNpmDependencyExists(dep, opts)` | Checks if npm package is installed | +| `checkNpmBinaryExists(binary, opts)` | Checks if binary exists in node_modules/.bin | +| `checkNpmDependencyVersion(dep, opts)` | Validates semver with conditions (gte, lt, eq) | +| `installNpmDependency(deps, opts)` | Installs dependencies (npm/yarn, dev/exact flags) | +| `getNpmCommand(args)` | Returns npm command with `meteor npm` fallback | +| `getNpxCommand(args)` | Returns npx command with `meteor npx` fallback | +| `getYarnCommand(args)` | Gets yarn command path | +| `isYarnProject(opts)` | Detects yarn projects (yarn.lock, packageManager) | +| `getMonorepoPath(opts)` | Detects monorepo root (workspaces, lerna, pnpm) | +| `isMonorepo(opts)` | Boolean monorepo detection | + +### Process Management Module (`lib/process.js`) + +| Function | Description | +|----------|-------------| +| `spawnProcess(cmd, args, opts)` | Spawns process with streaming output, color preservation | +| `stopProcess(proc, opts)` | Graceful termination with SIGTERM/SIGKILL fallback | +| `isProcessRunning(proc)` | Checks if process is still running | +| `isPortAvailable(port, host)` | Checks if port is free | +| `waitForPort(port, opts)` | Waits for port availability with timeout | + +Options for `spawnProcess`: `env`, `cwd`, `detached`, `onStdout`, `onStderr`, `onExit`, `onError` + +### Meteor Configuration Module (`lib/meteor.js`) + +**Application Configuration:** + +| Function | Description | +|----------|-------------| +| `getMeteorAppDir()` | Gets application root directory | +| `getMeteorAppPackageJson()` | Parses app's package.json | +| `getMeteorAppConfig()` | Retrieves Meteor config from package.json or Plugin | +| `getMeteorAppPort()` | Gets app port from environment | +| `getMeteorAppConfigModern()` | Gets modern bundler configuration | +| `isMeteorAppConfigModernVerbose()` | Checks verbose flag | +| `hasMeteorAppConfigAutoInstallDeps()` | Auto-install deps flag | + +**Entry Points:** + +| Function | Description | +|----------|-------------| +| `getMeteorAppEntrypoints()` | Gets main/test modules for client/server | +| `getMeteorInitialAppEntrypoints()` | Gets initial entry points with HTML detection | +| `isMeteorAppTestModule()` | Checks if project is test module | +| `setMeteorAppEntrypoints(opts)` | Sets entry points via environment variables | +| `setMeteorAppIgnore(pattern)` | Sets file ignore patterns | +| `setMeteorAppCustomScriptUrl(url)` | Sets custom script URLs | + +**Command Detection:** + +| Function | Description | +|----------|-------------| +| `isMeteorAppRun()` | Running in 'run' mode | +| `isMeteorAppBuild()` | Running in 'build' or 'deploy' | +| `isMeteorAppUpdate()` | Running in 'update' | +| `isMeteorAppTest()` | In test mode | +| `isMeteorAppTestFullApp()` | Test mode with full-app flag | +| `isMeteorAppTestWatch()` | Test mode in watch mode | +| `isMeteorAppNativeAndroid()` | Native Android mode | +| `isMeteorAppNativeIos()` | Native iOS mode | +| `isMeteorAppNative()` | Any native mode | +| `isMeteorAppDevelopment()` | Development mode | +| `isMeteorAppProduction()` | Production mode | +| `isMeteorAppDebug()` | Debug mode | + +**Package Detection:** + +| Function | Description | +|----------|-------------| +| `isMeteorBlazeProject()` | Has blaze/blaze-html-templates | +| `isMeteorBlazeHotProject()` | Blaze with hot reload | +| `isMeteorCoffeescriptProject()` | Has CoffeeScript | +| `isMeteorLessProject()` | Has Less CSS | +| `isMeteorScssProject()` | Has SCSS/Sass | +| `isMeteorTypescriptProject()` | Has TypeScript | +| `isMeteorBundleVisualizerProject()` | Has bundle visualizer | +| `isMeteorPackagesTest()` | test-packages command | + +**File Operations:** + +| Function | Description | +|----------|-------------| +| `getMeteorAppFilesAndFolders(opts)` | Scans app directory (recursive, with ignore) | +| `getMeteorAppPackages()` | Lists all loaded packages | +| `getMeteorEnvPackageDirs()` | Gets package directories from env vars | +| `getMeteorToolsRequire(filePath)` | Requires module relative to Meteor tools | + +### Global State Module (`lib/global-state.js`) + +Maintains persistent state across file changes during development: + +```javascript +import { getGlobalState, setGlobalState, removeGlobalState, clearGlobalState } from 'meteor/tools-core'; + +setGlobalState('buildStartTime', Date.now()); +const startTime = getGlobalState('buildStartTime'); +``` + +### Git Management Module (`lib/git.js`) + +| Function | Description | +|----------|-------------| +| `isGitRepository(dir)` | Checks if directory is git repo | +| `gitignoreExists(dir)` | Checks .gitignore existence | +| `ensureGitignoreExists(dir, entries)` | Creates .gitignore with initial entries | +| `getMissingGitignoreEntries(dir, entries)` | Finds missing entries | +| `addGitignoreEntries(dir, entries, ctx)` | Adds entries with context logging | + +### String Utilities (`lib/string.js`) + +| Function | Description | +|----------|-------------| +| `capitalizeFirstLetter(str)` | Capitalizes first character | +| `shuffleString(str)` | Shuffles string characters | +| `joinWithAnd(items, opts)` | Human-readable list ("a, b, and c") | + +--- + +## Rspack Integration (`/packages/rspack`) + +Modern bundler integration using Rspack (Rust-based Webpack alternative). + +### Package Structure + +| File | Description | +|------|-------------| +| `lib/constants.js` | Default versions, global state keys, build contexts | +| `lib/dependencies.js` | Dependency checking and auto-installation | +| `lib/build-context.js` | Build directory management | +| `lib/config.js` | Meteor configuration for Rspack | +| `lib/processes.js` | Rspack process spawning | +| `lib/compilation.js` | Compilation tracking | + +### Build Contexts + +| Context | Directory | Purpose | +|---------|-----------|---------| +| `RSPACK_BUILD_CONTEXT` | `_build` | Build output | +| `RSPACK_ASSETS_CONTEXT` | `build-assets` | Static assets | +| `RSPACK_CHUNKS_CONTEXT` | `build-chunks` | Chunk bundles | +| `RSPACK_DOCTOR_CONTEXT` | `.rsdoctor` | Analysis/diagnostics | + +### Key Dependencies + +- `@rspack/core` ^1.7.1 +- `@meteorjs/rspack` ^0.3.56 (configuration logic) +- `@rspack/plugin-react-refresh` ^1.4.3 +- `swc-loader` ^0.2.6 + +### Integration with tools-core + +- Uses `getMeteorInitialAppEntrypoints()` for entry points +- Uses command detection functions for build mode awareness +- Uses process spawning and npm utilities + +--- + +## TypeScript Compiler (`/packages/typescript`) + +Compiler plugin for TypeScript/TSX file compilation. + +**Registered Plugin:** `compile-typescript` + +**Supported Extensions:** `.ts`, `.tsx` + +**Implied Packages:** `modules`, `ecmascript-runtime`, `babel-runtime`, `promise`, `dynamic-import` + +**Features:** +- Transpiles TypeScript before Babel processing +- Supports client/server/legacy browser targets +- Integrates with React Fast Refresh for HMR + +**Limitations:** +- Per-file transpilation (no cross-file type analysis) +- No tsconfig.json support (Meteor manages settings) +- No type checking during compilation +- No .d.ts generation + +--- + +## WebApp & Express (`/packages/webapp`) + +HTTP server integration using Express.js 5.x framework. + +### Key APIs + +```javascript +import { WebApp } from 'meteor/webapp'; + +// Middleware registration +WebApp.connectHandlers.use('/api', myMiddleware); +WebApp.handlers.use(compression()); + +// Direct Express access +WebApp.expressApp.get('/health', (req, res) => res.send('OK')); + +// Server instance +WebApp.httpServer; + +// Hooks +WebApp.onListening(() => console.log('Server ready')); +``` + +### Express Exports + +| Property | Description | +|----------|-------------| +| `WebApp.connectHandlers` | Express middleware registry (legacy name) | +| `WebApp.handlers` | Current middleware registry | +| `WebApp.rawConnectHandlers` | Raw Express handlers | +| `WebApp.expressApp` | Direct Express app instance | +| `WebApp.httpServer` | HTTP server instance | +| `WebApp.express` | Express module export | + +**Dependencies:** express@5.1.0, cookie-parser@1.4.6, compression@1.7.4, errorhandler@1.5.1 diff --git a/.github/skills/packages/SKILL.md b/.github/skills/packages/SKILL.md new file mode 100644 index 0000000000..4c0b1b467e --- /dev/null +++ b/.github/skills/packages/SKILL.md @@ -0,0 +1,152 @@ +--- +name: packages +description: Use when exploring the package ecosystem, finding which package handles a feature, understanding package relationships, or adding dependencies. Lists all core packages by domain. +--- + +# Core Packages + +Overview of Meteor's package ecosystem organized by domain. + +## Authentication & Accounts + +| Package | Description | +|---------|-------------| +| `accounts-base` | Foundation for the user account system | +| `accounts-password` | Password-based authentication | +| `accounts-passwordless` | Magic-link/token-based authentication | +| `accounts-2fa` | Two-factor authentication support | +| `accounts-ui` / `accounts-ui-unstyled` | Pre-built UI components for auth | +| `accounts-oauth` | OAuth protocol support | +| `oauth` / `oauth1` / `oauth2` | OAuth implementation | +| `oauth-encryption` | Encrypted OAuth token storage | +| `service-configuration` | OAuth provider configuration | + +**Social Login Providers:** +- `accounts-facebook`, `accounts-github`, `accounts-google` +- `accounts-twitter`, `accounts-meetup`, `accounts-weibo` +- `accounts-meteor-developer` + +## Data & Database + +| Package | Description | +|---------|-------------| +| `mongo` | MongoDB integration and collection API | +| `minimongo` | Client-side MongoDB emulation | +| `mongo-id` | MongoDB ObjectID generation | +| `mongo-livedata` | Reactive MongoDB queries | +| `npm-mongo` | MongoDB Node.js driver wrapper | +| `mongo-dev-server` | Development MongoDB server | +| `ddp` | Distributed Data Protocol meta-package | +| `ddp-common` | Shared DDP utilities | +| `ddp-client` | DDP client implementation | +| `ddp-server` | DDP server implementation | +| `ddp-rate-limiter` | Rate limiting for DDP methods/subscriptions | +| `ejson` | Extended JSON serialization | + +## Build System & Compilation + +| Package | Description | +|---------|-------------| +| `babel-compiler` | JavaScript transpilation via Babel | +| `babel-runtime` | Babel runtime helpers | +| `ecmascript` | ECMAScript 2015+ support | +| `ecmascript-runtime` | ES6+ runtime polyfills | +| `typescript` | TypeScript compilation support | +| `modules` | ES modules system | +| `modules-runtime` | Module runtime implementation | +| `modules-runtime-hot` | Hot module reloading runtime | +| `hot-code-push` | Live code updates | +| `hot-module-replacement` | HMR support | +| `rspack` | Rspack bundler integration | +| `boilerplate-generator` | HTML boilerplate generation | +| `dynamic-import` | Dynamic `import()` support | +| `caching-compiler` | Build cache management | + +## Minification & Assets + +| Package | Description | +|---------|-------------| +| `minifier-js` | JavaScript minification (terser) | +| `minifier-css` | CSS minification | +| `standard-minifier-js` | Default JS minifier package | +| `standard-minifier-css` | Default CSS minifier package | +| `standard-minifiers` | Meta-package for minifiers | +| `static-html` | Static HTML file processing | + +## Web & Server + +| Package | Description | +|---------|-------------| +| `webapp` | HTTP server and request handling | +| `webapp-hashing` | Asset fingerprinting | +| `reload` | Client-side app reload mechanism | +| `reload-safetybelt` | Reload failure recovery | +| `autoupdate` | Automatic client updates | +| `browser-policy` | Content Security Policy | +| `force-ssl` | HTTPS enforcement | +| `allow-deny` | Collection permission rules | +| `fetch` | HTTP Fetch API polyfill | +| `routepolicy` | Route-based policies | + +## Client-Side Utilities + +| Package | Description | +|---------|-------------| +| `tracker` | Reactive dependency tracking | +| `reactive-var` | Single reactive value | +| `reactive-dict` | Reactive key-value store | +| `session` | Client-side session storage | +| `localstorage` | LocalStorage wrapper | +| `socket-stream-client` | WebSocket client | +| `random` | Cryptographic random generation | +| `check` | Runtime type checking | +| `underscore` | Utility library | +| `base64` | Base64 encoding/decoding | +| `diff-sequence` | Array diffing algorithm | +| `id-map` | ID-based mapping | +| `ordered-dict` | Ordered dictionary | + +## Testing (6 packages) + +| Package | Description | +|---------|-------------| +| `tinytest` | Meteor's built-in test framework | +| `tinytest-harness` | Test harness utilities | +| `test-helpers` | Testing utility functions | +| `test-in-browser` | Browser-based test runner | +| `test-in-console` | Console-based test runner | + +## Context & Roles + +| Package | Description | +|---------|-------------| +| `context` | Request context management (AsyncLocalStorage) | +| `roles` | User roles and permissions system | + +## Deprecated Packages (`packages/deprecated/`) + +40+ legacy packages maintained for backward compatibility: +- UI libraries: `amplify`, `backbone`, `d3`, `handlebars` +- Legacy OAuth: `facebook`, `github`, `google` (use `accounts-*` instead) +- Config UIs: `*-config-ui` packages +- Others: `jquery-history`, `jshint`, `jsparse`, `deps` (use `tracker`) + +## Development-Only Packages + +| Package | Description | +|---------|-------------| +| `autopublish` | Auto-publish all collections (remove in production) | +| `insecure` | Allow all database writes (remove in production) | + +## NPM Packages (`/npm-packages`) + +Packages published to npm for external use: + +| Package | npm Name | Description | +|---------|----------|-------------| +| `meteor-babel` | `@meteorjs/babel` | Babel wrapper for ES2015+ transpilation | +| `babel-preset-meteor` | `@meteorjs/babel-preset-meteor` | Babel preset with Meteor-specific transforms | +| `meteor-rspack` | `@meteorjs/rspack` | Rspack configuration builder | +| `meteor-promise` | `meteor-promise` | ES6 Promise with Fiber support | +| `meteor-node-stubs` | `meteor-node-stubs` | Node.js core module polyfills for browser | +| `eslint-plugin-meteor` | `eslint-plugin-meteor` | Meteor-specific ESLint rules | diff --git a/.github/skills/testing/SKILL.md b/.github/skills/testing/SKILL.md new file mode 100644 index 0000000000..d54d674e95 --- /dev/null +++ b/.github/skills/testing/SKILL.md @@ -0,0 +1,147 @@ +--- +name: testing +description: Use when writing tests, debugging test failures, running the test suite, or setting up test infrastructure. Covers self-test, package tests, and modern E2E tests. +--- + +# Testing + +Test patterns, commands, and utilities for the Meteor codebase. + +## Test Commands + +```bash +# CLI self-tests +./meteor self-test # Run all CLI tests +./meteor self-test "test name" # Run specific test +./meteor self-test --list # List available tests +./meteor self-test --exclude "^[a-b]" # Exclude tests by regex +./meteor self-test --retries 0 # Skip retries in development + +# Package tests (TinyTest — view results at http://localhost:3000) +./meteor test-packages # Test all core packages +./meteor test-packages mongo # Test specific package +TINYTEST_FILTER="collection" ./meteor test-packages # Filter specific tests + +# Package tests in console (headless via Puppeteer — prints results to terminal) +# Use this for automation or when you need terminal output without a browser. +PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh +./packages/test-in-console/run.sh # Test all core packages +./packages/test-in-console/run.sh "mongo" # Test specific package + +# E2E tests (Jest + Playwright) +npm run install:e2e # Install dependencies +npm run test:e2e # Run all E2E tests +npm run test:e2e -- -t="React" # Run specific test +``` + +## E2E Tests (`tools/e2e-tests/`) + +Jest + Playwright suite for verifying bundler integrations (rspack). Tests cover framework skeletons and build scenarios. + +**Test apps:** `apps/{react,vue,svelte,solid,blaze,typescript,babel,coffeescript,monorepo}` + +## Test Helpers Package (`packages/test-helpers`) + +Comprehensive testing utilities for Meteor applications. + +### Async Testing + +```javascript +import { testAsyncMulti, simplePoll, waitUntil } from 'meteor/test-helpers'; + +// Wait for condition +await waitUntil(() => someCondition, { timeout: 5000, interval: 100 }); + +// Poll until ready +simplePoll(() => isReady(), successCallback, failCallback); +``` + +### DOM/UI Testing + +```javascript +import { clickElement, simulateEvent, canonicalizeHtml, renderToDiv } from 'meteor/test-helpers'; + +clickElement(button); +simulateEvent(input, 'keydown', { keyCode: 13 }); +const normalized = canonicalizeHtml(html); +``` + +### Connection Testing + +```javascript +import { makeTestConnection, captureConnectionMessages } from 'meteor/test-helpers'; + +const conn = makeTestConnection(clientId); +const messages = captureConnectionMessages(server); +``` + +### Utilities + +| Function | Description | +|----------|-------------| +| `SeededRandom` | Predictable random for deterministic tests | +| `try_all_permutations()` | Test all permutations of inputs | +| `withCallbackLogger()` | Track callback invocations | +| `mockBehaviours()` | Behavior mocking | + +## Tinytest (`packages/tinytest`) + +Meteor's built-in test framework. + +```javascript +Tinytest.add('my test', function (test) { + test.equal(1 + 1, 2); + test.isTrue(true); + test.throws(function () { throw new Error(); }); +}); + +Tinytest.addAsync('async test', async function (test) { + const result = await asyncOperation(); + test.equal(result, expected); +}); +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `TEST_METADATA` | Test configuration JSON | +| `METEOR_TEST_PACKAGES` | Packages to test | + +## Debug Commands + +```bash +# Verbose build output +METEOR_DEBUG_BUILD=1 ./meteor run + +# Profile build performance +METEOR_PROFILE=1 ./meteor build + +# Force rebuild +./meteor reset && ./meteor run + +# Debug Meteor tool with Chrome inspector +TOOL_NODE_FLAGS="--inspect-brk" ./meteor +``` + +## Writing Package Tests + +In `package.js`: + +```javascript +Package.onTest(function(api) { + api.use(['tinytest', 'test-helpers', 'my-package']); + api.addFiles('my-package-tests.js'); +}); +``` + +In `my-package-tests.js`: + +```javascript +import { MyPackage } from 'meteor/my-package'; + +Tinytest.add('MyPackage - basic functionality', function (test) { + const result = MyPackage.doSomething(); + test.equal(result, expected); +}); +``` diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000000..ce29d07dfb --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,95 @@ +name: E2E Tests + +on: + pull_request: + paths: + - 'meteor' + - 'tools/e2e-tests/**' + - 'packages/rspack/**' + - 'packages/tools-core/**' + - 'packages/babel-compiler/**' + - 'packages/meteor-tool/**' + - 'npm-packages/meteor-rspack/**' + - 'tools/static-assets/skel-**' + - '.github/workflows/e2e-tests.yml' + +concurrency: + group: meteor-rspack-tests-${{ github.ref }} + cancel-in-progress: true + +env: + TOOL_NODE_FLAGS: "--max_old_space_size=12288" + NODE_OPTIONS: "--max_old_space_size=12288" + +jobs: + test: + name: ${{ matrix.category }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - Angular + - Babel + - Blaze + - Coffeescript + - Monorepo + - Other + - React + - R.Router + - Solid + - Svelte + - Typescript + - Vue + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.npm + node_modules + tools/e2e-tests/node_modules + packages/**/.npm + .meteor + dev_bundle + .babel-cache + ~/.cache/ms-playwright + + key: ${{ runner.os }}-meteor-${{ hashFiles('**/package-lock.json', 'meteor') }} + restore-keys: | + ${{ runner.os }}-meteor- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Install deps + run: npm install + + - name: Install test deps + run: npm run install:e2e + + - name: Set NPM_LINK_RSPACK=false for release branches + run: | + echo "Current branch: ${{ github.head_ref || github.ref_name }}" + if [[ "${{ github.head_ref || github.ref_name }}" == release-* ]]; then + echo "NPM_LINK_RSPACK=false" >> $GITHUB_ENV + echo "::warning::NPM_LINK_RSPACK=false on release branch. E2E tests will install @meteorjs/rspack from npm — make sure the latest version is published or tests may fail." + fi + + - name: Prepare Meteor + run: ./meteor --get-ready + + - name: Run tests for ${{ matrix.category }} + uses: nick-fields/retry@v3 + with: + max_attempts: 3 + retry_on: error + timeout_minutes: 15 + retry_wait_seconds: 90 + command: npm run test:e2e -- -t="${{ matrix.category }}" diff --git a/.github/workflows/inactive-issues.yml b/.github/workflows/inactive-issues.yml index 2d8cba7a3f..620b484db6 100644 --- a/.github/workflows/inactive-issues.yml +++ b/.github/workflows/inactive-issues.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v3 - name: Manage inactive issues - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: | const script = require('./.github/scripts/inactive-issues.js') diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml new file mode 100644 index 0000000000..29b8c48646 --- /dev/null +++ b/.github/workflows/test-packages.yml @@ -0,0 +1,52 @@ +name: Package tests + +on: + pull_request: + +jobs: + test-packages: + runs-on: ubuntu-22.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + timeout-minutes: 90 + env: + CXX: g++-12 + phantom: false + PUPPETEER_DOWNLOAD_PATH: /home/runner/.npm/chromium + TEST_PACKAGES_EXCLUDE: stylus + METEOR_MODERN: true + NODE_ENV: CI + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.17.0 + + - name: Restore caches + uses: actions/cache@v4 + with: + path: | + ~/.npm + .meteor + .babel-cache + dev_bundle + /home/runner/.npm/chromium + key: ${{ runner.os }}-node-22.17-${{ hashFiles('meteor', '**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-22.17- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y g++-12 libnss3 + + - name: Install npm dependencies + run: npm install + + - name: Run test-in-console suite + run: ./packages/test-in-console/run.sh diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..a98248e760 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,36 @@ +name: Unit Tests + +on: + pull_request: + paths: + - 'tools/**' + - 'scripts/**' + - 'package.json' + - '.github/workflows/unit-tests.yml' + push: + branches: + - devel + +concurrency: + group: unit-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Install unit test deps + run: npm run install:unit + + - name: Run unit tests + run: npm run test:unit diff --git a/.github/workflows/meteor-selftest-windows.yml b/.github/workflows/windows-selftest.yml similarity index 50% rename from .github/workflows/meteor-selftest-windows.yml rename to .github/workflows/windows-selftest.yml index c328e7bf2d..97a22ad4cb 100644 --- a/.github/workflows/meteor-selftest-windows.yml +++ b/.github/workflows/windows-selftest.yml @@ -1,4 +1,4 @@ -name: Meteor Selftest Windows +name: Windows Selftest on: pull_request: @@ -6,10 +6,19 @@ on: - opened - reopened - synchronize + paths: + - 'meteor' + - 'meteor.bat' + - 'tools/**' + - 'packages/babel-compiler/**' + - 'packages/dynamic-import/**' + - 'packages/meteor/**' + - 'packages/meteor-tool/**' + - '.github/workflows/windows-selftest.yml' + push: branches: - devel - - 2.x.x env: METEOR_PRETTY_OUTPUT: 0 @@ -28,7 +37,7 @@ jobs: cancel-in-progress: true steps: - - name: cleanup + - name: Cleanup shell: powershell run: Remove-Item -Recurse -Force ${{ github.workspace }}\* @@ -36,27 +45,47 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: 22.x + - name: Cache dependencies + id: meteor-cache + uses: actions/cache@v4 + with: + path: | + dev_bundle/ + .babel-cache/ + .meteor/ + ~/.npm + node_modules/ + packages/**/.npm + key: ${{ runner.os }}-meteor-${{ hashFiles('meteor', 'meteor.bat') }} + restore-keys: | + ${{ runner.os }}-meteor- + + - name: Reset submodules (force refetch) + shell: pwsh + run: | + git submodule deinit -f --all + if (Test-Path ".git/modules") { Remove-Item -Recurse -Force ".git/modules" } + - name: Install dependencies shell: pwsh run: | $env:PATH = "C:\Program Files\7-Zip;$env:PATH" .\scripts\windows\ci\install.ps1 + # Run ONLY when the cache was NOT restored + - name: Prepare Meteor (cache miss) + if: steps.meteor-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + $env:PATH = "C:\Program Files\7-Zip;$env:PATH" + .\meteor.bat --get-ready + - name: Run tests shell: pwsh run: | $env:PATH = "C:\Program Files\7-Zip;$env:PATH" .\scripts\windows\ci\test.ps1 - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - .\dev_bundle - .\.babel-cache - .\.meteor - key: ${{ runner.os }}-meteor-${{ hashFiles('**/package-lock.json') }} diff --git a/.gitignore b/.gitignore index 4742e13056..36de1287c6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules \#*\# .\#* .idea +!.idea/icon.svg *.iml *.sublime-project *.sublime-workspace diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100755 index 0000000000..16ecae9b10 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,21 @@ +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg2" preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 160.10664 156.98515" height="156.98515" width="160.10664"> + <metadata id="metadata26"> + <rdf:rdf xmlns="http://www.w3.org/1999/xhtml"> + <cc:work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></dc:type> + <dc:title></dc:title> + </cc:work> + </rdf:rdf> + </metadata> + <defs id="defs24"/> + <g transform="matrix(0.62649123,0,0,0.62649123,-0.27477954,-0.27455194)" id="g6" style="fill:#de4f4f"> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 0.43860078,0.43823749 219.30039,232.26587 c 0,0 7.45622,5.25885 13.15803,-0.87647 5.70181,-6.13533 1.3158,-12.27065 1.3158,-12.27065 L 0.43860078,0.43823749 l 0,0 z" id="path8"/> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 69.737525,22.350112 236.40582,202.02749 c 0,0 7.45622,5.25884 13.15803,-0.87648 5.70181,-6.13533 1.3158,-12.27065 1.3158,-12.27065 L 69.737525,22.350112 l 0,0 z" id="path10"/> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 21.052838,69.241524 187.72114,248.9189 c 0,0 7.45621,5.25885 13.15802,-0.87648 5.70181,-6.13532 1.3158,-12.27065 1.3158,-12.27065 L 21.052838,69.241524 l 0,0 z" id="path12"/> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 128.32077,41.194324 244.76195,166.72418 c 0,0 5.20922,3.67404 9.19273,-0.61234 3.98351,-4.28639 0.91927,-8.57278 0.91927,-8.57278 L 128.32077,41.194324 l 0,0 z" id="path14"/> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 37.091803,123.58297 153.53299,249.11282 c 0,0 5.20921,3.67405 9.19273,-0.61234 3.98351,-4.28638 0.91927,-8.57277 0.91927,-8.57277 L 37.091803,123.58297 l 0,0 z" id="path16"/> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="m 188.15974,68.365049 52.77506,57.067161 c 0,0 2.57683,1.72156 4.54735,-0.28693 1.97051,-2.00849 0.45473,-4.01699 0.45473,-4.01699 l -57.77714,-52.763241 0,0 z" id="path18"/> + <path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="m 66.228719,181.43032 52.775071,57.06716 c 0,0 2.57682,1.72156 4.54734,-0.28693 1.97051,-2.00849 0.45473,-4.01698 0.45473,-4.01698 l -57.777141,-52.76325 0,0 z" id="path20"/> + </g> + </svg> \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..6e04b3ad4b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# Meteor + +Full-stack JavaScript platform for modern web and mobile applications. + +## Commands + +```bash +./meteor run # Run from source +./meteor create my-app # Create app +./meteor self-test # CLI tests +./meteor test-packages ./packages/<name> # Package tests (browser UI at localhost:3000) +./packages/test-in-console/run.sh "<name>" # Package tests (terminal output via Puppeteer) +npm run test:unit # Unit tests (Jest) +npm run test:e2e # E2E tests (Jest + Playwright) +``` + +> **Note:** `./meteor test-packages` starts a web server and waits for a browser — +> it produces no terminal output. For automated/headless runs, use +> `./packages/test-in-console/run.sh "<package>"` instead, which runs the same tests +> via Puppeteer and prints pass/fail results to stdout. + +## Structure + +``` +packages/ # Core Meteor packages (~100+) +tools/ # CLI & build system (Isobuild) +npm-packages/ # Published @meteorjs/* packages +scripts/ # Build & release automation +``` + +## Key Entry Points + +| Task | Location | +|------|----------| +| CLI commands | `tools/cli/commands.js` | +| Build system | `tools/isobuild/bundler.js` | +| Package lookup | `packages/<name>/package.js` | +| Modern bundler | `packages/rspack/`, `packages/tools-core/` | + +## Skills + +Load these for detailed context on specific topics: + +| Skill | When to use | +|-------|-------------| +| [codebase](.github/skills/codebase/SKILL.md) | Build system, CLI, isobuild, tools/ directory | +| [conventions](.github/skills/conventions/SKILL.md) | Writing packages, CLI commands, code patterns | +| [testing](.github/skills/testing/SKILL.md) | Writing tests, debugging failures, test infrastructure | +| [packages](.github/skills/packages/SKILL.md) | Finding packages by feature, understanding dependencies | +| [modern-tools](.github/skills/modern-tools/SKILL.md) | tools-core utilities, rspack, modern integrations | +| [e2e-coverage](.github/skills/e2e-coverage/SKILL.md) | Updating the E2E test coverage report when apps/skeletons change | +| [ai-context](.github/skills/ai-context/SKILL.md) | Creating, updating, or maintaining AI documentation files | + +## Package Domains + +| Category | Packages | +|----------|----------| +| Auth | `accounts-base`, `accounts-password`, `accounts-oauth` | +| Database | `mongo`, `minimongo`, `ddp-server`, `ddp-client` | +| Build | `babel-compiler`, `ecmascript`, `typescript`, `rspack` | +| Web | `webapp`, `autoupdate`, `reload` | +| Reactivity | `tracker`, `reactive-var`, `reactive-dict` | + +## Notes + +- `docs/` and `guide/` are the public documentation website, not agent context +- `v3-docs/` contains Meteor 3.x documentation +- See [DEVELOPMENT.md](DEVELOPMENT.md) for contributor setup diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..87a39b6cd2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +Read [AGENTS.md](AGENTS.md) before starting any task. + +## Skills + +Load these for detailed context on specific topics: + +| Skill | When to use | +|-------|-------------| +| [codebase](.github/skills/codebase/SKILL.md) | Build system, CLI, isobuild, tools/ directory | +| [conventions](.github/skills/conventions/SKILL.md) | Writing packages, CLI commands, code patterns | +| [testing](.github/skills/testing/SKILL.md) | Writing tests, debugging failures, test infrastructure | +| [packages](.github/skills/packages/SKILL.md) | Finding packages by feature, understanding dependencies | +| [modern-tools](.github/skills/modern-tools/SKILL.md) | tools-core utilities, rspack, modern integrations | +| [e2e-coverage](.github/skills/e2e-coverage/SKILL.md) | Updating the E2E test coverage report when apps/skeletons change | +| [ai-context](.github/skills/ai-context/SKILL.md) | Creating, updating, or maintaining AI documentation files | diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a34dd51841..63853d9791 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -33,6 +33,14 @@ can run Meteor directly from a Git checkout using these steps: $ ./meteor --help ``` + > **Note for Windows (PowerShell):** + > + > * In PowerShell, use `.\meteor` (not `./meteor`). + > * Meteor may need `7z.exe` available in your `PATH` to download/extract binaries (dev_bundle). + > * Verify: `where.exe 7z` + > * If missing, install 7-Zip and ensure it is on your PATH (for example via `choco install 7zip -y` or `scoop install 7zip`). + + 3. **Ready to Go!** Your local Meteor checkout is now ready to use! You can use this `./meteor` @@ -115,79 +123,98 @@ For the rest, try looking nearby for a `README.md`. For example, [`isobuild`](t ## Tests -### Test against the local meteor copy +When running tests that use `./meteor`, be sure to run them against the checked-out copy of Meteor instead of the globally-installed version. This ensures tests run against your local development version. -When running any tests, be sure to run them against the checked-out copy of Meteor instead of -the globally-installed version. This means ensuring that the command is `path-to-meteor-checkout/meteor` and not just `meteor`. +The repository has four test layers, each covering a different scope: -This is important so that tests are run against your local development version and not the stable (installed) Meteor release. +| Command | Layer | Scope | +|---------|-------|-------| +| `npm run test:unit` | **Unit** (Jest) | Pure logic in `tools/`, `scripts/`, and helpers: fast, no Meteor runtime needed | +| `npm run test:e2e` | **E2E** (Jest + Playwright) | Bundler integration and skeleton apps: creates real Meteor projects, launches a browser | +| `./meteor self-test` | **Self-test** (custom) | Meteor CLI tool itself, spawns sandboxed Meteor processes to verify commands end-to-end | +| `./meteor test-packages` | **Package** (TinyTest) | Atmosphere packages in `packages/`, runs inside a Meteor app with the full reactive runtime | -### Running tests on Meteor core +### Unit tests (Jest) -When you are working with code in the core Meteor packages, you will want to make sure you run the -full test-suite (including the tests you added) to ensure you haven't broken anything in Meteor. The -`test-packages` command will do just that for you: +Unit tests cover pure helpers, scripts, and tool logic that does not require the Meteor runtime. They use [Jest](https://jestjs.io/) configured in `tools/unit-tests/`, targeting `tools/**/*.test.js` and `scripts/**/*.test.js`. - ./meteor test-packages +```sh +# Install dependencies (first time) +npm run install:unit -Exactly in the same way that [`test-packages` works in standalone Meteor apps](https://guide.meteor.com/writing-atmosphere-packages.html#testing), the `test-packages` command will start up a Meteor app with [TinyTest](./packages/tinytest/README.md). To view the results, just connect to `http://localhost:3000`. +# Run all unit tests +npm run test:unit -If you want to see results in the console you can use: +# Run a specific test file +npm run test:unit -- tools/path/to/file.test.js - PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh +# Run tests matching a name pattern +npm run test:unit -- -t "my test name" +``` -> [PUPPETEER_DOWNLOAD_PATH](https://github.com/dfernandez79/puppeteer/blob/main/README.md#q-chromium-gets-downloaded-on-every-npm-ci-run-how-can-i-cache-the-download) is optional but this is useful to skip Downloading Chromium on every run +Place test files next to the module they test using the `*.test.js` naming convention. Jest will pick them up automatically. -> We run our tests on Travis like above. +### E2E tests (Jest + Playwright) -#### Running specific tests +End-to-end tests in `tools/e2e-tests/` validate that Meteor skeletons and bundler integrations work correctly. They create real Meteor apps, start dev servers, and assert behavior in a headless Chromium browser. -Specific package tests can be run by passing a `<package name>` or `<package path>` to the `test-packages` command. For example, to run `mongo` tests, it's possible to run: +```sh +# Install dependencies (first time) +npm run install:e2e - ./meteor test-packages mongo +# Run all E2E tests +npm run test:e2e -For more fine-grained control, if you're interested in running only the specific tests that relate to the functionality you're working on, you can filter individual tests by using the `TINYTEST_FILTER` environment variable (which supports regex's). For example, to run only the package tests that verify `new Mongo.Collection` behavior, try: +# Run a specific suite +npm run test:e2e -- -t="React" +``` - TINYTEST_FILTER="collection - call new Mongo.Collection" ./meteor test-packages +Each test has a corresponding app fixture in `tools/e2e-tests/apps/`. See that directory for examples when adding new E2E tests. -You can also provide the same filters for `./packages/test-in-console/run.sh` explained above. +### Self-tests (Meteor tool) -### Running Meteor Tool self-tests +The Meteor CLI has its own "self-test" framework that spawns sandboxed Meteor processes. It tests commands like `create`, `build`, `deploy`, and `publish`. -While TinyTest and the `test-packages` command can be used to test internal Meteor packages, they cannot be used to test the Meteor Tool itself. The Meteor Tool is a node app that uses a home-grown "self test" system. +```sh +# List all self-tests +./meteor self-test --list -#### Listing available tests +# Run all self-tests +./meteor self-test -To see a list of tests included in the self-test system, use the `--list` option: +# Run tests matching a regex +./meteor self-test "^[a-b]" - ./meteor self-test --list +# Exclude tests matching a regex +./meteor self-test --exclude "^[a-b]" -#### Running specific tests +# Skip retries during development +./meteor self-test --retries 0 +``` -The self-test commands support a regular-expression syntax in order to specific/search for specific tests. For example, to search for tests starting with `a` or `b`, it's possible to run: +### Package tests (TinyTest) - ./meteor self-test "^[a-b]" --list +When working with core Atmosphere packages, use `test-packages` to run their tests via [TinyTest](./packages/tinytest/README.md). This starts a Meteor app, view results at `http://localhost:3000`. -Simply remove the `--list` flag to actually run the matching tests. +```sh +# Test all packages +./meteor test-packages -#### Excluding specific tests +# Test a specific package +./meteor test-packages mongo -In a similar way to the method of specifying which tests TO run, there is a way to specify which tests should NOT run. Again, using regular-expressions, this command will NOT list any tests which start with `a` or `b`: +# Filter by test name (supports regex), using --filter or -f +./meteor test-packages --filter "collection - call new Mongo.Collection" - ./meteor self-test --exclude "^[a-b]" --list +# Equivalent using the environment variable +TINYTEST_FILTER="collection - call new Mongo.Collection" ./meteor test-packages +``` -Simply remove the `--list` flag to actually run the matching tests. +For headless console output: -#### Avoiding retries - -On CI we want to retry the tests to avoid false failures but in development can take some time if you retry every time a test is failing. So to avoid retries use: - - ./meteor self-test --retries 0 - - -#### More reading - -For even more details on how to run Meteor Tool "self tests", please refer to the [Testing section of the Meteor Tool README](https://github.com/meteor/meteor/blob/master/tools/README.md#testing). +```sh +PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh +``` ### Continuous integration diff --git a/README.md b/README.md index 11aa1c4c6c..e37bdc7cae 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ How about trying a tutorial to get started with your favorite technology? | [<img align="left" width="25" src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg"> React](https://docs.meteor.com/tutorials/react/) | | - | | [<img align="left" width="25" src="https://progsoft.net/images/blaze-css-icon-3e80acb3996047afd09f1150f53fcd78e98c1e1b.png"> Blaze](https://blaze-tutorial.meteor.com/) | -| [<img align="left" width="25" src="https://vuejs.org/images/logo.png"> Vue](https://docs.meteor.com/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.html) | +| [<img align="left" width="25" src="https://vuejs.org/images/logo.png"> Vue](https://docs.meteor.com/tutorials/vue/meteorjs3-vue3.html) | # 🚀 Quick Start diff --git a/dev/modern-tools/rspack/E2E_COVERAGE.md b/dev/modern-tools/rspack/E2E_COVERAGE.md new file mode 100644 index 0000000000..f01e58e4c3 --- /dev/null +++ b/dev/modern-tools/rspack/E2E_COVERAGE.md @@ -0,0 +1,300 @@ +# E2E Test Coverage + +> To update this report, follow the [e2e-coverage skill](/.github/skills/e2e-coverage/SKILL.md). + +End-to-end tests using Jest + Playwright that verify Meteor apps with the Rspack bundler across frameworks, build modes, and features. + +Test infrastructure lives in `tools/e2e-tests/`, with app fixtures in `tools/e2e-tests/apps/` and matching test files at `tools/e2e-tests/<name>.test.js`. + +## Test Lifecycle + +Every app and skeleton goes through these phases (unless skipped): + +| Phase | What it does | +|-------|-------------| +| **Init** | Copies app, installs deps, adds rspack, generates config | +| **Run (dev)** | `meteor run` — asserts build artifacts, app loads, client/server hot rebuild | +| **Run (prod)** | `meteor run --production` — same checks in production mode | +| **Test** | `meteor test` — runs mocha test driver, verifies test rebuild | +| **Test once** | `meteor test --once` — runs tests to completion, checks exit code | +| **Build** | `meteor build` — verifies bundle structure (main.js, programs/server, web.browser, web.browser.legacy) | +| **Reset** | `meteor reset` — clears rspack build artifacts, caches, asset/chunk context dirs, and `.meteor/local` subdirectories | + +Default assertions on every run phase: build artifacts exist, page title matches, body styles render, `__rspack__` script tag is present. + +--- + +## Apps + +Each app lives in `apps/<name>/` and has a matching `<name>.test.js`. + +### react + +Core React integration with custom Meteor local directory. + +| What is covered | Phase | +|----------------|-------| +| Custom `METEOR_LOCAL_DIR` (`.meteor/local-custom`) | All (env prefix) | +| Custom build dir (`_build-local-custom`) created | Run | +| `.gitignore` updated with custom local dir | Run | +| React + JSX environment detection | Run, Prod, Test, Build | +| Image assets load (generated + public + background) | Run, Prod | +| `Meteor.disablePlugins` suppresses rspack plugins | Run, Prod, Test, Build | +| Unplugin transform hook fires on first run (fresh cache) | Init | +| Unplugin factory created on cached run — #14031 regression | Run | +| Unplugin transform + buildDependencies tracking in production | Prod | +| Custom rspack config (`rspack.config.cjs`) | All | +| HMR works in dev, disabled in prod | Run, Prod | + +### react-router + +Full-featured React Router app with custom packages, Less, and advanced rspack config. + +| What is covered | Phase | +|----------------|-------| +| `METEOR_PACKAGE_DIRS` custom packages dir | All (env prefix) | +| `babel-plugin-react-compiler` integration | Init, Prod, Build | +| Compiler output cached in dev (babel.config.js) | Run | +| 404 page routing (renders "Page Not Found") | Run, Prod | +| Less stylesheet support (`white-space: break-spaces`) | Run, Prod | +| Meteor modules config styles (`align-content: center`) | Run, Prod | +| Custom HTML meta tags (`theme-color`) | Run, Prod | +| Default + custom package loading | Run | +| `resolve.extensions` loading (`.jsx`) | Run | +| `rspack.config.override.js` custom plugin loading | Run, Test, Build | +| React + TSX environment detection | Run, Prod, Test, Build | +| Full-app test mode (`--full-app`) | Test | +| Static assets in bundle (png, md) | Build | +| HMR works in dev, disabled in prod | Run, Prod | + +### blaze + +Blaze templating engine integration. + +| What is covered | Phase | +|----------------|-------| +| Blaze environment detection (`isBlazeEnabled`) | Run, Prod, Test, Build | +| HMR disabled (incompatible with Blaze) | Run, Prod | + +### full-blaze + +Full Blaze app (with `imports/` structure for tests). + +| What is covered | Phase | +|----------------|-------| +| Blaze environment detection | Run, Prod, Test, Build | +| `imports/api/` test path structure | Test | +| HMR disabled (incompatible with Blaze) | Run, Prod | + +### typescript + +TypeScript with SCSS, type checking, `.ts` rspack config, and `.ts` SWC config. + +| What is covered | Phase | +|----------------|-------| +| TypeScript rspack config (`rspack.config.ts`) | All | +| TypeScript SWC config (`swc.config.ts`) with automatic JSX runtime | All | +| `@swc/core` type-only import for SWC config typings | All | +| Custom build dir (`build`) | All | +| Custom asset/chunk context dirs (`assets`, `chunks`) | All | +| SCSS styles support (`white-space: break-spaces`) | Run, Prod | +| TypeScript + TSX environment detection | Run, Prod, Test, Build | +| Portable build (Meteor.isDevelopment/isProduction not defined) | Run, Prod, Build | +| `Meteor.extendSwcConfig` with path aliases (`@ui/*`, `@api/*`) | All | +| `TsCheckerRspackPlugin` type checking (no errors) | Run | +| `.meteor/local/types` directory generated | Run | +| Separate client/server test files | Test | +| CI: removes TsCheckerRspackPlugin (resource limits) | Init | +| HMR works in dev, disabled in prod | Run, Prod | + +### babel + +Babel transpilation with custom module rules and `.mjs` rspack config. + +| What is covered | Phase | +|----------------|-------| +| Custom rspack config (`rspack.config.mjs`) | All | +| Custom `NODE_ENV` compilation per phase | All (env prefix) | +| Rspack mode assertion (development/production) | Run, Prod, Test, Build | +| `Meteor.isDevelopment`/`Meteor.isProduction` defines | Run, Prod, Test, Build | +| Module rules for `.js`/`.jsx` files | Run, Prod, Test, Build | +| Module rules for `.tsx`/`.ts`/`.mts`/`.cts`/`.mjs`/`.cjs` | Run, Prod, Test, Build | +| Module rules for `.graphql`/`.gql` files | Run, Prod, Test, Build | +| Default rules negated (custom rules override) | Run, Prod, Test, Build | +| HMR works in dev, disabled in prod | Run, Prod | + +### coffeescript + +CoffeeScript language support. + +| What is covered | Phase | +|----------------|-------| +| `.coffee` file compilation (client + server + test) | All | +| CoffeeScript-specific conditional syntax | Run, Prod | +| HMR works in dev, disabled in prod | Run, Prod | + +### vue + +Vue.js framework with Tailwind CSS. + +| What is covered | Phase | +|----------------|-------| +| Vue single-file components | All | +| Tailwind CSS styles (`.p-8` padding) | Run, Prod | +| HMR works in dev, disabled in prod | Run, Prod | + +### solid + +SolidJS framework integration. + +| What is covered | Phase | +|----------------|-------| +| SolidJS compilation and rendering | All | +| HMR works in dev, disabled in prod | Run, Prod | + +### svelte + +Svelte framework integration. + +| What is covered | Phase | +|----------------|-------| +| Svelte compilation and rendering | All | +| HMR works in dev, disabled in prod | Run, Prod | + +### monorepo + +Monorepo structure with app in subdirectory. + +| What is covered | Phase | +|----------------|-------| +| Monorepo layout (`app/` subdirectory) | All | +| Custom rspack config (`rspack.config.cjs`) | All | +| `rspack.config.override.cjs` custom plugin loading | Run, Test, Build | +| Static assets in bundle (png, md) | Build | +| HMR works in dev, disabled in prod | Run, Prod | + +### server-only + +Server-only app (no client entry point). + +| What is covered | Phase | +|----------------|-------| +| No client bundle (client skipped) | All | +| No client tests (test client skipped) | Test | +| Server entry loads (`server/main.js loaded`) | Run | + +--- + +## Skeletons + +Tested via `skeleton.test.js` using `meteor create --<skeleton>`. Each skeleton verifies: app creation, dev run, production run, test once, build, and reset. + +| Skeleton | Port | Language | Extra coverage | +|----------|------|----------|----------------| +| angular | 3213 | TypeScript | | +| apollo | 3201 | JSX | | +| babel | 3212 | JSX | | +| bare | 3219 | JS | No title/style checks, no client tests, skip build cache check | +| blaze | 3202 | JS | | +| chakra-ui | 3203 | JSX | No body style checks (custom UI library) | +| coffeescript | 3211 | CoffeeScript | | +| full | 3204 | JS | `imports/api/` test structure | +| react | 3205 | JSX | Custom body styles (Inter font, padding) | +| solid | 3206 | JS | | +| svelte | 3207 | JS | | +| tailwind | 3208 | TypeScript | Tailwind `bg-gray-100` styles (dev + prod color formats) | +| typescript | 3209 | TypeScript | CI: removes TsCheckerRspackPlugin | +| vue | 3210 | JS | | + +--- + +## NPM Package Compatibility + +Several apps import specific npm packages to verify that Meteor + Rspack handles different module formats and edge cases without errors. The app boots successfully only if these imports resolve correctly. + +### react-router (`apps/react-router/server/main.js`) + +| Package | Reason | +|---------|--------| +| `s3mini` | ESM-only package (no CJS fallback) | +| `@modelcontextprotocol/sdk/client/streamableHttp.js` | ESM subpath export (deep path into ESM package) | +| `bcrypt` | Native Node.js bindings (compiled C++ addon) | +| `puppeteer` | Large ESM-compatible package with complex dependency tree (`server/browser-tests/browser.app-test.js`) | + +### monorepo (`apps/monorepo/app/`) + +| Package | File | Reason | +|---------|------|--------| +| `pino` + `pino-pretty` | `server/main.js` | ESM-first logger; `pino-pretty` uses `thread-stream` which has worker file resolution issues — needs `Meteor.compileWithMeteor(["thread-stream"])` in rspack config | +| `grubba-rpc` | `server/main.js` | Untranspiled npm dependency — needs `Meteor.compileWithRspack(["grubba-rpc"])` to compile it through rspack | +| `node:buffer` | `imports/api/links.js` | Node.js built-in via `node:` protocol in shared client/server code — must be ignored on client without errors | +| `@react-email/components` | `imports/emails/TestEmail.jsx` | JSX-heavy ESM package with many subpath exports | + +### react (`apps/react/plugins/demo-unplugin.js`) + +| Package | Reason | +|---------|--------| +| `unplugin` | Unplugin transform hook integration — validates rspack cache tracks plugin dependency files (#14031) | + +### babel (`apps/babel/server/apollo.js`) + +| Package | Reason | +|---------|--------| +| `@apollo/server` | ESM-first GraphQL server | +| `@apollo/server/express4` | ESM subpath export (middleware from deep path) | +| `graphql` | Peer dependency, dual CJS/ESM package | + +### typescript (`apps/typescript/rspack.config.ts`, `apps/typescript/swc.config.ts`) + +| Package | Reason | +|---------|--------| +| `node:module` (`createRequire`) | Node.js built-in in a `.ts` config file — tests CJS interop via `createRequire(import.meta.url)` in an ESM context | +| `@swc/core` | Type-only import (`import type { Config }`) — provides typings for `swc.config.ts`, stripped at compile time | + +--- + +## Feature Coverage Matrix + +Where each feature is tested across apps and skeletons. + +| Feature | Apps | Skeletons | +|---------|------|-----------| +| HMR (dev) | react, react-router, babel, coffeescript, vue, solid, svelte, monorepo, typescript | | +| HMR disabled (prod) | all apps with HMR | | +| HMR incompatible | blaze, full-blaze | | +| Custom rspack config | react (.cjs), react-router, babel (.mjs), monorepo (.cjs), typescript (.ts) | | +| Custom SWC config (.ts) | typescript | | +| Config override file | react-router, monorepo | | +| Custom build dir | react, typescript | | +| Custom asset/chunk context dirs | typescript | | +| Custom env vars | react (METEOR_LOCAL_DIR), react-router (METEOR_PACKAGE_DIRS) | | +| Static asset bundling | react-router, monorepo | | +| Less styles | react-router | | +| SCSS styles | typescript | | +| Tailwind CSS | vue | tailwind | +| Image asset loading | react | | +| 404 routing | react-router | | +| Meta tags | react-router | | +| Babel compiler plugin | react-router | | +| TypeScript type checking | typescript | | +| Meteor.disablePlugins | react | | +| Unplugin transform with cache (#14031) | react | | +| Custom package dirs | react-router | | +| CoffeeScript compilation | coffeescript | coffeescript | +| Server-only (no client) | server-only | | +| Monorepo layout | monorepo | | +| Full-app test mode | react-router | | +| Module rules override | babel | | +| Custom NODE_ENV compilation | babel | | +| Portable build (no isDev/isProd defines) | typescript | | +| `Meteor.extendSwcConfig` (path aliases) | typescript | | +| `meteor reset` cleanup | all apps | all skeletons | +| Skeleton creation | | all 14 skeletons | +| Body style assertions | | react, tailwind (custom); most others (default) | +| Custom .gitignore entries | react | | +| ESM-only packages | react-router, monorepo, babel | | +| ESM subpath exports | react-router, babel | | +| Native bindings (C++ addon) | react-router | | +| `node:` protocol imports | monorepo, typescript | | +| Untranspiled npm deps (`compileWithRspack`) | monorepo | | +| Worker resolution (`compileWithMeteor`) | monorepo | | diff --git a/docs/source/api/check.md b/docs/source/api/check.md index 107c3c865d..208b6c5747 100644 --- a/docs/source/api/check.md +++ b/docs/source/api/check.md @@ -87,6 +87,10 @@ Matches a primitive of the given type. Matches a signed 32-bit integer. Doesn't match `Infinity`, `-Infinity`, or `NaN`. {% enddtdd %} +{% dtdd name:"<code>Match.NonEmptyString</code>" %} +Matches a non-empty string. +{% enddtdd %} + {% dtdd name:"<code>[<em>pattern</em>]</code>" %} A one-element array matches an array of elements, each of which match *pattern*. For example, `[Number]` matches a (possibly empty) array of numbers; @@ -160,12 +164,13 @@ from the call to `check` or `Match.test`. Examples: {% codeblock lang:js %} check(buffer, Match.Where(EJSON.isBinary)); -const NonEmptyString = Match.Where((x) => { - check(x, String); - return x.length > 0; +// Example: creating a custom pattern for positive numbers +const PositiveNumber = Match.Where((x) => { + check(x, Number); + return x > 0; }); -check(arg, NonEmptyString); +check(arg, PositiveNumber); {% endcodeblock %} {% enddtdd %} </dl> diff --git a/docs/source/commandline.md b/docs/source/commandline.md index e1494f5a7c..34f7595ae6 100644 --- a/docs/source/commandline.md +++ b/docs/source/commandline.md @@ -36,6 +36,9 @@ same as `meteor run`. To pass additional options to Node.js use the `SERVER_NODE_OPTIONS` environment variable. E.g. for Windows PowerShell: `$env:SERVER_NODE_OPTIONS = '--inspect' | meteor run`. Or for Linux: `SERVER_NODE_OPTIONS=--inspect-brk meteor run`. +Quoted values are supported, so you can pass options that contain spaces or special characters: +`SERVER_NODE_OPTIONS='--test-name-pattern="my test"' meteor run`. + To specify a port to listen on (instead of the default 3000), use `--port [PORT]`. (The development server also uses port `N+1` for the default MongoDB instance) diff --git a/guide/source/build-tool.md b/guide/source/build-tool.md index 46820815bd..1bb7d892f3 100644 --- a/guide/source/build-tool.md +++ b/guide/source/build-tool.md @@ -4,7 +4,7 @@ description: How to use Meteor's build system to compile your app. discourseTopicId: 19669 --- -The Meteor build system is the actual command line tool that you get when you install Meteor. You run it by typing the `meteor` command in your terminal, possibly followed by a set of arguments. Read the [docs about the command line tool](https://docs.meteor.com/commandline.html) or type `meteor help` in your terminal to learn about all of the commands. +The Meteor build system is the actual command line tool that you get when you install Meteor. You run it by typing the `meteor` command in your terminal, possibly followed by a set of arguments. Read the [docs about the command line tool](https://docs.meteor.com/cli/) or type `meteor help` in your terminal to learn about all of the commands. <h2 id="what-it-does">What does it do?</h2> @@ -16,7 +16,7 @@ After executing the `meteor` command to start the build tool you should leave it <h3 id="compiles-with-build-plugins">Compiles files with build plugins</h3> -The main function of the Meteor build tool is to run "build plugins". These plugins define different parts of your app build process. Meteor puts heavy emphasis on reducing or removing build configuration files, so you won't see any large build process config files like you would in Gulp or Webpack. The Meteor build process is configured almost entirely through adding and removing packages to your app and putting files in specially named directories. For example, to get all of the newest stable ES2015 JavaScript features in your app, you add the [`ecmascript` package](http://docs.meteor.com/#/full/ecmascript). This package provides support for ES2015 modules, which gives you even more fine grained control over file load order using ES2015 `import` and `export`. As new Meteor releases add new features to this package you get them for free. +The main function of the Meteor build tool is to run "build plugins". These plugins define different parts of your app build process. Meteor puts heavy emphasis on reducing or removing build configuration files, so you won't see any large build process config files like you would in Gulp or Webpack. The Meteor build process is configured almost entirely through adding and removing packages to your app and putting files in specially named directories. For example, to get all of the newest stable ES2015 JavaScript features in your app, you add the [`ecmascript` package](https://docs.meteor.com/packages/ecmascript.html). This package provides support for ES2015 modules, which gives you even more fine grained control over file load order using ES2015 `import` and `export`. As new Meteor releases add new features to this package you get them for free. <h4 id="controlling-build-files">Controlling which files to build</h4> @@ -224,7 +224,7 @@ For more examples and details on importing styles and using `@imports` with pack <h3 id="sass">Sass</h3> -The best Sass build plugin for Meteor is [`fourseven:scss`](https://atmospherejs.com/fourseven/scss). +The best Sass build plugin for Meteor is [`leonardoventurini:scss`](https://atmospherejs.com/leonardoventurini/scss). An alternative to the previous recommended [`fourseven:scss`](https://atmospherejs.com/fourseven/scss) package. <h3 id="less">Less</h3> diff --git a/guide/source/cordova.md b/guide/source/cordova.md index 0cc34b552b..0d7b1966a8 100644 --- a/guide/source/cordova.md +++ b/guide/source/cordova.md @@ -695,3 +695,9 @@ From this point on, the process for submitting the app to the Play Store is the Because Crosswalk bundles native code for Chromium, you will end up with APKs for both ARM and x86. You can find the generated APKs in the `<build-output-directory>/android/project/build/outputs/apk` directory. You will have to sign and `zipalign` both APKs. You will also have to submit both to the Play Store, see [submitting multiple APKs](http://developer.android.com/google/play/publishing/multiple-apks.html) for more information. + +<h2>Other tips</h2> + +The back gesture is disabled by default on iOS, but it can be enabled at runtime like this: + +```window.WkWebView.allowsBackForwardNavigationGestures(true);``` diff --git a/guide/source/deployment.md b/guide/source/deployment.md index fa9d8f08de..7c095350ee 100644 --- a/guide/source/deployment.md +++ b/guide/source/deployment.md @@ -196,6 +196,7 @@ MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://my-app.com PORT=3000 n * `ROOT_URL` is the base URL for your Meteor project * `PORT` is the port at which the application is running * `MONGO_URL` is a [Mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/) supplied by the MongoDB provider. +* `METEOR_SETTINGS` is a JSON object containing your application settings (can also be set via --settings flag). **Warning:** Any settings under the `public` key will be sent to the client - never put secrets there. Unless you have a specific need to roll your own hosting environment, the other options here are definitely easier, and probably make for a better setup than doing everything from scratch. Operating a Meteor app in a way that it works correctly for everyone can be complex, and [Galaxy](#galaxy) handles a lot of the specifics like routing clients to the right containers and handling coordinated version updates for you. diff --git a/meteor b/meteor index 517e4bd0fe..64eda442e4 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.18.0.3 +BUNDLE_VERSION=22.22.1.0 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js b/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js index aedf49c8c0..432d64b0fb 100644 --- a/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js +++ b/npm-packages/eslint-plugin-meteor/scripts/dev-bundle-tool-package.js @@ -10,7 +10,7 @@ var packageJson = { dependencies: { // Explicit dependency because we are replacing it with a bundled version // and we want to make sure there are no dependencies on a higher version - npm: "10.9.3", + npm: "10.9.4", pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8", "node-gyp": "9.4.0", "@mapbox/node-pre-gyp": "1.0.11", diff --git a/npm-packages/meteor-installer/config.js b/npm-packages/meteor-installer/config.js index 4f96ac3ad4..a771459f81 100644 --- a/npm-packages/meteor-installer/config.js +++ b/npm-packages/meteor-installer/config.js @@ -1,7 +1,7 @@ const os = require('os'); const path = require('path'); -const METEOR_LATEST_VERSION = '3.3.2'; +const METEOR_LATEST_VERSION = '3.4'; const sudoUser = process.env.SUDO_USER || ''; function isRoot() { return process.getuid && process.getuid() === 0; diff --git a/npm-packages/meteor-installer/package-lock.json b/npm-packages/meteor-installer/package-lock.json index 95184ff90f..8b670686b6 100644 --- a/npm-packages/meteor-installer/package-lock.json +++ b/npm-packages/meteor-installer/package-lock.json @@ -1,12 +1,12 @@ { "name": "meteor", - "version": "3.3.2", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meteor", - "version": "3.3.2", + "version": "3.4.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/npm-packages/meteor-installer/package.json b/npm-packages/meteor-installer/package.json index e9fc928561..61ebd3389c 100644 --- a/npm-packages/meteor-installer/package.json +++ b/npm-packages/meteor-installer/package.json @@ -1,6 +1,6 @@ { "name": "meteor", - "version": "3.3.2", + "version": "3.4.0", "description": "Install Meteor", "main": "install.js", "scripts": { diff --git a/npm-packages/meteor-node-stubs/package-lock.json b/npm-packages/meteor-node-stubs/package-lock.json index 960afee14a..883dbf25ca 100644 --- a/npm-packages/meteor-node-stubs/package-lock.json +++ b/npm-packages/meteor-node-stubs/package-lock.json @@ -1,12 +1,12 @@ { "name": "meteor-node-stubs", - "version": "1.2.23", + "version": "1.2.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meteor-node-stubs", - "version": "1.2.23", + "version": "1.2.26", "bundleDependencies": [ "@meteorjs/crypto-browserify", "assert", @@ -171,9 +171,9 @@ } }, "node_modules/@meteorjs/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -254,9 +254,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -319,9 +319,9 @@ "license": "MIT" }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "inBundle": true, "license": "MIT" }, @@ -654,9 +654,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -1184,9 +1184,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -1459,9 +1459,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -1473,9 +1473,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/npm-packages/meteor-node-stubs/package.json b/npm-packages/meteor-node-stubs/package.json index 3e2867429e..8dbeb95104 100644 --- a/npm-packages/meteor-node-stubs/package.json +++ b/npm-packages/meteor-node-stubs/package.json @@ -2,7 +2,7 @@ "name": "meteor-node-stubs", "author": "Ben Newman <ben@meteor.com>", "description": "Stub implementations of Node built-in modules, a la Browserify", - "version": "1.2.24", + "version": "1.2.26", "main": "index.js", "license": "MIT", "homepage": "https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-node-stubs/README.md", diff --git a/npm-packages/meteor-rspack/README.md b/npm-packages/meteor-rspack/README.md new file mode 100644 index 0000000000..e5ea9413a4 --- /dev/null +++ b/npm-packages/meteor-rspack/README.md @@ -0,0 +1,141 @@ +# @meteorjs/rspack + +The default [Rspack](https://rspack.dev) configuration for Meteor applications. This package provides everything you need to bundle your Meteor app with Rspack out of the box: client and server builds, SWC transpilation, React/Blaze/Angular support, hot module replacement, asset management, and all the Meteor-specific wiring so you don't have to. + +When Meteor runs with the Rspack bundler enabled, this package is what generates the underlying Rspack configuration. It detects your project setup (TypeScript, React, Blaze, Angular), sets up the right loaders and plugins, defines `Meteor.isClient`/`Meteor.isServer` and friends, configures caching, and exposes a set of helpers you can use in your own `rspack.config.js` to customize the build without breaking Meteor integration. + +## What it provides + +- **Dual client/server builds** with the correct targets, externals, and output paths +- **SWC-based transpilation** for JS/TS/JSX/TSX with automatic framework detection +- **React Fast Refresh** in development when React is enabled +- **Blaze template handling** via ignore-loader when Blaze is enabled +- **Persistent filesystem caching** for fast rebuilds +- **Asset externals and HTML generation** through custom Rspack plugins +- **A `defineConfig` helper** that accepts a factory function receiving Meteor environment flags and build utilities +- **Customizable config** via `rspack.config.js` in your project root, with safe merging that warns if you try to override reserved settings + +## Installation + +[Rspack integration](https://docs.meteor.com/about/modern-build-stack/rspack-bundler-integration.html) is automatically managed by the rspack Atmosphere package. + +```bash +meteor add rspack +``` + +By doing this, your Meteor app will automatically serve `@meteorjs/rspack` and the required `@rspack/cli`, `@rspack/core`, among others. + +## Usage + +In your project's `rspack.config.js`, use the `defineConfig` helper to customize the build. The factory function receives a `env` object with Meteor environment flags and helper utilities: + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig((env, argv) => { + // env.isClient, env.isServer, env.isDevelopment, env.isProduction + // env.isReactEnabled, env.isBlazeEnabled, etc. + + return { + // Your custom Rspack configuration here. + // It gets safely merged with the Meteor defaults. + }; +}); +``` + +More information is available in the official docs: [Rspack Bundler Integration](https://docs.meteor.com/about/modern-build-stack/rspack-bundler-integration.html#custom-rspack-config-js). + +## Development + +### Install dependencies + +```bash +npm install +``` + +### Version bumping + +Use `npm run bump` to update the version in `package.json` before publishing. + +```bash +npm run bump -- <major|minor|patch> [--beta] +``` + +**Standard bumps** increment the version and remove any prerelease suffix: + +```bash +npm run bump -- patch # 1.0.1 -> 1.0.2 +npm run bump -- minor # 1.0.1 -> 1.1.0 +npm run bump -- major # 1.0.1 -> 2.0.0 +``` + +**Beta bumps** append or increment a `-beta.N` prerelease suffix: + +```bash +npm run bump -- patch --beta # 1.0.1 -> 1.0.2-beta.0 +npm run bump -- patch --beta # 1.0.2-beta.0 -> 1.0.2-beta.1 +npm run bump -- patch --beta # 1.0.2-beta.1 -> 1.0.2-beta.2 +``` + +If you change the bump level while on a beta, the base version updates and the beta counter resets: + +```bash +npm run bump -- minor --beta # 1.0.2-beta.2 -> 1.1.0-beta.0 +npm run bump -- major --beta # 1.1.0-beta.0 -> 2.0.0-beta.0 +``` + +### Publishing a beta release + +After bumping to a beta version, publish to the `beta` dist-tag: + +```bash +npm run bump -- patch --beta +npm run publish:beta +``` + +Users can then install the beta with: + +```bash +npm install @meteorjs/rspack@beta +``` + +You can pass extra flags to `npm publish` through the script: + +```bash +npm run publish:beta -- --dry-run +``` + +### Publishing an official release + +After bumping to a stable version, publish with the default `latest` tag: + +```bash +npm run bump -- patch +npm publish +``` + +### Typical workflows + +**Beta iteration**: ship multiple beta builds for the same upcoming patch: + +```bash +npm run bump -- patch --beta # 1.0.1 -> 1.0.2-beta.0 +npm run publish:beta +# ... fix issues ... +npm run bump -- patch --beta # 1.0.2-beta.0 -> 1.0.2-beta.1 +npm run publish:beta +``` + +**Promote beta to stable**: once the beta is ready, bump to the stable version and publish: + +```bash +npm run bump -- patch # 1.0.2-beta.1 -> 1.0.3 +npm publish +``` + +**Direct stable release**: skip the beta phase entirely: + +```bash +npm run bump -- minor # 1.0.1 -> 1.1.0 +npm publish +``` diff --git a/npm-packages/meteor-rspack/index.d.ts b/npm-packages/meteor-rspack/index.d.ts new file mode 100644 index 0000000000..20a17a1e7d --- /dev/null +++ b/npm-packages/meteor-rspack/index.d.ts @@ -0,0 +1,116 @@ +/** + * Extend Rspack’s Configuration with Meteor-specific options. + */ +import { + defineConfig as _rspackDefineConfig, + Configuration as _RspackConfig, +} from '@rspack/cli'; +import { HtmlRspackPluginOptions, RuleSetConditions, SwcLoaderOptions } from '@rspack/core'; + +export interface MeteorRspackConfig extends _RspackConfig { + meteor?: { + packageNamespace?: string; + }; +} + +type MeteorEnv = Record<string, any> & { + isDevelopment: boolean; + isProduction: boolean; + isClient: boolean; + isServer: boolean; + isTest: boolean; + isDebug: boolean; + isRun: boolean; + isBuild: boolean; + isReactEnabled: boolean; + isBlazeEnabled: boolean; + isBlazeHotEnabled: boolean; + /** + * A function that creates an instance of HtmlRspackPlugin with default options. + * @param options - Optional configuration options that will be merged with defaults + * @returns An instance of HtmlRspackPlugin + */ + HtmlRspackPlugin: (options?: HtmlRspackPluginOptions) => HtmlRspackPlugin; + /** + * Wrap externals for Meteor runtime. + * @param deps - Package names or module IDs + * @returns A config object with externals configuration + */ + compileWithMeteor: (deps: RuleSetConditions) => Record<string, object>; + /** + * Add SWC transpilation rules limited to specific deps (monorepo-friendly). + * @param deps - Package names to include in SWC loader + * @param options - Optional configuration options + * @returns A config object with module rules configuration + */ + compileWithRspack: (deps: RuleSetConditions, options?: SwcLoaderOptions) => Record<string, object>; + /** + * Enable or disable Rspack cache config. + * @param enabled - Whether to enable caching + * @param cacheConfig - Optional cache configuration + * @returns A config object with cache configuration + */ + setCache: (enabled: boolean | 'memory') => Record<string, object>; + /** + * Enable Rspack split vendor chunk. + * @returns A config object with optimization configuration + */ + splitVendorChunk: () => Record<string, object>; + /** + * Extend the SWC loader config by smart-merging custom options on top of + * Meteor's defaults. Only the properties you specify are overridden; + * everything else is preserved. + * @param swcConfig - SWC loader options to merge with defaults + * @returns A config object with SWC loader config + */ + extendSwcConfig: (swcConfig: SwcLoaderOptions) => Record<string, object>; + /** + * Replace the SWC loader config entirely, discarding Meteor's defaults. + * Use this when you need full control over SWC options and don't want any + * automatic merging with Meteor's built-in configuration. + * @param swcConfig - Complete SWC loader options (replaces defaults) + * @returns A config object with SWC loader config + */ + replaceSwcConfig: (swcConfig: SwcLoaderOptions) => Record<string, object>; + /** + * Extend Rspack configs. + * @returns A config object with merged configs + */ + extendConfig: (...configs: Record<string, object>[]) => Record<string, object>; + + /** + * Remove plugins from a Rspack config by name, RegExp, predicate, or array of them. + * @param matchers - String, RegExp, function, or array of them to match plugin names + * @returns The modified config object + */ + disablePlugins: ( + matchers: string | RegExp | ((plugin: any, index: number) => boolean) | Array<string | RegExp | ((plugin: any, index: number) => boolean)> + ) => Record<string, any>; + /** + * Omit `Meteor.isDevelopment` and `Meteor.isProduction` from the DefinePlugin so + * the bundle is not tied to a specific Meteor environment (portable / isomorphic builds). + * @returns A config fragment with `meteor.enablePortableBuild: true` + */ + enablePortableBuild: () => Record<string, any>; +} + +export type ConfigFactory = ( + env: MeteorEnv, + argv: Record<string, any> +) => MeteorRspackConfig; + +export function defineConfig( + factory: ConfigFactory +): ReturnType<typeof _rspackDefineConfig>; + +/** + * A plugin that composes the original HtmlRspackPlugin from @rspack/core + * and RspackMeteorHtmlPlugin, in that order. + */ +export class HtmlRspackPlugin { + constructor(options?: HtmlRspackPluginOptions); + apply(compiler: any): void; +} + +// Re-export HtmlRspackPluginOptions from @rspack/cli +export { HtmlRspackPluginOptions }; diff --git a/npm-packages/meteor-rspack/index.js b/npm-packages/meteor-rspack/index.js new file mode 100644 index 0000000000..696487151f --- /dev/null +++ b/npm-packages/meteor-rspack/index.js @@ -0,0 +1,28 @@ +const { defineConfig: rspackDefineConfig } = require('@rspack/cli'); +const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js'); + +/** + * @typedef {import('rspack').Configuration & { + * meteor?: { packageNamespace?: string } + * }} MeteorRspackConfig + */ + +/** + * @typedef {(env: Record<string, any>, argv: Record<string, any>) => MeteorRspackConfig} ConfigFactory + */ + +/** + * Wrap rspack.defineConfig but only accept a factory function. + * @param {ConfigFactory} factory + * @returns {ReturnType<typeof rspackDefineConfig>} + */ +function defineConfig(factory) { + return rspackDefineConfig(factory); +} + +// Export our helper plus passthrough as default export +module.exports = defineConfig; + +// Export the HtmlRspackPlugin and defineConfig as named exports +module.exports.defineConfig = defineConfig; +module.exports.HtmlRspackPlugin = HtmlRspackPlugin; diff --git a/npm-packages/meteor-rspack/lib/ignore.js b/npm-packages/meteor-rspack/lib/ignore.js new file mode 100644 index 0000000000..7abc3ba959 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/ignore.js @@ -0,0 +1,139 @@ +var fs = require('fs'); +var path = require('path'); + +/** + * Reads the .meteorignore file from the given project directory and returns + * the parsed entries. Empty lines and comment lines (starting with #) are filtered out. + * + * @param {string} projectDir - The project directory path + * @returns {string[]} - Array of ignore patterns + */ +const getMeteorIgnoreEntries = function (projectDir) { + const meteorIgnorePath = path.join(projectDir, '.meteorignore'); + + // Check if .meteorignore file exists + try { + const fileContent = fs.readFileSync(meteorIgnorePath, 'utf8'); + + // Process each line in the file + const entries = fileContent.split(/\r?\n/) + .map(line => line.trim()) + .filter(line => line !== '' && !line.startsWith('#')); + + return entries; + } catch (e) { + // If the file doesn't exist or can't be read, return empty array + return []; + } +}; + +/** + * Creates a glob config array for ignoring specified patterns. + * Transforms .gitignore-style entries into chokidar-compatible glob patterns. + * @param {string[]} entries - Array of .gitignore-style patterns + * @returns {string[]} - Array of glob patterns for chokidar + */ +function createIgnoreGlobConfig(entries = []) { + if (!Array.isArray(entries)) { + throw new Error('Entries must be an array'); + } + + const globPatterns = []; + + entries.forEach(entry => { + // Skip empty entries + if (!entry.trim()) { + return; + } + + // Handle comments + if (entry.startsWith('#')) { + return; + } + + // Check if it's a negation pattern + const isNegation = entry.startsWith('!'); + let pattern = isNegation ? entry.substring(1).trim() : entry.trim(); + + // Remove leading ./ or / if present + pattern = pattern.replace(/^(\.\/|\/)/g, ''); + + // If it ends with /, it's a directory pattern, add ** to match all contents + if (pattern.endsWith('/')) { + pattern = pattern.slice(0, -1) + '/**'; + } + + // If it doesn't include a /, it could match anywhere in the path + if (!pattern.includes('/')) { + pattern = '**/' + pattern; + } else if (!pattern.startsWith('**/') && !pattern.startsWith('/')) { + // If it has a / but doesn't start with **/, add **/ to match anywhere + pattern = '**/' + pattern; + } + + // Add the negation back if it was present + if (isNegation) { + pattern = '!' + pattern; + } + + globPatterns.push(pattern); + }); + + return globPatterns; +} + +/** + * Creates a regex pattern to match the specified glob patterns. + * Converts glob patterns with * and ** into regex equivalents. + * + * @param {string[]} globPatterns - Array of glob patterns from createIgnoreGlobConfig + * @returns {RegExp} - Regex pattern to match the specified patterns + */ +function createIgnoreRegex(globPatterns) { + if (!Array.isArray(globPatterns) || globPatterns.length === 0) { + throw new Error('globPatterns must be a non-empty array'); + } + + // Process each glob pattern and convert to regex + const regexPatterns = globPatterns.map(pattern => { + // Skip negation patterns for the regex + if (pattern.startsWith('!')) { + return null; + } + + // Escape special regex characters, but not * and / + let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + + // Use a temporary placeholder for ** that won't be affected by the * replacement + // This is necessary because if we directly replace ** with .* and then replace * with [^/]* + const DOUBLE_ASTERISK_PLACEHOLDER = '__DOUBLE_ASTERISK__'; + regexPattern = regexPattern.replace(/\*\*/g, DOUBLE_ASTERISK_PLACEHOLDER); + + // Convert * to regex equivalent (any number of characters except /) + regexPattern = regexPattern.replace(/\*/g, '[^/]*'); + + // Convert the ** placeholder to its regex equivalent (any number of characters including /) + regexPattern = regexPattern.replace(new RegExp(DOUBLE_ASTERISK_PLACEHOLDER, 'g'), '.*'); + + // For absolute paths, we don't want to force the pattern to match from the beginning + // but we still want to ensure it matches to the end of the path segment + regexPattern = '(?:^|/)' + regexPattern; + + return regexPattern; + }).filter(pattern => pattern !== null); + + if (regexPatterns.length === 0) { + // If all patterns were negations, return a regex that matches nothing + return new RegExp('^$'); + } + + // Join all patterns with | to create a single regex + const combinedPattern = regexPatterns.join('|'); + return new RegExp(combinedPattern); +} + +module.exports = { + createIgnoreRegex, + getMeteorIgnoreEntries, + createIgnoreGlobConfig, +}; diff --git a/npm-packages/meteor-rspack/lib/localDependenciesHelpers.js b/npm-packages/meteor-rspack/lib/localDependenciesHelpers.js new file mode 100644 index 0000000000..b43199d87e --- /dev/null +++ b/npm-packages/meteor-rspack/lib/localDependenciesHelpers.js @@ -0,0 +1,184 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Extract local file dependencies from a config file by parsing require/import statements using AST + * @param {string} configFilePath - Path to the config file to parse + * @returns {string[]} - Array of absolute paths to local dependencies + */ +function extractLocalDependencies(configFilePath) { + if (!configFilePath || !fs.existsSync(configFilePath)) { + return []; + } + + try { + const swc = require('@swc/core'); + const content = fs.readFileSync(configFilePath, 'utf-8'); + const configDir = path.dirname(configFilePath); + const projectDir = process.cwd(); + const dependencies = []; + + // Parse the file into an AST + const ast = swc.parseSync(content, { + syntax: 'ecmascript', + dynamicImport: true, + target: 'es2020', + }); + + // Visit all nodes to find import/require statements + visitNode(ast, (node) => { + let modulePath = null; + + // Handle require() calls: require('./plugin') + if (node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.value === 'require' && + node.arguments.length > 0) { + const arg = node.arguments[0]; + if (arg.expression?.type === 'StringLiteral') { + modulePath = arg.expression.value; + } + } + + // Handle dynamic import() calls: import('./plugin') + if (node.type === 'CallExpression' && + node.callee.type === 'Import' && + node.arguments.length > 0) { + const arg = node.arguments[0]; + if (arg.expression?.type === 'StringLiteral') { + modulePath = arg.expression.value; + } + } + + // Handle static imports: import x from './plugin' + if (node.type === 'ImportDeclaration' && node.source?.type === 'StringLiteral') { + modulePath = node.source.value; + } + + // Handle export re-exports: export * from './plugin' + if (node.type === 'ExportAllDeclaration' && node.source?.type === 'StringLiteral') { + modulePath = node.source.value; + } + + // Handle named export re-exports: export { x } from './plugin' + if (node.type === 'ExportNamedDeclaration' && node.source?.type === 'StringLiteral') { + modulePath = node.source.value; + } + + // If we found a module path, try to resolve it + if (modulePath) { + const resolvedPath = resolveLocalModule(modulePath, configDir, projectDir); + if (resolvedPath) { + dependencies.push(resolvedPath); + } + } + }); + + // Remove duplicates + return [...new Set(dependencies)]; + } catch (error) { + console.warn('[Rspack Cache] Failed to parse config dependencies:', error.message); + return []; + } +} + +/** + * Recursively visit all nodes in an AST + * @param {Object} node - AST node + * @param {Function} callback - Function to call for each node + */ +function visitNode(node, callback) { + if (!node || typeof node !== 'object') { + return; + } + + callback(node); + + // Visit all properties of the node + for (const key in node) { + if (Object.prototype.hasOwnProperty.call(node, key)) { + const value = node[key]; + if (Array.isArray(value)) { + value.forEach(child => visitNode(child, callback)); + } else if (typeof value === 'object') { + visitNode(value, callback); + } + } + } +} + +/** + * Resolve a module path to an absolute path if it's a local file + * @param {string} modulePath - Module path from require/import statement + * @param {string} configDir - Directory containing the config file + * @param {string} projectDir - Project root directory + * @returns {string|null} - Resolved absolute path or null + */ +function resolveLocalModule(modulePath, configDir, projectDir) { + // Only process relative paths (starts with . or ..) + if (!modulePath.startsWith('.')) { + return null; + } + + try { + let resolvedPath = path.resolve(configDir, modulePath); + const extensions = ['.js', '.mjs', '.cjs', '.ts', '.json']; + + // If the path exists as-is, check if it's a directory needing index resolution + if (fs.existsSync(resolvedPath)) { + if (fs.statSync(resolvedPath).isDirectory()) { + let found = false; + for (const ext of extensions) { + const indexPath = path.join(resolvedPath, `index${ext}`); + if (fs.existsSync(indexPath)) { + resolvedPath = indexPath; + found = true; + break; + } + } + if (!found) { + return null; + } + } + } else { + // Try common extensions if file doesn't exist as-is + let found = false; + + for (const ext of extensions) { + const pathWithExt = resolvedPath + ext; + if (fs.existsSync(pathWithExt)) { + resolvedPath = pathWithExt; + found = true; + break; + } + } + + // If still not found, return null + if (!found) { + return null; + } + } + + // Verify file is within project (not node_modules) + const resolvedReal = fs.realpathSync(resolvedPath); + const projectReal = fs.realpathSync(projectDir); + + const isWithinProject = + resolvedReal === projectReal || + resolvedReal.startsWith(projectReal + path.sep); + const hasNodeModulesSegment = resolvedReal.split(path.sep).includes('node_modules'); + + if (isWithinProject && !hasNodeModulesSegment) { + return resolvedPath; + } + } catch (error) { + // Silently ignore resolution errors + } + + return null; +} + +module.exports = { + extractLocalDependencies, + resolveLocalModule, +}; diff --git a/npm-packages/meteor-rspack/lib/mergeRulesSplitOverlap.js b/npm-packages/meteor-rspack/lib/mergeRulesSplitOverlap.js new file mode 100644 index 0000000000..c7a73234d2 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/mergeRulesSplitOverlap.js @@ -0,0 +1,325 @@ +/** + * Utilities for merging webpack/rspack configurations with special handling for + * overlapping file extensions in module rules. + */ + +const { mergeWithCustomize } = require('webpack-merge'); +const isEqual = require('fast-deep-equal'); + +/** + * File extensions to check when determining rule overlaps. + */ +const EXT_CATALOG = [ + '.tsx', '.ts', '.mts', '.cts', + '.jsx', '.js', '.mjs', '.cjs', +]; + +/** + * Converts rule.test to predicate functions. + * @param {Object} rule - Rule object + * @returns {Function[]} Predicate functions + */ +function testsFrom(rule) { + const t = rule.test; + if (!t) return [() => true]; // no test means match all; you can tighten if you want + const arr = Array.isArray(t) ? t : [t]; + return arr.map(el => { + if (el instanceof RegExp) return (s) => el.test(s); + if (typeof el === 'function') return el; + if (typeof el === 'string') { + // Webpack allows string match; treat as substring + return (s) => s.includes(el); + } + return () => false; + }); +} + +/** + * Checks if rule matches a file extension. + * @param {Object} rule - Rule object + * @param {string} ext - File extension + * @returns {boolean} True if matches + */ +function ruleMatchesExt(rule, ext) { + // simulate a filename to test against + const filename = `x${ext}`; + const preds = testsFrom(rule); + return preds.some(fn => { + try { return !!fn(filename); } catch { return false; } + }); +} + +/** + * Creates regex for matching file extensions. + * @param {string[]} exts - File extensions + * @returns {RegExp} Regex like /\.(js|jsx)$/ + */ +function regexFromExts(exts) { + const body = exts.map(e => e.replace(/^\./, '')).join('|'); + return new RegExp(`\\.(${body})$`, 'i'); +} + +/** + * Clones rule with new test property. + * @param {Object} rule - Rule to clone + * @param {RegExp|Function|string} newTest - New test value + * @returns {Object} Cloned rule + */ +function cloneWithTest(rule, newTest) { + return { ...rule, test: newTest }; +} + +/** + * Merges rules with special handling for overlapping extensions. + * - Replaces overlapping parts with B rules + * - Preserves non-overlapping parts from A rules + * + * @param {Array} aRules - Base rules + * @param {Array} bRules - Rules to merge in + * @returns {Array} Merged rules + */ +function splitOverlapRulesMerge(aRules, bRules) { + const result = [...aRules]; + + for (const bRule of bRules) { + // Try to find an A rule that overlaps B by extensions + let replaced = false; + + for (let i = 0; i < result.length; i++) { + const aRule = result[i]; + + + const isMergeableRule = isEqual(aRule?.include || [], bRule?.include || []); + if (!isMergeableRule) continue; + + // Determine which extensions each rule matches (within our catalog) + const aExts = EXT_CATALOG.filter(ext => ruleMatchesExt(aRule, ext)); + const bExts = EXT_CATALOG.filter(ext => ruleMatchesExt(bRule, ext)); + + if (aExts.length === 0 || bExts.length === 0) { + continue; // nothing meaningful to compare in our catalog + } + + const overlap = aExts.filter(e => bExts.includes(e)); + if (overlap.length === 0) continue; + + // 1) Replace the overlapping A rule with B + result[i] = bRule; + + // 2) Add a "residual" A rule for the non-overlapping extensions + const residual = aExts.filter(e => !overlap.includes(e)); + if (residual.length > 0) { + const residualRule = cloneWithTest(aRule, regexFromExts(residual)); + result.splice(i, 0, residualRule); // keep residual before B, or after—your choice + i++; // skip over the newly inserted residual + } + + replaced = true; + break; + } + + // If we didn’t overlap with any A rule, just add B + if (!replaced) { + result.push(bRule); + } + } + + return result; +} + +/** + * Creates a customizer function for unique plugins. + * + * @param {string} key - The key to check for uniqueness + * @param {string[]} pluginNames - Array of plugin constructor names to make unique + * @param {Function} getter - Function to get the identifier from the plugin + * @returns {Function} Customizer function + */ +function unique(key, pluginNames = [], getter = item => item.constructor && item.constructor.name) { + return (a, b, k) => { + if (k !== key) return undefined; + + const aItems = Array.isArray(a) ? a : []; + const bItems = Array.isArray(b) ? b : []; + + // If not dealing with plugins, return undefined to use default merging + if (key !== 'plugins') return undefined; + + // Create a map to track plugins by their identifier + const uniquePlugins = new Map(); + + // Process all plugins from both arrays + [...aItems, ...bItems].forEach(plugin => { + const id = getter(plugin); + + // If this is a plugin we want to make unique and we can identify it + if (id && pluginNames.includes(id)) { + uniquePlugins.set(id, plugin); // Keep only the last instance + } + }); + + // Create the result array with all non-unique plugins from a + const result = aItems.filter(plugin => { + const id = getter(plugin); + return !id || !pluginNames.includes(id) || uniquePlugins.get(id) === plugin; + }); + + // Add unique plugins from b that weren't already in the result + bItems.forEach(plugin => { + const id = getter(plugin); + if (!id || !pluginNames.includes(id)) { + result.push(plugin); + } else if (uniquePlugins.get(id) === plugin) { + result.push(plugin); + } + }); + + return result; + }; +} + +/** + * Helper function to clean fields in an object based on omit paths. + * Supports nested path strings like 'output.filename'. + * + * @param {Object} obj - The object to clean + * @param {Object} options - Configuration options + * @param {string[]} [options.omitPaths] - Paths to omit from the object (e.g., 'output.filename') + * @param {Function} [options.warningFn] - Custom warning function that receives the path string + * @returns {Object} The cleaned object with specified paths removed + */ +function cleanOmittedPaths(obj, options = {}) { + if (!obj || typeof obj !== 'object') { + return obj; + } + + const { omitPaths = [], warningFn } = options; + + // If no omit paths, return the original object + if (!omitPaths.length) { + return obj; + } + + const result = { ...obj }; + + // Process each omit path + omitPaths.forEach(path => { + // Convert path to array of keys + const pathArray = Array.isArray(path) ? path : path.split('.'); + const pathString = Array.isArray(path) ? path.join('.') : path; + + // Start with the root object + let current = result; + let parent = null; + let lastKey = null; + + // Traverse the path to find the target property + for (let i = 0; i < pathArray.length - 1; i++) { + const key = pathArray[i]; + if (current && typeof current === 'object' && key in current) { + parent = current; + lastKey = key; + current = current[key]; + } else { + // Path doesn't exist in the object, nothing to remove + return; + } + } + + // Get the final key in the path + const finalKey = pathArray[pathArray.length - 1]; + + // Handle single-level paths (from root) + if (pathArray.length === 1) { + const rootKey = pathArray[0]; + if (rootKey in result) { + // Log warning + if (typeof warningFn === 'function') { + warningFn(pathString); + } + delete result[rootKey]; + } + return; + } + + // If we found the property for nested paths, remove it + if (parent && lastKey && finalKey) { + if (current && typeof current === 'object' && finalKey in current) { + // Log warning + if (typeof warningFn === 'function') { + warningFn(pathString); + } + delete current[finalKey]; + } + } + }); + + return result; +} + +/** + * Normalizes externals configuration to ensure consistent handling. + * @param {Object} config - The configuration object + * @returns {Object} - The normalized configuration + */ +function normalizeExternals(config) { + if (!config || !config.externals) return config; + + // Create a deep clone of the config to avoid modifying the original + const result = { ...config }; + + // If externals is not an array, convert it to an array + if (!Array.isArray(result.externals)) { + result.externals = [result.externals]; + } + + return result; +} + +/** + * Merges webpack/rspack configs with smart handling of overlapping rules. + * + * @param {...Object} configs - Configs to merge + * @returns {Object} Merged config + */ +function mergeSplitOverlap(...configs) { + // Normalize externals in all configs before merging + const normalizedConfigs = configs.map(normalizeExternals); + + return mergeWithCustomize({ + customizeArray(a, b, key) { + if (key === 'module.rules') { + const aRules = Array.isArray(a) ? a : []; + const bRules = Array.isArray(b) ? b : []; + return splitOverlapRulesMerge(aRules, bRules); + } + + // Ensure custom extensions first + if (key === 'resolve.extensions') { + const aRules = Array.isArray(a) ? a : []; + const bRules = Array.isArray(b) ? b : []; + const merged = [...bRules, ...aRules]; + return [...new Set(merged)]; + } + + // Handle plugins uniqueness + if (key === 'plugins') { + return unique( + 'plugins', + ['HtmlRspackPlugin', 'RsdoctorRspackPlugin'], + (plugin) => plugin.constructor && plugin.constructor.name + )(a, b, key); + } + + // fall through to default merging + return undefined; + } + })(...normalizedConfigs); +} + +module.exports = { + EXT_CATALOG, + unique, + cleanOmittedPaths, + mergeSplitOverlap +}; diff --git a/npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js b/npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js new file mode 100644 index 0000000000..3dbeaf04a5 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js @@ -0,0 +1,99 @@ +// meteorRspackConfigFactory.js + +const { mergeSplitOverlap } = require("./mergeRulesSplitOverlap.js"); + +const DEFAULT_PREFIX = "meteorRspackConfig"; +let counter = 0; + +/** + * Create a uniquely keyed Rspack config fragment. + * Example return: { meteorRspackConfig1: { ...customConfig } } + * + * @param {object} customConfig + * @param {{ key?: number|string, prefix?: string }} [opts] + * @returns {Record<string, object>} + */ +function prepareMeteorRspackConfig(customConfig, opts = {}) { + if (!customConfig || typeof customConfig !== "object") { + throw new TypeError("customConfig must be an object"); + } + const prefix = opts.prefix || DEFAULT_PREFIX; + + let name; + if (opts.key != null) { + const k = String(opts.key).trim(); + if (/^\d+$/.test(k)) name = `${prefix}${k}`; + else if (k.startsWith(prefix) && /^\d+$/.test(k.slice(prefix.length))) + name = k; + else + throw new Error(`opts.key must be a positive integer or "${prefix}<n>"`); + + const n = parseInt(name.slice(prefix.length), 10); + if (Number.isFinite(n) && n > counter) counter = n; + } else { + counter += 1; + name = `${prefix}${counter}`; + } + + return { [name]: customConfig }; +} + +/** + * Merge all `{prefix}<n>` fragments into `config` using `mergeSplitOverlap`, + * then remove those temporary keys. Mutates `config`. + * + * Position-aware merge: + * Walk the config in insertion order and fold: + * - for a fragment key: out = mergeSplitOverlap(out, fragment) + * - for a normal key: out = mergeSplitOverlap(out, { [key]: value }) + * + * Result: fragments behave like spreads at their exact position; + * later inline keys override earlier ones (including fragments). + * + * @param {object} config + * @param {{ prefix?: string }} [opts] + * @returns {object} same (mutated) config + */ +function mergeMeteorRspackFragments(config, opts = {}) { + if (!config || typeof config !== "object" || Array.isArray(config)) { + throw new TypeError("config must be a plain object"); + } + const prefix = opts.prefix || DEFAULT_PREFIX; + + let out = {}; + for (const key of Object.keys(config)) { + const val = config[key]; + + const isFragment = + typeof key === "string" && + key.startsWith(prefix) && + /^\d+$/.test(key.slice(prefix.length)); + + if (isFragment) { + if (!val || typeof val !== "object" || Array.isArray(val)) { + throw new Error(`Fragment "${key}" must be a plain object`); + } + out = mergeSplitOverlap(out, val); + } else { + out = mergeSplitOverlap(out, { [key]: val }); + } + } + + // keep object identity; fragments disappear because `out` doesn't include them + replaceObject(config, out); + return config; +} + +function replaceObject(target, source) { + for (const k of Object.keys(target)) { + if (!(k in source)) delete target[k]; + } + for (const k of Object.keys(source)) { + target[k] = source[k]; + } +} + +module.exports = { + prepareMeteorRspackConfig, + mergeMeteorRspackFragments, +}; diff --git a/npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js b/npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js new file mode 100644 index 0000000000..8cac1656c3 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js @@ -0,0 +1,121 @@ +const path = require('path'); +const fs = require('fs'); +const { cleanOmittedPaths } = require("./mergeRulesSplitOverlap.js"); +const { mergeMeteorRspackFragments } = require("./meteorRspackConfigFactory.js"); + +// Helper function to load and process config files +async function loadAndProcessConfig(configPath, configType, Meteor, argv, disableWarnings) { + try { + // Load the config file + let config; + if (path.extname(configPath) === '.mjs') { + // For ESM modules, we need to use dynamic import + const fileUrl = `file://${configPath}`; + const module = await import(fileUrl); + config = module.default || module; + } else { + // For CommonJS modules, we can use require + config = require(configPath)?.default || require(configPath); + } + + // Process the config + const rawConfig = typeof config === 'function' ? config(Meteor, argv) : config; + const resolvedConfig = await Promise.resolve(rawConfig); + const userConfig = resolvedConfig && '0' in resolvedConfig ? resolvedConfig[0] : resolvedConfig; + + // Define omitted paths and warning function + const omitPaths = [ + "name", + "target", + "entry", + "output.path", + "output.filename", + ...(Meteor.isServer ? ["optimization.splitChunks", "optimization.runtimeChunk"] : []), + ].filter(Boolean); + + const warningFn = path => { + if (disableWarnings) return; + console.warn( + `[${configType}] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`, + ); + }; + + // Clean omitted paths and merge Meteor Rspack fragments + let nextConfig = cleanOmittedPaths(userConfig, { + omitPaths, + warningFn, + }); + nextConfig = mergeMeteorRspackFragments(nextConfig); + + return nextConfig; + } catch (error) { + console.error(`Error loading ${configType} from ${configPath}:`, error); + if (configType === 'rspack.config.js') { + throw error; // Only rethrow for project config + } + return null; + } +} + +/** + * Loads both the user's Rspack configuration and its potential override. + * + * @param {string|undefined} projectConfigPath + * @param {object} Meteor + * @param {object} argv + * @returns {Promise<{ nextUserConfig: object|null, nextOverrideConfig: object|null }>} + */ +async function loadUserAndOverrideConfig(projectConfigPath, Meteor, argv) { + let nextUserConfig = null; + let nextOverrideConfig = null; + + const projectDir = process.cwd(); + const isMeteorPackageConfig = projectDir.includes("/packages/rspack"); + + if (projectConfigPath) { + const configDir = path.dirname(projectConfigPath); + const configFileName = path.basename(projectConfigPath); + const configExt = path.extname(configFileName); + const configNameWithoutExt = configFileName.replace(configExt, ''); + const configNameFull = `${configNameWithoutExt}.override${configExt}`; + const overrideConfigPath = path.join(configDir, configNameFull); + + if (fs.existsSync(overrideConfigPath)) { + nextOverrideConfig = await loadAndProcessConfig( + overrideConfigPath, + configNameFull, + Meteor, + argv, + Meteor.isAngularEnabled + ); + } + + if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) { + // Check if there's a .mjs or .cjs version of the config file + const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs'); + const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs'); + + let projectConfigPathToUse = projectConfigPath; + if (fs.existsSync(mjsConfigPath)) { + projectConfigPathToUse = mjsConfigPath; + } else if (fs.existsSync(cjsConfigPath)) { + projectConfigPathToUse = cjsConfigPath; + } + + nextUserConfig = await loadAndProcessConfig( + projectConfigPathToUse, + 'rspack.config.js', + Meteor, + argv, + Meteor.isAngularEnabled + ); + } + } + + return { nextUserConfig, nextOverrideConfig }; +} + +module.exports = { + loadAndProcessConfig, + loadUserAndOverrideConfig, +}; diff --git a/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js b/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js new file mode 100644 index 0000000000..2fbbac3027 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js @@ -0,0 +1,264 @@ +const path = require("path"); +const { prepareMeteorRspackConfig } = require("./meteorRspackConfigFactory"); +const { builtinModules } = require("module"); + +/** + * Wrap externals for Meteor runtime (marks deps as externals). + * Usage: compileWithMeteor(["sharp", "vimeo", "fs"]) + * + * @param {string[]} deps - package names or module IDs + * @returns {Record<string, object>} `{ meteorRspackConfigX: { externals: [...] } }` + */ +function compileWithMeteor(deps) { + const flat = deps.flat().filter(Boolean); + return prepareMeteorRspackConfig({ + externals: flat, + }); +} + +/** + * Add SWC transpilation rules limited to specific deps (monorepo-friendly). + * Usage: compileWithRspack(["@org/lib-a", "zod"]) + * + * Requires global `Meteor.swcConfigOptions` (as in your setup). + * + * @param {string[]} deps - package names to include in SWC loader + * @returns {Record<string, object>} `{ meteorRspackConfigX: { module: { rules: [...] } } }` + */ +function compileWithRspack(deps, { options = {} } = {}) { + const includeDirs = deps.flat().filter(Boolean) + .map(pkg => typeof pkg === 'string' && !pkg.includes('node_modules') + ? path.join(process.cwd(), 'node_modules', pkg) + : pkg + ); + + return prepareMeteorRspackConfig({ + module: { + rules: [ + { + test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i, + include: includeDirs, + loader: "builtin:swc-loader", + options, + }, + ], + }, + }); +} + +/** + * Enable or disable Rspack cache config + * Usage: setCache(false) + * + * @param {boolean} enabled + * @param {Record<string, object>} cacheConfig + * @returns {Record<string, object>} `{ meteorRspackConfigX: { cache: {} } }` + */ +function setCache( + enabled, + cacheConfig = { cache: true, experiments: { cache: true } } +) { + return prepareMeteorRspackConfig( + enabled + ? cacheConfig + : { + cache: false, // disable cache + experiments: { + cache: false, // disable persistent cache (experimental flag) + }, + } + ); +} + +/** + * Build an alias map that disables ALL Node core modules in a web build. + * - Includes both 'fs' and 'node:fs' keys + * - Optional extras let you block non-core modules too + */ +function makeWebNodeBuiltinsAlias(extras = []) { + // Node core list, normalized (strip `node:` prefix) + const core = new Set(builtinModules.map((m) => m.replace(/^node:/, ""))); + + // browser-safe allowlist (these we *don't* mark as false) + const allowlist = new Set([ + "process", + "util", + "events", + "path", + "stream", + "assert", + "assert/strict", + ]); + + const names = new Set(); + for (const m of core) { + // Add both 'fs' and 'node:fs' variants + names.add(m); + names.add(`node:${m}`); + } + for (const x of extras) names.add(x); + + // ❌ Everything except the allowlist gets mapped to false + const entries = [...names] + .filter((m) => !allowlist.has(m.replace(/^node:/, ""))) + .map((m) => [m, false]); + + return Object.fromEntries(entries); +} + +/** + * Enable Rspack split vendor chunk config + * Usage: splitVendorChunk() + * + * @returns {Record<string, object>} `{ meteorRspackConfigX: { optimization: { ... } } }` + */ +function splitVendorChunk() { + return prepareMeteorRspackConfig({ + optimization: { + splitChunks: { + chunks: "all", // split both sync and async imports + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: "vendor", + enforce: true, + priority: 10, + chunks: "all", + }, + }, + }, + }, + }); +} + +/** + * Extend SWC loader config by smart-merging custom options on top of Meteor's + * defaults (via `mergeSplitOverlap`). Only the properties you specify are + * overridden; everything else is preserved. + * + * Usage: Meteor.extendSwcConfig({ jsc: { parser: { decorators: true } } }) + * + * @param {object} swcConfig - SWC loader options to merge with defaults + * @returns {Record<string, object>} config fragment for spreading into rspack config + */ +function extendSwcConfig(swcConfig) { + return prepareMeteorRspackConfig({ + module: { + rules: [ + { + test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i, + exclude: /node_modules|\.meteor\/local/, + loader: 'builtin:swc-loader', + options: swcConfig, + }, + ], + }, + }); +} + +/** + * Replace the SWC loader config entirely, discarding Meteor's defaults. + * Use this when you need full control over SWC options and don't want any + * automatic merging with Meteor's built-in configuration. + * + * Usage: Meteor.replaceSwcConfig({ jsc: { parser: { syntax: 'typescript' }, target: 'es2020' } }) + * + * @param {object} swcConfig - Complete SWC loader options (replaces defaults) + * @returns {Record<string, object>} config fragment for spreading into rspack config + */ +function replaceSwcConfig(swcConfig) { + return prepareMeteorRspackConfig({ + module: { + rules: [ + { + test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i, + exclude: /node_modules|\.meteor\/local/, + loader: 'builtin:swc-loader', + options: swcConfig, + }, + ], + }, + }); +} + +/** + * Signal that `Meteor.isDevelopment` and `Meteor.isProduction` should be omitted + * from DefinePlugin, making the bundle portable across Meteor environments. + * Usage: return Meteor.enablePortableBuild() in your rspack.config.js + * + * @returns {Record<string, object>} config fragment with `meteor.enablePortableBuild: true` + */ +function enablePortableBuild() { + return prepareMeteorRspackConfig({ + "meteor.enablePortableBuild": true, + }); +} + +/** + * Remove plugins from a Rspack config by name, RegExp, predicate, or array of them. + * When using a function predicate, it receives both the plugin and its index in the plugins array. + * + * @param {object} config Rspack config object + * @param {string | RegExp | ((plugin: any, index: number) => boolean) | Array<string|RegExp|Function>} matchers + * @returns {object} The modified config object + */ +function disablePlugins(config, matchers) { + if (!config || typeof config !== "object") { + throw new TypeError("disablePlugins: `config` must be an object"); + } + + const plugins = Array.isArray(config.plugins) ? config.plugins : []; + const kept = []; + + const list = Array.isArray(matchers) ? matchers : [matchers]; + + const getPluginName = (p) => { + if (!p) return ""; + return ( + (p.constructor && typeof p.constructor.name === "string" && p.constructor.name) || + (typeof p.name === "string" && p.name) || + (typeof p.pluginName === "string" && p.pluginName) || + (typeof p.__pluginName === "string" && p.__pluginName) || + "" + ); + }; + + const predicates = list.map((m) => { + if (typeof m === "function") return m; + if (m instanceof RegExp) { + return (p) => m.test(getPluginName(p)); + } + if (typeof m === "string") { + return (p) => getPluginName(p) === m; + } + throw new TypeError( + "disablePlugins: matchers must be string, RegExp, function, or array of them" + ); + }); + + config.plugins = plugins.filter((p, index) => { + const matches = predicates.some(fn => fn(p, index)); + return !matches; + }); + + return config; +} + +function outputMeteorRspack(data) { + const jsonString = JSON.stringify(data); + const output = `[Meteor-Rspack]${jsonString}[/Meteor-Rspack]`; + console.log(output); +} + +module.exports = { + compileWithMeteor, + compileWithRspack, + setCache, + splitVendorChunk, + extendSwcConfig, + replaceSwcConfig, + makeWebNodeBuiltinsAlias, + disablePlugins, + outputMeteorRspack, + enablePortableBuild, +}; diff --git a/npm-packages/meteor-rspack/lib/swc.js b/npm-packages/meteor-rspack/lib/swc.js new file mode 100644 index 0000000000..e113ad1626 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/swc.js @@ -0,0 +1,97 @@ +const fs = require('fs'); +const vm = require('vm'); + +/** + * Reads and parses the SWC configuration file. + * @param {string} file - The name of the SWC configuration file (default: '.swcrc') + * @returns {Object|undefined} The parsed SWC configuration or undefined if an error occurs + */ +function getMeteorAppSwcrc(file = '.swcrc') { + try { + const filePath = `${process.cwd()}/${file}`; + if (file.endsWith('.js') || file.endsWith('.ts')) { + let content = fs.readFileSync(filePath, 'utf-8'); + + if (file.endsWith('.ts')) { + try { + const swc = require('@swc/core'); + const result = swc.transformSync(content, { + jsc: { + parser: { + syntax: 'typescript', + }, + target: 'es2015', + }, + }); + content = result.code; + } catch (swcError) { + content = content + .replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+['"][^'"]+['"];?/g, '') + .replace(/export\s+default\s+/, 'module.exports = ') + .replace(/export\s+/g, '') + .replace(/:\s*\w+(\[\])?(\s*=)/g, '$2') + .replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)') + .replace(/\):\s*\w+(\[\])?\s*\{/g, ') {') + .replace(/interface\s+\w+\s*\{[^}]*\}/g, '') + .replace(/type\s+\w+\s*=\s*[^;]+;/g, '') + .replace(/as\s+\w+(\[\])?/g, ''); + } + } + + if (content.includes('export default')) { + content = content.replace(/export\s+default\s+/, 'module.exports = '); + } + const script = new vm.Script(` + (function() { + const module = {}; + module.exports = {}; + (function(exports, module) { + ${content} + })(module.exports, module); + return module.exports; + })() + `); + const context = vm.createContext({ process }); + const result = script.runInContext(context); + // Handle CJS interop wrapper (e.g. { __esModule: true, default: config }) + return result && result.__esModule && result.default ? result.default : result; + } else { + // For .swcrc and other JSON files, parse as JSON + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } + } catch (e) { + return undefined; + } +} + +/** + * Checks for SWC configuration files and returns the configuration. + * If the configuration has a baseUrl property, it will be set to process.cwd(). + * @returns {Object|undefined} The SWC configuration or undefined if no configuration exists + */ +function getMeteorAppSwcConfig() { + const hasSwcRc = fs.existsSync(`${process.cwd()}/.swcrc`); + const hasSwcJs = !hasSwcRc && fs.existsSync(`${process.cwd()}/swc.config.js`); + const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${process.cwd()}/swc.config.ts`); + + if (!hasSwcRc && !hasSwcJs && !hasSwcTs) { + return undefined; + } + + const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc'; + const config = getMeteorAppSwcrc(swcFile); + + // Set baseUrl to process.cwd() if it exists + if (config?.jsc && config.jsc.baseUrl) { + config.jsc.baseUrl = process.cwd(); + } + + return config; +} + +module.exports = { + getMeteorAppSwcrc, + getMeteorAppSwcConfig +}; diff --git a/npm-packages/meteor-rspack/lib/test.js b/npm-packages/meteor-rspack/lib/test.js new file mode 100644 index 0000000000..1780d114bb --- /dev/null +++ b/npm-packages/meteor-rspack/lib/test.js @@ -0,0 +1,106 @@ +const fs = require('fs'); +const path = require('path'); +const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js"); + +// Normalize a path to always use forward slashes (POSIX style). +// Module identifiers in bundled JS must use '/' regardless of OS. +const toPosix = (p) => p.replace(/\\/g, '/'); + +/** + * Generates eager test files dynamically + * @param {Object} options - Options for generating the test file + * @param {boolean} options.isAppTest - Whether this is an app test + * @param {string} options.projectDir - The project directory + * @param {string} options.buildContext - The build context + * @param {string[]} options.ignoreEntries - Array of ignore patterns + * @param {string[]} options.meteorIgnoreEntries - Array of meteor ignore patterns + * @param {string} options.extraEntry - Extra entry to load + * @returns {string} The path to the generated file + */ +const generateEagerTestFile = ({ + isAppTest, + projectDir, + buildContext, + ignoreEntries: inIgnoreEntries = [], + meteorIgnoreEntries: inMeteorIgnoreEntries = [], + prefix: inPrefix = '', + extraEntry, + globalImportPath, +}) => { + const distDir = path.resolve(projectDir, ".meteor/local/test"); + if (!fs.existsSync(distDir)) { + fs.mkdirSync(distDir, { recursive: true }); + } + + // Combine all ignore entries + const ignoreEntries = [ + "**/node_modules/**", + "**/.meteor/**", + "**/public/**", + "**/private/**", + `**/${buildContext}/**`, + ...inIgnoreEntries, + ]; + + // Create regex from ignore entries + const excludeFoldersRegex = createIgnoreRegex( + createIgnoreGlobConfig(ignoreEntries) + ); + console.log("inMeteorIgnoreEntries", inMeteorIgnoreEntries); + // Create regex from meteor ignore entries + const excludeMeteorIgnoreRegex = inMeteorIgnoreEntries.length > 0 + ? createIgnoreRegex(createIgnoreGlobConfig(inMeteorIgnoreEntries)) + : null; + + const prefix = (inPrefix && `${inPrefix}-`) || ""; + const filename = isAppTest + ? `${prefix}eager-app-tests.mjs` + : `${prefix}eager-tests.mjs`; + const filePath = path.resolve(distDir, filename); + const regExp = isAppTest + ? "/\\.app-(?:test|spec)s?\\.[^.]+$/" + : "/\\.(?:test|spec)s?\\.[^.]+$/"; + + const content = `${ + globalImportPath ? `import '${toPosix(globalImportPath)}';\n\n` : "" + }${ + excludeMeteorIgnoreRegex + ? `const MeteorIgnoreRegex = ${excludeMeteorIgnoreRegex.toString()};` + : "" + } +{ + const ctx = import.meta.webpackContext('${toPosix(projectDir)}', { + recursive: true, + regExp: ${regExp}, + exclude: ${excludeFoldersRegex.toString()}, + mode: 'eager', + }); + ctx.keys().filter((k) => { + ${ + excludeMeteorIgnoreRegex + ? `// Only exclude based on *relative* path segments. + return !MeteorIgnoreRegex.test(k);` + : "return true;" + } + }).forEach(ctx); + ${ + extraEntry + ? `const extra = import.meta.webpackContext('${toPosix(path.dirname( + extraEntry + ))}', { + recursive: false, + regExp: ${new RegExp(`${path.basename(extraEntry)}$`).toString()}, + mode: 'eager', + }); + extra.keys().forEach(extra);` + : "" + } +}`; + + fs.writeFileSync(filePath, content); + return filePath; +}; + +module.exports = { + generateEagerTestFile, +}; diff --git a/npm-packages/meteor-rspack/package-lock.json b/npm-packages/meteor-rspack/package-lock.json new file mode 100644 index 0000000000..739b4cd71d --- /dev/null +++ b/npm-packages/meteor-rspack/package-lock.json @@ -0,0 +1,5733 @@ +{ + "name": "@meteorjs/rspack", + "version": "1.1.0-beta.31", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@meteorjs/rspack", + "version": "1.1.0-beta.31", + "license": "ISC", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "ignore-loader": "^0.1.2", + "node-polyfill-webpack-plugin": "^4.1.0", + "webpack-merge": "^6.0.1" + }, + "peerDependencies": { + "@rspack/cli": ">=1.3.0", + "@rspack/core": ">=1.3.0", + "@swc/core": ">=1.3.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", + "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", + "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", + "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT", + "peer": true + }, + "node_modules/@module-federation/error-codes": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.16.0.tgz", + "integrity": "sha512-TfmA45b8vvISniGudMg8jjIy1q3tLPon0QN/JdFp5f8AJ8/peICN5b+dkEQnWsAVg2fEusYhk9dO7z3nUeJM8A==", + "license": "MIT", + "peer": true + }, + "node_modules/@module-federation/runtime": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.16.0.tgz", + "integrity": "sha512-6o84WI8Qhc9O3HwPLx89kTvOSkyUOHQr73R/zr0I04sYhlMJgw5xTwXeGE7bQAmNgbJclzW9Kh7JTP7+3o3CHg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/error-codes": "0.16.0", + "@module-federation/runtime-core": "0.16.0", + "@module-federation/sdk": "0.16.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.16.0.tgz", + "integrity": "sha512-5SECQowG4hlUVBRk/y6bnYLfxbsl5NcMmqn043WPe7NDOhGQWbTuYibJ3Bk+ZBv5U4uYLEmXipBGDc1FKsHklQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/error-codes": "0.16.0", + "@module-federation/sdk": "0.16.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.16.0.tgz", + "integrity": "sha512-OzmXNluXBQ2E6znzX4m9CJt1MFHVGmbN8c8MSKcYIDcLzLSKBQAiaz9ZUMhkyWx2YrPgD134glyPEqJrc+fY8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.16.0", + "@module-federation/webpack-bundler-runtime": "0.16.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.16.0.tgz", + "integrity": "sha512-UXJW1WWuDoDmScX0tpISjl4xIRPzAiN62vg9etuBdAEUM+ja9rz/zwNZaByiUPFS2aqlj2RHenCRvIapE8mYEg==", + "license": "MIT", + "peer": true + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.16.0.tgz", + "integrity": "sha512-yqIDQTelJZP0Rxml0OXv4Er8Kbdxy7NFh6PCzPwDFWI1SkiokJ3uXQJBvtlxZ3lOnCDYOzdHstqa8sJG4JP02Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.16.0", + "@module-federation/sdk": "0.16.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "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==", + "license": "MIT", + "peer": true + }, + "node_modules/@rspack/binding": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.4.8.tgz", + "integrity": "sha512-VKE+2InUdudBUOn3xMZfK9a6KlOwmSifA0Nupjsh7N9/brcBfJtJGSDCnfrIKCq54FF+QAUCgcNAS0DB4/tZmw==", + "license": "MIT", + "peer": true, + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.4.8", + "@rspack/binding-darwin-x64": "1.4.8", + "@rspack/binding-linux-arm64-gnu": "1.4.8", + "@rspack/binding-linux-arm64-musl": "1.4.8", + "@rspack/binding-linux-x64-gnu": "1.4.8", + "@rspack/binding-linux-x64-musl": "1.4.8", + "@rspack/binding-wasm32-wasi": "1.4.8", + "@rspack/binding-win32-arm64-msvc": "1.4.8", + "@rspack/binding-win32-ia32-msvc": "1.4.8", + "@rspack/binding-win32-x64-msvc": "1.4.8" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.8.tgz", + "integrity": "sha512-PQRNjC3Fc0avpx8Gk+sT5P+HAXxTSzmBA8lU7QLlmbW5GGXO2taVhNstbZ4oxyIX5uDVZpQ2yQ2E0zXirK6/UQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.4.8.tgz", + "integrity": "sha512-ZnPZbo1dhhbfevxSS99y8w02xuEbxyiV1HaUie/S8jzy9DPmk+4Br+DddufnibPNU85e3BZKjp+HDFMYkdn6cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.8.tgz", + "integrity": "sha512-mJK9diM4Gd8RIGO90AZnl27WwUuAOoRplPQv9G+Vxu2baCt1xE1ccf8PntIJ70/rMgsUdnmkR5qQBaGxhAMJvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.4.8.tgz", + "integrity": "sha512-+n9QxeDDZKwVB4D6cwpNRJzsCeuwNqd/fwwbMQVTctJ+GhIHlUPsE8y5tXN7euU7kDci81wMBBFlt6LtXNcssA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.8.tgz", + "integrity": "sha512-rEypDlbIfv9B/DcZ2vYVWs56wo5VWE5oj/TvM9JT+xuqwvVWsN/A2TPMiU6QBgOKGXat3EM/MEgx8NhNZUpkXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.4.8.tgz", + "integrity": "sha512-o9OsvJ7olH0JPU9exyIaYTNQ+aaR5CNAiinkxr+LkV2i3DMIi/+pDVveDiodYjVhzZjWfsP/z8QPO4c6Z06bEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.4.8.tgz", + "integrity": "sha512-hF5gqT0aQ66VUclM2A9MSB6zVdEJqzp++TAXaShBK/eVBI0R4vWrMfJ2TOdzEsSbg4gXgeG4swURpHva3PKbcA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.12" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.4.8.tgz", + "integrity": "sha512-umD0XzesJq4nnStv9/2/VOmzNUWHfLMIjeHmiHYHpc7iVC0SkXgIdc6Ac7c+g2q7/V3/MFxL66Y60oy7lQE3fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.4.8.tgz", + "integrity": "sha512-Uu+F/sxz7GgIMbuCCZVOD1HPjoHQdyrFHi/TE2EmuZzs9Ji9a9mtNJNrKc8+h9YFpaLeade7cbMDjRu4MHxiVA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.4.8.tgz", + "integrity": "sha512-BVkOfJDZnexHNpGgc/sWENyGrsle1jUQTeUEdSyNYsu4Elsgk/T9gnGK8xyLRd2c6k20M5FN38t0TumCp4DscQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/cli": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.4.8.tgz", + "integrity": "sha512-rqQ8iI/zKaT+xiETFQvzzZI4Bpx5hk0IR4BXJwiR/llPQLN/oc1saKyatsn2/p4r0+ABLMftdzKPv6FzIvnzZA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.7", + "@rspack/dev-server": "~1.1.3", + "colorette": "2.0.20", + "exit-hook": "^4.0.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-bundle-analyzer": "4.10.2", + "yargs": "17.7.2" + }, + "bin": { + "rspack": "bin/rspack.js" + }, + "peerDependencies": { + "@rspack/core": "^1.0.0-alpha || ^1.x" + } + }, + "node_modules/@rspack/core": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.4.8.tgz", + "integrity": "sha512-ARHuZ+gx3P//RIUKSjk/riQUn/D5tCwCWbfgeM5pk/Ti2JsgVnqiP9Sksge8JovVPf7b6Zgw73Cq5FpX4aOXeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@module-federation/runtime-tools": "0.16.0", + "@rspack/binding": "1.4.8", + "@rspack/lite-tapable": "1.0.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/dev-server": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-1.1.3.tgz", + "integrity": "sha512-jWPeyiZiGpbLYGhwHvwxhaa4rsr8CQvsWkWslqeMLb2uXwmyy3UWjUR1q+AhAPnf0gs3lZoFZ1hjBQVecHKUvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^3.6.0", + "http-proxy-middleware": "^2.0.9", + "p-retry": "^6.2.0", + "webpack-dev-server": "5.2.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@rspack/core": "*" + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", + "integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@swc/core": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.21.tgz", + "integrity": "sha512-fkk7NJcBscrR3/F8jiqlMptRHP650NxqDnspBMrRe5d8xOoCy9MLL5kOBLFXjFLfMo3KQQHhk+/jUULOMlR1uQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.21", + "@swc/core-darwin-x64": "1.15.21", + "@swc/core-linux-arm-gnueabihf": "1.15.21", + "@swc/core-linux-arm64-gnu": "1.15.21", + "@swc/core-linux-arm64-musl": "1.15.21", + "@swc/core-linux-ppc64-gnu": "1.15.21", + "@swc/core-linux-s390x-gnu": "1.15.21", + "@swc/core-linux-x64-gnu": "1.15.21", + "@swc/core-linux-x64-musl": "1.15.21", + "@swc/core-win32-arm64-msvc": "1.15.21", + "@swc/core-win32-ia32-msvc": "1.15.21", + "@swc/core-win32-x64-msvc": "1.15.21" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.21.tgz", + "integrity": "sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.21.tgz", + "integrity": "sha512-//fOVntgowz9+V90lVsNCtyyrtbHp3jWH6Rch7MXHXbcvbLmbCTmssl5DeedUWLLGiAAW1wksBdqdGYOTjaNLw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.21.tgz", + "integrity": "sha512-meNI4Sh6h9h8DvIfEc0l5URabYMSuNvyisLmG6vnoYAS43s8ON3NJR8sDHvdP7NJTrLe0q/x2XCn6yL/BeHcZg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.21.tgz", + "integrity": "sha512-QrXlNQnHeXqU2EzLlnsPoWEh8/GtNJLvfMiPsDhk+ht6Xv8+vhvZ5YZ/BokNWSIZiWPKLAqR0M7T92YF5tmD3g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.21.tgz", + "integrity": "sha512-8/yGCMO333ultDaMQivE5CjO6oXDPeeg1IV4sphojPkb0Pv0i6zvcRIkgp60xDB+UxLr6VgHgt+BBgqS959E9g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.21.tgz", + "integrity": "sha512-ucW0HzPx0s1dgRvcvuLSPSA/2Kk/VYTv9st8qe1Kc22Gu0Q0rH9+6TcBTmMuNIp0Xs4BPr1uBttmbO1wEGI49Q==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.21.tgz", + "integrity": "sha512-ulTnOGc5I7YRObE/9NreAhQg94QkiR5qNhhcUZ1iFAYjzg/JGAi1ch+s/Ixe61pMIr8bfVrF0NOaB0f8wjaAfA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.21.tgz", + "integrity": "sha512-D0RokxtM+cPvSqJIKR6uja4hbD+scI9ezo95mBhfSyLUs9wnPPl26sLp1ZPR/EXRdYm3F3S6RUtVi+8QXhT24Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.21.tgz", + "integrity": "sha512-nER8u7VeRfmU6fMDzl1NQAbbB/G7O2avmvCOwIul1uGkZ2/acbPH+DCL9h5+0yd/coNcxMBTL6NGepIew+7C2w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.21.tgz", + "integrity": "sha512-+/AgNBnjYugUA8C0Do4YzymgvnGbztv7j8HKSQLvR/DQgZPoXQ2B3PqB2mTtGh/X5DhlJWiqnunN35JUgWcAeQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.21.tgz", + "integrity": "sha512-IkSZj8PX/N4HcaFhMQtzmkV8YSnuNoJ0E6OvMwFiOfejPhiKXvl7CdDsn1f4/emYEIDO3fpgZW9DTaCRMDxaDA==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.21.tgz", + "integrity": "sha512-zUyWso7OOENB6e1N1hNuNn8vbvLsTdKQ5WKLgt/JcBNfJhKy/6jmBmqI3GXk/MyvQKd5SLvP7A0F36p7TeDqvw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "24.0.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", + "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz", + "integrity": "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "peer": true, + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT", + "peer": true + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT", + "peer": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "license": "MIT" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "peer": true, + "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==", + "license": "MIT", + "peer": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT", + "peer": true + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT", + "peer": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "peer": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "peer": true + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT", + "peer": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.245", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz", + "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==", + "license": "ISC", + "peer": true + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT", + "peer": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT", + "peer": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/exit-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-4.0.0.tgz", + "integrity": "sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.22", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "peer": true + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT", + "peer": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "peer": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT", + "peer": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT", + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT", + "peer": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "license": "MIT" + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-loader": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ignore-loader/-/ignore-loader-0.1.2.tgz", + "integrity": "sha512-yOJQEKrNwoYqrWLS4DcnzM7SEQhRKis5mB+LdKKh4cPmGYlLPR0ozRzHV5jmEk2IxptqJNQA5Cc0gw8Fj12bXA==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "peer": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-polyfill-webpack-plugin": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-b4ei444EKkOagG/yFqojrD3QTYM5IOU1f8tn9o6uwrG4qL+brI7oVhjPVd0ZL2xy+Z6CP5bu9w8XTvlWgiXHcw==", + "license": "MIT", + "dependencies": { + "node-stdlib-browser": "^1.3.0", + "type-fest": "^4.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "webpack": ">=5" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT", + "peer": true + }, + "node_modules/node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT", + "peer": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "peer": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT", + "peer": true + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "peer": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/ripemd160/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT", + "peer": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC", + "peer": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC", + "peer": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "license": "Unlicense", + "peer": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT", + "peer": true + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", + "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "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==", + "license": "0BSD", + "peer": true + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "license": "MIT" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT", + "peer": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "peer": true, + "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/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "peer": true, + "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 + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/npm-packages/meteor-rspack/package.json b/npm-packages/meteor-rspack/package.json new file mode 100644 index 0000000000..a880bf3f19 --- /dev/null +++ b/npm-packages/meteor-rspack/package.json @@ -0,0 +1,20 @@ +{ + "name": "@meteorjs/rspack", + "version": "1.1.0-beta.31", + "description": "Configuration logic for using Rspack in Meteor projects", + "main": "index.js", + "type": "commonjs", + "author": "", + "license": "ISC", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "ignore-loader": "^0.1.2", + "node-polyfill-webpack-plugin": "^4.1.0", + "webpack-merge": "^6.0.1" + }, + "peerDependencies": { + "@rspack/cli": ">=1.3.0", + "@rspack/core": ">=1.3.0", + "@swc/core": ">=1.3.0" + } +} diff --git a/npm-packages/meteor-rspack/plugins/AssetExternalsPlugin.js b/npm-packages/meteor-rspack/plugins/AssetExternalsPlugin.js new file mode 100644 index 0000000000..ca3d33879c --- /dev/null +++ b/npm-packages/meteor-rspack/plugins/AssetExternalsPlugin.js @@ -0,0 +1,40 @@ +// AssetExternalsPlugin.js +// +// This plugin externalizes assets within CSS/SCSS and other files. +// It prevents Rspack from bundling assets referenced in CSS url() and similar contexts, +// allowing them to be served directly from the public directory. + +// Regular expression to match CSS, SCSS, and other style files +const CSS_EXT_REGEX = /\.(css|scss|sass|less|styl)$/; + +class AssetExternalsPlugin { + constructor(options = {}) { + this.pluginName = 'AssetExternalsPlugin'; + this.options = options; + } + + apply(compiler) { + // Add the externals function to handle asset URLs in CSS files + compiler.options.externals = [ + ...compiler.options.externals || [], + (data, callback) => { + const req = data.request; + + // Webpack provides dependencyType === "url" for CSS url() deps. + // Rspack is webpack-compatible here, but keep this tolerant. + const isUrlDep = data.dependencyType === 'url'; + const issuer = data.contextInfo?.issuer || ''; + const fromCss = CSS_EXT_REGEX.test(issuer); + + if (req && req.startsWith('/') && (isUrlDep || fromCss)) { + // Keep the URL as-is (served by your server from /public) + return callback(null, `asset ${req}`); + } + + callback(); + } + ]; + } +} + +module.exports = { AssetExternalsPlugin }; diff --git a/npm-packages/meteor-rspack/plugins/HtmlRspackPlugin.js b/npm-packages/meteor-rspack/plugins/HtmlRspackPlugin.js new file mode 100644 index 0000000000..5af36fc29e --- /dev/null +++ b/npm-packages/meteor-rspack/plugins/HtmlRspackPlugin.js @@ -0,0 +1,31 @@ +const RspackMeteorHtmlPlugin = require('./RspackMeteorHtmlPlugin.js'); +const { loadHtmlRspackPluginFromHost } = RspackMeteorHtmlPlugin; + +/** + * A plugin that composes the original HtmlRspackPlugin from @rspack/core + * and RspackMeteorHtmlPlugin, in that order. + */ +class HtmlRspackPlugin { + constructor(options = {}) { + this.options = options; + } + + apply(compiler) { + // Load the original HtmlRspackPlugin from the host project + const OriginalHtmlRspackPlugin = loadHtmlRspackPluginFromHost(compiler); + + if (!OriginalHtmlRspackPlugin) { + throw new Error('Could not load HtmlRspackPlugin from host project.'); + } + + // Apply the original HtmlRspackPlugin + const originalPlugin = new OriginalHtmlRspackPlugin(this.options); + originalPlugin.apply(compiler); + + // Apply the RspackMeteorHtmlPlugin + const meteorPlugin = new RspackMeteorHtmlPlugin(); + meteorPlugin.apply(compiler); + } +} + +module.exports = HtmlRspackPlugin; diff --git a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js new file mode 100644 index 0000000000..a4494ae49e --- /dev/null +++ b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js @@ -0,0 +1,36 @@ +// MeteorRspackOutputPlugin.js +// +// This plugin outputs a JSON stringified with a tag delimiter each time +// a new Rspack compilation happens. The JSON content is configurable +// via plugin instantiation options. + +const { outputMeteorRspack } = require('../lib/meteorRspackHelpers'); + +class MeteorRspackOutputPlugin { + constructor(options = {}) { + this.pluginName = 'MeteorRspackOutputPlugin'; + this.options = options; + this.compilationCount = 0; + // The data to be output as JSON, can be a static object or a function + this.getData = + typeof options.getData === 'function' + ? options.getData + : () => options.data || {}; + } + + apply(compiler) { + // Hook into the 'done' event which fires after each compilation completes + compiler.hooks.done.tap(this.pluginName, stats => { + this.compilationCount++; + const data = { + ...(this.getData(stats, { + compilationCount: this.compilationCount, + isRebuild: this.compilationCount > 1, + }) || {}), + }; + outputMeteorRspack(data); + }); + } +} + +module.exports = { MeteorRspackOutputPlugin }; diff --git a/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js b/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js new file mode 100644 index 0000000000..51cf0958fd --- /dev/null +++ b/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js @@ -0,0 +1,502 @@ +// RequireExternalsPlugin.js +// +// This plugin prepare the require of externals used to be lazy required by Meteor bundler. +// +// It can describe additional externals using the externals option by array, RegExp or function. +// These externals will be lazy required as well, and optionally could be resolved using +// the externalMap function if provided. +// Used for Blaze to translate require of html files to require of js files bundled by Meteor. + +const fs = require('fs'); +const path = require('path'); + +// Normalize a path to always use forward slashes (POSIX style). +// Module identifiers in bundled JS must use '/' regardless of OS. +const toPosix = (p) => p.replace(/\\/g, '/'); + +class RequireExternalsPlugin { + constructor({ + filePath, + // Externals can be: + // - An array of strings: module name must be included in the array + // - A RegExp: module name must match the regex + // - A function: function(name) must return true for the module name + externals = null, + // ExternalMap is a function that receives the request object and returns the external request path + // It can be used to customize how external modules are mapped to file paths + // If not provided, the default behavior is to map the external module name. + externalMap = null, + // Enable global polyfill for module and exports + // If true, globalThis.module and globalThis.exports will be defined if they don't exist + enableGlobalPolyfill = true, + // Check function to determine if an external import should be eager + // If provided, it will be called with the package name and should return true for eager imports + // If not provided or returns false, the import will be lazy (default behavior) + isEagerImport = null, + // Array of module paths that should always be imported at the end of the file + // These will be treated as eager imports but will always be placed after all other imports + lastImports = null, + } = {}) { + this.pluginName = 'RequireExternalsPlugin'; + + // Prepare externals + this._externals = externals; + this._externalMap = externalMap; + this._enableGlobalPolyfill = enableGlobalPolyfill; + this._isEagerImport = isEagerImport; + this._lastImports = lastImports; + this._defaultExternalPrefix = 'external '; + + // Prepare paths + this.filePath = path.resolve(process.cwd(), filePath); + this.backRoot = '../'.repeat( + filePath.replace(/^\.?[/\\]+/, '').split(/[/\\]/).length - 1 + ); + + // Initialize funcCount based on existing helpers in the file + this._funcCount = this._computeNextFuncCount(); + } + + // Helper method to check if a module name matches the externals or default prefix + _isExternalModule(name) { + if (typeof name !== 'string') return false; + + // Check externals if provided + if (this._externals) { + // If externals is an array, use includes method + if (Array.isArray(this._externals)) { + if (this._externals.includes(name)) { + return { isExternal: true, type: 'externals', value: name }; + } + } + // If externals is a RegExp, use test method + else if (this._externals instanceof RegExp) { + if (this._externals.test(name)) { + return { isExternal: true, type: 'externals', value: name }; + } + } + // If externals is a function, call it with the name + else if (typeof this._externals === 'function') { + if (this._externals(name)) { + return { isExternal: true, type: 'externals', value: name }; + } + } + } + + if (name.startsWith(this._defaultExternalPrefix)) { + return { isExternal: true, type: 'prefix', value: name }; + } + + return { isExternal: false }; + } + + // Helper method to extract package name from module name + _extractPackageName(name) { + let pkg = name.slice(this._defaultExternalPrefix.length); + if (pkg.startsWith('"') && pkg.endsWith('"')) pkg = pkg.slice(1, -1); + const depInfo = path.parse(name); + // If the extracted package name is a path, use the path as is + if ( + pkg && + (path.isAbsolute(pkg) || + pkg.startsWith('./') || + pkg.startsWith('.\\') || + pkg.startsWith('../') || + pkg.startsWith('..\\') || + !!depInfo.ext) + ) { + const module = this.externalsMeta.get(pkg); + if (module) { + return `${this.backRoot}${toPosix(module.relativeRequest)}`; + } + return `${this.backRoot}${toPosix(name)}`; + } + + return pkg; + } + + apply(compiler) { + // Initialize externalsMeta if it doesn't exist + this.externalsMeta = this.externalsMeta || new Map(); + + // Only set compiler.options.externals if both externals and externalMap are defined + if (this._externals && this._externalMap) { + compiler.options.externals = [ + ...compiler.options.externals || [], + (module, callback) => { + const { request, context } = module; + const matchInfo = this._isExternalModule(request); + if (matchInfo.isExternal) { + + let externalRequest; + // Use externalMap function if provided + if (this._externalMap && typeof this._externalMap === 'function') { + externalRequest = this._externalMap(module); + + const relContext = path.relative(process.cwd(), context); + // Store the original request to resolve properly the lazy html require later + this.externalsMeta.set(externalRequest, { + originalRequest: request, + externalRequest, + relativeRequest: toPosix(path.join(relContext, request)), + }); + + // tell Rspack "don't bundle this, import it at runtime" + return callback(null, externalRequest); + } + } + + callback(); // otherwise normal resolution + } + ]; + } + + compiler.hooks.done.tap({ name: this.pluginName, stage: -10 }, (stats) => { + // 1) Ensure globalThis.module / exports block is present if enabled + if (this._enableGlobalPolyfill) { + this._ensureGlobalThisModule(); + } + + // 2) Re-load existing requires from disk on every run + const existing = this._readExistingRequires(); + + // 2a) Compute the *current* externals in this build + const info = stats.toJson({ modules: true }); + const current = new Set(); + for (const m of info.modules) { + const matchInfo = this._isExternalModule(m.name); + if (matchInfo.isExternal) { + const pkg = this._extractPackageName(m.name, matchInfo); + if (pkg) { + current.add(pkg); + } + } + } + + // 2b) Remove any requires that are no longer in `current` + const toRemove = [...existing].filter(p => !current.has(p)); + if (toRemove.length) { + let content = fs.readFileSync(this.filePath, 'utf-8'); + + // Strip stale require(...) lines + for (const pkg of toRemove) { + const re = new RegExp(`^.*require\\('${pkg}'\\);?.*(\\r?\\n)?`, 'gm'); + content = content.replace(re, ''); + } + + // Strip out any now-empty helper functions: + // function lazyExternalImportsX() { + // } + // or new format: + // // (function eagerExternalImportsX() { + // // }) + // or lastImports format: + // // (function lastImports() { + // // }) + const emptyLazyFnRe = /^function\s+lazyExternalImports\d+\s*\(\)\s*{\s*}\s*(\r?\n)?/gm; + const emptyEagerFnRe = /^\/\/\s*\(function\s+eagerExternalImports\d+\s*\(\)\s*{\s*\n\/\/\s*\}\)\s*(\r?\n)?/gm; + const emptyLastFnRe = /^\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n\/\/\s*\}\)\s*(\r?\n)?/gm; + content = content.replace(emptyLazyFnRe, ''); + content = content.replace(emptyEagerFnRe, ''); + content = content.replace(emptyLastFnRe, ''); + + // Write the cleaned file back + fs.writeFileSync(this.filePath, content, 'utf-8'); + + // Re-populate `existing` so the add-diff is accurate + existing.clear(); + // Check for require statements + for (const match of content.matchAll(/require\('([^']+)'\)/g)) { + existing.add(match[1]); + } + // Also check for import statements (used in the new format) + for (const match of content.matchAll(/import\s+'([^']+)'/g)) { + existing.add(match[1]); + } + } + + // 3) Collect any new externals from this build and separate into eager, lazy, and last + const newLazyRequires = []; + const newEagerRequires = []; + const newLastRequires = []; + + for (const module of info.modules) { + const name = module.name; + const matchInfo = this._isExternalModule(name); + if (!matchInfo.isExternal) continue; + + const pkg = this._extractPackageName(name, matchInfo); + if (pkg && !existing.has(pkg)) { + existing.add(pkg); + + // Check if this should be a last import + if (this._lastImports && Array.isArray(this._lastImports) && this._lastImports.includes(pkg)) { + newLastRequires.push(`require('${pkg}')`); + } + // Check if this should be an eager import + else if (this._isEagerImport && typeof this._isEagerImport === 'function' && this._isEagerImport(pkg)) { + newEagerRequires.push(`require('${pkg}')`); + } else { + // Default to lazy import + newLazyRequires.push(`require('${pkg}')`); + } + } + } + + // 4) Append new lazy imports if any + if (newLazyRequires.length) { + const fnName = `lazyExternalImports${this._funcCount++}`; + const body = newLazyRequires.map(req => ` ${req};`).join('\n'); + const fnCode = `\nfunction ${fnName}() {\n${body}\n}\n`; + try { + fs.appendFileSync(this.filePath, fnCode); + } catch (err) { + console.error(`Failed to append lazy imports to ${this.filePath}:`, err); + } + } + + // 5) Append new eager imports if any + if (newEagerRequires.length) { + const fnName = `eagerExternalImports${this._funcCount++}`; + // Convert require statements to import statements + const body = newEagerRequires + .map(req => { + // Extract the module path from require('path') + const modulePath = req.match(/require\('([^']+)'\)/)[1]; + return `import '${modulePath}';`; + }) + .join('\n'); + // Use comments instead of actual function + const fnCode = `\n// (function ${fnName}() {\n${body}\n// })\n`; + try { + fs.appendFileSync(this.filePath, fnCode); + } catch (err) { + console.error(`Failed to append eager imports to ${this.filePath}:`, err); + } + } + + // 6) Handle lastImports - these should always be at the end of the file + // First, check if lastImports already exist in the file + let lastImportsExist = false; + let lastImportsAtEnd = false; + let content = ''; + + if (fs.existsSync(this.filePath)) { + content = fs.readFileSync(this.filePath, 'utf-8'); + + // Check if lastImports exist in the file + const lastImportsRe = /\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n([\s\S]*?)\/\/\s*\}\)/g; + const match = lastImportsRe.exec(content); + + if (match) { + lastImportsExist = true; + + // Check if lastImports are at the end of the file + // We'll consider them at the end if there's only whitespace after them + const afterLastImports = content.substring(match.index + match[0].length); + if (/^\s*$/.test(afterLastImports)) { + lastImportsAtEnd = true; + } + } + } + + // If lastImports exist but are not at the end, move them to the end + if (lastImportsExist && !lastImportsAtEnd) { + // Remove the existing lastImports + const lastImportsRe = /\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n[\s\S]*?\/\/\s*\}\)\s*(\r?\n)?/g; + content = content.replace(lastImportsRe, ''); + + // Extract the imports from the existing lastImports + const importRe = /import\s+'([^']+)'/g; + const existingLastImports = []; + let match; + + while ((match = importRe.exec(content)) !== null) { + if (this._lastImports && Array.isArray(this._lastImports) && this._lastImports.includes(match[1])) { + existingLastImports.push(`import '${match[1]}';`); + } + } + + // Add any new lastImports + if (this._lastImports && Array.isArray(this._lastImports)) { + for (const pkg of this._lastImports) { + if (!existingLastImports.some(imp => imp === `import '${pkg}';`) && existing.has(pkg)) { + existingLastImports.push(`import '${pkg}';`); + } + } + } + + // Add the lastImports to the end of the file + if (existingLastImports.length > 0) { + const body = existingLastImports.join('\n'); + const fnCode = `\n// (function lastImports() {\n${body}\n// })\n`; + fs.writeFileSync(this.filePath, content + fnCode); + } else { + fs.writeFileSync(this.filePath, content); + } + } + // If lastImports don't exist, add them if needed + else if (!lastImportsExist) { + // Collect all lastImports + const allLastImports = []; + + // Add any new lastImports from this build + if (newLastRequires.length) { + for (const req of newLastRequires) { + const modulePath = req.match(/require\('([^']+)'\)/)[1]; + allLastImports.push(`import '${modulePath}';`); + } + } + + // Add any existing lastImports from the configuration + if (this._lastImports && Array.isArray(this._lastImports)) { + for (const pkg of this._lastImports) { + if (!allLastImports.some(imp => imp === `import '${pkg}';`) && !existing.has(pkg)) { + allLastImports.push(`import '${pkg}';`); + } + } + } + + // Add the lastImports to the end of the file + if (allLastImports.length > 0) { + const body = allLastImports.join('\n'); + const fnCode = `\n// (function lastImports() {\n${body}\n// })\n`; + try { + fs.appendFileSync(this.filePath, fnCode); + } catch (err) { + console.error(`Failed to append last imports to ${this.filePath}:`, err); + } + } + } + // If lastImports exist and are already at the end, add any new ones + else if (lastImportsExist && lastImportsAtEnd && newLastRequires.length) { + // Extract the existing lastImports + const lastImportsRe = /\/\/\s*\(function\s+lastImports(?:\d+)?\s*\(\)\s*{\s*\n([\s\S]*?)\/\/\s*\}\)/; + const match = lastImportsRe.exec(content); + + if (match) { + const existingBody = match[1]; + const existingImports = new Set(); + + // Extract the imports from the existing lastImports + const importRe = /import\s+'([^']+)'/g; + let importMatch; + + while ((importMatch = importRe.exec(existingBody)) !== null) { + existingImports.add(importMatch[1]); + } + + // Add any new lastImports + let newBody = existingBody; + for (const req of newLastRequires) { + const modulePath = req.match(/require\('([^']+)'\)/)[1]; + if (!existingImports.has(modulePath)) { + newBody += `import '${modulePath}';\n`; + } + } + + // Replace the existing lastImports with the updated ones + const updatedContent = content.replace( + lastImportsRe, + `// (function lastImports() {\n${newBody}// })` + ); + + fs.writeFileSync(this.filePath, updatedContent); + } + } + }); + } + + _computeNextFuncCount() { + let max = 0; + if (fs.existsSync(this.filePath)) { + try { + const content = fs.readFileSync(this.filePath, 'utf-8'); + // Check for lazy, eager, and last external imports functions + const lazyFnRe = /function\s+lazyExternalImports(\d+)\s*\(\)/g; + // Only match the new commented format + const eagerFnRe = /\/\/\s*\(function\s+eagerExternalImports(\d+)\s*\(\)/g; + // Match the lastImports format + const lastFnRe = /\/\/\s*\(function\s+lastImports(\d+)?\s*\(\)/g; + + let match; + // Check lazy imports + while ((match = lazyFnRe.exec(content)) !== null) { + const n = parseInt(match[1], 10); + if (n > max) max = n; + } + + // Check eager imports + while ((match = eagerFnRe.exec(content)) !== null) { + const n = parseInt(match[1], 10); + if (n > max) max = n; + } + + // Check last imports + while ((match = lastFnRe.exec(content)) !== null) { + if (match[1]) { + const n = parseInt(match[1], 10); + if (n > max) max = n; + } + } + } catch { + // ignore read errors + } + } + // next count is max found plus one + return max + 1; + } + + _ensureGlobalThisModule() { + const block = [ + `/* Polyfill globalThis.module, exports & module for legacy */`, + `if (typeof globalThis !== 'undefined') {`, + ` if (typeof globalThis.module === 'undefined') {`, + ` globalThis.module = { exports: {} };`, + ` }`, + ` if (typeof globalThis.exports === 'undefined') {`, + ` globalThis.exports = globalThis.module.exports;`, + ` }`, + `}`, + `if (typeof window.module === 'undefined') {`, + ` window.module = { exports: {} };`, + `}`, + ].join('\n') + '\n'; + + let content = ''; + if (fs.existsSync(this.filePath)) { + content = fs.readFileSync(this.filePath, 'utf-8'); + if (!content.includes(`typeof globalThis.module === 'undefined'`)) { + // Prepend so it lives at the very top + fs.writeFileSync(this.filePath, content + '\n' + block, 'utf-8'); + } + } else { + // File doesn’t exist yet: create with just the block + fs.writeFileSync(this.filePath, block, 'utf-8'); + } + } + + _readExistingRequires() { + const existing = new Set(); + try { + const content = fs.readFileSync(this.filePath, 'utf-8'); + // Check for require statements + const requireRegex = /require\('([^']+)'\)/g; + let match; + while ((match = requireRegex.exec(content)) !== null) { + existing.add(match[1]); + } + + // Also check for import statements (used in the new format) + const importRegex = /import\s+'([^']+)'/g; + while ((match = importRegex.exec(content)) !== null) { + existing.add(match[1]); + } + } catch { + // ignore if file missing or unreadable + } + return existing; + } +} + +module.exports = { RequireExternalsPlugin }; diff --git a/npm-packages/meteor-rspack/plugins/RspackMeteorHtmlPlugin.js b/npm-packages/meteor-rspack/plugins/RspackMeteorHtmlPlugin.js new file mode 100644 index 0000000000..0e1ec8dd59 --- /dev/null +++ b/npm-packages/meteor-rspack/plugins/RspackMeteorHtmlPlugin.js @@ -0,0 +1,50 @@ +const path = require('node:path'); +const { createRequire } = require('node:module'); + +function loadHtmlRspackPluginFromHost(compiler) { + // Prefer the compiler's context; fall back to process.cwd() + const ctx = compiler.options?.context || compiler.context || process.cwd(); + const requireFromHost = createRequire(path.join(ctx, 'package.json')); + + const core = requireFromHost('@rspack/core'); // host's instance + // Rspack exports can be shaped a couple ways; be defensive + return core.HtmlRspackPlugin || core.rspack?.HtmlRspackPlugin || core.default?.HtmlRspackPlugin; +} + +/** + * Rspack plugin to: + * 1. Remove the injected `*-rspack.js` script tags + * 2. Strip <!doctype> and <html>…</html> wrappers from the final HTML + */ +class RspackMeteorHtmlPlugin { + apply(compiler) { + const HtmlRspackPlugin = loadHtmlRspackPluginFromHost(compiler); + if (!HtmlRspackPlugin?.getCompilationHooks) { + throw new Error('Could not load HtmlRspackPlugin from host project.'); + } + + compiler.hooks.compilation.tap('RspackMeteorHtmlPlugin', compilation => { + const hooks = HtmlRspackPlugin.getCompilationHooks(compilation); + + // remove <script src="...*-rspack.js"> + hooks.alterAssetTags.tap('RspackMeteorHtmlPlugin', data => { + data.assetTags.scripts = data.assetTags.scripts.filter(t => { + const src = t.attributes?.src || t.asset || ''; + return !(t.tagName === 'script' && /(?:^|\/)[^\/]*-rspack\.js$/i.test(src)); + }); + }); + + // unwrap <!doctype> and <html>…</html> + hooks.beforeEmit.tap('RspackMeteorHtmlPlugin', data => { + data.html = data.html + .replace(/<!doctype[^>]*>\s*/i, '') + .replace(/<html[^>]*>\s*/i, '') + .replace(/\s*<\/html>\s*$/i, '') + .trim(); + }); + }); + } +} + +module.exports = RspackMeteorHtmlPlugin; +module.exports.loadHtmlRspackPluginFromHost = loadHtmlRspackPluginFromHost; diff --git a/npm-packages/meteor-rspack/rspack.config.js b/npm-packages/meteor-rspack/rspack.config.js new file mode 100644 index 0000000000..793512b1d3 --- /dev/null +++ b/npm-packages/meteor-rspack/rspack.config.js @@ -0,0 +1,868 @@ +const { DefinePlugin, BannerPlugin, NormalModuleReplacementPlugin } = require('@rspack/core'); +const fs = require('fs'); +const { inspect } = require('node:util'); +const path = require('path'); +const { merge } = require('webpack-merge'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); + +const { cleanOmittedPaths, mergeSplitOverlap } = require("./lib/mergeRulesSplitOverlap.js"); +const { getMeteorAppSwcConfig } = require('./lib/swc.js'); +const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js'); +const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js'); +const { AssetExternalsPlugin } = require('./plugins/AssetExternalsPlugin.js'); +const { MeteorRspackOutputPlugin } = require('./plugins/MeteorRspackOutputPlugin.js'); +const { generateEagerTestFile } = require("./lib/test.js"); +const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore"); +const { + compileWithMeteor, + compileWithRspack, + setCache, + splitVendorChunk, + extendSwcConfig, + replaceSwcConfig, + makeWebNodeBuiltinsAlias, + disablePlugins, + outputMeteorRspack, + enablePortableBuild, +} = require('./lib/meteorRspackHelpers.js'); +const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js'); +const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory"); +const { extractLocalDependencies } = require('./lib/localDependenciesHelpers.js'); + + +// Safe require that doesn't throw if the module isn't found +function safeRequire(moduleName) { + try { + return require(moduleName); + } catch (error) { + if ( + error.code === 'MODULE_NOT_FOUND' && + error.message.includes(moduleName) + ) { + return null; + } + throw error; // rethrow if it's a different error + } +} + +// Persistent filesystem cache strategy +function createCacheStrategy( + mode, + side, + { projectConfigPath, configPath, buildContext } = {}, +) { + // Check for configuration files + const tsconfigPath = path.join(process.cwd(), 'tsconfig.json'); + const hasTsconfig = fs.existsSync(tsconfigPath); + const babelRcConfig = path.join(process.cwd(), '.babelrc'); + const hasBabelRcConfig = fs.existsSync(babelRcConfig); + const babelJsConfig = path.join(process.cwd(), 'babel.config.js'); + const hasBabelJsConfig = fs.existsSync(babelJsConfig); + const swcrcPath = path.join(process.cwd(), '.swcrc'); + const hasSwcrcConfig = fs.existsSync(swcrcPath); + const swcJsPath = path.join(process.cwd(), 'swc.config.js'); + const hasSwcJsConfig = fs.existsSync(swcJsPath); + const swcTsPath = path.join(process.cwd(), 'swc.config.ts'); + const hasSwcTsConfig = fs.existsSync(swcTsPath); + const postcssConfigPath = path.join(process.cwd(), 'postcss.config.js'); + const hasPostcssConfig = fs.existsSync(postcssConfigPath); + const packageLockPath = path.join(process.cwd(), 'package-lock.json'); + const hasPackageLock = fs.existsSync(packageLockPath); + const yarnLockPath = path.join(process.cwd(), 'yarn.lock'); + const hasYarnLock = fs.existsSync(yarnLockPath); + + // Extract local dependencies from project config (e.g., plugin files) + const localDependencies = projectConfigPath + ? extractLocalDependencies(projectConfigPath) + : []; + + // Build dependencies array + const buildDependencies = [ + ...(projectConfigPath ? [projectConfigPath] : []), + ...(configPath ? [configPath] : []), + ...localDependencies, + ...(hasTsconfig ? [tsconfigPath] : []), + ...(hasBabelRcConfig ? [babelRcConfig] : []), + ...(hasBabelJsConfig ? [babelJsConfig] : []), + ...(hasSwcrcConfig ? [swcrcPath] : []), + ...(hasSwcJsConfig ? [swcJsPath] : []), + ...(hasSwcTsConfig ? [swcTsPath] : []), + ...(hasPostcssConfig ? [postcssConfigPath] : []), + ...(hasPackageLock ? [packageLockPath] : []), + ...(hasYarnLock ? [yarnLockPath] : []), + ].filter(Boolean); + + return { + cache: true, + experiments: { + cache: { + version: `cache-${mode}${(side && `-${side}`) || ""}`, + type: "persistent", + storage: { + type: "filesystem", + directory: `node_modules/.cache/rspack/${ + [buildContext, side].filter(Boolean).join('-') || 'default' + }`, + }, + ...(buildDependencies.length > 0 && { + buildDependencies: buildDependencies, + }) + }, + }, + }; +} + +// SWC loader rule (JSX/JS) +function createSwcConfig({ + isTypescriptEnabled, + isReactEnabled, + isJsxEnabled, + isTsxEnabled, + externalHelpers, + isDevEnvironment, + isClient, + isAngularEnabled, +}) { + const defaultConfig = { + jsc: { + baseUrl: process.cwd(), + paths: { '/*': ['*', '/*'] }, + parser: { + syntax: isTypescriptEnabled ? 'typescript' : 'ecmascript', + ...(isTsxEnabled && { tsx: true }), + ...(isJsxEnabled && { jsx: true }), + ...(isAngularEnabled && { decorators: true }), + }, + target: 'es2015', + ...(isReactEnabled && { + transform: { + react: { + development: isDevEnvironment, + ...(isClient && { refresh: isDevEnvironment }), + }, + }, + }), + externalHelpers, + }, + }; + + // Swcrc config not customizable + const omitPaths = [ + 'jsc.target', + ]; + // Define warning function + const warningFn = path => { + console.warn( + `[.swcrc] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`, + ); + }; + const customConfig = getMeteorAppSwcConfig() || {}; + const cleanedCustomConfig = cleanOmittedPaths(customConfig, { omitPaths, warningFn }); + const swcConfig = merge(defaultConfig, cleanedCustomConfig); + return { + test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i, + exclude: /node_modules|\.meteor\/local/, + loader: "builtin:swc-loader", + options: swcConfig, + }; +} + +function createRemoteDevServerConfig() { + const rootUrl = process.env.ROOT_URL; + let hostname; + let protocol; + let port; + + if (rootUrl) { + try { + const url = new URL(rootUrl); + // Detect if it's remote (not localhost or 127.x) + const isLocal = + url.hostname.includes('localhost') || + url.hostname.startsWith('127.') || + url.hostname.endsWith('.local'); + if (!isLocal) { + hostname = url.hostname; + protocol = url.protocol === 'https:' ? 'wss' : 'ws'; + port = url.port ? Number(url.port) : (url.protocol === 'https:' ? 443 : 80); + + return { + client: { + webSocketURL: { + hostname, + port, + protocol, + }, + }, + }; + } + } catch (err) { + console.warn(`Invalid ROOT_URL "${rootUrl}", falling back to localhost`); + } + } + + // If local doesn't provide any extra config + return {}; +} + +// Keep files outside of build folders +function keepOutsideBuild() { + return (p) => { + const normalized = '/' + path.normalize(p).replaceAll(path.sep, '/').replace(/^\/+/, ''); + const isInBuildRoot = /\/build(\/|$)/.test(normalized); + const isInBuildStar = /\/build-[^/]+(\/|$)/.test(normalized); + return !(isInBuildRoot || isInBuildStar); + }; +} + +/** + * @param {{ isClient: boolean; isServer: boolean; isDevelopment?: boolean; isProduction?: boolean; isTest?: boolean }} Meteor + * @param {{ mode?: string; clientEntry?: string; serverEntry?: string; clientOutputFolder?: string; serverOutputFolder?: string; chunksContext?: string; assetsContext?: string; serverAssetsContext?: string }} argv + * @returns {Promise<import('@rspack/cli').Configuration[]>} + */ +module.exports = async function (inMeteor = {}, argv = {}) { + // Transform Meteor env properties to proper boolean values + const Meteor = { ...inMeteor }; + // Convert string boolean values to actual booleans + for (const key in Meteor) { + if (Meteor[key] === "true" || Meteor[key] === true) { + Meteor[key] = true; + } else if (Meteor[key] === "false" || Meteor[key] === false) { + Meteor[key] = false; + } + } + + const isTestLike = !!Meteor.isTestLike; + const swcExternalHelpers = !!Meteor.swcExternalHelpers; + const isNative = !!Meteor.isNative; + const devServerPort = Meteor.devServerPort || 8080; + + const projectDir = process.cwd(); + const projectConfigPath = + Meteor.projectConfigPath || path.resolve(projectDir, "rspack.config.js"); + + // Determine context for bundles and assets + const meteorLocalDirName = process.env.METEOR_LOCAL_DIR + ? path.basename(process.env.METEOR_LOCAL_DIR.replace(/\\/g, "/")) + : ""; + const buildContext = + Meteor.buildContext || + process.env.RSPACK_BUILD_CONTEXT || + `_build${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`; + const assetsContext = + Meteor.assetsContext || + process.env.RSPACK_ASSETS_CONTEXT || + `build-assets${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`; + const chunksContext = + Meteor.chunksContext || + process.env.RSPACK_CHUNKS_CONTEXT || + `build-chunks${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`; + + // Compute build paths before loading user config (needed by Meteor helpers below) + const outputPath = Meteor.outputPath; + const outputDir = path.dirname(Meteor.outputPath || ""); + Meteor.buildOutputDir = path.resolve(projectDir, buildContext, outputDir); + + // Meteor flags derived purely from input; independent of loaded user/override configs + const isTest = !!Meteor.isTest; + const isClient = !!Meteor.isClient; + const isServer = !!Meteor.isServer; + const isRun = !!Meteor.isRun; + const isBuild = !!Meteor.isBuild; + const isReactEnabled = !!Meteor.isReactEnabled; + const isTestModule = !!Meteor.isTestModule; + const isTestEager = !!Meteor.isTestEager; + const isTestFullApp = !!Meteor.isTestFullApp; + const isProfile = !!Meteor.isProfile; + const isVerbose = !!Meteor.isVerbose; + const configPath = Meteor.configPath; + const testEntry = Meteor.testEntry; + + const isTypescriptEnabled = Meteor.isTypescriptEnabled || false; + const isJsxEnabled = + Meteor.isJsxEnabled || (!isTypescriptEnabled && isReactEnabled) || false; + const isTsxEnabled = + Meteor.isTsxEnabled || (isTypescriptEnabled && isReactEnabled) || false; + const isBundleVisualizerEnabled = Meteor.isBundleVisualizerEnabled || false; + const isAngularEnabled = Meteor.isAngularEnabled || false; + const enableSwcExternalHelpers = !isServer && swcExternalHelpers; + + // Defined here so it can be called both before and after the first config load; + // without loaded configs it falls through to argv/Meteor flags. + const getModeFromConfig = (userConfig, overrideConfig) => { + if (overrideConfig?.mode) return overrideConfig.mode; + if (userConfig?.mode) return userConfig.mode; + if (argv.mode) return argv.mode; + if (Meteor.isProduction) return "production"; + if (Meteor.isDevelopment) return "development"; + return null; + }; + + // Initial mode before user/override configs are loaded + const initialCurrentMode = getModeFromConfig(); + const initialIsProd = initialCurrentMode + ? initialCurrentMode === "production" + : !!Meteor.isProduction; + const initialIsDev = initialCurrentMode + ? initialCurrentMode === "development" + : !!Meteor.isDevelopment || !initialIsProd; + const initialMode = initialIsProd ? "production" : "development"; + + // Initialized with pre-load values so helpers work during the first config load; + // reassigned after load once mode is fully resolved. + let cacheStrategy = createCacheStrategy( + initialMode, + (Meteor.isClient && "client") || "server", + { projectConfigPath, configPath, buildContext } + ); + let swcConfigRule = createSwcConfig({ + isTypescriptEnabled, + isReactEnabled, + isJsxEnabled, + isTsxEnabled, + externalHelpers: enableSwcExternalHelpers, + isDevEnvironment: isRun && initialIsDev && !isTest && !isNative, + isClient, + isAngularEnabled, + }); + Meteor.swcConfigOptions = swcConfigRule.options; + + // Expose Meteor's helpers to expand Rspack configs + Meteor.compileWithMeteor = (deps) => compileWithMeteor(deps); + Meteor.compileWithRspack = (deps, options = {}) => + compileWithRspack(deps, { + options: mergeSplitOverlap(Meteor.swcConfigOptions, options), + }); + Meteor.setCache = (enabled) => + setCache(!!enabled, enabled === "memory" ? undefined : cacheStrategy); + Meteor.splitVendorChunk = () => splitVendorChunk(); + Meteor.extendSwcConfig = (customSwcConfig) => + extendSwcConfig( + mergeSplitOverlap(Meteor.swcConfigOptions, customSwcConfig) + ); + Meteor.replaceSwcConfig = (customSwcConfig) => + replaceSwcConfig(customSwcConfig); + Meteor.extendConfig = (...configs) => mergeSplitOverlap(...configs); + Meteor.disablePlugins = (matchers) => + prepareMeteorRspackConfig({ + disablePlugins: matchers, + }); + Meteor.enablePortableBuild = () => enablePortableBuild(); + + // Add HtmlRspackPlugin function to Meteor + Meteor.HtmlRspackPlugin = (options = {}) => { + return new HtmlRspackPlugin({ + inject: false, + cache: true, + filename: `../${buildContext}/${outputDir}/index.html`, + templateContent: ` + <head> + <% for tag in htmlRspackPlugin.tags.headTags { %> + <%= toHtml(tag) %> + <% } %> + </head> + <body> + <% for tag in htmlRspackPlugin.tags.bodyTags { %> + <%= toHtml(tag) %> + <% } %> + </body> + `, + ...options, + }); + }; + + // First pass: resolve user/override configs early so mode overrides (e.g. "production") + // are available before computing isProd/isDev and the rest of the build flags. + // Skipped for Angular since it manages its own mode via the second pass. + let { nextUserConfig, nextOverrideConfig } = isAngularEnabled + ? {} + : await loadUserAndOverrideConfig(projectConfigPath, Meteor, argv); + + // Determine the final mode with loaded configs + const currentMode = getModeFromConfig(nextUserConfig, nextOverrideConfig); + const isProd = currentMode + ? currentMode === "production" + : !!Meteor.isProduction; + const isDev = currentMode + ? currentMode === "development" + : !!Meteor.isDevelopment || !isProd; + const mode = isProd ? "production" : "development"; + const isPortableBuild = !!( + nextUserConfig?.["meteor.enablePortableBuild"] || + nextOverrideConfig?.["meteor.enablePortableBuild"] + ); + + // Determine entry points + const entryPath = Meteor.entryPath || ""; + + // Determine output points + const outputFilename = Meteor.outputFilename; + + cacheStrategy = createCacheStrategy( + mode, + (Meteor.isClient && "client") || "server", + { projectConfigPath, configPath } + ); + + // Determine run point + const runPath = Meteor.runPath || ""; + + // Determine banner + const bannerOutput = JSON.parse( + Meteor.bannerOutput || process.env.RSPACK_BANNER || '""' + ); + + // Determine output directories + const clientOutputDir = path.resolve(projectDir, "public"); + const serverOutputDir = path.resolve(projectDir, "private"); + + // Get Meteor ignore entries + const meteorIgnoreEntries = getMeteorIgnoreEntries(projectDir); + + // Additional ignore entries + const additionalEntries = [ + "**/.meteor/local/**", + "**/dist/**", + ...(isTest && isTestEager + ? [`**/${buildContext}/**`, "**/.meteor/local/**", "node_modules/**"] + : []), + ]; + + // Set default watch options + const watchOptions = { + ignored: [ + ...createIgnoreGlobConfig([...meteorIgnoreEntries, ...additionalEntries]), + ], + }; + + if (Meteor.isDebug || Meteor.isVerbose) { + console.log("[i] Rspack mode:", mode); + console.log("[i] Meteor flags:", Meteor); + } + + const isDevEnvironment = isRun && isDev && !isTest && !isNative; + swcConfigRule = createSwcConfig({ + isTypescriptEnabled, + isReactEnabled, + isJsxEnabled, + isTsxEnabled, + externalHelpers: enableSwcExternalHelpers, + isDevEnvironment, + isClient, + isAngularEnabled, + }); + Meteor.swcConfigOptions = swcConfigRule.options; + + const externals = [ + /^meteor\/.*/, + ...(isReactEnabled ? [/^react$/, /^react-dom$/] : []), + ...(isServer ? [/^bcrypt$/] : []), + ]; + const alias = { + "/": path.resolve(process.cwd()), + }; + const fallback = { + ...(isClient && makeWebNodeBuiltinsAlias()), + }; + const extensions = [ + ".ts", + ".tsx", + ".mts", + ".cts", + ".js", + ".jsx", + ".mjs", + ".cjs", + ".json", + ".wasm", + ]; + const extraRules = []; + + const reactRefreshModule = isReactEnabled + ? safeRequire("@rspack/plugin-react-refresh") + : null; + + const requireExternalsPlugin = new RequireExternalsPlugin({ + filePath: path.join(buildContext, runPath), + ...(Meteor.isBlazeEnabled && { + externals: /\.html$/, + isEagerImport: (module) => module.endsWith(".html"), + ...(isProd && { + lastImports: [`./${outputFilename}`], + }), + }), + enableGlobalPolyfill: isDevEnvironment && !isServer, + }); + + // Handle assets + const assetExternalsPlugin = new AssetExternalsPlugin(); + const assetModuleFilename = (_fileInfo) => { + const filename = _fileInfo.filename; + const isPublic = filename.startsWith("/") || filename.startsWith("public"); + if (isPublic) return `[name][ext][query]`; + return `${assetsContext}/[hash][ext][query]`; + }; + + const rsdoctorModule = isBundleVisualizerEnabled + ? safeRequire("@rsdoctor/rspack-plugin") + : null; + const doctorPluginConfig = + isRun && isBundleVisualizerEnabled && rsdoctorModule?.RsdoctorRspackPlugin + ? [ + new rsdoctorModule.RsdoctorRspackPlugin({ + port: isClient + ? parseInt(Meteor.rsdoctorClientPort || "8888", 10) + : parseInt(Meteor.rsdoctorServerPort || "8889", 10), + }), + ] + : []; + const bannerPluginConfig = !isBuild + ? [ + new BannerPlugin({ + banner: bannerOutput, + entryOnly: true, + }), + ] + : []; + // Not supported in Meteor yet (Rspack 1.7+ is enabled by default) + const lazyCompilationConfig = { lazyCompilation: false }; + const shouldLogVerbose = isProfile || isVerbose; + const loggingConfig = shouldLogVerbose + ? {} + : { stats: "errors-warnings", infrastructureLogging: { level: "warn" } }; + + const clientEntry = + isClient && isTest && isTestEager && isTestFullApp + ? generateEagerTestFile({ + isAppTest: true, + projectDir, + buildContext, + ignoreEntries: ["**/server/**"], + meteorIgnoreEntries, + prefix: "client", + extraEntry: path.resolve(process.cwd(), Meteor.mainClientEntry), + globalImportPath: path.resolve(projectDir, buildContext, entryPath), + }) + : isClient && isTest && isTestEager + ? generateEagerTestFile({ + isAppTest: false, + isClient: true, + projectDir, + buildContext, + ignoreEntries: ["**/server/**"], + meteorIgnoreEntries, + prefix: "client", + globalImportPath: path.resolve(projectDir, buildContext, entryPath), + }) + : isClient && isTest && testEntry + ? path.resolve(process.cwd(), testEntry) + : path.resolve(process.cwd(), buildContext, entryPath); + const clientNameConfig = `[${(isTest && "test-") || ""}client-rspack]`; + // Base client config + let clientConfig = { + name: clientNameConfig, + target: "web", + mode, + entry: clientEntry, + output: { + path: clientOutputDir, + filename: (_module) => { + const chunkName = _module.chunk?.name; + const isMainChunk = !chunkName || chunkName === "main"; + const chunkSuffix = `${chunksContext}/[id]${ + isProd ? ".[chunkhash]" : "" + }.js`; + if (isDevEnvironment) { + if (isMainChunk) return outputFilename; + return chunkSuffix; + } + if (isMainChunk) return `../${buildContext}/${outputPath}`; + return chunkSuffix; + }, + libraryTarget: "commonjs2", + publicPath: "/", + chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`, + assetModuleFilename, + cssFilename: `${chunksContext}/[name]${ + isProd ? ".[contenthash]" : "" + }.css`, + cssChunkFilename: `${chunksContext}/[id]${ + isProd ? ".[contenthash]" : "" + }.css`, + ...(isProd && { clean: { keep: keepOutsideBuild() } }), + }, + optimization: { + usedExports: true, + splitChunks: { chunks: "async" }, + }, + module: { + rules: [ + swcConfigRule, + ...(Meteor.isBlazeEnabled + ? [ + { + test: /\.html$/i, + loader: "ignore-loader", + }, + ] + : []), + ...extraRules, + ], + }, + resolve: { extensions, alias, fallback }, + externals, + plugins: [ + ...[ + ...(isReactEnabled && reactRefreshModule && isDevEnvironment + ? [new reactRefreshModule()] + : []), + requireExternalsPlugin, + assetExternalsPlugin, + ].filter(Boolean), + new DefinePlugin({ + "Meteor.isClient": JSON.stringify(true), + "Meteor.isServer": JSON.stringify(false), + "Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp), + "Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp), + ...(!isPortableBuild && { + "Meteor.isDevelopment": JSON.stringify(isDev), + "Meteor.isProduction": JSON.stringify(isProd), + }), + }), + ...bannerPluginConfig, + Meteor.HtmlRspackPlugin(), + ...doctorPluginConfig, + new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => { + res.request = res.request.replace(/^node:/, ""); + }), + ], + watchOptions, + devtool: + isDevEnvironment || isNative || isTest + ? "source-map" + : "hidden-source-map", + ...(isDevEnvironment && { + devServer: { + ...createRemoteDevServerConfig(), + static: { directory: clientOutputDir, publicPath: "/__rspack__/" }, + hot: true, + liveReload: true, + ...(Meteor.isBlazeEnabled && { hot: false }), + port: devServerPort, + devMiddleware: { + writeToDisk: (filePath) => + /\.(html)$/.test(filePath) || filePath.endsWith('sw.js'), + }, + onListening(devServer) { + if (!devServer) return; + const { host, port } = devServer.options; + const protocol = + devServer.options.server?.type === "https" ? "https" : "http"; + const devServerUrl = `${protocol}://${host || "localhost"}:${port}`; + outputMeteorRspack({ devServerUrl }); + }, + }, + }), + ...merge(cacheStrategy, { experiments: { css: true } }), + ...lazyCompilationConfig, + ...loggingConfig, + }; + + const serverEntry = + isServer && isTest && isTestEager && isTestFullApp + ? generateEagerTestFile({ + isAppTest: true, + projectDir, + buildContext, + ignoreEntries: ["**/client/**"], + meteorIgnoreEntries, + prefix: "server", + globalImportPath: path.resolve(projectDir, buildContext, entryPath), + }) + : isServer && isTest && isTestEager + ? generateEagerTestFile({ + isAppTest: false, + projectDir, + buildContext, + ignoreEntries: ["**/client/**"], + meteorIgnoreEntries, + prefix: "server", + globalImportPath: path.resolve(projectDir, buildContext, entryPath), + }) + : isServer && isTest && testEntry + ? path.resolve(process.cwd(), testEntry) + : path.resolve(projectDir, buildContext, entryPath); + const serverNameConfig = `[${(isTest && "test-") || ""}server-rspack]`; + // Base server config + let serverConfig = { + name: serverNameConfig, + target: "node", + mode, + entry: serverEntry, + output: { + path: serverOutputDir, + filename: () => `../${buildContext}/${outputPath}`, + libraryTarget: "commonjs2", + chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`, + assetModuleFilename, + ...(isProd && { clean: { keep: keepOutsideBuild() } }), + }, + optimization: { + usedExports: true, + splitChunks: false, + runtimeChunk: false, + }, + module: { + rules: [swcConfigRule, ...extraRules], + parser: { + javascript: { + // Dynamic imports on the server are treated as bundled in the same chunk + dynamicImportMode: "eager", + }, + }, + }, + resolve: { + extensions, + alias, + modules: ["node_modules", path.resolve(projectDir)], + conditionNames: ["import", "require", "node", "default"], + }, + externals, + externalsPresets: { node: true }, + plugins: [ + new DefinePlugin( + isTest && (isTestModule || isTestEager) + ? { + "Meteor.isTest": JSON.stringify(isTest && !isTestFullApp), + "Meteor.isAppTest": JSON.stringify(isTest && isTestFullApp), + ...(!isPortableBuild && { + "Meteor.isDevelopment": JSON.stringify(isDev), + }), + } + : { + "Meteor.isClient": JSON.stringify(false), + "Meteor.isServer": JSON.stringify(true), + "Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp), + "Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp), + ...(!isPortableBuild && { + "Meteor.isDevelopment": JSON.stringify(isDev), + "Meteor.isProduction": JSON.stringify(isProd), + }), + } + ), + ...bannerPluginConfig, + requireExternalsPlugin, + assetExternalsPlugin, + ...doctorPluginConfig, + ], + watchOptions, + devtool: + isDevEnvironment || isNative || isTest + ? "source-map" + : "hidden-source-map", + ...((isDevEnvironment || (isTest && !isTestEager) || isNative) && + cacheStrategy), + ...lazyCompilationConfig, + ...loggingConfig, + }; + + // Establish Angular overrides to ensure proper integration + const angularExpandConfig = isAngularEnabled + ? { + mode: isProd ? "production" : "development", + devServer: { port: devServerPort }, + stats: { preset: "normal" }, + infrastructureLogging: { level: "info" }, + ...(isProd && isClient && { output: { module: false } }), + } + : {}; + + // Establish test client overrides to ensure proper running + const testClientExpandConfig = + isTest && isClient + ? { + module: { + parser: { + javascript: { + dynamicImportMode: "eager", + dynamicImportPrefetch: true, + dynamicImportPreload: true, + }, + }, + }, + optimization: { + splitChunks: false, + }, + plugins: [new NodePolyfillPlugin()], + } + : {}; + + // Second pass: re-run only when a mode override was detected, so the user config + // can depend on fully-computed Meteor flags and helpers (swcConfigOptions, buildOutputDir, etc.). + if (nextUserConfig?.mode || nextOverrideConfig?.mode || isAngularEnabled) { + ({ nextUserConfig, nextOverrideConfig } = await loadUserAndOverrideConfig( + projectConfigPath, + Meteor, + argv + )); + } + let statsOverrided = false; + let config = isClient ? clientConfig : serverConfig; + if (nextUserConfig) { + config = mergeSplitOverlap(config, nextUserConfig); + if (nextUserConfig.stats != null) { + statsOverrided = true; + } + } + + config = mergeSplitOverlap(config, angularExpandConfig); + config = mergeSplitOverlap(config, testClientExpandConfig); + + if (nextOverrideConfig) { + config = mergeSplitOverlap(config, nextOverrideConfig); + if (nextOverrideConfig.stats != null) { + statsOverrided = true; + } + } + + const shouldDisablePlugins = config?.disablePlugins != null; + if (shouldDisablePlugins) { + config = disablePlugins(config, config.disablePlugins); + delete config.disablePlugins; + } + + delete config["meteor.enablePortableBuild"]; + + if (Meteor.isDebug || Meteor.isVerbose) { + console.log("Config:", inspect(config, { depth: null, colors: true })); + } + + // Check if lazyCompilation is enabled and warn the user + if ( + config.lazyCompilation === true || + typeof config.lazyCompilation === "object" + ) { + console.warn( + "\n⚠️ Warning: lazyCompilation may not work correctly in the current Meteor-Rspack integration.\n" + + " This feature will be evaluated for support in future Meteor versions.\n" + + " If you encounter any issues, please disable it in your rspack config.\n" + ); + } + + // Add MeteorRspackOutputPlugin as the last plugin to output compilation info + const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({ + getData: (stats, { isRebuild, compilationCount }) => ({ + name: config.name, + mode: config.mode, + hasErrors: stats.hasErrors(), + hasWarnings: stats.hasWarnings(), + timestamp: Date.now(), + statsOverrided, + compilationCount, + isRebuild, + }), + }); + config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])]; + + return [config]; +} diff --git a/npm-packages/meteor-rspack/scripts/bump-version.js b/npm-packages/meteor-rspack/scripts/bump-version.js new file mode 100755 index 0000000000..81be21edc5 --- /dev/null +++ b/npm-packages/meteor-rspack/scripts/bump-version.js @@ -0,0 +1,89 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const semver = require('semver'); + +const VALID_LEVELS = ['major', 'minor', 'patch']; + +function usage() { + console.log(`Usage: node ${path.basename(__filename)} <major|minor|patch> [--beta]`); + console.log(''); + console.log('Examples:'); + console.log(' patch # 1.0.1 -> 1.0.2'); + console.log(' minor # 1.0.1 -> 1.1.0'); + console.log(' major # 1.0.1 -> 2.0.0'); + console.log(' patch --beta # 1.0.1 -> 1.0.2-beta.0'); + console.log(' patch --beta # 1.0.2-beta.0 -> 1.0.2-beta.1 (already beta, bumps beta number)'); + console.log(' minor --beta # 1.0.2-beta.1 -> 1.1.0-beta.0 (different bump level resets)'); + process.exit(1); +} + +const args = process.argv.slice(2); +const level = args[0]; +const beta = args.includes('--beta'); + +if (!level || !VALID_LEVELS.includes(level)) { + if (level) console.error(`Error: first argument must be major, minor, or patch`); + usage(); +} + +const pkgPath = path.resolve(__dirname, '..', 'package.json'); +const raw = fs.readFileSync(pkgPath, 'utf8'); +const pkg = JSON.parse(raw); +const current = pkg.version; +const parsed = semver.parse(current); + +if (!parsed) { + console.error(`Error: invalid current version "${current}"`); + process.exit(1); +} + +let newVersion; + +if (beta) { + const isBeta = parsed.prerelease.length > 0 && parsed.prerelease[0] === 'beta'; + + if (isBeta) { + // Already a beta. The base version already has a prior bump applied. + // Check if the same bump level is being requested by inspecting which + // components are zeroed out (major resets minor+patch, minor resets patch). + // If the same level, just increment the beta number. + const { major, minor, patch } = parsed; + const betaNum = typeof parsed.prerelease[1] === 'number' ? parsed.prerelease[1] : 0; + let sameLevel = false; + + if (level === 'patch') { + sameLevel = true; + } else if (level === 'minor') { + sameLevel = patch === 0 && minor > 0; + } else if (level === 'major') { + sameLevel = minor === 0 && patch === 0; + } + + if (sameLevel) { + newVersion = `${major}.${minor}.${patch}-beta.${betaNum + 1}`; + } else { + const bumped = semver.inc(`${major}.${minor}.${patch}`, level); + newVersion = `${bumped}-beta.0`; + } + } else { + // Not a beta yet: bump the base and start at beta.0 + const bumped = semver.inc(current, level); + newVersion = `${bumped}-beta.0`; + } +} else { + if (parsed.prerelease.length > 0) { + // Currently a prerelease: bump base version from the clean base + const cleanBase = `${parsed.major}.${parsed.minor}.${parsed.patch}`; + newVersion = semver.inc(cleanBase, level); + } else { + newVersion = semver.inc(current, level); + } +} + +pkg.version = newVersion; +fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + +console.log(`Bumped version: ${current} -> ${newVersion}`); diff --git a/npm-packages/meteor-rspack/scripts/publish-beta.sh b/npm-packages/meteor-rspack/scripts/publish-beta.sh new file mode 100755 index 0000000000..29ec756bbf --- /dev/null +++ b/npm-packages/meteor-rspack/scripts/publish-beta.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +npm publish --tag beta "$@" diff --git a/package-lock.json b/package-lock.json index ea3a5d33e3..a94c9cf4c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,31 +54,48 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", - "integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.6", - "@babel/parser": "^7.23.6", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.6", - "@babel/types": "^7.23.6", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -93,122 +110,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/traverse": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", - "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -226,12 +127,6 @@ } } }, - "node_modules/@babel/core/node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, "node_modules/@babel/core/node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -284,41 +179,46 @@ "eslint": "^7.5.0 || ^8.0.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -341,78 +241,40 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -421,258 +283,69 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", - "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.6", - "@babel/types": "^7.23.6" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helpers/node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/traverse": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", - "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helpers/node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/@babel/helpers/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -681,12 +354,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -744,20 +418,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", @@ -806,6 +466,72 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -983,17 +709,14 @@ "dev": true }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1005,26 +728,19 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1549,10 +1265,11 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, "node_modules/acorn": { "version": "8.11.2", @@ -1601,15 +1318,19 @@ } }, "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "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": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { @@ -1840,6 +1561,19 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1864,9 +1598,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1882,11 +1616,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1914,6 +1650,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1924,9 +1674,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001570", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", - "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -1941,36 +1691,45 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "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.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "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/concat-map": { "version": "0.0.1", @@ -1985,10 +1744,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "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", @@ -2137,11 +1897,27 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { - "version": "1.4.615", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.615.tgz", - "integrity": "sha512-/bKPPcgZVUziECqDc+0HkT87+0zhaWSZHNXqF8FLd2lQcptpmUFwoCSWjCdOng9Gdq+afKArPdEg/0ZW461Eng==", - "dev": true + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" }, "node_modules/es-abstract": { "version": "1.23.3", @@ -2204,13 +1980,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2250,10 +2024,11 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2262,14 +2037,16 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2302,10 +2079,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2679,55 +2457,6 @@ "node": ">=10" } }, - "node_modules/eslint/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, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/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, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/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 - }, "node_modules/eslint/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2837,15 +2566,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2867,21 +2587,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -2897,18 +2602,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -3038,19 +2731,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3178,16 +2858,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3196,6 +2882,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3246,15 +2946,6 @@ "node": ">= 6" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -3292,12 +2983,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3319,12 +3011,13 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { @@ -3352,10 +3045,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3527,12 +3221,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3828,10 +3526,11 @@ } }, "node_modules/js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -3846,15 +3545,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -3960,6 +3660,16 @@ "loose-envify": "cli.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3969,6 +3679,20 @@ "node": ">= 8" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4009,10 +3733,11 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", @@ -4165,6 +3890,22 @@ "node": ">= 0.8.0" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4220,10 +3961,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4375,18 +4117,22 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4682,15 +4428,16 @@ } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -4711,15 +4458,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4886,9 +4624,9 @@ "dev": true }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -4904,9 +4642,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index ff1baa3e4c..6508b0c3ef 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,11 @@ "typescript": "^5.4.5" }, "scripts": { + "install:unit": "cd tools/unit-tests && npm install", + "test:unit": "cd tools/unit-tests && npm test", + "install:e2e": "cd tools/e2e-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell", + "test:e2e": "cd tools/e2e-tests && npm test -- ", + "create-app:e2e": "cd tools/e2e-tests && node scripts/create-app.js", "test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js" }, "jshintConfig": { diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index 801233d294..673ce77c15 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -123,7 +123,7 @@ Meteor.methods({ ); }, async has2faEnabled() { - return await Accounts._is2faEnabledForUser(); + return Accounts._is2faEnabledForUser(); }, }); diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 6aa2a58e2a..73e26ae913 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -3,11 +3,20 @@ import { Meteor } from 'meteor/meteor'; import { Configuration } from 'meteor/service-configuration'; import { DDP } from 'meteor/ddp'; +/** + * Object containing functions that generate URLs for account-related emails. + * Override these to customize URLs in password reset, enrollment, and verification emails. + * URL methods can return either a string or a Promise that resolves to a string. + */ export interface URLS { - resetPassword: (token: string) => string; - verifyEmail: (token: string) => string; - loginToken: (token: string) => string; - enrollAccount: (token: string) => string; + /** Generates the URL for password reset emails. Can return a Promise for async URL generation. */ + resetPassword: (token: string, extraParams?: Record<string, string>) => string | Promise<string>; + /** Generates the URL for email verification emails. Can return a Promise for async URL generation. */ + verifyEmail: (token: string, extraParams?: Record<string, string>) => string | Promise<string>; + /** Generates the URL for login token emails. Can return a Promise for async URL generation. */ + loginToken: (selector: string, token: string, extraParams?: Record<string, string>) => string | Promise<string>; + /** Generates the URL for account enrollment emails. Can return a Promise for async URL generation. */ + enrollAccount: (token: string, extraParams?: Record<string, string>) => string | Promise<string>; } export interface EmailFields { @@ -362,11 +371,11 @@ export namespace Accounts { * - a login method result object **/ function registerLoginHandler( - handler: (options: any) => undefined | LoginMethodResult + handler: (options: any) => undefined | LoginMethodResult | Promise<undefined | LoginMethodResult> ): void; function registerLoginHandler( name: string, - handler: (options: any) => undefined | LoginMethodResult + handler: (options: any) => undefined | LoginMethodResult | Promise<undefined | LoginMethodResult> ): void; type Password = @@ -387,7 +396,7 @@ export namespace Accounts { function _checkPasswordAsync( user: Meteor.User, password: Password - ): Promise<{ userId: string; error?: any }> + ): Promise<{ userId: string; error?: any }>; } export namespace Accounts { diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index aef200ed39..f2535b6dc6 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -150,6 +150,30 @@ export class AccountsClient extends AccountsCommon { }); } + /** + * @summary Log out all clients logged in as the current user and logs the current user out as well. + * @locus Client + * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. + */ + logoutAllClients(callback) { + this._loggingOut.set(true); + + this.connection.applyAsync('logoutAllClients', [], { + // TODO[FIBERS]: Look this { wait: true } later. + wait: true + }) + .then((result) => { + this._loggingOut.set(false); + this._loginCallbacksCalled = false; + this.makeClientLoggedOut(); + callback && callback(); + }) + .catch((e) => { + this._loggingOut.set(false); + callback && callback(e); + }); + } + /** * @summary Log out other clients logged in as the current user, but does not log out the client that calls this function. * @locus Client @@ -793,6 +817,14 @@ Meteor.loggingOut = () => Accounts.loggingOut(); */ Meteor.logout = callback => Accounts.logout(callback); +/** + * @summary Log out all clients logged in as the current user and logs the current user out as well. + * @locus Client + * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. + * @importFromPackage meteor + */ +Meteor.logoutAllClients = callback => Accounts.logoutAllClients(callback); + /** * @summary Log out other clients logged in as the current user, but does not log out the client that calls this function. * @locus Client diff --git a/packages/accounts-base/accounts_client_tests.js b/packages/accounts-base/accounts_client_tests.js index f5466216f5..06d88066d8 100644 --- a/packages/accounts-base/accounts_client_tests.js +++ b/packages/accounts-base/accounts_client_tests.js @@ -302,48 +302,80 @@ Tinytest.addAsync( }); }); }); - } + }, ); -Tinytest.addAsync('accounts - storage', - async function(test) { - const expectWhenSessionStorage = () => { - test.isNotUndefined(sessionStorage.getItem('Meteor.loginToken')); - test.isNull(localStorage.getItem('Meteor.loginToken')); - }; - const expectWhenLocalStorage = () => { - test.isNotUndefined(localStorage.getItem('Meteor.loginToken')); - test.isNull(sessionStorage.getItem('Meteor.loginToken')); - }; +Tinytest.addAsync('accounts - logoutAllClients', async function (test, done) { + logoutAndCreateUser(test, done, async () => { + const user = await Meteor.userAsync()._id; + test.equal(user.services.resume.loginTokens.length, 1); + await Meteor.users.updateAsync(user._id, { + $push: { + 'services.resume.loginTokens': { + hashedToken: 'test-token', + when: new Date(), + }, + }, + }); + await Meteor.users.updateAsync(user._id, { + $push: { + 'services.resume.loginTokens': { + hashedToken: 'test-token2', + when: new Date(), + }, + }, + }); + test.equal(user.services.resume.loginTokens.length, 3); + Meteor.logoutAllClients(async () => { + test.isUndefined(Meteor.user()); + test.equal( + (await Meteor.users.findOneAsync(user._id)).services.resume.loginTokens?.length, + 0, + ); + removeTestUser(done); + }); + }); +}); - const testCases = [{ +Tinytest.addAsync('accounts - storage', async function (test) { + const expectWhenSessionStorage = () => { + test.isNotUndefined(sessionStorage.getItem('Meteor.loginToken')); + test.isNull(localStorage.getItem('Meteor.loginToken')); + }; + const expectWhenLocalStorage = () => { + test.isNotUndefined(localStorage.getItem('Meteor.loginToken')); + test.isNull(sessionStorage.getItem('Meteor.loginToken')); + }; + + const testCases = [{ clientStorage: undefined, expectStorage: expectWhenLocalStorage, - }, { + }, + { clientStorage: 'local', expectStorage: expectWhenLocalStorage, - }, { - clientStorage: 'session', - expectStorage: expectWhenSessionStorage, - }]; - for await (const testCase of testCases) { - await new Promise(resolve => { - sessionStorage.clear(); - localStorage.clear(); + }, { + clientStorage: 'session', + expectStorage: expectWhenSessionStorage, + }]; + for await (const testCase of testCases) { + await new Promise(resolve => { + sessionStorage.clear(); + localStorage.clear(); - const { clientStorage, expectStorage } = testCase; - Accounts.config({ clientStorage }); - test.equal(Accounts._options.clientStorage, clientStorage); + const { clientStorage, expectStorage } = testCase; + Accounts.config({ clientStorage }); + test.equal(Accounts._options.clientStorage, clientStorage); - // Login a user and test that tokens are in expected storage - logoutAndCreateUser(test, resolve, () => { - Accounts.logout(); - expectStorage(); - removeTestUser(resolve); - }); + // Login a user and test that tokens are in expected storage + logoutAndCreateUser(test, resolve, () => { + Accounts.logout(); + expectStorage(); + removeTestUser(resolve); }); - } - }); + }); + } +}); Tinytest.addAsync('accounts - should only start subscription when connected', async function (test) { const { conn, messages, cleanup } = await captureConnectionMessagesClient(test); @@ -365,4 +397,4 @@ Tinytest.addAsync('accounts - should only start subscription when connected', as test.equal(parsedMessages, expectedMessages) cleanup() -}); \ No newline at end of file +}); diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index c910659684..48234064d8 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -205,7 +205,7 @@ export class AccountsCommon { * @locus Anywhere * @param {Object} options * @param {Boolean} options.sendVerificationEmail New users with an email address will receive an address verification email. - * @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available. + * @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available. **Important**: This option must be set on both the client and server to take full effect. If only set on the server, account creation will be blocked but the UI will still show the "Create account" link. * @param {String | Function} options.restrictCreationByEmailDomain If set to a string, only allows new users if the domain part of their email address matches the string. If set to a function, only allows new users if the function returns true. The function is passed the full email address of the proposed new user. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmailDomain: 'school.edu' })`. * @param {Number} options.loginExpiration The number of milliseconds from when a user logs in until their token expires and they are logged out, for a more granular control. If `loginExpirationInDays` is set, it takes precedent. * @param {Number} options.loginExpirationInDays The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration. @@ -226,6 +226,19 @@ export class AccountsCommon { * @param {Number} options.loginTokenExpirationHours When using the package `accounts-2fa`, use this to set the amount of time a token sent is valid. As it's just a number, you can use, for example, 0.5 to make the token valid for just half hour. The default is 1 hour. * @param {Number} options.tokenSequenceLength When using the package `accounts-2fa`, use this to the size of the token sequence generated. The default is 6. * @param {'session' | 'local'} options.clientStorage By default login credentials are stored in local storage, setting this to true will switch to using session storage. + * + * @example + * // For UI-related options like forbidClientAccountCreation, call Accounts.config on both client and server + * // Create a shared configuration file (e.g., lib/accounts-config.js): + * import { Accounts } from 'meteor/accounts-base'; + * + * Accounts.config({ + * forbidClientAccountCreation: true, + * sendVerificationEmail: true, + * }); + * + * // Then import this file in both client/main.js and server/main.js: + * // import '../lib/accounts-config.js'; */ config(options) { // We don't want users to accidentally only call Accounts.config on the diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 6f0ba2098e..f76fce23d0 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -1,5 +1,6 @@ import crypto from 'crypto'; -import { Meteor } from 'meteor/meteor'; +import { Meteor } from 'meteor/meteor' +import { check, Match } from 'meteor/check'; import { AccountsCommon, EXPIRE_TOKENS_INTERVAL_MS, @@ -8,13 +9,6 @@ import { URL } from 'meteor/url'; const hasOwn = Object.prototype.hasOwnProperty; -// XXX maybe this belongs in the check package -const NonEmptyString = Match.Where(x => { - check(x, String); - return x.length > 0; -}); - - /** * @summary Constructor for the `Accounts` namespace on the server. * @locus Server @@ -89,6 +83,25 @@ export class AccountsServer extends AccountsCommon { return Meteor._isPromise(value) ? await value : value; }; + /** + * @summary Object containing functions that generate URLs for account-related emails. + * Override these to customize URLs in emails sent by + * [`Accounts.sendResetPasswordEmail`](#Accounts-sendResetPasswordEmail), + * [`Accounts.sendEnrollmentEmail`](#Accounts-sendEnrollmentEmail), and + * [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail). + * + * By default, URLs use hash fragments (e.g., `#/reset-password/:token`) for security: + * hash fragments are not sent to the server in HTTP requests, preventing tokens from + * appearing in server logs or referrer headers. + * @locus Server + * @memberof Accounts + * @name urls + * @type {Object} + * @property {Function} resetPassword - `(token, extraParams) => string` - Generates password reset URL. + * @property {Function} verifyEmail - `(token, extraParams) => string` - Generates email verification URL. + * @property {Function} enrollAccount - `(token, extraParams) => string` - Generates account enrollment URL. + * @property {Function} loginToken - `(selector, token, extraParams) => string` - Generates login token URL. + */ this.urls = { resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams), verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), @@ -99,6 +112,16 @@ export class AccountsServer extends AccountsCommon { this.addDefaultRateLimit(); + /** + * @summary Builds a URL for account-related emails by combining the app's + * root URL with a path and optional extra parameters. + * @locus Server + * @memberof Accounts + * @name buildEmailUrl + * @param {String} path - The path to append to the root URL (e.g., `#/reset-password/TOKEN`). + * @param {Object} [extraParams={}] - Additional query parameters to include in the URL. + * @returns {String} The complete URL. + */ this.buildEmailUrl = (path, extraParams = {}) => { const url = new URL(Meteor.absoluteUrl(path)); const params = Object.entries(extraParams); @@ -537,7 +560,7 @@ export class AccountsServer extends AccountsCommon { type, fn ) { - return await this._attemptLogin( + return this._attemptLogin( methodInvocation, methodName, methodArgs, @@ -668,7 +691,6 @@ export class AccountsServer extends AccountsCommon { // this variable is available in their scope. const accounts = this; - // This object will be populated with methods and then passed to // accounts._server.methods further below. const methods = {}; @@ -685,7 +707,7 @@ export class AccountsServer extends AccountsCommon { const result = await accounts._runLoginHandlers(this, options); //console.log({result}); - return await accounts._attemptLogin(this, "login", arguments, result); + return accounts._attemptLogin(this, "login", arguments, result); }; methods.logout = async function () { @@ -698,6 +720,17 @@ export class AccountsServer extends AccountsCommon { await this.setUserId(null); }; + // Logs out the current user and closes all the connections + // associated with the user. + // + methods.logoutAllClients = async function() { + const logoutUserId = this.userId; + accounts._setLoginToken(logoutUserId, this.connection, null); + accounts._clearAllLoginTokens(logoutUserId); + await accounts._successfulLogout(this.connection, logoutUserId); + await this.setUserId(null); + }; + // Generates a new login token with the same expiration as the // connection's current token and saves it to the database. Associates // the connection with this new token and returns it. Throws an error @@ -727,7 +760,7 @@ export class AccountsServer extends AccountsCommon { const newStampedToken = accounts._generateStampedLoginToken(); newStampedToken.when = currentStampedToken.when; await accounts._insertLoginToken(this.userId, newStampedToken); - return await accounts._loginUser(this, this.userId, newStampedToken); + return accounts._loginUser(this, this.userId, newStampedToken); }; // Removes all tokens except the token associated with the current @@ -961,8 +994,8 @@ export class AccountsServer extends AccountsCommon { _clearAllLoginTokens(userId) { this.users.updateAsync(userId, { $set: { - 'services.resume.loginTokens': [] - } + 'services.resume.loginTokens': [], + }, }); }; @@ -1565,9 +1598,9 @@ export class AccountsServer extends AccountsCommon { _userQueryValidator = Match.Where(user => { check(user, { - id: Match.Optional(NonEmptyString), - username: Match.Optional(NonEmptyString), - email: Match.Optional(NonEmptyString) + id: Match.Optional(Match.NonEmptyString), + username: Match.Optional(Match.NonEmptyString), + email: Match.Optional(Match.NonEmptyString) }); if (Object.keys(user).length !== 1) throw new Match.Error("User property must have exactly one field"); @@ -1647,13 +1680,13 @@ const defaultResumeLoginHandler = async (accounts, options) => { // {hashedToken, when} for a hashed token or {token, when} for an // unhashed token. let oldUnhashedStyleToken; - let token = await user.services.resume.loginTokens.find(token => + let token = user.services.resume.loginTokens.find(token => token.hashedToken === hashedToken ); if (token) { oldUnhashedStyleToken = false; } else { - token = await user.services.resume.loginTokens.find(token => + token = user.services.resume.loginTokens.find(token => token.token === options.resume ); oldUnhashedStyleToken = true; diff --git a/packages/accounts-base/accounts_tests_setup.js b/packages/accounts-base/accounts_tests_setup.js index 0e211828c0..7f8cf2a40d 100644 --- a/packages/accounts-base/accounts_tests_setup.js +++ b/packages/accounts-base/accounts_tests_setup.js @@ -32,7 +32,7 @@ Meteor.methods({ }, } ); - return await getTokenFromSecret({ selector, secret }); + return getTokenFromSecret({ selector, secret }); }, getTokenFromSecret, }); diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 4190011694..931e4edb42 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "A user account system", - version: "3.1.2", + version: "3.2.0", }); Package.onUse((api) => { diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index fe393fb663..577f4555fb 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -55,7 +55,7 @@ Meteor.methods( check(email, String); const userId = await Accounts.createUser({ email }); await Accounts.sendEnrollmentEmail(userId); - return await Meteor.users.findOneAsync(userId); + return Meteor.users.findOneAsync(userId); } } ); diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index f41e5b8127..6da4c3d030 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -5,12 +5,13 @@ Package.describe({ // 2.2.x in the future. The version was also bumped to 2.0.0 temporarily // during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2 // through -beta.5 and -rc.0 have already been published. - version: "3.2.1", + version: "3.2.2", }); Npm.depends({ bcrypt: "5.0.1", argon2: "0.41.1", + "node-gyp-build": "4.8.4", }); Package.onUse((api) => { diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 151f9c8d5f..711c163fe6 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1,6 +1,7 @@ import argon2 from "argon2"; -import { hash as bcryptHash, compare as bcryptCompare } from "bcrypt"; import { Accounts } from "meteor/accounts-base"; +import { check, Match } from 'meteor/check'; +import { hash as bcryptHash, compare as bcryptCompare } from 'bcrypt'; // Utility for grabbing user const getUserById = @@ -288,14 +289,8 @@ Accounts._checkPasswordAsync = checkPasswordAsync; -// XXX maybe this belongs in the check package -const NonEmptyString = Match.Where(x => { - check(x, String); - return x.length > 0; -}); - const passwordValidator = Match.OneOf( - Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256), { + Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)), { digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') } @@ -322,7 +317,7 @@ Accounts.registerLoginHandler("password", async options => { check(options, { user: Accounts._userQueryValidator, password: passwordValidator, - code: Match.Optional(NonEmptyString), + code: Match.Optional(Match.NonEmptyString), }); @@ -374,10 +369,9 @@ Accounts.registerLoginHandler("password", async options => { * @param {String} newUsername A new username for the user. * @importFromPackage accounts-base */ -Accounts.setUsername = - async (userId, newUsername) => { - check(userId, NonEmptyString); - check(newUsername, NonEmptyString); +Accounts.setUsername = async (userId, newUsername) => { + check(userId, Match.NonEmptyString); + check(newUsername, Match.NonEmptyString); const user = await getUserById(userId, { fields: { @@ -478,7 +472,7 @@ Meteor.methods( Accounts.setPasswordAsync = async (userId, newPlaintextPassword, options) => { check(userId, String); - check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)); + check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256))); check(options, Match.Maybe({ logout: Boolean })); options = { logout: true, ...options }; @@ -513,6 +507,7 @@ Meteor.methods({forgotPassword: async options => { const user = await Accounts.findUserByEmail(options.email, { fields: { emails: 1 } }); if (!user) { + if (Accounts._options.ambiguousErrorMessages) return; Accounts._handleError("User not found"); } @@ -1006,9 +1001,9 @@ Meteor.methods( * @importFromPackage accounts-base */ Accounts.replaceEmailAsync = async (userId, oldEmail, newEmail, verified) => { - check(userId, NonEmptyString); - check(oldEmail, NonEmptyString); - check(newEmail, NonEmptyString); + check(userId, Match.NonEmptyString); + check(oldEmail, Match.NonEmptyString); + check(newEmail, Match.NonEmptyString); check(verified, Match.Optional(Boolean)); if (verified === void 0) { @@ -1050,8 +1045,8 @@ Accounts.replaceEmailAsync = async (userId, oldEmail, newEmail, verified) => { * @importFromPackage accounts-base */ Accounts.addEmailAsync = async (userId, newEmail, verified) => { - check(userId, NonEmptyString); - check(newEmail, NonEmptyString); + check(userId, Match.NonEmptyString); + check(newEmail, Match.NonEmptyString); check(verified, Match.Optional(Boolean)); if (verified === void 0) { @@ -1161,8 +1156,8 @@ Accounts.addEmailAsync = async (userId, newEmail, verified) => { */ Accounts.removeEmail = async (userId, email) => { - check(userId, NonEmptyString); - check(email, NonEmptyString); + check(userId, Match.NonEmptyString); + check(email, Match.NonEmptyString); const user = await getUserById(userId, { fields: { _id: 1 } }); if (!user) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 49f94544a0..3b55a0ede8 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1,4 +1,6 @@ Accounts._connectionCloseDelayMsForTests = 1000; +Accounts._options.ambiguousErrorMessages = false; + const makeTestConnAsync = (test) => new Promise((resolve, reject) => { @@ -1136,6 +1138,56 @@ if (Meteor.isClient) (() => { })(); +if (Meteor.isServer) { + Tinytest.add( + 'passwords - passwordValidator accepts passwords within default maxLength', + test => { + // A password of 256 chars (default max) should be accepted + const validPassword = 'a'.repeat(256); + test.isTrue( + Match.test(validPassword, Match.OneOf( + Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)), + { digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') } + )), + 'Password of exactly 256 chars should be accepted' + ); + } + ); + + Tinytest.add( + 'passwords - passwordValidator rejects passwords exceeding default maxLength', + test => { + // A password of 257 chars should be rejected + const longPassword = 'a'.repeat(257); + test.isFalse( + Match.test(longPassword, Match.OneOf( + Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)), + { digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') } + )), + 'Password exceeding 256 chars should be rejected' + ); + } + ); + + Tinytest.add( + 'passwords - passwordValidator operator precedence is correct for maxLength fallback', + test => { + // This test verifies the fix: without proper parentheses around the || operator, + // `str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256` + // would evaluate as `(str.length <= undefined) || 256` which is always truthy (256), + // allowing passwords of any length. + const veryLongPassword = 'a'.repeat(1000); + test.isFalse( + Match.test(veryLongPassword, Match.OneOf( + Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)), + { digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') } + )), + 'Very long password (1000 chars) should be rejected when no custom maxLength is configured' + ); + } + ); +} + if (Meteor.isServer) (() => { Tinytest.add('passwords - setup more than one onCreateUserHook', test => { @@ -1415,9 +1467,8 @@ if (Meteor.isServer) (() => { ); Accounts._options.ambiguousErrorMessages = true; - await test.throwsAsync( - async () => await Meteor.callAsync('forgotPassword', wrongOptions), - 'Something went wrong. Please check your credentials' + await test.doesNotThrowsAsync( + async () => await Meteor.callAsync("forgotPassword", wrongOptions) ); Accounts._options.ambiguousErrorMessages = false; diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index 060171cd54..0f1f62d4d7 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'No-password login/sign-up support for accounts', - version: '3.0.2', + version: '3.1.0', }); Package.onUse(api => { diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 20c23e17e8..3f9f595444 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -1,12 +1,12 @@ import { Accounts } from 'meteor/accounts-base'; +import { Random } from 'meteor/random'; +import { check, Match } from 'meteor/check'; import { DEFAULT_TOKEN_SEQUENCE_LENGTH, getUserById, - NonEmptyString, tokenValidator, checkToken, } from './server_utils'; -import { Random } from 'meteor/random'; const findUserWithOptions = async ({ selector }) => { if (!selector) { @@ -33,7 +33,7 @@ Accounts.registerLoginHandler('passwordless', async options => { check(options, { token: tokenValidator(), - code: Match.Optional(NonEmptyString), + code: Match.Optional(Match.NonEmptyString), selector: Accounts._userQueryValidator, }); diff --git a/packages/accounts-passwordless/server_tests.js b/packages/accounts-passwordless/server_tests.js index 768023b952..9855e1c020 100644 --- a/packages/accounts-passwordless/server_tests.js +++ b/packages/accounts-passwordless/server_tests.js @@ -28,6 +28,9 @@ const getData = ({ createdAt }) => { }; Tinytest.add('passwordless - time expired', test => { + // The test suite for accounts-passwordless includes testing whether it gets the right error messages from the server. + // So, we need this disabled, otherwise those tests incorrectly fail when you run them. + Accounts._options.ambiguousErrorMessages = false; const createdAt = new Date('July 17, 2022 13:00:00'); const currentDate = new Date('July 17, 2022 14:01:00'); diff --git a/packages/accounts-passwordless/server_utils.js b/packages/accounts-passwordless/server_utils.js index 4e9a2b20d5..ce4f642be6 100644 --- a/packages/accounts-passwordless/server_utils.js +++ b/packages/accounts-passwordless/server_utils.js @@ -1,5 +1,5 @@ import { Accounts } from 'meteor/accounts-base'; -import { check, Match } from 'meteor/check'; +import { Match } from 'meteor/check'; import { SHA256 } from 'meteor/sha'; const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000; @@ -16,11 +16,6 @@ export const tokenValidator = () => { ); }; -export const NonEmptyString = Match.Where(x => { - check(x, String); - return x.length > 0; -}); - export const checkToken = ({ user, sequence, diff --git a/packages/accounts-ui-unstyled/login_buttons.import.css b/packages/accounts-ui-unstyled/login_buttons.import.css new file mode 100644 index 0000000000..a60efb883c --- /dev/null +++ b/packages/accounts-ui-unstyled/login_buttons.import.css @@ -0,0 +1,551 @@ +/* VARIABLES */ +:root { + /* Layout & Sizing */ + --login-buttons-accounts-dialog-width: 250px; + --meteor-accounts-base-padding: 8px; + --meteor-accounts-dialog-border-width: 1px; + --configure-login-service-dialog-width: 530px; + --button-border-radius: 4px; + --dialog-border-radius: 8px; + --input-border-radius: 4px; + + /* Colors - Primary */ + --login-buttons-color: #4e40b8; + --login-buttons-color-active: #6c5ce7; + + /* Colors - Config */ + --login-buttons-config-color: #cc3a1a; + --login-buttons-config-color-border: #a32e15; + --login-buttons-config-color-active: #e5532e; + --login-buttons-config-color-active-border: #cc3a1a; + + /* Colors - UI */ + --color-text-primary: #2d2d2d; + --color-text-secondary: #4a4a4a; + --color-text-disabled: #999; + --color-background-primary: #fff; + --color-background-secondary: #f8f9fa; + --color-background-disabled: #e0e0e0; + --color-border: #e6e6e6; + --color-input-border: #d1d1d1; + --color-input-focus-border: var(--login-buttons-color); + --color-error: #e74c3c; + --color-success: #2ecc71; + --color-overlay: rgba(0, 0, 0, 0.6); + + /* Typography */ + --font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-family-monospace: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + --font-size-base: 16px; + --font-size-small: 0.875rem; + --font-size-smaller: 0.8125rem; + --font-size-smallest: 0.75rem; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-bold: 600; + --line-height-base: 1.5; + + /* Effects */ + --box-shadow-dialog: 0 10px 25px rgba(0, 0, 0, 0.1); + --box-shadow-button-active: 0 2px 4px 0 rgba(0, 0, 0, 0.1) inset; + --box-shadow-input-focus: 0 0 0 3px rgba(78, 64, 184, 0.2); + + /* Transitions */ + --transition-speed-fast: 0.1s; + --transition-speed-normal: 0.2s; + --transition-speed-slow: 0.3s; + --transition-timing: cubic-bezier(0.25, 0.1, 0.25, 1); + + /* Dark Theme Variables - These can be overridden by users */ + --login-buttons-color-dark: #8c7ae6; + --login-buttons-color-active-dark: #a29bfe; + --color-text-primary-dark: #f5f5f5; + --color-text-secondary-dark: #d1d1d1; + --color-text-disabled-dark: #777; + --color-background-primary-dark: #121212; + --color-background-secondary-dark: #1e1e1e; + --color-background-disabled-dark: #444; + --color-border-dark: #333; + --color-input-border-dark: #444; + --color-input-focus-border-dark: var(--login-buttons-color-dark); + --color-error-dark: #ff6b6b; + --color-success-dark: #55efc4; + --color-overlay-dark: rgba(0, 0, 0, 0.8); + --box-shadow-dialog-dark: 0 10px 25px rgba(0, 0, 0, 0.3); + --box-shadow-button-active-dark: 0 2px 4px 0 rgba(0, 0, 0, 0.3) inset; + --box-shadow-input-focus-dark: 0 0 0 3px rgba(140, 122, 230, 0.3); +} + +/* Dark Theme */ +@media (prefers-color-scheme: dark) { + :root { + /* Colors (Dark) - Use the dark theme variables with fallbacks */ + --login-buttons-color: var(--login-buttons-color-dark, #7986CB); + --login-buttons-color-active: var(--login-buttons-color-active-dark, #9FA8DA); + --color-text-primary: var(--color-text-primary-dark, #eee); + --color-text-secondary: var(--color-text-secondary-dark, #bbb); + --color-text-disabled: var(--color-text-disabled-dark, #666); + --color-background-primary: var(--color-background-primary-dark, #121212); + --color-background-secondary: var(--color-background-secondary-dark, #1e1e1e); + --color-background-disabled: var(--color-background-disabled-dark, #444); + --color-border: var(--color-border-dark, #333); + --color-input-border: var(--color-input-border-dark, #444); + --color-input-focus-border: var(--color-input-focus-border-dark, var(--login-buttons-color, #7986CB)); + --color-error: var(--color-error-dark, #e57373); + --color-success: var(--color-success-dark, #81c784); + --color-overlay: var(--color-overlay-dark, rgba(0, 0, 0, 0.8)); + + /* Effects (Dark) */ + --box-shadow-dialog: var(--box-shadow-dialog-dark, 0 4px 12px rgba(0, 0, 0, 0.5)); + --box-shadow-button-active: var(--box-shadow-button-active-dark, 0 2px 3px 0 rgba(0, 0, 0, 0.4) inset); + --box-shadow-input-focus: var(--box-shadow-input-focus-dark, 0 0 0 2px rgba(121, 134, 203, 0.25)); + } +} + +/* LOGIN BUTTONS */ + +#login-buttons { + display: inline-block; + line-height: 1; +} + +#login-buttons .login-button { + position: relative; +} + +#login-buttons button.login-button { + width: 100%; +} + +#login-buttons .login-buttons-with-only-one-button { + display: inline-block; +} + +#login-buttons .login-buttons-with-only-one-button .login-button { + display: inline-block; +} + +#login-buttons .login-buttons-with-only-one-button .login-text-and-button { + display: inline-block; +} + +#login-buttons .login-display-name { + display: inline-block; + padding-right: 2px; + line-height: var(--line-height-base); + font-family: var(--font-family-primary); +} + +#login-buttons .loading { + line-height: 1; + background-image: url(data:image/gif;base64,R0lGODlhEAALAPQAAP///wAAANra2tDQ0Orq6gYGBgAAAC4uLoKCgmBgYLq6uiIiIkpKSoqKimRkZL6+viYmJgQEBE5OTubm5tjY2PT09Dg4ONzc3PLy8ra2tqCgoMrKyu7u7gAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCwAAACwAAAAAEAALAAAFLSAgjmRpnqSgCuLKAq5AEIM4zDVw03ve27ifDgfkEYe04kDIDC5zrtYKRa2WQgAh+QQJCwAAACwAAAAAEAALAAAFJGBhGAVgnqhpHIeRvsDawqns0qeN5+y967tYLyicBYE7EYkYAgAh+QQJCwAAACwAAAAAEAALAAAFNiAgjothLOOIJAkiGgxjpGKiKMkbz7SN6zIawJcDwIK9W/HISxGBzdHTuBNOmcJVCyoUlk7CEAAh+QQJCwAAACwAAAAAEAALAAAFNSAgjqQIRRFUAo3jNGIkSdHqPI8Tz3V55zuaDacDyIQ+YrBH+hWPzJFzOQQaeavWi7oqnVIhACH5BAkLAAAALAAAAAAQAAsAAAUyICCOZGme1rJY5kRRk7hI0mJSVUXJtF3iOl7tltsBZsNfUegjAY3I5sgFY55KqdX1GgIAIfkECQsAAAAsAAAAABAACwAABTcgII5kaZ4kcV2EqLJipmnZhWGXaOOitm2aXQ4g7P2Ct2ER4AMul00kj5g0Al8tADY2y6C+4FIIACH5BAkLAAAALAAAAAAQAAsAAAUvICCOZGme5ERRk6iy7qpyHCVStA3gNa/7txxwlwv2isSacYUc+l4tADQGQ1mvpBAAIfkECQsAAAAsAAAAABAACwAABS8gII5kaZ7kRFGTqLLuqnIcJVK0DeA1r/u3HHCXC/aKxJpxhRz6Xi0ANAZDWa+kEAA7AAAAAAAAAAAA); + width: 16px; + background-position: center center; + background-repeat: no-repeat; +} + +#login-buttons .login-button, .accounts-dialog .login-button { + cursor: pointer; + -webkit-user-select: none; /* Safari support */ + user-select: none; + padding: 0.625rem 1.25rem; + + font-size: var(--font-size-small); + font-family: var(--font-family-primary); + line-height: var(--line-height-base); + font-weight: var(--font-weight-medium); + + text-align: center; + color: var(--color-background-primary); + + background: var(--login-buttons-color); + border: none; + + border-radius: var(--button-border-radius); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: background-color var(--transition-speed-normal) var(--transition-timing), + box-shadow var(--transition-speed-normal) var(--transition-timing), + transform var(--transition-speed-fast) var(--transition-timing); +} + +#login-buttons .login-button:hover, .accounts-dialog .login-button:hover { + background: var(--login-buttons-color-active); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); +} + +#login-buttons .login-button:active, .accounts-dialog .login-button:active { + background: var(--login-buttons-color-active); + transform: translateY(1px); + box-shadow: var(--box-shadow-button-active); +} + +#login-buttons .login-button.login-button-disabled, +#login-buttons .login-button.login-button-disabled:active, +.accounts-dialog .login-button.login-button-disabled, +.accounts-dialog .login-button.login-button-disabled:active { + color: var(--color-text-disabled); + background: var(--color-background-disabled); + border: none; + box-shadow: none; + transform: none; + cursor: not-allowed; + opacity: 0.7; +} + +/* Reset styles for dialog elements */ +.accounts-dialog * { + padding: 0; + margin: 0; + line-height: inherit; + color: inherit; + font: inherit; + font-family: var(--font-family-primary); +} + +.accounts-dialog .login-button { + width: auto; + margin-bottom: 4px; +} + +#login-buttons .login-buttons-padding { + display: inline-block; + width: 30px; +} + +#login-buttons .login-display-name { + margin-right: 4px; +} + +#login-buttons .configure-button { + background: var(--login-buttons-config-color); + border-color: var(--login-buttons-config-color-border); +} + +#login-buttons .configure-button:active, +#login-buttons .configure-button:hover { + background: var(--login-buttons-config-color-active); + border-color: var(--login-buttons-config-color-active-border); +} + +#login-buttons .login-image { + display: inline-block; + position: absolute; + left: 6px; + top: 6px; + width: 16px; + height: 16px; +} + +#login-buttons .text-besides-image { + margin-left: 18px; +} + +#login-buttons .no-services { + color: red; +} + +#login-buttons .login-link-and-dropdown-list { + position: relative; +} + +#login-buttons .login-close-text { + float: left; + position: relative; + padding-bottom: 8px; +} + +#login-buttons .login-text-and-button .loading, +#login-buttons .login-link-and-dropdown-list .loading { + display: inline-block; +} + +#login-buttons.login-buttons-dropdown-align-left #login-dropdown-list .loading { + float: right; +} + +#login-buttons.login-buttons-dropdown-align-right #login-dropdown-list .loading { + float: left; +} + +#login-buttons .login-close-text-clear { + clear: both; +} + +#login-buttons .or { + text-align: center; +} + +#login-buttons .hline { + text-decoration: line-through; + color: lightgrey; +} + +#login-buttons .or-text { + font-weight: bold; +} + +#login-buttons #signup-link { + float: right; +} + +#login-buttons #forgot-password-link, +#login-buttons #resend-passwordless-code { + float: left; +} + +#login-buttons #back-to-login-link { + float: right; +} + +#login-buttons a, .accounts-dialog a { + cursor: pointer; + text-decoration: none; + color: var(--login-buttons-color); + transition: color var(--transition-speed-normal) var(--transition-timing); +} + +#login-buttons a:hover, .accounts-dialog a:hover { + color: var(--login-buttons-color-active); + text-decoration: underline; +} + +#login-buttons.login-buttons-dropdown-align-right .login-close-text { + float: right; +} + +.accounts-dialog { + border: var(--meteor-accounts-dialog-border-width) solid var(--color-border); + z-index: 1000; + background: var(--color-background-primary); + border-radius: var(--dialog-border-radius); + + padding: 24px; + margin: -8px -12px 0 -12px; + + width: var(--login-buttons-accounts-dialog-width); + + box-shadow: var(--box-shadow-dialog); + + font-size: var(--font-size-base); + color: var(--color-text-primary); +} + +.accounts-dialog > * { + line-height: 1.6; +} + +.accounts-dialog > .login-close-text { + line-height: inherit; + font-size: inherit; + font-family: inherit; +} + +.accounts-dialog label, .accounts-dialog .title { + font-size: var(--font-size-small); + margin-top: 1rem; + margin-bottom: 0.375rem; + display: block; + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + letter-spacing: 0.01em; +} + +.accounts-dialog input[type=text], +.accounts-dialog input[type=email], +.accounts-dialog input[type=password] { + box-sizing: border-box; + width: 100%; + height: auto; + font-size: 1rem; + padding: 0.5rem; + border-radius: 4px; + transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; +} + +#login-buttons input[type=text]:focus, +#login-buttons input[type=email]:focus, +#login-buttons input[type=password]:focus, +.accounts-dialog input[type=text]:focus, +.accounts-dialog input[type=email]:focus, +.accounts-dialog input[type=password]:focus { + outline: none; + border-color: var(--color-input-focus-border); + box-shadow: var(--box-shadow-input-focus); +} + +.accounts-dialog .login-button-form-submit { + margin-top: 8px; +} + +.accounts-dialog .message { + font-size: var(--font-size-smaller); + margin-top: 10px; + line-height: 1.4; + padding: 0.375rem 0; +} + +.accounts-dialog .error-message { + color: var(--color-error); + padding: 0.375rem 0.625rem; + background-color: rgba(231, 76, 60, 0.1); + border-radius: 4px; + margin-bottom: 0.75rem; +} + +.accounts-dialog .info-message { + color: var(--color-success); + padding: 0.375rem 0.625rem; + background-color: rgba(46, 204, 113, 0.1); + border-radius: 4px; + margin-bottom: 0.75rem; +} + +.accounts-dialog .additional-link { + font-size: var(--font-size-smallest); + margin-top: 1rem; + display: inline-block; +} + +.accounts-dialog .accounts-close { + position: absolute; + top: 12px; + right: 16px; + + font-size: 18px; + font-weight: var(--font-weight-bold); + line-height: 18px; + text-decoration: none; + color: var(--color-text-secondary); + opacity: 0.6; + transition: opacity var(--transition-speed-normal) var(--transition-timing); + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.accounts-dialog .accounts-close:hover { + opacity: 1; + background-color: rgba(0, 0, 0, 0.05); +} + +.accounts-dialog #login-buttons-cancel-reset-password { + float: right; +} + +.accounts-dialog #login-buttons-cancel-enroll-account { + float: right; +} + +#login-dropdown-list { + position: absolute; + top: calc(-1 * var(--meteor-accounts-dialog-border-width)); + left: calc(-1 * var(--meteor-accounts-dialog-border-width)); +} + +#login-buttons.login-buttons-dropdown-align-right #login-dropdown-list { + left: auto; + right: calc(-1 * var(--meteor-accounts-dialog-border-width)); +} + +#login-buttons-message-dialog .message { + /* we intentionally want it bigger on this dialog since it's the only thing displayed */ + font-size: 100%; +} + +.accounts-centered-dialog { + font-family: var(--font-family-primary); + + z-index: 1001; + position: fixed; + + /* Modern centering approach using transform */ + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: var(--login-buttons-accounts-dialog-width); +} + +#configure-login-service-dialog { + width: var(--configure-login-service-dialog-width); + /* Using transform for centering instead of negative margins */ +} + +#configure-login-service-dialog table { + width: 100%; +} + +#configure-login-service-dialog input[type=text] { + width: 100%; + font-family: var(--font-family-monospace); +} + +#configure-login-service-dialog ol { + margin-top: 10px; + margin-bottom: 10px; +} + +#configure-login-service-dialog ol li { + margin-left: 30px; +} + +#configure-login-service-dialog .configuration_labels { + width: 30%; +} + +#configure-login-service-dialog .configuration_inputs { + width: 70%; +} + +#configure-login-service-dialog .new-section { + margin-top: 10px; +} + +#configure-login-service-dialog .url { + font-family: var(--font-family-monospace); +} + +#configure-login-service-dialog-save-configuration { + float: right; +} + +.configure-login-service-dismiss-button { + float: left; +} + +#just-verified-dismiss-button, #messages-dialog-dismiss-button { + margin-top: 8px; +} + +.hide-background { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 999; + background-color: var(--color-overlay); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +#login-buttons input[type=text], +#login-buttons input[type=email], +#login-buttons input[type=password], +.accounts-dialog input[type=text], +.accounts-dialog input[type=email], +.accounts-dialog input[type=password] { + padding: 0.625rem 0.75rem; + border: 1px solid var(--color-input-border); + border-radius: var(--input-border-radius); + line-height: var(--line-height-base); + font-size: var(--font-size-base); + color: var(--color-text-primary); + background-color: var(--color-background-primary); + width: 100%; + box-sizing: border-box; + transition: border-color var(--transition-speed-normal) var(--transition-timing), + box-shadow var(--transition-speed-normal) var(--transition-timing); +} diff --git a/packages/accounts-ui-unstyled/login_buttons.import.less b/packages/accounts-ui-unstyled/login_buttons.import.less deleted file mode 100644 index 9b191823fb..0000000000 --- a/packages/accounts-ui-unstyled/login_buttons.import.less +++ /dev/null @@ -1,418 +0,0 @@ -//////////////////// MIXINS - -// Minimal, well-documented, general-purpose CSS mixins. -// (Some are same as Bootstrap.) - -////////// Box-Sizing: Border-Box - -// Setting `box-sizing: border-box` on an element causes the CSS -// layout algorithm to interpret `width` and `height` declarations -// as referring to the size of the border box (outside the border), -// not the content box as usual (inside the padding). -// -// This is especially useful for stretching a form element to the -// width of its container even if the form element has arbitrary -// padding and borders, which can be done using `width: 100%`. -// -// Browser support is IE 8+ and all modern browsers, with the caveat -// that `-moz-box-sizing` in Firefox is considered to have some -// buggy or non-compliant behavior. For example, min/max-width/height -// may not interact correctly. See -// https://bugzilla.mozilla.org/show_bug.cgi?id=243412. -.box-sizing-by-border () { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -////////// Box-Shadow - -.box-shadow (...) { - box-shadow: @arguments; - -webkit-box-shadow: @arguments; // For Android -} - -////////// Unselectable - -.unselectable () { - -webkit-user-select: none; // Chrome/Safari - -moz-user-select: none; // Firefox - -ms-user-select: none; // IE10+ - - // These delarations not implemented in browsers yet: - -o-user-select: none; - user-select: none; - - // In IE <= 9 and Opera, need unselectable="on" in the HTML. -} - -//////////////////// LOGIN BUTTONS - -@login-buttons-accounts-dialog-width: 250px; -@login-buttons-color: #596595; -@login-buttons-color-border: darken(@login-buttons-color, 10%); -@login-buttons-color-active: lighten(@login-buttons-color, 10%); -@login-buttons-color-active-border: darken(@login-buttons-color-active, 10%); - -@login-buttons-config-color: darken(#f53, 10%); -@login-buttons-config-color-border: darken(@login-buttons-config-color, 10%); -@login-buttons-config-color-active: lighten(@login-buttons-config-color, 10%); -@login-buttons-config-color-active-border: darken(@login-buttons-config-color-active, 10%); - -#login-buttons { - - display: inline-block; - margin-right: 0.2px; // Fixes display on IE8: http://www.compsoft.co.uk/Blog/2009/11/inline-block-not-quite-inline-blocking.html - - // This seems to keep the height of the line from - // being sensitive to the presence of the unicode down arrow, - // which otherwise bumps the baseline down by 1px. - line-height: 1; - - .login-button { - position: relative; // so that we can position the image absolutely within the button - } - - button.login-button { - width: 100%; - } - - .login-buttons-with-only-one-button { - display: inline-block; - .login-button { display: inline-block; } - .login-text-and-button { - display: inline-block; - } - } - - .login-display-name { - display: inline-block; - padding-right: 2px; - line-height: 1.5; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - } - .loading { - line-height: 1; - background-image: url(data:image/gif;base64,R0lGODlhEAALAPQAAP///wAAANra2tDQ0Orq6gYGBgAAAC4uLoKCgmBgYLq6uiIiIkpKSoqKimRkZL6+viYmJgQEBE5OTubm5tjY2PT09Dg4ONzc3PLy8ra2tqCgoMrKyu7u7gAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCwAAACwAAAAAEAALAAAFLSAgjmRpnqSgCuLKAq5AEIM4zDVw03ve27ifDgfkEYe04kDIDC5zrtYKRa2WQgAh+QQJCwAAACwAAAAAEAALAAAFJGBhGAVgnqhpHIeRvsDawqns0qeN5+y967tYLyicBYE7EYkYAgAh+QQJCwAAACwAAAAAEAALAAAFNiAgjothLOOIJAkiGgxjpGKiKMkbz7SN6zIawJcDwIK9W/HISxGBzdHTuBNOmcJVCyoUlk7CEAAh+QQJCwAAACwAAAAAEAALAAAFNSAgjqQIRRFUAo3jNGIkSdHqPI8Tz3V55zuaDacDyIQ+YrBH+hWPzJFzOQQaeavWi7oqnVIhACH5BAkLAAAALAAAAAAQAAsAAAUyICCOZGme1rJY5kRRk7hI0mJSVUXJtF3iOl7tltsBZsNfUegjAY3I5sgFY55KqdX1GgIAIfkECQsAAAAsAAAAABAACwAABTcgII5kaZ4kcV2EqLJipmnZhWGXaOOitm2aXQ4g7P2Ct2ER4AMul00kj5g0Al8tADY2y6C+4FIIACH5BAkLAAAALAAAAAAQAAsAAAUvICCOZGme5ERRk6iy7qpyHCVStA3gNa/7txxwlwv2isSacYUc+l4tADQGQ1mvpBAAIfkECQsAAAAsAAAAABAACwAABS8gII5kaZ7kRFGTqLLuqnIcJVK0DeA1r/u3HHCXC/aKxJpxhRz6Xi0ANAZDWa+kEAA7AAAAAAAAAAAA); - width: 16px; - background-position: center center; - background-repeat: no-repeat; - } -} - -#login-buttons .login-button, .accounts-dialog .login-button { - cursor: pointer; - .unselectable(); - padding: 4px 8px; - - font-size: 80%; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - line-height: 1.5; - - text-align: center; - color: #fff; - - background: @login-buttons-color; - border: 1px solid @login-buttons-color-border; - - border-radius: 4px; - - &:hover { - background: @login-buttons-color-active; - } - &:active { - background: @login-buttons-color-active; - .box-shadow(0 2px 3px 0 rgba(0, 0, 0, 0.2) inset); - } - - &.login-button-disabled, &.login-button-disabled:active { - color: #ddd; - background: #aaa; - border: 1px solid lighten(#aaa, 10%); - .box-shadow(none); - } -} - -// precendence of this selector is significant -.accounts-dialog * { - // A base for our dialog CSS, to reset browser styles and protect against - // the app's CSS. Dialogs include the dropdown, config modals, and the - // reset password modal. We can't completely isolate the dialogs from - // the app's CSS, and that isn't the goal because the app can style them. - // This rule is a compromise that should take precedence over some very - // broad rules but be overridden by more specific ones. - - // Add more declarations here if they help the dialogs look good - // out-of-the-box in more apps. - - padding: 0; - margin: 0; - line-height: inherit; - color: inherit; - font: inherit; - - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.accounts-dialog .login-button { - width: auto; - margin-bottom: 4px; -} - -#login-buttons { - .login-buttons-padding { - display: inline-block; - width: 30px; - } - - .login-display-name { margin-right: 4px; } - - .configure-button { - background: @login-buttons-config-color; - border-color: @login-buttons-config-color-border; - - &:active, &:hover { - background: @login-buttons-config-color-active; - border-color: @login-buttons-config-color-active-border; - } - } - - .login-image { - display: inline-block; - position: absolute; - left: 6px; - top: 6px; - width: 16px; - height: 16px; - } - - .text-besides-image { - margin-left: 18px; - } - - .no-services { color: red; } - - .login-link-and-dropdown-list { - position: relative; - } - .login-close-text { - float: left; - position: relative; - padding-bottom: 8px; - } - - .login-text-and-button .loading, .login-link-and-dropdown-list .loading { - display: inline-block; - } - &.login-buttons-dropdown-align-left #login-dropdown-list .loading { - float: right; - } - &.login-buttons-dropdown-align-right #login-dropdown-list .loading { - float: left; - } - - - .login-close-text-clear { clear: both; } - - .or { text-align: center; } - .hline { text-decoration: line-through; color: lightgrey; } - .or-text { font-weight: bold; } - - #signup-link { float: right; } - #forgot-password-link, #resend-passwordless-code { float: left; } - #back-to-login-link { float: right; } -} - -#login-buttons a, .accounts-dialog a { - cursor: pointer; - text-decoration: underline; -} - -#login-buttons.login-buttons-dropdown-align-right .login-close-text { - float: right; -} - -@meteor-accounts-base-padding: 8px; -@meteor-accounts-dialog-border-width: 1px; - -.accounts-dialog { - border: @meteor-accounts-dialog-border-width solid #ccc; - z-index: 1000; - background: white; - border-radius: 4px; - - padding: 8px 12px; - margin: -8px -12px 0 -12px; - - width: @login-buttons-accounts-dialog-width; - - .box-shadow(0 0 3px 0 rgba(0, 0, 0, 0.2)); - - // Labels and links inherit app's font with this line commented out: - //font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 16px; - color: #333; - - // XXX Make the dropdown and dialogs look good without a top-level - // line-height: 1.6. For now, we apply it to everything except - // the "Close" link, which we want to have the same line-height - // as the "Sign in" link. - & > * { line-height: 1.6; } - & > .login-close-text { - line-height: inherit; - font-size: inherit; - font-family: inherit; - } - - label, .title { - font-size: 80%; - margin-top: 7px; - margin-bottom: -2px; - } - - label { - // Bootstrap sets labels as 'display: block;'. Undo that. - display: inline; - } - - input[type=text], input[type=email], input[type=password] { - // Be pixel-accurate in IE 8+ regardless of our borders and - // paddings, at the expense of IE 7. - // Any heights or widths applied to this element will set the - // size of the border box (including padding and borders) - // instead of the content box. This makes it possible to - // do width 100%. - .box-sizing-by-border(); - width: 100%; - // A fix purely for the "meteor add bootstrap" experience. - // Bootstrap sets "height: 20px" on form fields, which is too - // small when applied to the border box. People have complained - // that Bootstrap takes this approach for the sake of IE 7: - // https://github.com/twitter/bootstrap/issues/2935 - // Our work-around is to override Bootstrap's rule (with higher - // precedence). - &[type] { height: auto; } - } - - .login-button-form-submit { margin-top: 8px; } - .message { font-size: 80%; margin-top: 8px; line-height: 1.3; } - .error-message { color: red; } - .info-message { color: green; } - .additional-link { font-size: 75%; } - - .accounts-close { - position: absolute; - top: 0; - right: 5px; - - font-size: 20px; - font-weight: bold; - line-height: 20px; - text-decoration: none; - color: #000; - opacity: 0.4; - - &:hover { - opacity: 0.8; - } - } - - #login-buttons-cancel-reset-password { float: right; } - #login-buttons-cancel-enroll-account { float: right; } -} - -#login-dropdown-list { - position: absolute; - // The top-left of the border-box of the dropdown is absolutely - // positioned within its container, so we need to compensate - // for the border. The padding is already compensated for by - // negative margins on the dropdown. - // XXX We could use negative margins to compensate for the - // border too. - top: -@meteor-accounts-dialog-border-width; - left: -@meteor-accounts-dialog-border-width; -} - -#login-buttons.login-buttons-dropdown-align-right #login-dropdown-list { - left: auto; - right: -@meteor-accounts-dialog-border-width; -} - -#login-buttons-message-dialog .message { - /* we intentionally want it bigger on this dialog since it's the only thing displayed */ - font-size: 100%; -} - -.accounts-centered-dialog { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - - z-index: 1001; - position: fixed; - - left: 50%; - margin-left: -(@login-buttons-accounts-dialog-width - + @meteor-accounts-base-padding) / 2; - - top: 50%; - margin-top: -40px; /* = approximately -height/2, though height can change */ -} - -@configure-login-service-dialog-width: 530px; - -#configure-login-service-dialog { - width: @configure-login-service-dialog-width; - margin-left: -(@configure-login-service-dialog-width - + @meteor-accounts-base-padding) / 2; - margin-top: -300px; /* = approximately -height/2, though height can change */ - - table { width: 100%; } - input[type=text] { - width: 100%; - font-family: "Courier New", Courier, monospace; - } - ol { - margin-top: 10px; - margin-bottom: 10px; - - li { margin-left: 30px; } - } - .configuration_labels { width: 30%; } - .configuration_inputs { width: 70%; } - .new-section { margin-top: 10px; } - .url { font-family: "Courier New", Courier, monospace; } -} - -#configure-login-service-dialog-save-configuration { - float: right; -} - -.configure-login-service-dismiss-button { - float: left; -} - -#just-verified-dismiss-button, #messages-dialog-dismiss-button { - margin-top: 8px; -} - -.hide-background { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 999; - - /* XXX consider replacing with DXImageTransform */ - background-color: rgb(0.2, 0.2, 0.2); /* fallback for IE7-8 */ - - background-color: rgba(0, 0, 0, 0.7); -} - -#login-buttons, .accounts-dialog { - input[type=text], input[type=email], input[type=password] { - padding: 4px; - border: 1px solid #aaa; - border-radius: 3px; - line-height: 1; - } -} diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js index ae508a96de..b8b6caa59e 100644 --- a/packages/accounts-ui-unstyled/package.js +++ b/packages/accounts-ui-unstyled/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Unstyled version of login widgets', - version: '1.7.2', + version: '1.8.0', }); Package.onUse(function(api) { @@ -10,7 +10,7 @@ Package.onUse(function(api) { 'service-configuration', 'accounts-base', 'ecmascript', - 'templating@1.4.1', + 'templating@1.4.4', 'session', ], 'client' @@ -45,12 +45,11 @@ Package.onUse(function(api) { 'client' ); - // The less source defining the default style for accounts-ui. Just adding + // The CSS source defining the default style for accounts-ui. Just adding // this package doesn't actually apply these styles; they need to be - // `@import`ed from some non-import less file. The accounts-ui package does + // imported from another CSS file. The accounts-ui package does // that for you, or you can do it in your app. - api.use('less@3.0.2 || 4.0.0'); - api.addFiles('login_buttons.import.less'); + api.addFiles('login_buttons.import.css'); }); Package.onTest(api => { diff --git a/packages/accounts-ui/login_buttons.css b/packages/accounts-ui/login_buttons.css new file mode 100644 index 0000000000..90e06415d7 --- /dev/null +++ b/packages/accounts-ui/login_buttons.css @@ -0,0 +1,2 @@ +/* Import the CSS from accounts-ui-unstyled */ +@import url("{accounts-ui-unstyled}/login_buttons.import.css"); diff --git a/packages/accounts-ui/login_buttons.less b/packages/accounts-ui/login_buttons.less deleted file mode 100644 index 230e14d529..0000000000 --- a/packages/accounts-ui/login_buttons.less +++ /dev/null @@ -1 +0,0 @@ -@import "{accounts-ui-unstyled}/login_buttons.import.less"; diff --git a/packages/accounts-ui/package.js b/packages/accounts-ui/package.js index d31273bc30..714e4936c1 100644 --- a/packages/accounts-ui/package.js +++ b/packages/accounts-ui/package.js @@ -1,13 +1,12 @@ Package.describe({ summary: "Simple templates to add login widgets to an app", - version: '1.4.3', + version: '1.5.0', }); Package.onUse(api => { // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); api.use('accounts-ui-unstyled', 'client'); - api.use('less@3.0.2 || 4.0.0', 'client'); - api.addFiles(['login_buttons.less'], 'client'); + api.addFiles(['login_buttons.css'], 'client'); }); diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 5606e78665..2fdb836de0 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -101,17 +101,18 @@ let lastModifiedSwcConfigTime; BCp.initializeMeteorAppSwcrc = function () { const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`); const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`); - if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) { + const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${getMeteorAppDir()}/swc.config.ts`); + if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs && !hasSwcTs) { return; } - const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc'; + const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc'; const filePath = `${getMeteorAppDir()}/${swcFile}`; const fileStats = fs.statSync(filePath); const fileModTime = fileStats?.mtime?.getTime(); let currentLastModifiedConfigTime; - if (hasSwcJs) { - // For dynamic JS files, first get the resolved configuration + if (hasSwcJs || hasSwcTs) { + // For dynamic JS/TS files, first get the resolved configuration const resolvedConfig = lastModifiedSwcConfigTime?.includes(`${fileModTime}`) ? lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile) : getMeteorAppSwcrc(swcFile); @@ -142,16 +143,6 @@ BCp.initializeMeteorAppSwcrc = function () { return lastModifiedSwcConfig; }; -let lastModifiedSwcLegacyConfig; -BCp.initializeMeteorAppLegacyConfig = function () { - const swcLegacyConfig = convertBabelTargetsForSwc(Babel.getMinimumModernBrowserVersions()); - if (this.isVerbose() && !lastModifiedSwcLegacyConfig) { - logConfigBlock('SWC Legacy Config', swcLegacyConfig); - } - lastModifiedSwcLegacyConfig = swcLegacyConfig; - return lastModifiedSwcConfig; -}; - // Helper function to check if @swc/helpers is available function hasSwcHelpers() { return fs.existsSync(`${getMeteorAppDir()}/node_modules/@swc/helpers`); @@ -196,7 +187,6 @@ BCp.processFilesForTarget = function (inputFiles) { this.initializeMeteorAppConfig(); this.initializeMeteorAppSwcrc(); - this.initializeMeteorAppLegacyConfig(); this.initializeMeteorAppSwcHelpersAvailable(); inputFiles.forEach(function (inputFile) { @@ -242,6 +232,43 @@ BCp.processOneFileForTarget = function (inputFile, source) { sourceMap: null, bare: !! fileOptions.bare }; + const arch = inputFile.getArch(); + const isLegacyWebArch = arch.includes('legacy'); + + // Check if the file is a Rspack output file + // If it is, bypass SWC/Babel and just read the file and its map file + // as the contents are already transpiled by Rspack. + if (Plugin?.rspackHelpers?.isRspackOutputFile(inputFilePath) && !isLegacyWebArch) { + try { + // Get the full path to the file + const fullPath = inputFile.getPathInPackage(); + // Read the file directly + toBeAdded.data = source; + + // Try to read the corresponding map file + const mapPath = fullPath + '.map'; + if (fs.existsSync(mapPath)) { + const mapContent = fs.readFileSync(mapPath, 'utf8'); + toBeAdded.sourceMap = JSON.parse(mapContent); + } + + if (this.isVerbose()) { + const arch = inputFile.getArch(); + logTranspilation({ + usedRspack: true, + inputFilePath, + packageName, + cacheHit: true, + arch, + }); + } + + return toBeAdded; + } catch (e) { + // If there's an error reading the file or map, log it and continue with normal processing + console.error('Error reading Rspack file:', e); + } + } // If you need to exclude a specific file within a package from Babel // compilation, pass the { transpile: false } options to api.addFiles @@ -255,16 +282,16 @@ BCp.processOneFileForTarget = function (inputFile, source) { ! excludedFileExtensionPattern.test(inputFilePath)) { const features = Object.assign({}, this.extraFeatures); - const arch = inputFile.getArch(); - if (arch.startsWith("os.")) { + const isNodeTarget = arch.startsWith("os."); + if (isNodeTarget) { // Start with a much simpler set of Babel presets and plugins if // we're compiling for Node 8. features.nodeMajorVersion = parseInt(process.versions.node, 10); } else if (arch === "web.browser") { features.modernBrowsers = true; } else if (arch === "web.cordova") { - features.modernBrowsers = ! getMeteorConfig()?.cordova?.disableModern; + features.modernBrowsers = ! getMeteorConfig()?.modern?.cordova === false; } features.topLevelAwait = inputFile.supportsTopLevelAwait && @@ -331,8 +358,11 @@ BCp.processOneFileForTarget = function (inputFile, source) { tsx: hasTSXSupport, }, ...(hasSwcHelpersAvailable && + !isNodeTarget && (packageName == null || - !['modules-runtime'].includes(packageName)) && { + !['core-runtime', 'modules', 'modules-runtime'].includes( + packageName, + )) && { externalHelpers: true, }), }, @@ -342,13 +372,29 @@ BCp.processOneFileForTarget = function (inputFile, source) { filename, sourceFileName: filename, ...(isLegacyWebArch && { - env: { targets: lastModifiedSwcLegacyConfig || {} }, + env: { + targets: { + chrome: '49', + edge: '15', + firefox: '30', + safari: '10', + ios: '10', + android: '5', + opera: '42', + ie: '11', + node: '8', + electron: '1.6', + }, + mode: 'entry', + coreJs: '3.37', + }, }), }; // Merge with app-level SWC config if (lastModifiedSwcConfig) { swcOptions = deepMerge(swcOptions, lastModifiedSwcConfig, [ + 'jsc.target', 'env.targets', 'module.type', ]); @@ -374,7 +420,6 @@ BCp.processOneFileForTarget = function (inputFile, source) { const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/"); const isAppCode = packageName == null && !isNodeModulesCode; const isPackageCode = packageName != null; - const isLegacyWebArch = arch.includes('legacy'); const transpConfig = getMeteorConfig()?.modern?.transpiler; const hasModernTranspiler = transpConfig != null && transpConfig !== false; @@ -1029,8 +1074,37 @@ function getMeteorAppPackageJson() { function getMeteorAppSwcrc(file = '.swcrc') { try { const filePath = `${getMeteorAppDir()}/${file}`; - if (file.endsWith('.js')) { + if (file.endsWith('.js') || file.endsWith('.ts')) { let content = fs.readFileSync(filePath, 'utf-8'); + + if (file.endsWith('.ts')) { + try { + const swc = require('@meteorjs/swc-core'); + const result = swc.transformSync(content, { + jsc: { + parser: { + syntax: 'typescript', + }, + target: 'es2015', + }, + }); + content = result.code; + } catch (swcError) { + content = content + .replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+['"][^'"]+['"];?/g, '') + .replace(/export\s+default\s+/, 'module.exports = ') + .replace(/export\s+/g, '') + .replace(/:\s*\w+(\[\])?(\s*=)/g, '$2') + .replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)') + .replace(/\):\s*\w+(\[\])?\s*\{/g, ') {') + .replace(/interface\s+\w+\s*\{[^}]*\}/g, '') + .replace(/type\s+\w+\s*=\s*[^;]+;/g, '') + .replace(/as\s+\w+(\[\])?/g, ''); + } + } + // Check if the content uses ES module syntax (export default) if (content.includes('export default')) { // Transform ES module syntax to CommonJS @@ -1047,7 +1121,9 @@ function getMeteorAppSwcrc(file = '.swcrc') { })() `); const context = vm.createContext({ process }); - return script.runInContext(context); + const result = script.runInContext(context); + // Handle CJS interop wrapper (e.g. { __esModule: true, default: config }) + return result && result.__esModule && result.default ? result.default : result; } else { // For .swcrc and other JSON files, parse as JSON return JSON.parse(fs.readFileSync(filePath, 'utf-8')); @@ -1096,14 +1172,16 @@ function logTranspilation({ packageName, inputFilePath, usedSwc, + usedRspack, cacheHit, isNodeModulesCode, arch, errorMessage = '', tip = '', }) { - const transpiler = usedSwc ? 'SWC' : 'Babel'; - const transpilerColor = usedSwc ? 32 : 33; + let transpiler = usedSwc ? 'SWC' : 'Babel'; + transpiler = usedRspack ? 'Rspack' : transpiler; + const transpilerColor = usedSwc || usedRspack ? 32 : 33; const label = color('[Transpiler]', 36); const transpilerPart = `${label} Used ${color( transpiler, @@ -1126,7 +1204,7 @@ function logTranspilation({ : color(originPaddedRaw, 35); const cacheStatus = errorMessage ? color('⚠️ Fallback', 33) - : usedSwc + : usedSwc || usedRspack ? cacheHit ? color('🟢 Cache hit', 32) : color('🔴 Cache miss', 31) diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 2e69cdc028..b907fd3c85 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -1,14 +1,15 @@ Package.describe({ name: "babel-compiler", summary: "Parser/transpiler for ECMAScript 2015+ syntax", - version: '7.12.2', + version: '7.13.0', + devOnly: true, }); Npm.depends({ '@meteorjs/babel': '7.20.1', 'json5': '2.2.3', 'semver': '7.6.3', - "@meteorjs/swc-core": "1.12.14", + "@meteorjs/swc-core": "1.15.3", }); Package.onUse(function (api) { diff --git a/packages/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index de1d8f8e40..c068d9e490 100644 --- a/packages/boilerplate-generator/package.js +++ b/packages/boilerplate-generator/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Generates the boilerplate html from program's manifest", - version: '2.0.2', + version: '2.1.0', }); Npm.depends({ diff --git a/packages/boilerplate-generator/template-web.browser.js b/packages/boilerplate-generator/template-web.browser.js index 413151385d..dd0a920976 100644 --- a/packages/boilerplate-generator/template-web.browser.js +++ b/packages/boilerplate-generator/template-web.browser.js @@ -77,7 +77,11 @@ export const closeTemplate = ({ src: rootUrlPathPrefix + pathname, }) )), - + process.env.METEOR_APP_CUSTOM_SCRIPT_URL ? + template(" <script type=\"text/javascript\" src=\"<%- src %>\"></script>")({ + src: process.env.METEOR_APP_CUSTOM_SCRIPT_URL + }) + : '', '', '', '</body>', diff --git a/packages/check/check.d.ts b/packages/check/check.d.ts index 607528c3d2..84e2239ed0 100644 --- a/packages/check/check.d.ts +++ b/packages/check/check.d.ts @@ -66,6 +66,7 @@ export namespace Match { function Where<T>(condition: (val: any) => val is T): Matcher<T>; function Where(condition: (val: any) => boolean): Matcher<any>; + var NonEmptyString: Matcher<string>; /** * Returns true if the value matches the pattern. * @param value The value to check diff --git a/packages/check/match.js b/packages/check/match.js index d2836c701e..20f534c2e8 100644 --- a/packages/check/match.js +++ b/packages/check/match.js @@ -17,6 +17,11 @@ const format = result => { return err; } +function nonEmptyStringCondition(value) { + check(value, String); + return value.length > 0; +} + /** * @summary Check that a value matches a [pattern](#matchpatterns). * If the value does not match the pattern, throw a `Match.Error`. @@ -77,6 +82,8 @@ export const Match = { return new Where(condition); }, + NonEmptyString: ['__NonEmptyString__'], + ObjectIncluding: function(pattern) { return new ObjectIncluding(pattern) }, @@ -204,6 +211,7 @@ const stringForErrorMessage = (value, options = {}) => { return EJSON.stringify(value); }; + const typeofChecks = [ [String, 'string'], [Number, 'number'], @@ -283,6 +291,11 @@ const testSubtree = (value, pattern, collectErrors = false, errors = [], path = if (pattern === Object) { pattern = Match.ObjectIncluding({}); } + // This must be invoked before pattern instanceof Array as strings are regarded as arrays + // We invoke the pattern as IIFE so that `pattern isntanceof Where` catches it + if (pattern === Match.NonEmptyString) { + pattern = new Where(nonEmptyStringCondition); + } // Array (checked AFTER Any, which is implemented as an Array). if (pattern instanceof Array) { diff --git a/packages/check/match_test.js b/packages/check/match_test.js index 07771c9963..2aaa21cae7 100644 --- a/packages/check/match_test.js +++ b/packages/check/match_test.js @@ -175,7 +175,8 @@ Tinytest.add('check - check', test => { fails(true, false); fails(true, 'true'); fails('false', false); - + matches('xx', Match.NonEmptyString); + fails('', Match.NonEmptyString); matches(/foo/, RegExp); fails(/foo/, String); matches(new Date, Date); @@ -787,4 +788,4 @@ Tinytest.add( test.equal(new Match.ObjectIncluding(), Match.ObjectIncluding()); test.equal(new Match.ObjectWithValues(), Match.ObjectWithValues()); } -); +); \ No newline at end of file diff --git a/packages/check/package.js b/packages/check/package.js index 1f30b324c1..a095044bbb 100644 --- a/packages/check/package.js +++ b/packages/check/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Check whether a value matches a pattern', - version: '1.4.4', + version: '1.5.0', }); Package.onUse(api => { diff --git a/packages/ddp-client/common/livedata_connection.js b/packages/ddp-client/common/livedata_connection.js index 9755a8012a..03385b7b68 100644 --- a/packages/ddp-client/common/livedata_connection.js +++ b/packages/ddp-client/common/livedata_connection.js @@ -1120,27 +1120,6 @@ export class Connection { return Object.values(invokers).some((invoker) => !!invoker.sentMessage); } - async _processOneDataMessage(msg, updates) { - const messageType = msg.msg; - - // msg is one of ['added', 'changed', 'removed', 'ready', 'updated'] - if (messageType === 'added') { - await this._process_added(msg, updates); - } else if (messageType === 'changed') { - this._process_changed(msg, updates); - } else if (messageType === 'removed') { - this._process_removed(msg, updates); - } else if (messageType === 'ready') { - this._process_ready(msg, updates); - } else if (messageType === 'updated') { - this._process_updated(msg, updates); - } else if (messageType === 'nosub') { - // ignore this - } else { - Meteor._debug('discarding unknown livedata data message type', msg); - } - } - _prepareBuffersToFlush() { const self = this; if (self._bufferedWritesFlushHandle) { diff --git a/packages/ddp-server/livedata_server.js b/packages/ddp-server/livedata_server.js index 3f5efceafb..df167f94c4 100644 --- a/packages/ddp-server/livedata_server.js +++ b/packages/ddp-server/livedata_server.js @@ -1050,23 +1050,33 @@ Object.assign(Subscription.prototype, { // removed messages for the published objects; if that is necessary, call // _removeAllDocuments first. _deactivate: function() { - var self = this; - if (self._deactivated) + if (this._deactivated) return; - self._deactivated = true; - self._callStopCallbacks(); + this._deactivated = true; + this._callStopCallbacks().then(() => { + // Break reference chains to allow GC of the Session and its data. + // Without this, deactivated subscriptions retain live references + // to the (now-closed) session indefinitely. + this._session = null; + this._documents = new Map(); + }); Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact( "livedata", "subscriptions", -1); }, - _callStopCallbacks: function () { - var self = this; - // Tell listeners, so they can clean up - var callbacks = self._stopCallbacks; - self._stopCallbacks = []; - callbacks.forEach(function (callback) { - callback(); - }); + _callStopCallbacks: async function () { + // In Meteor 3, onStop callbacks can be async (e.g. observeHandle.stop() + // returns a Promise). We must await each one so that observer teardown + // completes before the subscription is considered fully deactivated. + const callbacks = this._stopCallbacks; + this._stopCallbacks = []; + for (const callback of callbacks) { + try { + await callback(); + } catch (e) { + Meteor._debug("Exception in onStop callback:", e); + } + } }, // Send remove messages for every document. @@ -1145,8 +1155,7 @@ Object.assign(Subscription.prototype, { // destroyed but the deferred call to _deactivateAllSubscriptions hasn't // happened yet. _isDeactivated: function () { - var self = this; - return self._deactivated || self._session.inQueue === null; + return this._deactivated || !this._session || this._session.inQueue === null; }, /** diff --git a/packages/ddp-server/livedata_server_async_tests.js b/packages/ddp-server/livedata_server_async_tests.js index 4ca4ca0864..3606b66044 100644 --- a/packages/ddp-server/livedata_server_async_tests.js +++ b/packages/ddp-server/livedata_server_async_tests.js @@ -168,9 +168,24 @@ Tinytest.addAsync('livedata server - async publish cursor', function( connection: clientConn, }); clientConn.subscribe('asyncPublishCursor', async () => { - const actual = await remoteCollection.find().fetch(); - test.equal(actual[0].name, 'async'); - onComplete(); + // Wait for data to arrive - the subscription is ready but data may still be in transit + // This can happen when a previous test run was interrupted (page reload) and the + // server is still processing the old session's grace period + let attempts = 0; + const maxAttempts = 50; // 5 seconds max wait + const checkData = async () => { + const actual = await remoteCollection.find().fetch(); + if (actual.length > 0) { + test.equal(actual[0].name, 'async'); + onComplete(); + } else if (attempts++ < maxAttempts) { + setTimeout(checkData, 100); + } else { + test.fail('Timed out waiting for data in async publish cursor test'); + onComplete(); + } + }; + await checkData(); }); }); }); diff --git a/packages/ddp-server/livedata_server_tests.js b/packages/ddp-server/livedata_server_tests.js index 15b0349e87..dbb49c63ee 100644 --- a/packages/ddp-server/livedata_server_tests.js +++ b/packages/ddp-server/livedata_server_tests.js @@ -593,4 +593,79 @@ function getTestConnections(test) { function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); -} \ No newline at end of file +} + +// ============================================================================ +// Async onStop cleanup tests (memory leak fix) +// ============================================================================ + +const asyncCleanupTracker = {}; + +Meteor.publish('test_async_onstop_cleanup', function (trackerId) { + this.onStop(async function () { + await new Promise(resolve => setTimeout(resolve, 50)); + asyncCleanupTracker[trackerId] = true; + }); + this.ready(); +}); + +Tinytest.addAsync( + 'livedata server - async onStop callbacks complete on unsubscribe', + async function (test) { + const trackerId = Random.id(); + asyncCleanupTracker[trackerId] = false; + + const { clientConn } = await getTestConnections(test); + const sub = clientConn.subscribe('test_async_onstop_cleanup', trackerId); + + await waitUntil( + () => sub.ready(), + { description: 'subscription is ready' } + ); + + sub.stop(); + + await waitUntil( + () => asyncCleanupTracker[trackerId] === true, + { description: 'async onStop callback completed after unsubscribe' } + ); + + test.isTrue( + asyncCleanupTracker[trackerId], + 'Async onStop callback should have completed' + ); + + clientConn.disconnect(); + delete asyncCleanupTracker[trackerId]; + } +); + +Tinytest.addAsync( + 'livedata server - async onStop callbacks complete on disconnect', + async function (test) { + const trackerId = Random.id(); + asyncCleanupTracker[trackerId] = false; + + const { clientConn } = await getTestConnections(test); + clientConn.subscribe('test_async_onstop_cleanup', trackerId); + + await waitUntil( + () => clientConn.status().connected, + { description: 'client is connected' } + ); + + clientConn.disconnect(); + + await waitUntil( + () => asyncCleanupTracker[trackerId] === true, + { description: 'async onStop callback completed after disconnect' } + ); + + test.isTrue( + asyncCleanupTracker[trackerId], + 'Async onStop callback should have completed on disconnect' + ); + + delete asyncCleanupTracker[trackerId]; + } +); \ No newline at end of file diff --git a/packages/ecmascript-runtime-client/legacy.js b/packages/ecmascript-runtime-client/legacy.js index 20a6d8fb3a..3d482c5438 100644 --- a/packages/ecmascript-runtime-client/legacy.js +++ b/packages/ecmascript-runtime-client/legacy.js @@ -1,8 +1,7 @@ try { - Symbol = exports.Symbol = require("core-js/es/symbol"); - Map = exports.Map = require("core-js/es/map"); - Set = exports.Set = require("core-js/es/set"); - + Symbol = exports.Symbol = global.Symbol || require("core-js/es/symbol"); + Map = exports.Map = global.Map || require("core-js/es/map"); + Set = exports.Set = global.Set || require("core-js/es/set"); } catch (e) { throw new Error([ "The core-js npm package could not be found in your node_modules ", diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index dcc86c7f0b..c975433f37 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.16.13', + version: '0.17.0', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/facebook-config-ui/facebook_configure.html b/packages/facebook-config-ui/facebook_configure.html index 6eca279ec5..678004abd0 100644 --- a/packages/facebook-config-ui/facebook_configure.html +++ b/packages/facebook-config-ui/facebook_configure.html @@ -7,46 +7,25 @@ Visit <a href="https://developers.facebook.com/apps" target="_blank">https://developers.facebook.com/apps</a> </li> <li> - Click "Add a New App". + Click "Create App" and fill out the required information. </li> <li> - Add a "Display Name" for your app and click on "Create App ID". + In "Use cases" select "Authenticate and request data from users with Facebook Login". </li> <li> - Answer the "Security Check" CAPTCHA and click on "Submit". + In the app dashboard, click "Add Product" and find "Facebook Login", then click "Set Up". </li> <li> - When the new app dashboard loads, click on "Settings" in the left hand menu. + Select "Web" as your platform. </li> <li> - From the top of the "Basic" settings page, note down your "App ID" and "App Secret" (you will be asked for them at the bottom of this popup). + In the "Facebook Login > Settings" from the left sidebar, set "Valid OAuth Redirect URIs" to <span class="url">{{siteUrl}}_oauth/facebook</span> and click "Save Changes". </li> <li> - Click on the "Add Platform" button, and select "Website". + Go to "Settings > Basic" in the left sidebar. </li> <li> - In the "Website" section, set the "Site URL" to <span class="url">{{siteUrl}}</span> and click on "Save Changes". - </li> - <li> - Click on "Add Product" in the left hand menu. - </li> - <li> - Hover over "Facebook Login", click on "Set Up". - </li> - <li> - Click on "Facebook Login > Settings" from the left hand menu. - </li> - <li> - Set "Valid OAuth redirect URIs" to <span class="url">{{siteUrl}}_oauth/facebook</span> and click on "Save Changes". - </li> - <li> - Select "App Review" from the left hand menu. - </li> - <li> - Toggle the "Make app public" switch to "Yes". - </li> - <li> - Select a "Category" in the "Make app public" popup and click on "Confirm". + Note down your "App ID" and "App Secret" (click "Show" to reveal the App Secret). You'll need these for configuration. </li> </ol> </template> diff --git a/packages/google-config-ui/google_configure.html b/packages/google-config-ui/google_configure.html index c8b4b2fb3a..a20a0e4ec2 100644 --- a/packages/google-config-ui/google_configure.html +++ b/packages/google-config-ui/google_configure.html @@ -4,28 +4,37 @@ </p> <ol> <li> - Visit <a href="https://console.developers.google.com/" target="blank">https://console.developers.google.com/</a> + Visit <a href="https://console.cloud.google.com/" target="blank">https://console.cloud.google.com/</a> </li> <li> - "Create Project", if needed. Wait for Google to finish provisioning. + Create a new project or select an existing one. </li> <li> - On the left sidebar, go to "Credentials" and, on the right, "OAuth consent screen". Make sure to enter an email address and a product name, and save. + In the left sidebar, go to "APIs & Services" > "OAuth consent screen". </li> <li> - On the left sidebar, go to "Credentials". Click the "Create credentials" button, then select "OAuth client ID" as the type. + Configure the consent screen: select "External" user type, enter your app name, user support email, and developer contact email, then click "Save and Continue". </li> <li> - Select "Web application" as your application type. + Skip the "Scopes" step (or add scopes if needed) and click "Save and Continue". </li> <li> - Set Authorized Javascript Origins to: <span class="url">{{siteUrl}}</span> + Add test users if needed, then click "Save and Continue". </li> <li> - Set Authorized Redirect URI to: <span class="url">{{siteUrl}}_oauth/google?close</span> + In the left sidebar, go to "Credentials" and click "Create Credentials" > "OAuth client ID". </li> <li> - Finish by clicking "Create". + Select "Web application" as the application type. + </li> + <li> + Add your site URL to "Authorized JavaScript origins": <span class="url">{{siteUrl}}</span> + </li> + <li> + Add to "Authorized redirect URIs": <span class="url">{{siteUrl}}_oauth/google</span> + </li> + <li> + Click "Create" and note down your "Client ID" and "Client Secret" from the popup. </li> </ol> </template> diff --git a/packages/meetup-config-ui/meetup_configure.html b/packages/meetup-config-ui/meetup_configure.html index 5433697dbd..a0a6b36d82 100644 --- a/packages/meetup-config-ui/meetup_configure.html +++ b/packages/meetup-config-ui/meetup_configure.html @@ -4,20 +4,25 @@ </p> <ol> <li> - Visit <a href="http://www.meetup.com/meetup_api/oauth_consumers/create/" target="blank">http://www.meetup.com/meetup_api/oauth_consumers/create/</a> + Visit <a href="https://www.meetup.com/api/oauth/list/" target="blank">https://www.meetup.com/api/oauth/list/</a> and sign in. </li> <li> - Click on "Create New Consumer". + Click "Create new client". </li> <li> - Set the Consumer name to the name of your application. + Set the "Client name" to the name of your application. </li> <li> - Optionally set the Application Website to the URL of your - website. You can leave this blank. + Set the "Application Website" to your site URL. </li> <li> - Set the <b>Redirect URI</b> to: <span class="url">{{siteUrl}}</span> (Do not append a path to this URL.) + Set the <b>Redirect URI</b> to: <span class="url">{{siteUrl}}</span> (Do not append a path to this URL.) + </li> + <li> + Fill out all the other required fields. + </li> + <li> + Click "Create" and note down your "Key" (Client ID) and "Secret" (Client Secret). </li> </ol> </template> diff --git a/packages/meteor-developer-config-ui/meteor_developer_configure.html b/packages/meteor-developer-config-ui/meteor_developer_configure.html index 6d34d867e6..592a11b8be 100644 --- a/packages/meteor-developer-config-ui/meteor_developer_configure.html +++ b/packages/meteor-developer-config-ui/meteor_developer_configure.html @@ -4,15 +4,17 @@ Follow these steps: </p> <ol> - <li> Visit <a href="https://www.meteor.com/account-settings" target="_blank">https://www.meteor.com/account-settings</a> and sign in. + <li> + Visit <a href="https://beta.galaxycloud.app/" target="_blank">https://beta.galaxycloud.app/</a> and sign in. </li> - <li> Click "NEW APPLICATION" in the "Meteor Account Services" section - and give your app a name.</li> - <li> Add - <span class="url"> - {{siteUrl}}_oauth/meteor-developer - </span> - as the Redirect URL. + <li> + Go to "Settings" -> "Authorized Domains" and "Add New Domain". + </li> + <li> + Set the "OAuth Redirect URL" to: <span class="url">{{siteUrl}}_oauth/meteor-developer</span> + </li> + <li> + Click "Create" and note down your "Client ID" and "Client Secret". </li> </ol> </template> diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index 00beae7bc0..f009d06cfb 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "The Meteor command-line tool", - version: "3.3.1", + version: "3.4.0", }); Package.includeTool(); diff --git a/packages/meteor/meteor.d.ts b/packages/meteor/meteor.d.ts index d14ec0d1ef..9edb9cff6d 100644 --- a/packages/meteor/meteor.d.ts +++ b/packages/meteor/meteor.d.ts @@ -1,6 +1,6 @@ -import { Mongo } from 'meteor/mongo'; -import { EJSONable, EJSONableProperty } from 'meteor/ejson'; -import { DDP } from 'meteor/ddp'; +import { Mongo } from "meteor/mongo"; +import { EJSONable, EJSONableProperty } from "meteor/ejson"; +import { DDP } from "meteor/ddp"; export type global_Error = Error; @@ -21,7 +21,7 @@ export namespace Meteor { var release: string; var meteorRelease: string; - + interface ErrorConstructor { new (...args: any[]): Error; errorType: string; @@ -181,7 +181,13 @@ export namespace Meteor { | EJSONable[] | EJSONableProperty | EJSONableProperty[] - >(name: string, ...args: any[]): Promise<Result> & { stubPromise: Promise<Result>, serverPromise: Promise<Result> }; + >( + name: string, + ...args: any[] + ): Promise<Result> & { + stubPromise: Promise<Result>; + serverPromise: Promise<Result>; + }; interface MethodApplyOptions< Result extends @@ -261,7 +267,10 @@ export namespace Meteor { error: global_Error | Meteor.Error | undefined, result?: Result ) => void - ): Promise<Result> & { stubPromise: Promise<Result>, serverPromise: Promise<Result> }; + ): Promise<Result> & { + stubPromise: Promise<Result>; + serverPromise: Promise<Result>; + }; /** Method **/ /** Url **/ @@ -317,6 +326,28 @@ export namespace Meteor { * @param func The function to run */ function defer(func: Function): void; + + /** + * Wrap a function so that it only runs in the specified environments. + * @param func The function to wrap + * @param options An object with an `on` property that is an array of environment names: `"development"`, `"production"`, and/or `"test"`. + */ + function deferrable<T extends Function>( + func: T, + options: { on: Array<"development" | "production" | "test"> } + ): T | void; + + /** + * Wrap a function so that it only runs in development environment. + * @param func The function to wrap + */ + function deferDev<T extends Function>(func: T): T | void; + + /** + * Wrap a function so that it only runs in production environment. + * @param func The function to wrap + */ + function deferProd<T extends Function>(func: T): T | void; /** Timeout **/ /** utils **/ @@ -336,7 +367,10 @@ export namespace Meteor { * @param func A function that takes a callback as its final parameter * @param context Optional `this` object against which the original function will be invoked */ - function wrapAsync<T extends Function>(func: T, context?: ThisParameterType<T>): Function; + function wrapAsync<T extends Function>( + func: T, + context?: ThisParameterType<T> + ): Function; function bindEnvironment<TFunc extends Function>(func: TFunc): TFunc; @@ -396,7 +430,7 @@ export namespace Meteor { * others can be set using Meteor's standard OAuth login parameters */ loginUrlParameters?: { include_granted_scopes: boolean; - }, + }; }, callback?: (error?: global_Error | Meteor.Error | Meteor.TypedError) => void ): void; @@ -440,7 +474,6 @@ export namespace Meteor { ): void; /** Login **/ - /** Connection **/ function reconnect(): void; @@ -518,7 +551,11 @@ export interface Subscription { * @param fields The fields in the document that have changed, together with their new values. If a field is not present in `fields` it was left unchanged; if it is present in `fields` and * has a value of `undefined` it was removed from the document. If `_id` is present it is ignored. */ - changed(collection: string, id: string, fields: Record<string, unknown>): void; + changed( + collection: string, + id: string, + fields: Record<string, unknown> + ): void; /** Access inside the publish function. The incoming connection for this subscription. */ connection: Meteor.Connection; /** diff --git a/packages/meteor/package.js b/packages/meteor/package.js index 9b7ca6575a..35bd3f00b6 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Core Meteor environment", - version: '2.1.1', + version: '2.2.0', }); Package.registerBuildPlugin({ diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index 9b0596bfa1..a6c5d25c0d 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -15,9 +15,8 @@ function withoutInvocation(f) { return function () { CurrentInvocation.withValue(null, f); }; - } else { - return f; } + return f; } function bindAndCatch(context, f) { @@ -56,7 +55,7 @@ Meteor.setInterval = function (f, duration) { * @locus Anywhere * @param {Object} id The handle returned by `Meteor.setInterval` */ -Meteor.clearInterval = function(x) { +Meteor.clearInterval = function (x) { return clearInterval(x); }; @@ -66,7 +65,7 @@ Meteor.clearInterval = function(x) { * @locus Anywhere * @param {Object} id The handle returned by `Meteor.setTimeout` */ -Meteor.clearTimeout = function(x) { +Meteor.clearTimeout = function (x) { return clearTimeout(x); }; @@ -84,3 +83,54 @@ Meteor.clearTimeout = function(x) { Meteor.defer = function (f) { Meteor._setImmediate(bindAndCatch("defer callback", f)); }; + +/** + * @memberOf Meteor + * @summary Defer execution of a function to run asynchronously in the background based on environment (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). + * @locus Anywhere + * @param {Function} func The function to run + * @param {Object} options The options object + * @param {Array<String>} options.on Condition to determine whether to defer the function, you can pass an array of environments ['development', 'production', 'test'] + */ +Meteor.deferrable = function (f, options) { + var on = (options && options.on) || []; + + // throw if on is not an array + if (!Array.isArray(on)) { + throw new Error("options.on must be an array"); + } + + var env = Meteor.isDevelopment + ? "development" + : Meteor.isProduction + ? "production" + : "test"; + + if (on.includes(env)) { + return Meteor.defer(f); + } + + return f(); +}; + +/** + * @memberOf Meteor + * @summary Defer execution of a function to run asynchronously in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). + * @locus Anywhere + * @param {Function} func The function to run + * @param {Object} options The options object + */ +Meteor.deferDev = function (f) { + return Meteor.deferrable(f, { on: ["development", "test"] }); +}; + +/** + * @memberOf Meteor + * @summary Defer execution of a function to run asynchronously in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)). + * @locus Anywhere + * @param {Function} func The function to run + * @param {Object} options The options object + */ +Meteor.deferProd = function (f) { + return Meteor.deferrable(f, { on: ["production"] }); +}; diff --git a/packages/meteor/timers_tests.js b/packages/meteor/timers_tests.js index 246f7e7b39..1db203dd00 100644 --- a/packages/meteor/timers_tests.js +++ b/packages/meteor/timers_tests.js @@ -1,21 +1,77 @@ -Tinytest.addAsync('timers - defer', function (test, onComplete) { - var x = 'a'; +Tinytest.addAsync("timers - defer", function (test, onComplete) { + let x = "a"; Meteor.defer(function () { - test.equal(x, 'b'); + test.equal(x, "b"); onComplete(); }); - x = 'b'; + x = "b"; }); -Tinytest.addAsync('timers - nested defer', function (test, onComplete) { - var x = 'a'; +Tinytest.addAsync("timers - nested defer", function (test, onComplete) { + let x = "a"; Meteor.defer(function () { - test.equal(x, 'b'); + test.equal(x, "b"); Meteor.defer(function () { - test.equal(x, 'c'); + test.equal(x, "c"); onComplete(); }); - x = 'c'; + x = "c"; }); - x = 'b'; + x = "b"; }); + +Tinytest.addAsync("timers - deferrable", function (test, onComplete) { + let x = "a"; + Meteor.deferrable( + function () { + test.equal(x, "b"); + onComplete(); + }, + { on: ["development", "production", "test"] } + ); + x = "b"; +}); + +Tinytest.addAsync( + "timers - deferrable not in current env", + function (test, onComplete) { + let x = "a"; + Meteor.deferrable( + function () { + x = "b"; + }, + { on: [] } + ); + test.equal(x, "b"); + onComplete(); + } +); + +Tinytest.addAsync( + "timers - deferrable works with async functions", + function (test, onComplete) { + let x = Meteor.deferrable( + function () { + return "start value"; + }, + { on: [] } + ); + test.equal(x, "start value"); + + Meteor.deferrable( + function () { + test.equal(x, "value"); + onComplete(); + }, + { on: ["development", "production", "test"] } + ); + + Meteor.deferrable( + async function () { + return "value"; + }, + { on: [] } + ).then((value) => (x = value)); + + } +); diff --git a/packages/minifier-js/package.js b/packages/minifier-js/package.js index 788cf783b6..15a6012d09 100644 --- a/packages/minifier-js/package.js +++ b/packages/minifier-js/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "JavaScript minifier", - version: '3.0.4', + version: '3.1.0', }); Npm.depends({ diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 7d564ca564..a532337674 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -176,7 +176,7 @@ export default class LocalCollection { const queriesToRecompute = []; // trigger live queries that match - for (const qid of Object.keys(this.queries)) { + for (const qid in this.queries) { const query = this.queries[qid]; if (query.dirty) { @@ -743,7 +743,7 @@ export default class LocalCollection { for (const id of specificIds) { const doc = this._docs.get(id); - if (doc && !fn(doc, id)) { + if (doc && fn(doc, id) === false) { break } } @@ -828,7 +828,7 @@ export default class LocalCollection { LocalCollection._modify(doc, mod, {arrayIndices}); const recomputeQids = {}; - for (const qid of Object.keys(this.queries)) { + for (const qid in this.queries) { const query = this.queries[qid]; if (query.dirty) { @@ -2293,11 +2293,12 @@ const NO_CREATE_MODIFIERS = { }; // Make sure field names do not contain Mongo restricted -// characters ('.', '$', '\0'). +// characters ('$', '\0') or invalid dot usage (leading/trailing/consecutive '.'). // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names const invalidCharMsg = { $: 'start with \'$\'', - '.': 'contain \'.\'', + '.': 'start or end with \'.\'', + '..': 'contain consecutive dots', '\0': 'contain null bytes' }; @@ -2313,7 +2314,7 @@ function assertHasValidFieldNames(doc) { function assertIsValidFieldName(key) { let match; - if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { + if (typeof key === 'string' && (match = key.match(/^\$|^\.|\.\.|\.$|^\.$|\0/))) { throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); } } diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 9801384815..65953b5f91 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -59,6 +59,36 @@ Tinytest.add('minimongo - wrapTransform', test => { handle.stop(); }); +Tinytest.add('minimongo - bulk remove with $in operator removes all matching documents', function(test) { + const coll = new LocalCollection(); + + // Insert multiple documents + const ids = ['id1', 'id2', 'id3', 'id4']; + ids.forEach(id => { + coll.insert({ _id: id, value: `item-${id}` }); + }); + + // Verify we have 4 documents + test.equal(coll.find().count(), 4); + + // Remove 2 documents using $in operator + const removedCount = coll.remove({ _id: { $in: ['id1', 'id2'] } }); + + // This should remove 2 documents, not just 1 + test.equal(removedCount, 2); + + // Verify only 2 documents remain + test.equal(coll.find().count(), 2); + + // Verify the correct documents were removed + test.isUndefined(coll.findOne('id1')); + test.isUndefined(coll.findOne('id2')); + + // Verify the other documents still exist + test.isNotUndefined(coll.findOne('id3')); + test.isNotUndefined(coll.findOne('id4')); +}); + if (Meteor.isClient) { Tinytest.add('minimongo - $geoIntersects should throw error', function(test) { const collection = new LocalCollection(); diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index 074705f33c..a0c5525bf6 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -2496,12 +2496,13 @@ Tinytest.addAsync('minimongo - modify', async test => { await modify({a: 12}, {}, {}); // tested against mongodb await modify({a: 12}, {a: 13}, {a: 13}); await modify({a: 12, b: 99}, {a: 13}, {a: 13}); + await modify({a: 12}, {b: {'a.b': 13}}, {b: {'a.b': 13}}); + await modify({_id: 1, a: 1}, {_id: 1, 'a.b': 2}, {_id: 1, 'a.b': 2}); await exception({a: 12}, {a: 13, $set: {b: 13}}); await exception({a: 12}, {$set: {b: 13}, a: 13}); await exception({a: 12}, {$a: 13}); // invalid operator await exception({a: 12}, {b: {$a: 13}}); - await exception({a: 12}, {b: {'a.b': 13}}); await exception({a: 12}, {b: {'\0a': 13}}); // keys @@ -2740,11 +2741,11 @@ Tinytest.addAsync('minimongo - modify', async test => { await exception({_id: 1}, {$set: {_id: 4}}); await modify({_id: 4}, {$set: {_id: 4}}, {_id: 4}); // not-changing _id is not bad // restricted field names + await modify({a: {}}, {$set: {a: {'a.b': 1}}}, {a: {'a.b': 1}}); await exception({a: {}}, {$set: {a: {$a: 1}}}); await exception({ a: {} }, { $set: { a: { c: [{ b: { $a: 1 } }] } } }); await exception({a: {}}, {$set: {a: {'\0a': 1}}}); - await exception({a: {}}, {$set: {a: {'a.b': 1}}}); // $unset await modify({}, {$unset: {a: 1}}, {}); @@ -2822,8 +2823,8 @@ Tinytest.addAsync('minimongo - modify', async test => { await exception({}, {$push: {'\0a': 1}}); await exception({}, {$push: {a: {$a: 1}}}); await exception({}, {$push: {a: {$each: [{$a: 1}]}}}); - await exception({}, {$push: {a: {$each: [{'a.b': 1}]}}}); await exception({}, {$push: {a: {$each: [{'\0a': 1}]}}}); + await modify({}, {$push: {a: {$each: [{'a.b': 1}]}}}, {a: [{'a.b': 1}]}); await modify({}, {$push: {a: {$each: [{'': 1}]}}}, {a: [ { '': 1 } ]}); await modify({}, {$push: {a: {$each: [{' ': 1}]}}}, {a: [ { ' ': 1 } ]}); await exception({}, {$push: {a: {$each: [{'.': 1}]}}}); @@ -2857,7 +2858,7 @@ Tinytest.addAsync('minimongo - modify', async test => { await modify({a: {}}, {$pushAll: {'a.x': []}}, {a: {x: []}}); await exception({a: [1]}, {$pushAll: {a: [{$a: 1}]}}); await exception({a: [1]}, {$pushAll: {a: [{'\0a': 1}]}}); - await exception({a: [1]}, {$pushAll: {a: [{'a.b': 1}]}}); + await modify({a: [1]}, {$pushAll: {a: [{'a.b': 1}]}}, {a: [1, {'a.b': 1}]}); // $addToSet await modify({}, {$addToSet: {a: 1}}, {a: [1]}); @@ -2883,14 +2884,16 @@ Tinytest.addAsync('minimongo - modify', async test => { // invalid field names await exception({}, {$addToSet: {a: {$b: 1}}}); - await exception({}, {$addToSet: {a: {'a.b': 1}}}); + await modify({}, {$addToSet: {a: {'a.b': 1}}}, {a: [{'a.b': 1}]}); await exception({}, {$addToSet: {a: {'a.': 1}}}); await exception({}, {$addToSet: {a: {'\u0000a': 1}}}); await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, {$a: 1}]}}}); await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, {'\0a': 1}]}}}); await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{$a: 1}]]}}}); - await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}); - await exception({a: [1, 2]}, {$addToSet: {a: {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}); + await modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}, + {a: [1, 2, 3, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}); + await modify({a: [1, 2]}, {$addToSet: {a: {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}, + {a: [1, 2, {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}]}); // $each is first element and thus an operator await modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}}, {a: [ 1, 2, 3, 4 ]}); // this should fail because $each is now a field name (not first in object) and thus invalid field name with $ @@ -2983,7 +2986,7 @@ Tinytest.addAsync('minimongo - modify', async test => { await upsertException({a: 0}, {$setOnInsert: {'\0a': 12}}); await upsert({a: 0}, {$setOnInsert: {b: {a: 1}}}, {a: 0, b: {a: 1}}); await upsertException({a: 0}, {$setOnInsert: {b: {$a: 1}}}); - await upsertException({a: 0}, {$setOnInsert: {b: {'a.b': 1}}}); + await upsert({a: 0}, {$setOnInsert: {b: {'a.b': 1}}}, {a: 0, b: {'a.b': 1}}); await upsertException({a: 0}, {$setOnInsert: {b: {'\0a': 1}}}); // Test for https://github.com/meteor/meteor/issues/8775. @@ -3919,7 +3922,7 @@ Tinytest.add('minimongo - reactive skip/limit count while updating', test => { }); // Makes sure inserts cannot be performed using field names that have -// Mongo restricted characters in them ('.', '$', '\0'): +// Mongo restricted characters in them ('$', '\0'): // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names Tinytest.add('minimongo - cannot insert using invalid field names', test => { const collection = new LocalCollection(); @@ -3930,18 +3933,13 @@ Tinytest.add('minimongo - cannot insert using invalid field names', test => { // Quick test to make sure field values with dots are allowed collection.insert({ a: 'b.c' }); - // Verify top level dot-field inserts are prohibited - ['a.b', '.b', 'a.', 'a.b.c'].forEach((field) => { + // Verify invalid dot patterns are rejected: leading dot, trailing dot, consecutive dots + ['.b', 'a.', '.', 'a..b', '. ', ' .', '...', 'a...b'].forEach((field) => { test.throws(() => { collection.insert({ [field]: 'c' }); - }, `Key ${field} must not contain '.'`); + }, `Key ${field}`); }); - // Verify nested dot-field inserts are prohibited - test.throws(() => { - collection.insert({ a: { b: { 'c.d': 'e' } } }); - }, "Key c.d must not contain '.'"); - // Verify field names starting with $ are prohibited test.throws(() => { collection.insert({ $a: 'b' }); @@ -3965,6 +3963,49 @@ Tinytest.add('minimongo - cannot insert using invalid field names', test => { }, 'Key \0c must not contain null bytes'); }); +// Verify that valid dotted field names are allowed (MongoDB 3.6+) +Tinytest.add('minimongo - can insert using valid dotted field names', test => { + const collection = new LocalCollection(); + + // Verify dotted field names work + ['a.b', 'a.b.c'].forEach((field) => { + const id = collection.insert({ [field]: 'd' }); + const doc = collection.findOne(id); + test.equal(doc[field], 'd', `Field ${field} should be allowed`); + collection.remove(id); + }); + + // Verify dotted fields in nested objects work + const id2 = collection.insert({ + nested: { + 'a.b': 'c', + 'a.b.c': 'd' + } + }); + const doc2 = collection.findOne(id2); + test.equal(doc2.nested['a.b'], 'c'); + test.equal(doc2.nested['a.b.c'], 'd'); + + // Verify update operations work with dotted field names in values + const id3 = collection.insert({ a: 'b' }); + collection.update(id3, { $set: { b: { 'a.b': 'c' } } }); + const doc3 = collection.findOne(id3); + test.equal(doc3.b['a.b'], 'c'); + + // Verify distinction: path semantics vs literal dotted keys + const id4 = collection.insert({ x: {} }); + // This uses path semantics - creates nested structure + collection.update(id4, { $set: { 'x.y': 1 } }); + const doc4a = collection.findOne(id4); + test.equal(doc4a.x.y, 1, 'Path semantics should create nested structure'); + + // This uses literal key - dot is part of the key name + collection.update(id4, { $set: { z: { 'x.y': 2 } } }); + const doc4b = collection.findOne(id4); + test.equal(doc4b.z['x.y'], 2, 'Literal key should store dot as part of name'); + test.equal(doc4b.z.x, undefined, 'Literal key should not create nesting'); +}); + // Makes sure $set's cannot be performed using null bytes // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names Tinytest.add('minimongo - cannot $set with null bytes', test => { @@ -4061,4 +4102,4 @@ Tinytest.addAsync('minimongo - operation result fields (async)', async test => { // Test remove const removeResult = await c.removeAsync({name: 'doc1'}); test.equal(removeResult, 1, 'remove should return removed count'); -}); +}); \ No newline at end of file diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index b3ff2000be..42ffdf3101 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", - version: "2.0.4", + version: "2.0.5", }); Package.onUse((api) => { diff --git a/packages/mongo/collection/collection.js b/packages/mongo/collection/collection.js index f41f97a68c..23cb81f7f0 100644 --- a/packages/mongo/collection/collection.js +++ b/packages/mongo/collection/collection.js @@ -62,8 +62,15 @@ Mongo.Collection = function Collection(name, options) { setupAutopublish(this, name, options); Mongo._collections.set(name, this); + + // Apply collection extensions + CollectionExtensions._applyExtensions(this, name, options); }; +// Apply static methods to the Collection constructor +CollectionExtensions._applyStaticMethods(Mongo.Collection); + + Object.assign(Mongo.Collection.prototype, { _getFindSelector(args) { if (args.length == 0) return {}; @@ -153,6 +160,118 @@ Object.assign(Mongo.Collection, { return selector; }, + + // Collection Extensions API - delegate to CollectionExtensions + /** + * @summary Add a constructor extension function that runs when collections are created. + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @param {Function} extension Extension function called with (name, options) and 'this' bound to collection instance + */ + addExtension(extension) { + return CollectionExtensions.addExtension(extension); + }, + + /** + * @summary Add a prototype method to all collection instances. + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @param {String} name The name of the method to add + * @param {Function} method The method function, bound to the collection instance + */ + addPrototypeMethod(name, method) { + return CollectionExtensions.addPrototypeMethod(name, method); + }, + + /** + * @summary Add a static method to the Mongo.Collection constructor. + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @param {String} name The name of the static method to add + * @param {Function} method The static method function + */ + addStaticMethod(name, method) { + return CollectionExtensions.addStaticMethod(name, method); + }, + + /** + * @summary Remove a constructor extension (useful for testing). + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @param {Function} extension The extension function to remove + */ + removeExtension(extension) { + return CollectionExtensions.removeExtension(extension); + }, + + /** + * @summary Remove a prototype method from all collection instances. + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @param {String} name The name of the method to remove + */ + removePrototypeMethod(name) { + return CollectionExtensions.removePrototypeMethod(name); + }, + + /** + * @summary Remove a static method from the Mongo.Collection constructor. + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @param {String} name The name of the static method to remove + */ + removeStaticMethod(name) { + return CollectionExtensions.removeStaticMethod(name); + }, + + /** + * @summary Clear all extensions, prototype methods, and static methods (useful for testing). + * @locus Anywhere + * @memberof Mongo.Collection + * @static + */ + clearExtensions() { + return CollectionExtensions.clearExtensions(); + }, + + /** + * @summary Get all registered constructor extensions (useful for debugging). + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @returns {Array<Function>} Array of registered extension functions + */ + getExtensions() { + return CollectionExtensions.getExtensions(); + }, + + /** + * @summary Get all registered prototype methods (useful for debugging). + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @returns {Map<String, Function>} Map of method names to functions + */ + getPrototypeMethods() { + return CollectionExtensions.getPrototypeMethods(); + }, + + /** + * @summary Get all registered static methods (useful for debugging). + * @locus Anywhere + * @memberof Mongo.Collection + * @static + * @returns {Map<String, Function>} Map of method names to functions + */ + getStaticMethods() { + return CollectionExtensions.getStaticMethods(); + } }); Object.assign(Mongo.Collection.prototype, ReplicationMethods, SyncMethods, AsyncMethods, IndexMethods); @@ -230,6 +349,13 @@ Object.assign(Mongo, { * @protected */ _collections: new Map(), + + /** + * @summary Collection Extensions API + * @memberof Mongo + * @static + */ + CollectionExtensions: CollectionExtensions }) diff --git a/packages/mongo/collection/collection_extensions.js b/packages/mongo/collection/collection_extensions.js new file mode 100644 index 0000000000..c259f9a4f2 --- /dev/null +++ b/packages/mongo/collection/collection_extensions.js @@ -0,0 +1,146 @@ +/** + * Collection Extensions System + * + * Provides a clean way to extend Mongo.Collection functionality + * without monkey patching. Supports constructor extensions, + * prototype methods, and static methods. + */ + +if (Package['lai:collection-extensions']) { + console.warn('lai:collection-extensions is not deprecated. Use Mongo.Collection.addExtension instead.'); +} + +CollectionExtensions = { + _extensions: [], + _prototypeMethods: new Map(), + _staticMethods: new Map(), + + /** + * Add a constructor extension function + * Extension function is called with (name, options) and 'this' bound to collection instance + */ + addExtension(extension) { + if (typeof extension !== 'function') { + throw new Error('Extension must be a function'); + } + this._extensions.push(extension); + }, + + /** + * Add a prototype method to all collection instances + * Method is bound to the collection instance + */ + addPrototypeMethod(name, method) { + if (typeof name !== 'string' || !name) { + throw new Error('Prototype method name must be a non-empty string'); + } + if (typeof method !== 'function') { + throw new Error('Prototype method must be a function'); + } + + this._prototypeMethods.set(name, method); + }, + + /** + * Add a static method to the Mongo.Collection constructor + */ + addStaticMethod(name, method) { + if (typeof name !== 'string' || !name) { + throw new Error('Static method name must be a non-empty string'); + } + if (typeof method !== 'function') { + throw new Error('Static method must be a function'); + } + + this._staticMethods.set(name, method); + }, + + /** + * Remove an extension (useful for testing) + */ + removeExtension(extension) { + const index = this._extensions.indexOf(extension); + if (index > -1) { + this._extensions.splice(index, 1); + } + }, + + /** + * Remove a prototype method + */ + removePrototypeMethod(name) { + this._prototypeMethods.delete(name); + }, + + /** + * Remove a static method + */ + removeStaticMethod(name) { + this._staticMethods.delete(name); + }, + + /** + * Clear all extensions (useful for testing) + */ + clearExtensions() { + this._extensions.length = 0; + this._prototypeMethods.clear(); + this._staticMethods.clear(); + }, + + /** + * Get all registered extensions (useful for debugging) + */ + getExtensions() { + return [...this._extensions]; + }, + + /** + * Get all registered prototype methods (useful for debugging) + */ + getPrototypeMethods() { + return new Map(this._prototypeMethods); + }, + + /** + * Get all registered static methods (useful for debugging) + */ + getStaticMethods() { + return new Map(this._staticMethods); + }, + + + + /** + * Apply all extensions to a collection instance + * Called during collection construction + */ + _applyExtensions(instance, name, options) { + // Apply constructor extensions + for (const extension of this._extensions) { + try { + extension.call(instance, name, options); + } catch (error) { + // Provide helpful error context + throw new Error(`Extension failed for collection '${name}': ${error.message}`); + } + } + + // Apply prototype methods + for (const [methodName, method] of this._prototypeMethods) { + instance[methodName] = method.bind(instance); + } + }, + + /** + * Apply static methods to the Mongo.Collection constructor + * Called during package initialization + */ + _applyStaticMethods(CollectionConstructor) { + for (const [methodName, method] of this._staticMethods) { + CollectionConstructor[methodName] = method; + } + }, + + +}; \ No newline at end of file diff --git a/packages/mongo/mongo.d.ts b/packages/mongo/mongo.d.ts index 2f3c941732..4f0ccce29a 100644 --- a/packages/mongo/mongo.d.ts +++ b/packages/mongo/mongo.d.ts @@ -53,6 +53,50 @@ export namespace Mongo { ? T : U; + /** + * Configuration options for Mongo Collection constructor + */ + interface CollectionOptions<T = any, U = T> { + /** + * The server connection that will manage this collection. Uses the default connection if not specified. + * Pass the return value of calling `DDP.connect` to specify a different server. Pass `null` to specify + * no connection. Unmanaged (`name` is null) collections cannot specify a connection. + */ + connection?: DDP.DDPStatic | null | undefined; + + /** + * The method of generating the `_id` fields of new documents in this collection. Possible values: + * - **`'STRING'`**: random strings + * - **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values + * + * The default id generation technique is `'STRING'`. + */ + idGeneration?: string | undefined; + + /** + * An optional transformation function. Documents will be passed through this function before being + * returned from `fetch` or `findOne`, and before being passed to callbacks of `observe`, `map`, + * `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges` + * or to cursors returned from publish functions. + */ + transform?: (doc: T) => U; + + /** + * Set to `false` to skip setting up the mutation methods that enable insert/update/remove from client code. + * Default `true`. + */ + defineMutationMethods?: boolean | undefined; + + // Internal options (from normalizeOptions function) + /** @internal */ + _driver?: any; + /** @internal */ + _preventAutopublish?: boolean; + + // Allow additional properties for extensibility + [key: string]: any; + } + var Collection: CollectionStatic; interface CollectionStatic { /** @@ -61,27 +105,7 @@ export namespace Mongo { */ new <T extends NpmModuleMongodb.Document, U = T>( name: string | null, - options?: { - /** - * The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling `DDP.connect` to specify a different - * server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection. - */ - connection?: DDP.DDPStatic | null | undefined; - /** The method of generating the `_id` fields of new documents in this collection. Possible values: - * - **`'STRING'`**: random strings - * - **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values - * - * The default id generation technique is `'STRING'`. - */ - idGeneration?: string | undefined; - /** - * An optional transformation function. Documents will be passed through this function before being returned from `fetch` or `findOne`, and before being passed to callbacks of - * `observe`, `map`, `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges` or to cursors returned from publish functions. - */ - transform?: (doc: T) => U; - /** Set to `false` to skip setting up the mutation methods that enable insert/update/remove from client code. Default `true`. */ - defineMutationMethods?: boolean | undefined; - } + options?: CollectionOptions<T, U> ): Collection<T, U>; /** @@ -92,6 +116,68 @@ export namespace Mongo { getCollection< TCollection extends Collection<any, any> | undefined = Collection<NpmModuleMongodb.Document> | undefined >(name: string): TCollection; + + // Collection Extensions API + /** + * Add a constructor extension function that runs when collections are created. + * @param extension Extension function called with (name, options) and 'this' bound to collection instance + */ + addExtension<T extends NpmModuleMongodb.Document, U = T>(extension: (this: Collection<T, U>, name: string | null, options?: CollectionOptions<T, U>) => void): void; + + /** + * Add a prototype method to all collection instances. + * @param name The name of the method to add + * @param method The method function, bound to the collection instance + */ + addPrototypeMethod<T extends NpmModuleMongodb.Document, U = T>(name: string, method: (this: Collection<T, U>, ...args: any[]) => any): void; + + /** + * Add a static method to the Mongo.Collection constructor. + * @param name The name of the static method to add + * @param method The static method function + */ + addStaticMethod(name: string, method: Function): void; + + /** + * Remove a constructor extension (useful for testing). + * @param extension The extension function to remove + */ + removeExtension(extension: Function): void; + + /** + * Remove a prototype method from all collection instances. + * @param name The name of the method to remove + */ + removePrototypeMethod(name: string): void; + + /** + * Remove a static method from the Mongo.Collection constructor. + * @param name The name of the static method to remove + */ + removeStaticMethod(name: string): void; + + /** + * Clear all extensions, prototype methods, and static methods (useful for testing). + */ + clearExtensions(): void; + + /** + * Get all registered constructor extensions (useful for debugging). + * @returns Array of registered extension functions + */ + getExtensions(): Array<Function>; + + /** + * Get all registered prototype methods (useful for debugging). + * @returns Map of method names to functions + */ + getPrototypeMethods(): Map<string, Function>; + + /** + * Get all registered static methods (useful for debugging). + * @returns Map of method names to functions + */ + getStaticMethods(): Map<string, Function>; } interface Collection<T extends NpmModuleMongodb.Document, U = T> { allow<Fn extends Transform<T> = undefined>(options: { @@ -452,8 +538,8 @@ export namespace Mongo { callbacks: ObserveChangesCallbacks<T>, options?: { nonMutatingCallbacks?: boolean | undefined } ): Meteor.LiveQueryHandle; - [Symbol.iterator](): Iterator<T>; - [Symbol.asyncIterator](): AsyncIterator<T>; + [Symbol.iterator](): Iterator<U>; + [Symbol.asyncIterator](): AsyncIterator<U>; /** * Watch a query. Receive callbacks as the result set changes. Only the differences between the old and new documents are passed to the callbacks. * @param callbacks Functions to call to deliver the result set as it changes @@ -479,6 +565,87 @@ export namespace Mongo { equals(otherID: ObjectID): boolean; } + /** + * Collection Extensions API + */ + interface CollectionExtensions { + /** + * Add a constructor extension function that runs when collections are created. + * @param extension Extension function called with (name, options) and 'this' bound to collection instance + */ + addExtension<T extends NpmModuleMongodb.Document, U = T>(extension: (this: Collection<T, U>, name: string | null, options?: CollectionOptions<T, U>) => void): void; + + /** + * Add a prototype method to all collection instances. + * @param name The name of the method to add + * @param method The method function, bound to the collection instance + */ + addPrototypeMethod<T extends NpmModuleMongodb.Document, U = T>(name: string, method: (this: Collection<T, U>, ...args: any[]) => any): void; + + /** + * Add a static method to the Mongo.Collection constructor. + * @param name The name of the static method to add + * @param method The static method function + */ + addStaticMethod(name: string, method: Function): void; + + /** + * Remove a constructor extension (useful for testing). + * @param extension The extension function to remove + */ + removeExtension(extension: Function): void; + + /** + * Remove a prototype method from all collection instances. + * @param name The name of the method to remove + */ + removePrototypeMethod(name: string): void; + + /** + * Remove a static method from the Mongo.Collection constructor. + * @param name The name of the static method to remove + */ + removeStaticMethod(name: string): void; + + /** + * Clear all extensions, prototype methods, and static methods (useful for testing). + */ + clearExtensions(): void; + + /** + * Get all registered constructor extensions (useful for debugging). + * @returns Array of registered extension functions + */ + getExtensions(): Array<Function>; + + /** + * Get all registered prototype methods (useful for debugging). + * @returns Map of method names to functions + */ + getPrototypeMethods(): Map<string, Function>; + + /** + * Get all registered static methods (useful for debugging). + * @returns Map of method names to functions + */ + getStaticMethods(): Map<string, Function>; + } + + var CollectionExtensions: CollectionExtensions; + + /** + * Retrieve a Meteor collection instance by name. Only collections defined with `new Mongo.Collection(...)` are available with this method. + * @param name Name of your collection as it was defined with `new Mongo.Collection()`. + * @returns The collection instance or undefined if not found + */ + function getCollection<T extends Collection<any, any> | undefined = Collection<NpmModuleMongodb.Document> | undefined>(name: string): T; + + /** + * A record of all defined Mongo.Collection instances, indexed by collection name. + * @internal + */ + var _collections: Map<string, Collection<any, any>>; + function setConnectionOptions(options: any): void; } diff --git a/packages/mongo/oplog_tailing.ts b/packages/mongo/oplog_tailing.ts index 2df4bf7aaa..1a84fd2c16 100644 --- a/packages/mongo/oplog_tailing.ts +++ b/packages/mongo/oplog_tailing.ts @@ -41,6 +41,8 @@ export class OplogHandle { excludeCollections?: string[]; includeCollections?: string[]; }; + private _includeNSRegex?: RegExp; + private _excludeNSRegex?: RegExp; private _stopped: boolean; private _tailHandle: any; private _readyPromiseResolver: (() => void) | null; @@ -82,6 +84,18 @@ export class OplogHandle { } this._oplogOptions = { includeCollections, excludeCollections }; + if (includeCollections?.length) { + const incAlt = includeCollections.map((c) => Meteor._escapeRegExp(c)).join('|'); + + this._includeNSRegex = new RegExp(`^${Meteor._escapeRegExp(this._dbName)}\\.(?:${incAlt})$`); + } + + if (excludeCollections?.length) { + const excAlt = excludeCollections.map((c) => Meteor._escapeRegExp(c)).join('|'); + + this._excludeNSRegex = new RegExp(`^${Meteor._escapeRegExp(this._dbName)}\\.(?:${excAlt})$`); + } + this._catchingUpResolvers = []; this._lastProcessedTS = null; @@ -92,6 +106,15 @@ export class OplogHandle { this._startTrailingPromise = this._startTailing(); } + private _nsAllowed(ns: string | undefined): boolean { + if (!ns) return false; + if (ns === 'admin.$cmd') return true; + if (this._includeNSRegex && !this._includeNSRegex.test(ns)) return false; + if (this._excludeNSRegex && this._excludeNSRegex.test(ns)) return false; + + return true; + } + private _getOplogSelector(lastProcessedTS?: any): any { const oplogCriteria: any = [ { @@ -104,40 +127,55 @@ export class OplogHandle { }, ]; - const nsRegex = new RegExp( - "^(?:" + - [ - // @ts-ignore - Meteor._escapeRegExp(this._dbName + "."), - // @ts-ignore - Meteor._escapeRegExp("admin.$cmd"), - ].join("|") + - ")" - ); - if (this._oplogOptions.excludeCollections?.length) { - oplogCriteria.push({ - ns: { - $regex: nsRegex, - $nin: this._oplogOptions.excludeCollections.map( - (collName: string) => `${this._dbName}.${collName}` - ), - }, - }); - } else if (this._oplogOptions.includeCollections?.length) { + const nsRegex = new RegExp( + '^(?:' + + [ + // @ts-ignore + Meteor._escapeRegExp(this._dbName + '.'), + ].join('|') + + ')' + ); + const excludeNs = { + $regex: nsRegex, + $nin: this._oplogOptions.excludeCollections.map( + (collName: string) => `${this._dbName}.${collName}` + ), + }; oplogCriteria.push({ $or: [ - { ns: /^admin\.\$cmd/ }, + { ns: excludeNs }, { - ns: { - $in: this._oplogOptions.includeCollections.map( - (collName: string) => `${this._dbName}.${collName}` - ), - }, + ns: /^admin\.\$cmd/, + 'o.applyOps': { $elemMatch: { ns: excludeNs } }, }, ], }); + } else if (this._oplogOptions.includeCollections?.length) { + const includeNs = { + $in: this._oplogOptions.includeCollections.map( + (collName: string) => `${this._dbName}.${collName}` + ), + }; + oplogCriteria.push({ + $or: [ + { + ns: includeNs, + }, + { ns: /^admin\.\$cmd/, 'o.applyOps.ns': includeNs }, + ], + }); } else { + const nsRegex = new RegExp( + "^(?:" + + [ + // @ts-ignore + Meteor._escapeRegExp(this._dbName + "."), + // @ts-ignore + Meteor._escapeRegExp("admin.$cmd"), + ].join("|") + + ")" + ); oplogCriteria.push({ ns: nsRegex, }); @@ -411,6 +449,11 @@ async function handleDoc(handle: OplogHandle, doc: OplogEntry): Promise<void> { op.ts = nextTimestamp; nextTimestamp = nextTimestamp.add(Long.ONE); } + // Only forward sub-ops whose ns is allowed + // See https://github.com/meteor/meteor/issues/13945 + if (!handle['_nsAllowed'](op.ns)) { + continue; + } await handleDoc(handle, op); } return; diff --git a/packages/mongo/package.js b/packages/mongo/package.js index c0ff30242f..46a5776273 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -9,7 +9,7 @@ Package.describe({ summary: "Adaptor for using MongoDB and Minimongo over DDP", - version: "2.1.4", + version: "2.2.0", }); Npm.depends({ @@ -79,6 +79,7 @@ Package.onUse(function (api) { api.export("MongoInternals", "server"); api.export("Mongo"); + api.export("CollectionExtensions"); api.export("ObserveMultiplexer", "server", { testOnly: true }); api.addFiles( @@ -100,6 +101,7 @@ Package.onUse(function (api) { ); api.addFiles("local_collection_driver.js", ["client", "server"]); api.addFiles("remote_collection_driver.ts", "server"); + api.addFiles("collection/collection_extensions.js", ["client", "server"]); api.addFiles("collection/collection.js", ["client", "server"]); api.addFiles("connection_options.ts", "server"); // For zodern:types to pick up our published types. @@ -130,6 +132,7 @@ Package.onTest(function (api) { api.addFiles("tests/collection_tests.js", ["client", "server"]); api.addFiles("tests/collection_async_tests.js", ["client", "server"]); api.addFiles("tests/observe_changes_tests.js", ["client", "server"]); + api.addFiles("tests/collection_extensions_tests.js", ["client", "server"]); api.addFiles("tests/oplog_tests.js", "server"); api.addFiles("tests/oplog_v2_converter_tests.js", "server"); api.addFiles("tests/doc_fetcher_tests.js", "server"); diff --git a/packages/mongo/tests/collection_extensions_tests.js b/packages/mongo/tests/collection_extensions_tests.js new file mode 100644 index 0000000000..0a6e9dc3c5 --- /dev/null +++ b/packages/mongo/tests/collection_extensions_tests.js @@ -0,0 +1,233 @@ +import { Tinytest } from "meteor/tinytest"; +import { Mongo } from "meteor/mongo"; +import { CollectionExtensions } from "meteor/mongo"; +import { Random } from "meteor/random"; + +// Test setup and teardown +function setupTest() { + CollectionExtensions.clearExtensions(); +} + +function teardownTest() { + CollectionExtensions.clearExtensions(); +} + +Tinytest.add("CollectionExtensions - constructor extension", function (test) { + setupTest(); + + let extensionCallCount = 0; + let extensionData = null; + + CollectionExtensions.addExtension(function(name, options) { + extensionCallCount++; + extensionData = { name, options, instance: this }; + }); + + const testCollection = new Mongo.Collection(Random.id()); + + test.equal(extensionCallCount, 1); + test.equal(extensionData.name, testCollection._name); + test.equal(extensionData.instance, testCollection); + test.isTrue(extensionData.options && typeof extensionData.options === 'object'); + + teardownTest(); +}); + +Tinytest.add("CollectionExtensions - multiple extensions", function (test) { + setupTest(); + + let callOrder = []; + + CollectionExtensions.addExtension(function(name, options) { + callOrder.push('extension1'); + }); + + CollectionExtensions.addExtension(function(name, options) { + callOrder.push('extension2'); + }); + + CollectionExtensions.addExtension(function(name, options) { + callOrder.push('extension3'); + }); + + const testCollection = new Mongo.Collection(Random.id()); + + test.equal(callOrder, ['extension1', 'extension2', 'extension3']); + + teardownTest(); +}); + +Tinytest.add("CollectionExtensions - prototype methods", function (test) { + setupTest(); + + CollectionExtensions.addPrototypeMethod('testMethod', function() { + return 'testResult'; + }); + + const testCollection = new Mongo.Collection(Random.id()); + + test.isTrue(typeof testCollection.testMethod === 'function'); + test.equal(testCollection.testMethod(), 'testResult'); + + teardownTest(); +}); + +// Test prototype method with collection context +Tinytest.add("CollectionExtensions - prototype method context", function (test) { + setupTest(); + + // Add prototype method that uses collection context + CollectionExtensions.addPrototypeMethod('getCollectionName', function() { + return this._name; + }); + + // Create collection + const testCollection = new Mongo.Collection(Random.id()); + + // Verify method has correct context + test.equal(testCollection.getCollectionName(), testCollection._name); + + teardownTest(); +}); + +// Test static methods +Tinytest.add("CollectionExtensions - static methods", function (test) { + setupTest(); + + // Add static method + CollectionExtensions.addStaticMethod('testStaticMethod', function() { + return 'staticResult'; + }); + + // Apply static methods (this happens automatically in real usage) + CollectionExtensions._applyStaticMethods(Mongo.Collection); + + // Verify static method was added + test.isTrue(typeof Mongo.Collection.testStaticMethod === 'function'); + test.equal(Mongo.Collection.testStaticMethod(), 'staticResult'); + + // Clean up + delete Mongo.Collection.testStaticMethod; + teardownTest(); +}); + +// Test error handling in extensions +Tinytest.add("CollectionExtensions - extension error handling", function (test) { + setupTest(); + + // Add extension that throws error + CollectionExtensions.addExtension(function(name, options) { + throw new Error('Test extension error'); + }); + + // Creating collection should throw with helpful error message + test.throws(() => { + new Mongo.Collection(Random.id()); + }, /Extension failed for collection/); + + teardownTest(); +}); + +// Test extension removal +Tinytest.add("CollectionExtensions - extension removal", function (test) { + setupTest(); + + let callCount = 0; + + const extension = function(name, options) { + callCount++; + }; + + CollectionExtensions.addExtension(extension); + + const testCollection1 = new Mongo.Collection(Random.id()); + test.equal(callCount, 1); + + CollectionExtensions.removeExtension(extension); + + // Create another collection - should not call extension + const testCollection2 = new Mongo.Collection(Random.id()); + test.equal(callCount, 1); // Still 1, not 2 + + teardownTest(); +}); + +Tinytest.add("CollectionExtensions - prototype method removal", function (test) { + setupTest(); + + CollectionExtensions.addPrototypeMethod('testMethod', function() { + return 'test'; + }); + + const testCollection1 = new Mongo.Collection(Random.id()); + test.isTrue(typeof testCollection1.testMethod === 'function'); + + CollectionExtensions.removePrototypeMethod('testMethod'); + + const testCollection2 = new Mongo.Collection(Random.id()); + test.isUndefined(testCollection2.testMethod); + + teardownTest(); +}); + +Tinytest.add("CollectionExtensions - input validation", function (test) { + setupTest(); + + test.throws(() => { + CollectionExtensions.addExtension("not a function"); + }, /Extension must be a function/); + + test.throws(() => { + CollectionExtensions.addPrototypeMethod("", function() {}); + }, /Prototype method name must be a non-empty string/); + + test.throws(() => { + CollectionExtensions.addPrototypeMethod(123, function() {}); + }, /Prototype method name must be a non-empty string/); + + test.throws(() => { + CollectionExtensions.addPrototypeMethod("test", "not a function"); + }, /Prototype method must be a function/); + + test.throws(() => { + CollectionExtensions.addStaticMethod("", function() {}); + }, /Static method name must be a non-empty string/); + + test.throws(() => { + CollectionExtensions.addStaticMethod("test", "not a function"); + }, /Static method must be a function/); + + teardownTest(); +}); + +Tinytest.add("CollectionExtensions - introspection", function (test) { + setupTest(); + + const extension1 = function() {}; + const extension2 = function() {}; + + test.equal(CollectionExtensions.getExtensions(), []); + test.equal(CollectionExtensions.getPrototypeMethods().size, 0); + test.equal(CollectionExtensions.getStaticMethods().size, 0); + + CollectionExtensions.addExtension(extension1); + CollectionExtensions.addExtension(extension2); + CollectionExtensions.addPrototypeMethod('test1', function() {}); + CollectionExtensions.addStaticMethod('test2', function() {}); + + // Test introspection + const extensions = CollectionExtensions.getExtensions(); + test.equal(extensions.length, 2); + test.equal(extensions[0], extension1); + test.equal(extensions[1], extension2); + + const prototypeMethods = CollectionExtensions.getPrototypeMethods(); + test.equal(prototypeMethods.size, 1); + test.isTrue(prototypeMethods.has('test1')); + + const staticMethods = CollectionExtensions.getStaticMethods(); + test.equal(staticMethods.size, 1); + test.isTrue(staticMethods.has('test2')); + + teardownTest(); +}); \ No newline at end of file diff --git a/packages/mongo/tests/mongo_livedata_tests.js b/packages/mongo/tests/mongo_livedata_tests.js index af69aee7b4..f3e32106a5 100644 --- a/packages/mongo/tests/mongo_livedata_tests.js +++ b/packages/mongo/tests/mongo_livedata_tests.js @@ -2776,6 +2776,53 @@ const setsEqual = function (a, b) { }); }); + // Test operation result fields with allow/deny rules (similar to issue #12159) + if (Meteor.isServer) { + testAsyncMulti('mongo-livedata - operation result fields with allow/deny, ' + idGeneration, [ + async function(test, expect) { + var collectionName = 'test_operation_results_' + Random.id(); + var coll = new Mongo.Collection(collectionName, { idGeneration: idGeneration }); + + // Set up allow rules for all operations + coll.allow({ + insert: function() { return true; }, + update: function() { return true; }, + remove: function() { return true; } + }); + + // Test insert + var insertedId = await coll.insertAsync({name: 'doc1'}); + test.isTrue(insertedId !== undefined, 'insert should return an ID'); + + // Test update + var updateResult = await coll.updateAsync({name: 'doc1'}, {$set: {value: 1}}); + test.equal(updateResult, 1, 'update should return affected count'); + + // Test upsert (update case) + var upsertUpdateResult = await coll.upsertAsync({name: 'doc1'}, {$set: {value: 2}}); + test.equal(upsertUpdateResult.numberAffected, 1); + test.isFalse(upsertUpdateResult.hasOwnProperty('insertedId')); + + // Test upsert (insert case) + var upsertInsertResult = await coll.upsertAsync({name: 'doc2'}, {$set: {value: 3}}); + test.equal(upsertInsertResult.numberAffected, 1); + test.isTrue(upsertInsertResult.hasOwnProperty('insertedId')); + + // Test remove + var removeResult = await coll.removeAsync({name: 'doc1'}); + test.equal(removeResult, 1, 'remove should return removed count'); + + // Test insert with explicit ID + var explicitId = idGeneration === 'MONGO' ? new Mongo.ObjectID() : 'explicit-test-id'; + var insertExplicitResult = await coll.insertAsync({_id: explicitId, name: 'explicit-doc'}); + test.equal(insertExplicitResult, explicitId, 'insert with explicit ID should return that ID'); + + // Clean up + await coll.dropCollectionAsync(); + } + ]); + } + }); // end idGeneration parametrization Tinytest.add('mongo-livedata - rewrite selector', function(test) { diff --git a/packages/mongo/tests/observe_changes_tests.js b/packages/mongo/tests/observe_changes_tests.js index 8dd50eed5d..ac7a671cac 100644 --- a/packages/mongo/tests/observe_changes_tests.js +++ b/packages/mongo/tests/observe_changes_tests.js @@ -519,8 +519,8 @@ if (Meteor.isServer) { const [resolver1, promise1] = getPromiseAndResolver(); const [resolver2, promise2] = getPromiseAndResolver(); - await self.insert({x: 2, y: 3}); self.expects.push(resolver1, resolver2); + await self.insert({x: 2, y: 3}); await self.insert({x: 3, y: 7}); // filtered out by the query await self.insert({x: 4}); // Expect two added calls to happen. diff --git a/packages/mongo/tests/oplog_tests.js b/packages/mongo/tests/oplog_tests.js index 2ffb0c32eb..0ef4bf8089 100644 --- a/packages/mongo/tests/oplog_tests.js +++ b/packages/mongo/tests/oplog_tests.js @@ -181,11 +181,46 @@ process.env.MONGO_OPLOG_URL && const defaultOplogHandle = MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle; let previousMongoPackageSettings = {}; +async function oplogSimpleInsertion(IncludeCollection, ExcludeCollection) { + await IncludeCollection.rawCollection().insertOne({ include: 'yes', foo: 'bar' }); + await ExcludeCollection.rawCollection().insertOne({ include: 'no', foo: 'bar' }); +} + +async function oplogInsertionTransaction(IncludeCollection, ExcludeCollection) { + const client = MongoInternals.defaultRemoteCollectionDriver().mongo.client; + const session = client.startSession(); + + try { + await session.withTransaction(async () => { + await IncludeCollection.rawCollection().insertOne({ include: 'yes', foo: 'bar' }, { session }); + await ExcludeCollection.rawCollection().insertOne({ include: 'no', foo: 'bar' }, { session }); + }); + } finally { + await session.endSession(); + } +} + +async function oplogMassiveInsertion(IncludeCollection, ExcludeCollection) { + const totalDocuments = 10000; + const documentInclude = Array.from( + { length: totalDocuments }, + (_, index) => ({ include: "yes", foo: "bar" + index }) + ); + const documentExclude = Array.from( + { length: totalDocuments }, + (_, index) => ({ include: "no", foo: "bar" + index }) + ); + + await IncludeCollection.rawCollection().insertMany(documentInclude); + await ExcludeCollection.rawCollection().insertMany(documentExclude); +} + async function oplogOptionsTest({ test, includeCollectionName, excludeCollectionName, - mongoPackageSettings = {} + mongoPackageSettings = {}, + functionToRun }) { try { previousMongoPackageSettings = { ...(Meteor.settings?.packages?.mongo || {}) }; @@ -199,9 +234,11 @@ async function oplogOptionsTest({ const IncludeCollection = new Mongo.Collection(includeCollectionName); const ExcludeCollection = new Mongo.Collection(excludeCollectionName); - const shouldBeTracked = new Promise((resolve) => { - IncludeCollection.find({ include: 'yes' }).observeChanges({ - added(id, fields) { resolve(true) } + const shouldBeTracked = new Promise((resolve, reject) => { + IncludeCollection.find({ include: "yes" }).observeChanges({ + added(id, fields) { + resolve(true); + }, }); }); const shouldBeIgnored = new Promise((resolve, reject) => { @@ -218,8 +255,7 @@ async function oplogOptionsTest({ }); // do the inserts: - await IncludeCollection.rawCollection().insertOne({ include: 'yes', foo: 'bar' }); - await ExcludeCollection.rawCollection().insertOne({ include: 'no', foo: 'bar' }); + await functionToRun(IncludeCollection, ExcludeCollection); test.equal(await shouldBeTracked, true); test.equal(await shouldBeIgnored, true); @@ -229,6 +265,73 @@ async function oplogOptionsTest({ MongoInternals.defaultRemoteCollectionDriver().mongo._setOplogHandle(defaultOplogHandle); } } +async function oplogTailingOptionsTest({ + test, + includeCollectionName, + excludeCollectionName, + mongoPackageSettings = {}, + functionToRun +}) { + let stopRaw; + try { + previousMongoPackageSettings = { ...(Meteor.settings?.packages?.mongo || {}) }; + if (!Meteor.settings.packages) Meteor.settings.packages = {}; + Meteor.settings.packages.mongo = mongoPackageSettings; + + const myOplogHandle = new MongoInternals.OplogHandle(process.env.MONGO_OPLOG_URL, 'meteor'); + await myOplogHandle._startTrailingPromise; + + const IncludeCollection = new Mongo.Collection(includeCollectionName); + const ExcludeCollection = new Mongo.Collection(excludeCollectionName); + + // Listen for INCLUDE collection oplog entries + const includeSeen = new Promise(async (resolve, reject) => { + const includeStop = await myOplogHandle.onOplogEntry( + { dropCollection: false, dropDatabase: false, collection: includeCollectionName }, + ({ op, collection, id }) => { + try { + // Only accept actual inserts for the include collection + if (op?.op === 'i' && collection === includeCollectionName && op?.o?.include === 'yes') { + includeStop.stop(); + resolve(true); + } + } catch (e) { + includeStop.stop(); + reject(e); + } + } + ); + }); + + // Ensure EXCLUDE collection does NOT get processed + const excludeNotSeen = new Promise(async (resolve, reject) => { + const excludeStop = await myOplogHandle.onOplogEntry( + { dropCollection: false, dropDatabase: false, collection: excludeCollectionName }, + ({ op, collection, id }) => { + // If anything for excluded collection arrives, fail + excludeStop.stop(); + reject("Recieved a document in a excluded collection"); + } + ); + // Resolve after 2s if nothing arrived + setTimeout(() => { + excludeStop.stop(); + resolve(true); + }, 2000); + }); + + // Do the inserts (e.g., oplogInsertionTransaction or your chosen function) + await functionToRun(IncludeCollection, ExcludeCollection); + + // Await raw-oplog assertions + test.equal(await includeSeen, true); + test.equal(await excludeNotSeen, true); + } finally { + if (stopRaw?.stop) await stopRaw.stop(); + // Reset: + Meteor.settings.packages.mongo = { ...previousMongoPackageSettings }; + } +} process.env.MONGO_OPLOG_URL && Tinytest.addAsync( 'mongo-livedata - oplog - oplogSettings - oplogExcludeCollections', @@ -242,7 +345,8 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync( test, includeCollectionName: collectionNameA, excludeCollectionName: collectionNameB, - mongoPackageSettings + mongoPackageSettings, + functionToRun: oplogSimpleInsertion }); } ); @@ -259,7 +363,8 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync( test, includeCollectionName: collectionNameB, excludeCollectionName: collectionNameA, - mongoPackageSettings + mongoPackageSettings, + functionToRun: oplogSimpleInsertion }); } ); @@ -279,7 +384,8 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync( test, includeCollectionName: collectionNameA, excludeCollectionName: collectionNameB, - mongoPackageSettings + mongoPackageSettings, + functionToRun: oplogSimpleInsertion }); test.fail(); } catch (err) { @@ -350,6 +456,78 @@ process.env.MONGO_OPLOG_URL && } ); +process.env.MONGO_OPLOG_URL && Tinytest.addAsync( + 'mongo-livedata - oplog - oplogSettings - massiveInsertion - oplogIncludeCollections', + async test => { + const collectionNameA = "oplog-a-massive-" + Random.id(); + const collectionNameB = "oplog-b-massive-" + Random.id(); + const mongoPackageSettings = { + oplogIncludeCollections: [collectionNameA] + }; + await oplogTailingOptionsTest({ + test, + includeCollectionName: collectionNameA, + excludeCollectionName: collectionNameB, + mongoPackageSettings, + functionToRun: oplogMassiveInsertion + }); + } +); + +process.env.MONGO_OPLOG_URL && Tinytest.addAsync( + 'mongo-livedata - oplog - oplogSettings - massiveInsertion - oplogExcludeCollections', + async test => { + const collectionNameA = "oplog-a-massive-" + Random.id(); + const collectionNameB = "oplog-b-massive-" + Random.id(); + const mongoPackageSettings = { + oplogExcludeCollections: [collectionNameA] + }; + await oplogTailingOptionsTest({ + test, + includeCollectionName: collectionNameB, + excludeCollectionName: collectionNameA, + mongoPackageSettings, + functionToRun: oplogMassiveInsertion + }); + } +); + +process.env.MONGO_OPLOG_URL && Tinytest.addAsync( + 'mongo-livedata - oplog - oplogSettings - transaction - oplogExcludeCollections', + async test => { + const collectionNameA = "oplog-a-transaction-" + Random.id(); + const collectionNameB = "oplog-b-transaction-" + Random.id(); + const mongoPackageSettings = { + oplogExcludeCollections: [collectionNameA] + }; + await oplogTailingOptionsTest({ + test, + includeCollectionName: collectionNameB, + excludeCollectionName: collectionNameA, + mongoPackageSettings, + functionToRun: oplogInsertionTransaction + }); + } +); + +process.env.MONGO_OPLOG_URL && Tinytest.addAsync( + 'mongo-livedata - oplog - oplogSettings - transaction - oplogIncludeCollections', + async test => { + const collectionNameA = "oplog-a-transaction-" + Random.id(); + const collectionNameB = "oplog-b-transaction-" + Random.id(); + const mongoPackageSettings = { + oplogIncludeCollections: [collectionNameA] + }; + await oplogTailingOptionsTest({ + test, + includeCollectionName: collectionNameA, + excludeCollectionName: collectionNameB, + mongoPackageSettings, + functionToRun: oplogInsertionTransaction + }); + } +); + // TODO this is commented for now, but we need to find out the cause // PR: https://github.com/meteor/meteor/pull/12057 // Meteor.isServer && Tinytest.addAsync( diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index 723aaa4b77..fb5dc1b720 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -1,9 +1,8 @@ Package.describe({ name: 'react-fast-refresh', - version: '0.2.9', + version: '0.3.0', summary: 'Automatically update React components with HMR', documentation: 'README.md', - devOnly: true, }); Npm.depends({ diff --git a/packages/roles/package.js b/packages/roles/package.js index dbcc7ac855..d0943ce407 100644 --- a/packages/roles/package.js +++ b/packages/roles/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Authorization package for Meteor", - version: "1.0.1", + version: "1.0.2", name: "roles", documentation: null, }); diff --git a/packages/roles/roles_common_async.js b/packages/roles/roles_common_async.js index a4f95af825..43bfb76e67 100644 --- a/packages/roles/roles_common_async.js +++ b/packages/roles/roles_common_async.js @@ -1052,8 +1052,13 @@ Object.assign(Roles, { * @return {Promise<Cursor>} Cursor of users in roles. */ getUsersInRoleAsync: async function (roles, options, queryOptions) { + options = Roles._normalizeOptions(options) + + const assignmentOptions = { ...options } + assignmentOptions.queryOptions = undefined + const ids = ( - await Roles.getUserAssignmentsForRole(roles, options).fetchAsync() + await Roles.getUserAssignmentsForRole(roles, assignmentOptions).fetchAsync() ).map((a) => a.user._id) return Meteor.users.find( diff --git a/packages/roles/tests/serverAsync.js b/packages/roles/tests/serverAsync.js index 197b587c37..2fa9c62dec 100644 --- a/packages/roles/tests/serverAsync.js +++ b/packages/roles/tests/serverAsync.js @@ -1612,6 +1612,38 @@ Tinytest.addAsync( } ); + +Tinytest.addAsync( + "roles -can get all users in role by scope and passes through mongo query arguments only to the users collection when included in the options", + async function (test) { + await clearData(); + await Roles.createRoleAsync("admin"); + await Roles.createRoleAsync("user"); + + await Roles.addUsersToRolesAsync( + [users.eve, users.joe], + ["admin", "user"], + "scope1" + ); + await Roles.addUsersToRolesAsync( + [users.bob, users.joe], + ["admin"], + "scope2" + ); + + const cursor = await Roles.getUsersInRoleAsync("admin", { scope: "scope1", queryOptions: { + fields: { _id: 1, username: 1 }, + limit: 1, + }}); + const results = await cursor.fetchAsync(); + + test.equal(1, results.length); + test.isTrue(hasProp(results[0], "_id")); + test.isTrue(hasProp(results[0], "username")); + } +); + + Tinytest.addAsync( "roles -can use Roles.GLOBAL_SCOPE to assign blanket roles", async function (test) { diff --git a/packages/rspack/README.md b/packages/rspack/README.md new file mode 100644 index 0000000000..651d1c3449 --- /dev/null +++ b/packages/rspack/README.md @@ -0,0 +1,3 @@ +# rspack + +The rspack package hooks into the Meteor lifecycle to run the rspack bundler independently, compiling app code while preserving Meteor packages as external. It automatically integrates the rspack dev server and HMR mechanism, and manages client and server bundles for development and production. By default, rspack is configured to support secured code for client and server, tree shaking, full ESM support with export fields in package.json, and so on. It also enables the user to provide custom configuration. diff --git a/packages/rspack/lib/build-context.js b/packages/rspack/lib/build-context.js new file mode 100644 index 0000000000..5ff8a7d08c --- /dev/null +++ b/packages/rspack/lib/build-context.js @@ -0,0 +1,736 @@ +/** + * @module build-context + * @description Functions for managing build context and module files for Rspack plugin + */ +import { RSPACK_DOCTOR_CONTEXT } from "./constants"; + +const fs = require('fs'); +const path = require('path'); + +const { getCustomConfigFilePath } = require('./processes'); + +const { logError } = require('meteor/tools-core/lib/log'); + +const { capitalizeFirstLetter } = require('meteor/tools-core/lib/string'); + +const { + getMeteorAppDir, + getMeteorInitialAppEntrypoints, + isMeteorAppDevelopment, + isMeteorAppRun, + isMeteorAppBuild, + isMeteorBlazeProject, + isMeteorAppNative, + isMeteorAppTestFullApp, +} = require('meteor/tools-core/lib/meteor'); + +const { + getGlobalState, + setGlobalState +} = require('meteor/tools-core/lib/global-state'); + +const { + addGitignoreEntries +} = require('meteor/tools-core/lib/git'); + +const { + RSPACK_BUILD_CONTEXT, + RSPACK_CHUNKS_CONTEXT, + RSPACK_ASSETS_CONTEXT, + GLOBAL_STATE_KEYS, + FILE_ROLE, +} = require('./constants'); + +// Common warning message for autogenerated files +const AUTO_GENERATED_WARNING = `* ⚠️ Note: This file is autogenerated. It is not meant to be modified manually. +* These files also act as a cache: they can be safely removed and will be +* regenerated on the next build. They should be ignored in IDE suggestions +* and version control.`; + +/** + * Gets entry points from Meteor configuration + * Retrieves from global state if already stored, otherwise gets from Meteor + * @returns {Object} Object containing entry points for client and server + */ +export function getInitialEntrypoints() { + const existingEntrypoint = getGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS); + if (existingEntrypoint) return existingEntrypoint; + const initialEntrypoints = getMeteorInitialAppEntrypoints(); + const hasInitialEntrypoints = initialEntrypoints && Object.values(initialEntrypoints).length > 0 && Object.values(initialEntrypoints).every((value) => value != null); + if (hasInitialEntrypoints) { + setGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS, initialEntrypoints); + } + return initialEntrypoints; +} + +/** + * Ensures the Rspack build context directory exists + * Creates the directory if it doesn't exist and adds it to .gitignore + * @returns {string} Path to the build context directory + * @throws {Error} If directory creation fails + */ +export function ensureRspackBuildContextExists() { + const appDir = getMeteorAppDir(); + const buildContextPath = path.join(appDir, RSPACK_BUILD_CONTEXT); + + if (!fs.existsSync(buildContextPath)) { + try { + fs.mkdirSync(buildContextPath, { recursive: true }); + } catch (error) { + logError(`Failed to create Rspack build context directory: ${error.message}`); + throw error; + } + } + + const commonBuildEntries = [ + RSPACK_BUILD_CONTEXT, + `*/${RSPACK_ASSETS_CONTEXT}`, + `*/${RSPACK_CHUNKS_CONTEXT}`, + RSPACK_DOCTOR_CONTEXT, + ]; + + if (process.env.METEOR_LOCAL_DIR) { + addGitignoreEntries( + appDir, + [process.env.METEOR_LOCAL_DIR, ...commonBuildEntries], + "Meteor custom local directory (METEOR_LOCAL_DIR)" + ); + return buildContextPath; + } + + addGitignoreEntries( + appDir, + commonBuildEntries, + "Meteor Modern-Tools build context directories" + ); + + return buildContextPath; +} + +/** + * Ensures module files exist in the build context directory + * Creates default module files if they don't exist + * @returns {void} + */ +export function ensureModuleFilesExist() { + const appDir = getMeteorAppDir(); + + const env = { + ...(isMeteorAppDevelopment() ? { isDevelopment: true } : { isProduction: true }), + isNative: isMeteorAppNative(), + }; + const commandRole = isMeteorAppRun() + ? { role: FILE_ROLE.run } + : isMeteorAppBuild() + ? { role: FILE_ROLE.build } + : { role: FILE_ROLE.run }; + const initialEntrypoints = getInitialEntrypoints(); + const mainClientFiles = { + entryFile: initialEntrypoints.mainClient || '', + outputFile: getBuildFilePath({ isMain: true, isClient: true, ...env, role: FILE_ROLE.output, onlyFilename: true }), + }; + const mainServerFiles = { + entryFile: initialEntrypoints.mainServer || '', + outputFile: getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output, onlyFilename: true }), + }; + const isTestEager = + initialEntrypoints.testModule == null && + initialEntrypoints.testClient == null && + initialEntrypoints.testServer == null; + const isTestModule = initialEntrypoints.testModule != null || isTestEager; + const testClientFiles = { + entryFile: initialEntrypoints.testClient || '', + outputFile: getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output, onlyFilename: true }), + mainEntryFile: mainClientFiles.entryFile, + }; + const testServerFiles = { + entryFile: initialEntrypoints.testServer || '', + outputFile: getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output, onlyFilename: true }), + mainEntryFile: mainServerFiles.entryFile, + }; + const isTestFullApp = isMeteorAppTestFullApp(); + + const moduleFiles = { + /* Main module files for client and server */ + [getBuildFilePath({ isMain: true, isClient: true, ...env, ...commandRole })]: + getBuildFileContent({ isMain: true, isClient: true, ...env, ...commandRole, ...mainClientFiles }), + [getBuildFilePath({ isMain: true, isClient: true, ...env, role: FILE_ROLE.entry })]: + getBuildFileContent({ isMain: true, isClient: true, ...env, role: FILE_ROLE.entry, ...mainClientFiles }), + [getBuildFilePath({ isMain: true, isClient: true, ...env, role: FILE_ROLE.output })]: + getBuildFileContent({ isMain: true, isClient: true, ...env, role: FILE_ROLE.output, ...mainClientFiles }), + [getBuildFilePath({ isMain: true, isServer: true, ...env, ...commandRole })]: + getBuildFileContent({ isMain: true, isServer: true, ...env, ...commandRole, ...mainServerFiles }), + [getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.entry })]: + getBuildFileContent({ isMain: true, isServer: true, ...env, role: FILE_ROLE.entry, ...mainServerFiles }), + [getBuildFilePath({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output })]: + getBuildFileContent({ isMain: true, isServer: true, ...env, role: FILE_ROLE.output, ...mainServerFiles }), + /* Test module files when test module, test module files for client and server are present or eager discovery */ + [getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isClient: true, ...commandRole })]: + getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isClient: true, ...commandRole, ...testClientFiles }), + [getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.entry })]: + getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.entry, ...testClientFiles }), + [getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.output })]: + getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.output, ...testClientFiles }), + [getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isServer: true, ...commandRole })]: + getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isServer: true, ...commandRole, ...testServerFiles }), + [getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.entry })]: + getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.entry, ...testServerFiles }), + [getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.output })]: + getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.output, ...testServerFiles }), + }; + + Object.entries(moduleFiles).forEach(([filename, defaultContent]) => { + // 1. Build full path and ensure directory exists + const filePath = path.join(appDir, RSPACK_BUILD_CONTEXT, filename); + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + try { + fs.mkdirSync(dir, { recursive: true }); + } catch (err) { + logError(`Failed to create directory ${dir}: ${err.message}`); + return; // stop here if we can’t make the folder + } + } + + // 2. If the file exists, check its contents + if (fs.existsSync(filePath)) { + let existing; + try { + existing = fs.readFileSync(filePath, 'utf8'); + } catch (err) { + logError(`Failed to read existing file ${filename}: ${err.message}`); + return; + } + + // 3. If it doesn't already start with the new defaultContent, overwrite it + if (!existing.includes(defaultContent)) { + try { + fs.writeFileSync(filePath, defaultContent, 'utf8'); + } catch (err) { + logError(`Failed to rewrite module file ${filename}: ${err.message}`); + } + } + + // 4. If the file doesn't exist at all, write it for the first time + } else { + try { + fs.writeFileSync(filePath, defaultContent, 'utf8'); + } catch (err) { + logError(`Failed to create module file ${filename}: ${err.message}`); + } + } + }); +} + +/** + * Generates a build file path based on configuration parameters + * @param {Object} config - Configuration object containing build settings + * @returns {string} The build file path or filename + */ +export function getBuildFilePath(config) { + // Determine the module part (directory name) + let module = ''; + if (config?.isTest) { + module = 'test'; + } else if (config?.isMain) { + module = 'main'; + } + + // Determine the side part (first part of filename) + let side = ''; + if (config?.isServer) { + side = 'server'; + } else if (config?.isClient) { + side = 'client'; + } + + // Determine the environment part (only for non-test files) + let env = ''; + if (!config?.isTest) { + if (config?.isDevelopment) { + env = 'dev'; + } else if (config?.isProduction) { + env = 'prod'; + } + } + + // Determine the role part + let role = config?.role; + if ([FILE_ROLE.run, FILE_ROLE.build].includes(role)) { + role = 'meteor'; + } else if ([FILE_ROLE.output].includes(role)) { + role = 'rspack'; + } + + // 5. Get file extension (default to js) + const extension = config?.extension || 'js'; + + // 6. Construct the filename: {side}-{role}.{extension} + const filename = `${side}-${role}.${extension}`; + + // Return either just the filename or the full path + if (config?.onlyFilename) { + return filename; + } else { + // Full path format: {module}[-{env}]/{filename} + const envSuffix = env ? `-${env}` : ''; + return `${module}${envSuffix}/${filename}`; + } +} + +/** + * Gets the appropriate banner based on file configuration + * @param {Object} config - Configuration object + * @param {string} side - The side (client, server, test) + * @param {string} env - The environment (development, production) + * @param {string} module - The module (main, test) + * @param {string} role - The role (build, entry, run, output) + * @returns {string} The banner content + */ +function getBanner(config, side, env, module, role) { + const envDisplay = capitalizeFirstLetter(env || module); + const sideDisplay = capitalizeFirstLetter(side); + + // For test mode, use the existing banners + if (module === 'test') { + // Test file banners + if (role === FILE_ROLE.entry) { + if (!config?.entryFile) { + return `/** +* @file ${side}-entry.js +* @description No code generated +* -------------------------------------------------------------------------- +* ⚡ Rspack Test ${sideDisplay} Entry (${envDisplay}) +* -------------------------------------------------------------------------- +* • [■ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is empty because \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` is not set in package.json. +* +${AUTO_GENERATED_WARNING} +*/`; + } + // For test mode, if side is client or server, include it in the title + const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`; + return `/** +* @file ${side}-entry.js +* @description Entry point for Rspack test build process +* -------------------------------------------------------------------------- +* ⚡ Rspack ${testType} Entry (${envDisplay}) +* -------------------------------------------------------------------------- +* • [■ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is the starting point for the Rspack test build. It imports your +* Meteor app's test modules so Rspack can resolve every dependency and +* generate the bundled output: \`${side}-rspack.js\`. +* +${AUTO_GENERATED_WARNING} +*/`; + } + + if (role === FILE_ROLE.output) { + if (!config?.entryFile) { + return `/** +* @file ${side}-rspack.js +* @description No code generated +* -------------------------------------------------------------------------- +* ⚡ Rspack Test ${sideDisplay} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is empty because \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` is not set in package.json. +* +${AUTO_GENERATED_WARNING} +*/`; + } + // For test mode, if side is client or server, include it in the title + const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`; + return `/** +* @file ${side}-rspack.js +* @description Bundled output generated by Rspack for tests +* -------------------------------------------------------------------------- +* ⚡ Rspack ${testType} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is the bundle that Rspack outputs for tests. It contains all of +* your test code in one optimized file. Next step is loading this bundle via +* \`${side}-meteor.js\`. +* +${AUTO_GENERATED_WARNING} +*/`; + } + + if (role === FILE_ROLE.run || role === FILE_ROLE.build) { + if (!config?.entryFile) { + return `/** +* @file ${side}-meteor.js +* @description No code generated +* -------------------------------------------------------------------------- +* ☄️ Meteor Test ${sideDisplay} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ] +* +* This file is empty because \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` is not set in package.json. +* +${AUTO_GENERATED_WARNING} +*/`; + } + // For test mode, if side is client or server, include it in the title + const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`; + return `/** +* @file ${side}-meteor.js +* @description Meteor runtime file that imports the Rspack test bundle +* -------------------------------------------------------------------------- +* ☄️ Meteor ${testType} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ] +* +* Defined under \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` in package.json. Meteor loads this +* file at runtime to import the Rspack test bundle (\`${side}-rspack.js\`) and +* run your tests. +* +${AUTO_GENERATED_WARNING} +*/`; + } + return ''; + } + + // For main modules (not test mode), use the new templates + // Entry files + if (role === FILE_ROLE.entry) { + if (!config?.entryFile) { + return `/** +* @file ${side}-entry.js +* @description No code generated +* -------------------------------------------------------------------------- +* 🔌 Rspack ${sideDisplay} Entry (${envDisplay}) +* -------------------------------------------------------------------------- +* • [■ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is empty because \`meteor.mainModule.${side}\` is not set in package.json. +* +${AUTO_GENERATED_WARNING} +*/`; + } + return `/** +* @file ${side}-entry.js +* @description Entry point for Rspack build process +* -------------------------------------------------------------------------- +* 🔌 Rspack ${sideDisplay} Entry (${envDisplay}) +* -------------------------------------------------------------------------- +* • [■ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is the entry point that Rspack uses to start the build process. +* It imports the module defined in \`meteor.mainModule.${side}\` inside package.json. +* From here, Rspack can trace the entire dependency graph of your application +* and generate the bundled output (\`${side}-rspack.js\`). +* +${AUTO_GENERATED_WARNING} +*/`; + } + + // Rspack output files + if (role === FILE_ROLE.output) { + if (!config?.entryFile) { + return `/** +* @file ${side}-rspack.js +* @description No code generated +* -------------------------------------------------------------------------- +* ⚡ Rspack ${sideDisplay} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is empty because \`meteor.mainModule.${side}\` is not set in package.json. +* +${AUTO_GENERATED_WARNING} +*/`; + } + return `/** +* @file ${side}-rspack.js +* @description Bundled output generated by Rspack +* -------------------------------------------------------------------------- +* ⚡ Rspack ${sideDisplay} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ] +* +* This file is the bundled output generated by Rspack. +* It contains all application code and assets combined into one build. +* It is not used directly, but will be imported by the Meteor main module +* file (\`${side}-meteor.js\`) so that Meteor runs the Rspack bundle. +* +${AUTO_GENERATED_WARNING} +*/`; + } + + // Meteor files (run or build role) + if (role === FILE_ROLE.run || role === FILE_ROLE.build) { + if (!config?.entryFile) { + return `/** +* @file ${side}-meteor.js +* @description No code generated +* -------------------------------------------------------------------------- +* ☄️ Meteor ${sideDisplay} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ] +* +* This file is empty because \`meteor.mainModule.${side}\` is not set in package.json. +* +${AUTO_GENERATED_WARNING} +*/`; + } + return `/** +* @file ${side}-meteor.js +* @description Meteor runtime file that imports the Rspack bundle +* -------------------------------------------------------------------------- +* ☄️ Meteor ${sideDisplay} App (${envDisplay}) +* -------------------------------------------------------------------------- +* • [ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ] +* +* This file overrides the corresponding \`meteor.mainModule.${side}\` entry in +* package.json. Meteor loads it at runtime, and it imports the Rspack +* bundle (\`${side}-rspack.js\`) so the application executes using the build +* produced by Rspack. +* +${AUTO_GENERATED_WARNING} +*/`; + } + + return ''; +} + +/** + * Gets the HMR code if applicable + * @returns {string} The HMR code or empty string + */ +function getHmrCode(config, role) { + if (!config?.entryFile && !config?.isTest) { + return ''; + } + + if (role === FILE_ROLE.entry && config?.isClient && !config?.isTest) { + return `/* Enables HMR */ +if (module.hot) { + module.hot.accept(); +}`; + } + return ''; +} + +/** + * Gets the import content based on configuration + * @returns {string} The import content + */ +function getImportContent(config, side, role) { + if (!config?.entryFile && !config?.isTest) { + return ''; + } + + if (role === FILE_ROLE.entry) { + if (config?.isTest) { + return `${ + config?.isTestFullApp && config?.mainEntryFile + ? `/* Link to 🔌 Meteor ${capitalizeFirstLetter( + side + )} Main Entry (--full-app mode) */ +import '../../${config.mainEntryFile}';` + : "" + } +${ + config?.entryFile + ? ` +/* Link to 🔌 Meteor ${capitalizeFirstLetter(side)} Test Entry */ +import '../../${config.entryFile}';` + : "" +}`; + } + + if (config?.entryFile) { + return `/* Link to 🔌 Meteor ${capitalizeFirstLetter(side)} Entry */ +import '../../${config?.entryFile}';`; + } + } + + if (config?.outputFile && + (role === FILE_ROLE.build || config?.isProduction || + (role === FILE_ROLE.run && + (config?.isServer || config?.isTest || config?.isNative))) + ) { + return `/* Link to ⚡ Rspack ${capitalizeFirstLetter(side)} App */ +${ + (isMeteorBlazeProject() && config?.isClient && '// In Blaze, import happens last so HTML files preload first') || + `import './${config?.outputFile || ''}';` +}`; + } + + if (role === FILE_ROLE.run && config?.isServer && !config?.isTest) { + return '/* No link to ☄️ Meteor Server App as served by HMR server */'; + } + + if (role === FILE_ROLE.run && config?.isClient && !config?.isTest) { + return '/* No link to ⚡ Rspack Client App as served by HMR server */'; + } + + if (role === FILE_ROLE.output && config?.isClient && !config?.isTest) { + return '/* No code generated as served by HMR server */'; + } + + if (role === FILE_ROLE.output && (config?.isServer || config?.isTest)) { + return '/* Code generated */'; + } + + if (role === FILE_ROLE.entry && config?.isTest) { + return '/* Tests automatically imported */'; + } + + return ''; +} + +/** + * Generates build file content based on configuration parameters + * @param {Object} config - Configuration object + * @returns {string} The build file content + */ +export function getBuildFileContent(config) { + // Extract configuration values + const module = config?.isTest ? 'test' : config?.isMain ? 'main' : ''; + const side = config?.isTestModule ? 'test' : config?.isServer ? 'server' : config?.isClient ? 'client' : ''; + const env = config?.isDevelopment ? 'development' : config?.isProduction ? 'production' : ''; + const role = config?.role; + + // Get banner based on configuration + const banner = getBanner(config, side, env, module, role); + + // Get HMR code if applicable + const hmr = getHmrCode(config, role); + + // Get import content based on configuration + const importContent = getImportContent(config, side, role); + + // Combine all parts to create the file content + return `${banner} +${hmr && ` +${hmr} +` || ''} +${importContent} +`; +} + +/** + * Cleans the build context files of the current environment + * Removes all build files and directories for the current environment + * Also cleans _build-* files from public and private folders + * @returns {void} + */ +export function cleanBuildContextFiles() { + const appDir = getMeteorAppDir(); + const buildContextPath = path.join(appDir, RSPACK_BUILD_CONTEXT); + + // Only proceed if the build context directory exists + if (!fs.existsSync(buildContextPath)) { + return; + } + + // Get current environment + const env = { + ...(isMeteorAppDevelopment() ? { isDevelopment: true } : { isProduction: true }), + isNative: isMeteorAppNative(), + }; + + try { + // Clean main module directories + const mainClientPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isMain: true, isClient: true, ...env }))); + const mainServerPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isMain: true, isServer: true, ...env }))); + + // Clean test module directories if they exist + const testModulePath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isTest: true, isTestModule: true }))); + const testClientPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isTest: true, isClient: true }))); + const testServerPath = path.dirname(path.join(buildContextPath, getBuildFilePath({ isTest: true, isServer: true }))); + + // Create a Set to ensure unique directory paths + const uniqueDirPaths = new Set([mainClientPath, mainServerPath, testModulePath, testClientPath, testServerPath]); + + // Remove directories if they exist + [...uniqueDirPaths].forEach(dirPath => { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + } + }); + + // Clean _build-* files from public and private folders + const publicDir = path.join(appDir, 'public'); + const privateDir = path.join(appDir, 'private'); + + [publicDir, privateDir].forEach(dir => { + if (fs.existsSync(dir)) { + try { + const files = fs.readdirSync(dir); + files.forEach(file => { + if ([RSPACK_ASSETS_CONTEXT, RSPACK_CHUNKS_CONTEXT, RSPACK_DOCTOR_CONTEXT].includes(file)) { + const filePath = path.join(dir, file); + fs.rmSync(filePath, { recursive: true, force: true }); + } + }); + + // Also remove client-rspack.js from public directory if it exists + if (dir === publicDir) { + const clientRspackPath = path.join(dir, 'client-rspack.js'); + if (fs.existsSync(clientRspackPath)) { + fs.rmSync(clientRspackPath, { force: true }); + } + } + } catch (err) { + logError(`Failed to clean _build-* files from ${dir}: ${err.message}`); + } + } + }); + } catch (error) { + logError(`Failed to clean build context files: ${error.message}`); + } +} + +/** + * Ensures the rspack.config.js file exists at the project level + * Creates the file if it doesn't exist with the required template + * Will not create a new file if rspack.config.mjs or rspack.config.cjs exists + * @returns {string} Path to the rspack.config file (.js, .mjs, or .cjs) + */ +export function ensureRspackConfigExists() { + const appDir = getMeteorAppDir(); + + // Check if any config file already exists using the helper function + const existingConfigPath = getCustomConfigFilePath(appDir); + if (existingConfigPath) { + return existingConfigPath; + } + + // If no config file exists, we'll create a .js one + const jsConfigPath = path.join(appDir, 'rspack.config.js'); + + const configTemplate = `const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the \`Meteor\` object, such as: + * - \`Meteor.isClient\` / \`Meteor.isServer\` + * - \`Meteor.isDevelopment\` / \`Meteor.isProduction\` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return {}; +}); +`; + + if (!fs.existsSync(jsConfigPath)) { + try { + fs.writeFileSync(jsConfigPath, configTemplate, 'utf8'); + } catch (error) { + logError(`Failed to create rspack.config.js file: ${error.message}`); + throw error; + } + } + + return jsConfigPath; +} diff --git a/packages/rspack/lib/compilation.js b/packages/rspack/lib/compilation.js new file mode 100644 index 0000000000..1a3de1511e --- /dev/null +++ b/packages/rspack/lib/compilation.js @@ -0,0 +1,226 @@ +/** + * @module compilation-helpers + * @description Helper functions for Rspack compilation tracking + * + * This module provides utility functions for tracking Rspack compilations, + * including setting up compilation tracking, waiting for first compilation, + * and formatting time values. + */ + +const { + GLOBAL_STATE_KEYS +} = require('./constants'); + +const { + getGlobalState, + setGlobalState +} = require('meteor/tools-core/lib/global-state'); + +// Helper function to format milliseconds with comma separators +function formatMilliseconds(ms) { + return ms.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +/** + * Sets up compilation tracking and callbacks + * @returns {Object} Object containing compilation tracking state and callbacks + */ +export function setupCompilationTracking() { + // Initialize global state for first compilation tracking + const clientFirstCompile = { + resolved: false, + resolve: null + }; + const serverFirstCompile = { + resolved: false, + resolve: null + }; + + // Store in global state + setGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientFirstCompile); + setGlobalState(GLOBAL_STATE_KEYS.SERVER_FIRST_COMPILE, serverFirstCompile); + + // Create promises for first compilation of client and server + const clientFirstCompilePromise = new Promise(resolve => { + clientFirstCompile.resolve = resolve; + }); + + const serverFirstCompilePromise = new Promise(resolve => { + serverFirstCompile.resolve = resolve; + }); + + // Create a shared state to track compilation times + const compilationState = { + clientMs: null, + serverMs: null, + timeoutId: null, + initialCompilationOccurred: false, + previousClientResolved: false, + previousServerResolved: false, + previousMaxTime: 0, + // Base delay in milliseconds + baseDelay: 100, + // Calculate dynamic defer time based on previous maximum time + calculateDeferTime: function() { + // Use a fixed base delay plus a margin based on previous maximum time + // The margin is 20% of the previous maximum time + return this.baseDelay + this.previousMaxTime; + }, + // Function to print the maximum time once compilations are complete + printMaxTime: function() { + const clientResolved = clientFirstCompile?.resolved || false; + const serverResolved = serverFirstCompile?.resolved || false; + + // Check if this is the first time both client and server are resolved + // but were previously not both resolved + if (clientResolved && serverResolved && + !(this.previousClientResolved && this.previousServerResolved) && + !this.initialCompilationOccurred) { + this.initialCompilationOccurred = true; + } + + // Update previous resolved states for next call + this.previousClientResolved = clientResolved; + this.previousServerResolved = serverResolved; + + const shouldPrint = this.initialCompilationOccurred && + (this.clientMs !== null || this.serverMs !== null); + + // Clear any existing timeout + if (this.timeoutId !== null) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + + // Handle cases where only one compilation runs + if (shouldPrint) { + // Use the available time or default to the other one + const clientTime = this.clientMs !== null ? this.clientMs : 0; + const serverTime = this.serverMs !== null ? this.serverMs : 0; + + // Calculate defer time based on previous maximum time + const deferTime = this.calculateDeferTime(); + + // Set a timeout to wait for both compilations to likely finish + this.timeoutId = setTimeout(() => { + const maxMs = Math.max(clientTime, serverTime); + console.log( + `| Total: ${formatMilliseconds(maxMs)} ms (Rspack ${ + this.initialCompilationOccurred ? 'Rebuild' : 'Build' + } App)` + ); + + // Store the current maximum time for future defer time calculations + this.previousMaxTime = Math.max(maxMs, this.previousMaxTime); + + // Reset the state for next compilation cycle + clearTimeout(this.timeoutId); + this.clientMs = null; + this.serverMs = null; + this.timeoutId = null; + }, deferTime); + } + }, + }; + + // Define separate onCompile callbacks for client and server + const onCompileClient = (data) => { + // Resolve the promise if it's the first compilation + const clientState = getGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientFirstCompile); + if (!clientState?.resolved) { + clientState.resolved = true; + clientState.resolve(); + setGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientState); + } + + if (process.env.METEOR_PROFILE) { + // Extract milliseconds from compilation message + const msMatch = data.match(/in (\d+) ms/); + if (msMatch && msMatch[1]) { + // Store the client compilation time + compilationState.clientMs = parseInt(msMatch[1], 10); + // Try to print max time if both compilations are complete + compilationState.printMaxTime(); + } + } + }; + + const onCompileServer = (data) => { + // Resolve the promise if it's the first compilation + const serverState = getGlobalState(GLOBAL_STATE_KEYS.SERVER_FIRST_COMPILE, serverFirstCompile); + if (!serverState?.resolved) { + serverState.resolved = true; + serverState.resolve(); + setGlobalState(GLOBAL_STATE_KEYS.SERVER_FIRST_COMPILE, serverState); + } + + if (process.env.METEOR_PROFILE) { + // Extract milliseconds from compilation message + const msMatch = data.match(/in (\d+) ms/); + if (msMatch && msMatch[1]) { + // Store the server compilation time + compilationState.serverMs = parseInt(msMatch[1], 10); + // Try to print max time if both compilations are complete + compilationState.printMaxTime(); + } + } + }; + + return { + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + onCompileClient, + onCompileServer + }; +} + +/** + * Waits for first compilation to complete + * @param {Object} clientFirstCompile - Client first compilation state + * @param {Object} serverFirstCompile - Server first compilation state + * @param {Promise} clientFirstCompilePromise - Promise for client first compilation + * @param {Promise} serverFirstCompilePromise - Promise for server first compilation + * @param {Object} options - Options for waiting + * @param {string} options.target - Target to wait for: 'client', 'server', or 'both' (default) + * @param {string} options.version - Specific version to wait for (optional) + * @returns {Promise<void>} A promise that resolves when first compilation is complete + */ +export async function waitForFirstCompilation( + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + options = { target: 'both' } +) { + const clientState = getGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientFirstCompile); + const serverState = getGlobalState(GLOBAL_STATE_KEYS.SERVER_FIRST_COMPILE, serverFirstCompile); + + // If compilation is already complete, return immediately + if (process.env.RSPACK_FIRST_COMPILATION_COMPLETE) { + return; + } + + // Determine which compilation(s) to wait for based on target + switch (options.target) { + case 'client': + if (!clientState?.resolved) { + await clientFirstCompilePromise; + } + break; + case 'server': + if (!serverState?.resolved) { + await serverFirstCompilePromise; + } + break; + case 'both': + default: + if (!clientState?.resolved && !serverState?.resolved) { + await Promise.all([clientFirstCompilePromise, serverFirstCompilePromise]); + } + break; + } + + process.env.RSPACK_FIRST_COMPILATION_COMPLETE = true; +} diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js new file mode 100644 index 0000000000..5771d25fb2 --- /dev/null +++ b/packages/rspack/lib/config.js @@ -0,0 +1,388 @@ +/** + * @module config + * @description Functions for configuring Meteor for Rspack + */ +import { glob } from 'glob'; +import path from 'path'; +import fs from 'fs'; + +const { logInfo } = require('meteor/tools-core/lib/log'); +const { + getMeteorAppFilesAndFolders, + setMeteorAppIgnore, + setMeteorAppEntrypoints, + setMeteorAppCustomScriptUrl, + isMeteorAppDevelopment, + isMeteorAppRun, + isMeteorAppBuild, + isMeteorAppNative, + isMeteorAppDebug, + isMeteorAppTest, + isMeteorAppTestFullApp, + isMeteorAppConfigModernVerbose, + isMeteorBlazeProject, + isMeteorLessProject, + isMeteorScssProject, + getMeteorEnvPackageDirs, + getMeteorAppConfig, + getMeteorAppDir, +} = require('meteor/tools-core/lib/meteor'); +const { buildUnignorePatterns } = require('meteor/tools-core/lib/ignore'); + +import { getInitialEntrypoints } from './build-context'; + +const { ensureModuleFilesExist, getBuildFilePath } = require('./build-context'); +const { RSPACK_BUILD_CONTEXT, FILE_ROLE } = require('./constants'); + +/** + * Checks if entries exist in .meteorignore file + * @param {string[]} entries - Entries to check + * @returns {Object} Results with entry keys and boolean values + */ +function checkMeteorIgnoreExactEntries(entries) { + const meteorIgnorePath = path.join(getMeteorAppDir(), '.meteorignore'); + const results = {}; + + // Initialize results object with false for each entry + entries.forEach(entry => { + results[entry] = false; + }); + + // Check if .meteorignore file exists + if (!fs.existsSync(meteorIgnorePath)) { + return results; + } + + // Read the .meteorignore file + try { + const content = fs.readFileSync(meteorIgnorePath, 'utf8'); + const lines = content.split('\n'); + + // Check each line against all entries + lines.forEach(line => { + // Skip empty lines and comments + if (!line.trim() || line.trim().startsWith('#')) { + return; + } + + const trimmedLine = line.trim(); + + // Check for exact matches + entries.forEach(entry => { + if (trimmedLine === entry) { + results[entry] = true; + } + }); + }); + } catch (error) { + // If there's an error reading the file, return the initialized results + } + + return results; +} + +/** + * Gets the list of file extensions to ignore based on project type + * For Blaze projects, it excludes .html as used by Blaze + * For Less projects, it excludes .less files + * For SCSS projects, it excludes .scss files + * @returns {string[]} Array of file extensions to ignore + */ +function getFileExtensionsToIgnore() { + const isAnyCompilerProject = + isMeteorBlazeProject() || isMeteorLessProject() || isMeteorScssProject(); + if (!isAnyCompilerProject) { + return []; + } + + const allFiles = glob.sync('**/*', { + nodir: true, + dot: true, + ignore: ['node_modules/**', '.meteor/**'], + }); + const existingExts = Array.from( + new Set(allFiles.map(f => path.extname(f).toLowerCase())), + ); + + // Base extensions to ignore + const baseExtensions = [ + '.ts', + '.tsx', + '.js', + '.jsx', + '.mjs', + '.cjs', + '.json', + ]; + + // Filter existing extensions based on project type + let filteredExts = existingExts; + + // For Blaze projects, exclude .html files + if (isMeteorBlazeProject()) { + filteredExts = existingExts.filter(ext => ext !== '.html'); + } + + // Check for Less projects and exclude .less files + if (isMeteorLessProject()) { + filteredExts = filteredExts.filter(ext => ext !== '.less'); + } + + // Check for SCSS projects and exclude .scss files + if (isMeteorScssProject()) { + filteredExts = filteredExts.filter(ext => ext !== '.scss'); + } + + return Array.from(new Set([...baseExtensions, ...filteredExts])).filter( + ext => ext !== '', + ); +} + +/** + * Configures Meteor settings for Rspack + * Sets up file ignores, entry points, and custom script URL + * Creates necessary module files and writes content to them + * @returns {void} + */ +export function configureMeteorForRspack() { + const meteorAppConfig = getMeteorAppConfig(); + const initialEntrypoints = getInitialEntrypoints(); + + // Ignore node_modules to prevent Meteor from processing them + const projectRootFilesAndFolders = getMeteorAppFilesAndFolders({ + recursive: false, + }); + + const initialEntrypointContexts = [ + initialEntrypoints.mainClient, + initialEntrypoints.mainServer, + ] + .filter(Boolean) + .map(entrypoint => path.dirname(entrypoint)); + const includedDirs = ['public', 'private', '.meteor', RSPACK_BUILD_CONTEXT]; + const ignoredDirs = projectRootFilesAndFolders.directories.filter( + dir => !includedDirs.includes(dir), + ); + + const envPackageDirs = getMeteorEnvPackageDirs().map( + dir => path.normalize(dir)?.split(path.sep)?.filter(Boolean)?.[0], + ); + let extraFoldersToIgnore = [ + ...ignoredDirs + .filter( + dir => + ![ + 'public', + 'private', + '.meteor', + 'packages', + ...envPackageDirs, + RSPACK_BUILD_CONTEXT, + ].includes(dir), + ) + .map(dir => `${dir}/**`), + ]; + let extraFilesToIgnore = []; + + // Get extensions to ignore based on project type + const extensionsToIgnore = getFileExtensionsToIgnore(); + // If we have extensions to ignore, apply them to the ignored directories + if (extensionsToIgnore.length > 0) { + extraFilesToIgnore = ignoredDirs.flatMap(dir => + extensionsToIgnore.map(ext => `${dir}/**/*${ext}`), + ); + extraFoldersToIgnore = []; + } + + // Skip CSS/HTML files in entrypoint contexts + extraFilesToIgnore = [ + ...extraFilesToIgnore, + ...initialEntrypointContexts.flatMap(entrypoint => { + const cssPattern = `${entrypoint}/*.css`; + const htmlPattern = `${entrypoint}/*.html`; + + const cssFiles = glob.sync(cssPattern); + const htmlFiles = glob.sync(htmlPattern); + + const entriesToCheck = [ + cssPattern, + htmlPattern, + ...cssFiles, + ...htmlFiles + ]; + + const entryResults = checkMeteorIgnoreExactEntries(entriesToCheck); + const hasMatchingCssPattern = entryResults[cssPattern]; + const hasMatchingHtmlPattern = entryResults[htmlPattern]; + const hasAnyCssFileInMeteorIgnore = cssFiles.some(file => entryResults[file]); + const hasAnyHtmlFileInMeteorIgnore = htmlFiles.some(file => entryResults[file]); + + const result = []; + + // Handle HTML files + if (hasAnyHtmlFileInMeteorIgnore) { + // Add individual HTML files that are not in meteorignore + htmlFiles.forEach(file => { + if (!entryResults[file]) { + result.push(`!${file}`); + } + }); + } else if (!hasMatchingHtmlPattern) { + // Skip HTML pattern if not in meteorignore + result.push(`!${htmlPattern}`); + } + + // Handle CSS files + if (hasAnyCssFileInMeteorIgnore) { + // Add individual CSS files that are not in meteorignore + cssFiles.forEach(file => { + if (!entryResults[file]) { + result.push(`!${file}`); + } + }); + } else if (!hasMatchingCssPattern) { + // Skip CSS pattern if not in meteorignore + result.push(`!${cssPattern}`); + } + + return result; + }), + ]; + + const testIgnorePath = `${RSPACK_BUILD_CONTEXT}/${path.dirname( + getBuildFilePath({ + isTest: true, + }), + )}/**`; + const otherMainIgnorePath = + (isMeteorAppDevelopment() && + `${RSPACK_BUILD_CONTEXT}/${path.dirname( + getBuildFilePath({ + isMain: true, + isProduction: true, + }), + )}/**`) || + `${RSPACK_BUILD_CONTEXT}/${path.dirname( + getBuildFilePath({ + isMain: true, + isDevelopment: true, + }), + )}/**`; + const foldersToIgnore = [ + ...((isMeteorAppTest() && [otherMainIgnorePath]) || [ + testIgnorePath, + otherMainIgnorePath, + ]), + 'node_modules/**', + ...extraFoldersToIgnore, + ].filter(Boolean); + const rootFilesToIgnore = [ + ...projectRootFilesAndFolders.files.filter( + file => + ![ + 'package.json', + '.meteorignore', + 'tsconfig.json', + 'postcss.config.js', + 'scss-config.json', + ].includes(file), + ), + ]; + const filesToIgnore = [...rootFilesToIgnore, ...extraFilesToIgnore]; + const unignoredFilesAndFolders = buildUnignorePatterns( + meteorAppConfig?.modules || [], + { skipLevel: 1 }, + ); + const meteorAppIgnores = `${foldersToIgnore.join(' ')} ${filesToIgnore.join( + ' ', + )} ${unignoredFilesAndFolders.join(' ')}`.trim(); + setMeteorAppIgnore(meteorAppIgnores); + + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] Meteor app ignores: ${meteorAppIgnores}`); + } + + const env = isMeteorAppDevelopment() + ? { isDevelopment: true } + : { isProduction: true }; + const commandRole = isMeteorAppRun() + ? { role: FILE_ROLE.run } + : isMeteorAppBuild() + ? { role: FILE_ROLE.build } + : { role: FILE_ROLE.run }; + const mainClientModule = getBuildFilePath({ + isMain: true, + ...env, + ...commandRole, + isClient: true, + }); + const mainServerModule = getBuildFilePath({ + isMain: true, + ...env, + ...commandRole, + isServer: true, + }); + const isTestEager = + initialEntrypoints.testModule == null && + initialEntrypoints.testClient == null && + initialEntrypoints.testServer == null; + const isTestModule = initialEntrypoints.testModule != null || isTestEager; + const testClientModule = getBuildFilePath({ + isTest: true, + ...env, + ...commandRole, + isTestModule, + isClient: true, + }); + const testServerModule = getBuildFilePath({ + isTest: true, + ...env, + ...commandRole, + isTestModule, + isServer: true, + }); + + let appEntrypoints = { + mainClient: `${RSPACK_BUILD_CONTEXT}/${mainClientModule}`, + mainServer: `${RSPACK_BUILD_CONTEXT}/${mainServerModule}`, + ...((isTestModule && { + testClient: `${RSPACK_BUILD_CONTEXT}/${testClientModule}`, + testServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`, + }) || { + testClient: `${RSPACK_BUILD_CONTEXT}/${testClientModule}`, + testServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`, + }), + }; + if (isMeteorAppTestFullApp()) { + appEntrypoints = { + ...appEntrypoints, + mainClient: `${RSPACK_BUILD_CONTEXT}/${testClientModule}`, + mainServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`, + }; + } + // Set entry points in environment variables if they exist + setMeteorAppEntrypoints(appEntrypoints); + + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] App entrypoints: ${JSON.stringify(appEntrypoints, null, 2)}`); + } + + // Ensure module files exist + ensureModuleFilesExist(); + + // Write content to module files + if (isMeteorAppRun() && isMeteorAppDevelopment() && !isMeteorAppNative()) { + const customScriptUrl = `/__rspack__/${getBuildFilePath({ + ...env, + isMain: true, + isClient: true, + role: FILE_ROLE.output, + onlyFilename: true, + })}`; + setMeteorAppCustomScriptUrl(customScriptUrl); + + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] App custom script: ${customScriptUrl}`); + } + } +} diff --git a/packages/rspack/lib/constants.js b/packages/rspack/lib/constants.js new file mode 100644 index 0000000000..1341f54ce7 --- /dev/null +++ b/packages/rspack/lib/constants.js @@ -0,0 +1,108 @@ +/** + * @module constants + * @description Constants and global state keys for Rspack plugin + */ + +import path from 'path'; + +export const DEFAULT_RSPACK_VERSION = '1.7.1'; + +export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.31'; + +export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3'; + +export const DEFAULT_METEOR_RSPACK_REACT_REFRESH_VERSION = '0.17.0'; + +export const DEFAULT_METEOR_RSPACK_SWC_LOADER_VERSION = '0.2.6'; + +export const DEFAULT_METEOR_RSPACK_SWC_HELPERS_VERSION = '0.5.17'; + +export const DEFAULT_RSDOCTOR_RSPACK_PLUGIN_VERSION = '1.2.3'; + +/** + * Global state keys used for storing and retrieving state across the application + * @constant {Object} + * @property {string} CLIENT_PROCESS - Key for storing the client process + * @property {string} SERVER_PROCESS - Key for storing the server process + * @property {string} RSPACK_INSTALLATION_CHECKED - Key for tracking if Rspack installation was checked + * @property {string} IS_REACT_ENABLED - Key for tracking if React is enabled + * @property {string} INITIAL_ENTRYPONTS - Key for storing initial entrypoints + * @property {string} CLIENT_FIRST_COMPILE - Key for tracking client first compilation state + * @property {string} SERVER_FIRST_COMPILE - Key for tracking server first compilation state + * @property {string} BUILD_CONTEXT_FILES_CLEANED - Key for tracking if build context files have been cleaned + */ +export const GLOBAL_STATE_KEYS = { + CLIENT_PROCESS: 'rspack.clientProcess', + SERVER_PROCESS: 'rspack.serverProcess', + RSPACK_INSTALLATION_CHECKED: 'rspack.rspackInstallationChecked', + RSPACK_REACT_INSTALLATION_CHECKED: 'rspack.rspackReactInstallationChecked', + RSPACK_DOCTOR_INSTALLATION_CHECKED: 'rspack.rspackDoctorInstallationChecked', + REACT_CHECKED: 'rspack.reactChecked', + TYPESCRIPT_CHECKED: 'rspack.typescriptChecked', + ANGULAR_CHECKED: 'rspack.angularChecked', + INITIAL_ENTRYPONTS: 'meteor.initialEntrypoints', + CLIENT_FIRST_COMPILE: 'rspack.clientFirstCompile', + SERVER_FIRST_COMPILE: 'rspack.serverFirstCompile', + BUILD_CONTEXT_FILES_CLEANED: 'rspack.buildContextFilesCleaned', +}; + +const meteorConfig = typeof Plugin !== 'undefined' ? Plugin?.getMeteorConfig() : null; + +const meteorLocalDirName = process.env.METEOR_LOCAL_DIR + ? path.basename(process.env.METEOR_LOCAL_DIR.replace(/\\/g, '/')) + : ''; + +/** + * Directory name for Rspack build context + * Can be overridden with RSPACK_BUILD_CONTEXT environment variable + * @constant {string} + */ +export const RSPACK_BUILD_CONTEXT = + meteorConfig?.buildContext || + process.env.RSPACK_BUILD_CONTEXT || + `_build${(meteorLocalDirName && `-${meteorLocalDirName}`) || ''}`; + +process.env.RSPACK_BUILD_CONTEXT = RSPACK_BUILD_CONTEXT; + +/** + * Directory name for Rspack assets context + * Can be overridden with RSPACK_ASSETS_CONTEXT environment variable + * @constant {string} + */ +export const RSPACK_ASSETS_CONTEXT = + meteorConfig?.assetsContext || + process.env.RSPACK_ASSETS_CONTEXT || + `build-assets${(meteorLocalDirName && `-${meteorLocalDirName}`) || ''}`; + +process.env.RSPACK_ASSETS_CONTEXT = RSPACK_ASSETS_CONTEXT; + +/** + * Directory name for Rspack bundles context + * Can be overridden with RSPACK_ASSETS_CONTEXT environment variable + * @constant {string} + */ +export const RSPACK_CHUNKS_CONTEXT = + meteorConfig?.chunksContext || + process.env.RSPACK_CHUNKS_CONTEXT || + `build-chunks${(meteorLocalDirName && `-${meteorLocalDirName}`) || ''}`; + +process.env.RSPACK_CHUNKS_CONTEXT = RSPACK_CHUNKS_CONTEXT; + +/** + * Directory name for Rspack doctor context + * @type {string} + */ +export const RSPACK_DOCTOR_CONTEXT = '.rsdoctor'; + +/** + * Regex pattern for hot update files + * @constant {RegExp} + */ +export const RSPACK_HOT_UPDATE_REGEX = /^\/(.+\.hot-update\.(?:json|js))$/; + +export const FILE_ROLE = { + build: 'build', + entry: 'entry', + run: 'run', + output: 'output', +}; diff --git a/packages/rspack/lib/dependencies.js b/packages/rspack/lib/dependencies.js new file mode 100644 index 0000000000..3133db5c6e --- /dev/null +++ b/packages/rspack/lib/dependencies.js @@ -0,0 +1,304 @@ +/** + * @module dependencies + * @description Functions for managing dependencies for Rspack plugin + */ +import { + DEFAULT_METEOR_RSPACK_REACT_REFRESH_VERSION, + DEFAULT_METEOR_RSPACK_SWC_HELPERS_VERSION, + DEFAULT_RSDOCTOR_RSPACK_PLUGIN_VERSION +} from "./constants"; + +const { + getGlobalState, + setGlobalState, +} = require('meteor/tools-core/lib/global-state'); +const { + logProgress, + logSuccess, + logInfo, + logError, +} = require('meteor/tools-core/lib/log'); +const { + isMeteorAppUpdate, + getMeteorAppDir, +} = require('meteor/tools-core/lib/meteor'); +const { + checkNpmDependencyExists, + installNpmDependency, + checkNpmDependencyVersion, +} = require('meteor/tools-core/lib/npm'); +const { + joinWithAnd, +} = require('meteor/tools-core/lib/string'); + +const { + DEFAULT_RSPACK_VERSION, + DEFAULT_METEOR_RSPACK_VERSION, + DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION, + GLOBAL_STATE_KEYS, +} = require('./constants'); + +/** + * Generic function to ensure dependencies are installed with correct versions + * @param {Object[]} dependencies - Array of dependency objects with name, version, and semverCondition + * @param {string} globalStateKey - Global state key to track if check has been done + * @param {string} packageName - Name of the package for logging purposes + * @returns {Promise<void>} A promise that resolves when the check/installation is complete + * @throws {Error} If installation fails + */ +async function ensureDependenciesInstalled(dependencies, globalStateKey, packageName) { + // Skip if already checked + if (getGlobalState(globalStateKey, false)) { + return; + } + + const appDir = getMeteorAppDir(); + + // Filter dependencies that need to be installed (missing or wrong version) + const allDepsToInstall = dependencies.filter(dep => + !checkNpmDependencyExists(dep.name, { cwd: appDir }) || + !checkNpmDependencyVersion(dep.name, { + cwd: appDir, + versionRequirement: dep.version, + semverCondition: dep.semverCondition || 'gte', + existenceOnly: dep.existenceOnly, + }) + ); + + // Format dependencies for installation + const dependencyStrings = allDepsToInstall.map(dep => `${dep.name}@${dep.version}`); + + if (allDepsToInstall.length > 0) { + let devDepsSuccess = true; + let regularDepsSuccess = true; + let devDepsStrings = []; + let regularDepsStrings = []; + + // Display a header for the installation process + logProgress(`=> 📦 ${packageName} Dependencies`); + + // Show what dependencies will be installed + dependencyStrings.forEach(dep => { + logInfo(` • ${dep}`); + }); + + // Check if this is a Yarn project + const isYarnProj = process.env.YARN_ENABLED === 'true'; + + // Install dev dependencies + const devDepsToInstall = allDepsToInstall.filter(dep => dep.dev === true || dep.dev == null); + if (devDepsToInstall.length > 0) { + devDepsStrings = devDepsToInstall.map(dep => `${dep.name}@${dep.version}`); + + // Log progress for dev dependencies + logProgress( + `=> 🔧 Installing ${devDepsToInstall.length} dev dependenc${ + devDepsToInstall.length === 1 ? "y" : "ies" + }...` + ); + + devDepsSuccess = await installNpmDependency(devDepsStrings, { + cwd: appDir, + dev: true, + yarn: isYarnProj, + }); + } + + // Install regular dependencies + const regularDepsToInstall = allDepsToInstall.filter(dep => dep.dev === false); + if (regularDepsToInstall.length > 0) { + regularDepsStrings = regularDepsToInstall.map(dep => `${dep.name}@${dep.version}`); + + // Log progress for regular dependencies + logProgress( + `=> 🔧 Installing ${regularDepsToInstall.length} dependenc${ + regularDepsToInstall.length === 1 ? "y" : "ies" + }...` + ); + + regularDepsSuccess = await installNpmDependency(regularDepsStrings, { + cwd: appDir, + dev: false, + yarn: isYarnProj, + }); + } + + const success = devDepsSuccess && regularDepsSuccess; + + if (!success) { + const isYarnProj = process.env.YARN_ENABLED === 'true'; + + logError(`=> ❌ Failed to install ${packageName}`); + + if (!devDepsSuccess && devDepsStrings.length > 0) { + const devInstallCommand = isYarnProj + ? `yarn add --dev ${devDepsStrings.join(' ').trim()}` + : `meteor npm install -D ${devDepsStrings.join(' ').trim()}`; + logError(` For dev dependencies, run: ${devInstallCommand}`); + } + + if (!regularDepsSuccess && regularDepsStrings.length > 0) { + const regularInstallCommand = isYarnProj + ? `yarn add ${regularDepsStrings.join(' ').trim()}` + : `meteor npm install ${regularDepsStrings.join(' ').trim()}`; + logError(` For regular dependencies, run: ${regularInstallCommand}`); + } + + const allFailedDeps = []; + if (!devDepsSuccess) allFailedDeps.push('dev dependencies'); + if (!regularDepsSuccess) allFailedDeps.push('regular dependencies'); + + throw new Error( + `Failed to install ${packageName} ${joinWithAnd(allFailedDeps)}. Please install them manually with the commands above.` + ); + } + + logSuccess(`=> ✅ Installed ${packageName} dependencies`); + + if (isMeteorAppUpdate()) { + const isYarnProj = process.env.YARN_ENABLED === 'true'; + const installCommand = isYarnProj ? 'yarn install' : 'npm install'; + + logInfo(`=> 🔔 Remember: Run \`${installCommand}\` after the Meteor update finishes.`); + logInfo(` This helps keep your dependencies correct and your project stable.`); + } + } + + // Mark as checked + setGlobalState(globalStateKey, true); +} + +/** + * Checks if Rspack is installed, and installs it if not + * @returns {Promise<void>} A promise that resolves when the check/installation is complete + * @throws {Error} If Rspack installation fails + */ +export async function ensureRspackInstalled() { + const dependencies = [ + { name: '@rspack/cli', version: DEFAULT_RSPACK_VERSION, semverCondition: 'gte', dev: true }, + { name: '@rspack/core', version: DEFAULT_RSPACK_VERSION, semverCondition: 'gte', dev: true }, + { name: '@meteorjs/rspack', version: DEFAULT_METEOR_RSPACK_VERSION, semverCondition: 'gte', dev: true }, + { name: '@swc/helpers', version: DEFAULT_METEOR_RSPACK_SWC_HELPERS_VERSION, semverCondition: 'gte', dev: false }, + { name: '@rsdoctor/rspack-plugin', version: DEFAULT_RSDOCTOR_RSPACK_PLUGIN_VERSION, semverCondition: 'gte', dev: true }, + ]; + + await ensureDependenciesInstalled( + dependencies, + GLOBAL_STATE_KEYS.RSPACK_INSTALLATION_CHECKED, + 'Rspack', + ); +} + +/** + * Checks if React is installed and sets global state accordingly + * Sets global state and environment variables based on React detection + * @returns {Promise<void>} A promise that resolves when the check is complete + */ +export function checkReactInstalled() { + // Skip if already checked + if (getGlobalState(GLOBAL_STATE_KEYS.REACT_CHECKED, false)) { + return; + } + + const appDir = getMeteorAppDir(); + // Check if React is a dependency in the project + const isReactInstalled = checkNpmDependencyExists('react', { cwd: appDir }) && !checkNpmDependencyExists('preact', { cwd: appDir }); + + if (isReactInstalled) { + // Set environment variable to indicate React is enabled + process.env.METEOR_REACT_ENABLED = 'true'; + } else { + process.env.METEOR_REACT_ENABLED = 'false'; + } + + // Mark as checked + setGlobalState(GLOBAL_STATE_KEYS.REACT_CHECKED, true); + + return isReactInstalled; +} + +export async function ensureRspackReactInstalled() { + const dependencies = [ + { name: '@rspack/plugin-react-refresh', version: DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION, semverCondition: 'gte', dev: true }, + { name: 'react-refresh', version: DEFAULT_METEOR_RSPACK_REACT_REFRESH_VERSION, semverCondition: 'gte', dev: true }, + ]; + + await ensureDependenciesInstalled( + dependencies, + GLOBAL_STATE_KEYS.RSPACK_REACT_INSTALLATION_CHECKED, + 'Rspack React' + ); +} + +/** + * Checks if Rspack Doctor is installed, and installs it if not + * @returns {Promise<void>} A promise that resolves when the check/installation is complete + * @throws {Error} If Rspack Doctor installation fails + */ +export async function ensureRspackDoctorInstalled() { + const dependencies = [ + { name: '@rsdoctor/rspack-plugin', version: DEFAULT_RSDOCTOR_RSPACK_PLUGIN_VERSION, semverCondition: 'gte', dev: true }, + ]; + + await ensureDependenciesInstalled( + dependencies, + GLOBAL_STATE_KEYS.RSPACK_DOCTOR_INSTALLATION_CHECKED, + 'Rspack Doctor' + ); +} + +/** + * Checks if TypeScript is installed and sets global state accordingly + * Sets global state and environment variables based on TypeScript detection + * @returns {boolean} Whether TypeScript is installed + */ +export function checkTypescriptInstalled() { + // Skip if already checked + if (getGlobalState(GLOBAL_STATE_KEYS.TYPESCRIPT_CHECKED, false)) { + return; + } + + const appDir = getMeteorAppDir(); + // Check if TypeScript is a dependency in the project + const isTypescriptInstalled = checkNpmDependencyExists('typescript', { cwd: appDir }); + + if (isTypescriptInstalled) { + // Set environment variable to indicate TypeScript is enabled + process.env.METEOR_TYPESCRIPT_ENABLED = 'true'; + } else { + process.env.METEOR_TYPESCRIPT_ENABLED = 'false'; + } + + // Mark as checked + setGlobalState(GLOBAL_STATE_KEYS.TYPESCRIPT_CHECKED, true); + + return isTypescriptInstalled; +} + +/** + * Checks if Angular is installed and sets global state accordingly + * Sets global state and environment variables based on Angular detection + * @returns {boolean} Whether Angular is installed + */ +export function checkAngularInstalled() { + // Skip if already checked + if (getGlobalState(GLOBAL_STATE_KEYS.ANGULAR_CHECKED, false)) { + return; + } + + const appDir = getMeteorAppDir(); + // Check if @nx/angular-rspack is a dependency in the project + const isAngularInstalled = checkNpmDependencyExists('@nx/angular-rspack', { cwd: appDir }); + + if (isAngularInstalled) { + // Set environment variable to indicate Angular is enabled + process.env.METEOR_ANGULAR_ENABLED = 'true'; + } else { + process.env.METEOR_ANGULAR_ENABLED = 'false'; + } + + // Mark as checked + setGlobalState(GLOBAL_STATE_KEYS.ANGULAR_CHECKED, true); + + return isAngularInstalled; +} diff --git a/packages/rspack/lib/logging.js b/packages/rspack/lib/logging.js new file mode 100644 index 0000000000..6c938843d5 --- /dev/null +++ b/packages/rspack/lib/logging.js @@ -0,0 +1,100 @@ +/** + * @module logging + * @description Functions for logging Rspack processes + */ + +const { logRaw } = require("meteor/tools-core/lib/log"); + +const { + isMeteorAppConfigModernVerbose, + isMeteorAppProfile, +} = require("meteor/tools-core/lib/meteor"); + +/** + * Checks if the logs should be verbose for Rspack processes. + * @returns {boolean} True if profiling or verbose mode is enabled, false otherwise. + */ +export function shouldLogVerbose() { + return isMeteorAppProfile() || isMeteorAppConfigModernVerbose(); +} + +/** + * Strips the leading label line (e.g. "[server-rspack]:\n") from Rspack output. + * @param {string} output - The raw output from an Rspack process + * @returns {string} The output without the leading label line, trimmed + */ +export function stripRspackLabel(output) { + return output.replace(/^\[.*?]:\s*\n/, "").trim(); +} + +/** + * Parses and extracts [Meteor-Rspack]{}[/Meteor-Rspack] content from data. + * Returns the cleaned data (without the tag content) and the parsed JSON config. + * @param {string} data - The raw data that may contain Meteor-Rspack tags + * @returns {{ cleanedData: string, config: Object|null }} Object with cleaned data and parsed config + */ +export function parseMeteorRspackOutput(data) { + const tagRegex = /\[Meteor-Rspack\](.*?)\[\/Meteor-Rspack\]/g; + let config = null; + let match; + + // Find all matches and parse the last one (in case of multiple) + while ((match = tagRegex.exec(data)) !== null) { + try { + config = JSON.parse(match[1]); + } catch (e) { + // If JSON parsing fails, keep config as null + config = null; + } + } + + // Remove all [Meteor-Rspack]...[/Meteor-Rspack] tags from the data + const cleanedData = data.replace(tagRegex, "").trim(); + + return { cleanedData, config }; +} + +const compilationCount = {}; +let hmrServerLogged = false; + +/** + * Logs "=> Started Rspack HMR server at <devServerUrl>" if devServerUrl exists in config. + * Only logs once per session. + * @param {Object|null} config - The parsed config from MeteorRspackOutputPlugin + */ +export function logHmrServerStarted(config) { + if (hmrServerLogged) return; + if (!config?.devServerUrl) return; + hmrServerLogged = true; + logRaw(`=> Started Rspack HMR server at ${config.devServerUrl}/`); +} + +/** + * Logs a friendly Meteor-style message with the raw Rspack output appended. + * Strips the leading label and logs as: + * "=> Compiled your client app compiled successfully in 342 ms" + * Adds a leading newline from the second compilation onwards per target. + * @param {string} output - The raw stdout line from an Rspack process + * @param {string} target - The build target label (e.g. "client", "server") + * @param {boolean} statsOverrided - If true, skip cleaning and use \n separator + */ +export function logCompilationOutput(output, target, statsOverrided = false) { + let cleaned; + let separator; + // Logs original Rspack logging when stats overrided by user + if (statsOverrided) { + cleaned = stripRspackLabel(output); + separator = "\n"; + } else { + cleaned = stripRspackLabel(output) + .replace(/^.*\[.*?]\s*/g, "") + .trim() + .replace(/\s*compiled\s*/g, ""); + separator = cleaned.includes("\n") ? ":\n" : " "; + // Ignore successful logs on default Meteor-Rspack logging + if (/\s*successfully\s*/g.test(cleaned)) return; + } + compilationCount[target] = (compilationCount[target] || 0) + 1; + const prefix = compilationCount[target] > 1 ? "\n=>" : "=>"; + logRaw(`${prefix} Compiled Rspack ${target} app${separator}${cleaned}`); +} diff --git a/packages/rspack/lib/processes.js b/packages/rspack/lib/processes.js new file mode 100644 index 0000000000..d453c5144f --- /dev/null +++ b/packages/rspack/lib/processes.js @@ -0,0 +1,649 @@ +/** + * @module processes + * @description Functions for managing Rspack processes + */ + +import fs from "fs"; +import path from "path"; + +const { + spawnProcess, + stopProcess, + isProcessRunning +} = require('meteor/tools-core/lib/process'); + +const { + logError, + logInfo, + logRaw, + getRunLog, +} = require("meteor/tools-core/lib/log"); + +const { + getMeteorAppDir, + isMeteorAppTest, + isMeteorAppTestFullApp, + isMeteorAppDevelopment, + isMeteorAppProduction, + isMeteorAppDebug, + isMeteorAppRun, + isMeteorAppBuild, + isMeteorAppNative, + isMeteorBlazeProject, + isMeteorBlazeHotProject, + getMeteorInitialAppEntrypoints, + isMeteorAppConfigModernVerbose, + isMeteorBundleVisualizerProject, + getMeteorAppPort, + inheritMeteorToolNodeFlags, +} = require('meteor/tools-core/lib/meteor'); + +const { + checkNpmDependencyExists, + getNpxCommand, + getMonorepoPath, +} = require('meteor/tools-core/lib/npm'); + +const { + getGlobalState, + setGlobalState +} = require('meteor/tools-core/lib/global-state'); + +const { + GLOBAL_STATE_KEYS, + RSPACK_CHUNKS_CONTEXT, + RSPACK_ASSETS_CONTEXT, + FILE_ROLE, +} = require('./constants'); + +const { + getBuildFilePath, + getBuildFileContent, +} = require('./build-context'); + +import { + logCompilationOutput, + logHmrServerStarted, + parseMeteorRspackOutput, + shouldLogVerbose, + stripRspackLabel, +} from "./logging"; +import { isMeteorAppProfile } from "../../tools-core/lib/meteor"; + +/** + * Calculates the devServerPort based on process.env.PORT + * Base port is 8077, and we add the sum of the digits of process.env.PORT + * @returns {number} The calculated devServerPort + */ +export function calculateDevServerPort() { + const port = getMeteorAppPort(); + const basePort = 8077; + + // Sum the digits of the port + const digitSum = port.split('').reduce((sum, digit) => sum + parseInt(digit, 10), 0); + + return basePort + digitSum; +} + +/** + * Calculates the Rsdoctor client port based on process.env.PORT + * Base port is 8885, and we add the sum of the digits of process.env.PORT + * @returns {number} The calculated Rsdoctor client port + */ +export function calculateRsdoctorClientPort() { + const port = getMeteorAppPort(); + const basePort = 8885; + + // Sum the digits of the port + const digitSum = port.split('').reduce((sum, digit) => sum + parseInt(digit, 10), 0); + + return basePort + digitSum; +} + +/** + * Calculates the Rsdoctor server port based on process.env.PORT + * Base port is 8885, and we add the sum of the digits of process.env.PORT + 1 + * @returns {number} The calculated Rsdoctor server port + */ +export function calculateRsdoctorServerPort() { + const port = getMeteorAppPort(); + const basePort = 8885; + + // Sum the digits of the port + const digitSum = port.split('').reduce((sum, digit) => sum + parseInt(digit, 10), 0); + + // Add 1 to differentiate from client port + return basePort + digitSum + 1; +} + +/** + * Helper function to check for a file with different extensions in order of priority + * @param {string} basePath - The base directory path (without 'rspack.config' and extension) + * @returns {string|null} The full path with extension if found, null otherwise + */ +export function getCustomConfigFilePath(basePath = getMeteorAppDir()) { + const configBasePath = path.join(basePath, 'rspack.config'); + + // Check for .js extension first (highest priority) + const jsPath = `${configBasePath}.js`; + if (fs.existsSync(jsPath)) { + return jsPath; + } + + // Check for .ts extension next + const tsPath = `${configBasePath}.ts`; + if (fs.existsSync(tsPath)) { + return tsPath; + } + + // Check for .mjs extension next + const mjsPath = `${configBasePath}.mjs`; + if (fs.existsSync(mjsPath)) { + return mjsPath; + } + + // Check for .cjs extension last + const cjsPath = `${configBasePath}.cjs`; + if (fs.existsSync(cjsPath)) { + return cjsPath; + } + + // No valid config file found with any extension + return null; +} + +/** + * Gets the appropriate config file name based on environment + * @returns {string} The name of the Rspack config file + * @throws {Error} If no valid config file is found + */ +export function getConfigFilePath() { + // Check if the config file exists at the current path with any of the supported extensions + const defaultConfigBasePath = path.join(process.cwd(), 'node_modules/@meteorjs/rspack'); + const defaultConfigPath = getCustomConfigFilePath(defaultConfigBasePath); + if (defaultConfigPath) { + return defaultConfigPath; + } + + // If not found, check if we're in a monorepo and look for alternative config + const monorepoPath = getMonorepoPath(); + if (monorepoPath) { + const alternativeConfigBasePath = path.join(monorepoPath, 'node_modules/@meteorjs/rspack'); + const alternativeConfigPath = getCustomConfigFilePath(alternativeConfigBasePath); + if (alternativeConfigPath) { + return alternativeConfigPath; + } + } + + // If no config file is found, throw an error with suggestion to run npm install + const isYarnProj = process.env.YARN_ENABLED === 'true'; + const installCommand = isYarnProj ? 'yarn install' : 'npm install'; + throw new Error( + `Could not find rspack.config.js, rspack.config.ts, rspack.config.mjs, or rspack.config.cjs.\n\n` + + `Try running \`${installCommand}\` in your project directory and then re-run the build.\n` + + `This will ensure @meteorjs/rspack is installed correctly.` + ); +} + +/** + * Gets the appropriate Rspack environment variables and command line arguments + * @param {Object} options - Options for environment variables + * @param {boolean} options.isClient - Whether this is for client-side build + * @param {boolean} options.isServer - Whether this is for server-side build + * @param {boolean} options.isTest - Whether this is for test build + * @param {boolean} options.isTestLike - Whether test envs should be inherited + * @returns {Object} Object containing params (command line arguments) and envs (environment variables) + */ +export function getRspackEnv({ isClient, isServer, isTest: inIsTest, isTestLike: inIsTestLike }) { + const RSPACK_BUILD_CONTEXT = require('./constants').RSPACK_BUILD_CONTEXT; + + const initialEntrypoints = getMeteorInitialAppEntrypoints(); + const isTest = inIsTest != null ? inIsTest : isMeteorAppTest(); + const isTestLike = isTest || inIsTestLike; + const isTestEager = + initialEntrypoints.testModule == null && + initialEntrypoints.testClient == null && + initialEntrypoints.testServer == null; + const isTestModule = initialEntrypoints.testModule != null || isTestEager; + const isTestFullApp = isMeteorAppTestFullApp(); + + const module = isTest ? { isTest: true } : { isMain: true }; + const env = isMeteorAppDevelopment() + ? { isDevelopment: true } + : { isProduction: true }; + const side = isClient ? { isClient: true } : { isServer: true }; + const commandRole = isMeteorAppRun() + ? { role: FILE_ROLE.run } + : isMeteorAppBuild() + ? { role: FILE_ROLE.build } + : { role: FILE_ROLE.run }; + + const entryKey = `${isTest && isTestModule ? 'test' : 'main'}${isClient ? 'Client' : 'Server'}`; + const inputFilePath = initialEntrypoints[entryKey]; + const isTypescriptEnabled = process.env.METEOR_TYPESCRIPT_ENABLED === 'true' || + inputFilePath?.endsWith('.ts') || + inputFilePath?.endsWith('.tsx'); + + const isReactEnabled = process.env.METEOR_REACT_ENABLED === 'true'; + const isAngularEnabled = process.env.METEOR_ANGULAR_ENABLED === 'true'; + const isTsxEnabled = isTypescriptEnabled && (inputFilePath?.endsWith('.tsx') || isReactEnabled); + const isJsxEnabled = !isTypescriptEnabled && (inputFilePath?.endsWith('.jsx') || isReactEnabled); + + const isBlazeEnabled = isMeteorBlazeProject(); + const isBlazeHotEnabled = isMeteorBlazeHotProject(); + const isBundleVisualizerEnabled = isMeteorBundleVisualizerProject(); + + const isProfile = isMeteorAppProfile(); + + const swcExternalHelpers = checkNpmDependencyExists('@swc/helpers'); + + const configPath = getConfigFilePath(); + const projectConfigPath = getCustomConfigFilePath(); + + const pairs = [ + ["isDevelopment", isMeteorAppDevelopment()], + ["isProduction", isMeteorAppProduction()], + ["isDebug", isMeteorAppDebug()], + ["isVerbose", isMeteorAppConfigModernVerbose()], + ...((isProfile && [["isProfile", isMeteorAppProfile()]]) || []), + ["isTest", isTest], + ...(isTestLike ? [["isTestLike", isTestLike || isTest]] : []), + ...((isTestLike && isTestFullApp && [["isTestFullApp", isTestFullApp]]) || + []), + ...((isTestLike && isTestModule && [["isTestModule", isTestModule]]) || []), + ...((isTestLike && isTestEager && [["isTestEager", isTestEager]]) || []), + ["isRun", isMeteorAppRun()], + ["isBuild", isMeteorAppBuild()], + ["isNative", isMeteorAppNative()], + ["isClient", isClient], + ["isServer", isServer], + [ + "entryPath", + getBuildFilePath({ + ...module, + ...env, + ...side, + isTestModule, + role: FILE_ROLE.entry, + }), + ], + [ + "outputPath", + getBuildFilePath({ + ...module, + ...env, + ...side, + isTestModule, + role: FILE_ROLE.output, + }), + ], + [ + "outputFilename", + getBuildFilePath({ + ...env, + ...side, + isMain: true, + role: FILE_ROLE.output, + onlyFilename: true, + }), + ], + [ + "runPath", + getBuildFilePath({ ...module, ...env, ...side, ...commandRole }), + ], + ["buildContext", RSPACK_BUILD_CONTEXT], + ["chunksContext", RSPACK_CHUNKS_CONTEXT], + ["assetsContext", RSPACK_ASSETS_CONTEXT], + ["devServerPort", process.env.RSPACK_DEVSERVER_PORT], + ["projectConfigPath", projectConfigPath], + ["configPath", configPath], + ...((isTest && + initialEntrypoints.testClient && + initialEntrypoints.testServer && [ + ["testClientEntry", initialEntrypoints.testClient], + ["testServerEntry", initialEntrypoints.testServer], + ]) || + (isTest && + initialEntrypoints.testModule && [ + ["testEntry", initialEntrypoints.testModule], + ]) || [ + ["mainClientEntry", initialEntrypoints.mainClient], + ["mainClientHtmlEntry", initialEntrypoints.mainClientHtml], + ["mainServerEntry", initialEntrypoints.mainServer], + ]), + ...((swcExternalHelpers && [["swcExternalHelpers", swcExternalHelpers]]) || + []), + ...((isReactEnabled && [["isReactEnabled", isReactEnabled]]) || []), + ...((isBlazeEnabled && [["isBlazeEnabled", isBlazeEnabled]]) || []), + ...((isBlazeHotEnabled && [["isBlazeHotEnabled", isBlazeHotEnabled]]) || + []), + ...((isTypescriptEnabled && [ + ["isTypescriptEnabled", isTypescriptEnabled], + ]) || + []), + ...((isAngularEnabled && [["isAngularEnabled", isAngularEnabled]]) || []), + ...((isTsxEnabled && [["isTsxEnabled", isTsxEnabled]]) || []), + ...((isJsxEnabled && [["isJsxEnabled", isJsxEnabled]]) || []), + ...((isBundleVisualizerEnabled && [ + ["isBundleVisualizerEnabled", isBundleVisualizerEnabled], + ["rsdoctorClientPort", process.env.RSDOCTOR_CLIENT_PORT], + ["rsdoctorServerPort", process.env.RSDOCTOR_SERVER_PORT], + ]) || + []), + ].filter(Boolean); + + // Create environment variables object with bannerOutput + const envs = { + RSPACK_BANNER: JSON.stringify(getBuildFileContent({ ...module, ...env, ...side, role: FILE_ROLE.output })) + }; + + // Create params from pairs + const params = pairs.flatMap(([key, val]) => [ + '--env', + `${key}=${val}` + ]); + + return { params, envs }; +} + +/** + * Starts Rspack for client in serve mode + * @param {Object} options - Options for client serve + * @param {Function} options.onCompile - Callback function to be called when compilation is complete + * @returns {Object} The client process object + */ +export function startRspackClientServe(options = {}) { + const { onCompile } = options; + // Get the current client process from global state + const clientProcess = getGlobalState(GLOBAL_STATE_KEYS.CLIENT_PROCESS, null); + + // Skip if client process is already running + if (clientProcess && isProcessRunning(clientProcess)) { + return clientProcess; + } + + const appDir = getMeteorAppDir(); + const configFile = getConfigFilePath(); + const { params, envs } = getRspackEnv({ isClient: true, isServer: false }); + const { command, args } = getNpxCommand(['rspack', 'serve', '--config', configFile, ...params]); + const newClientProcess = spawnProcess( + command, + args, { + cwd: appDir, + env: inheritMeteorToolNodeFlags({ ...process.env, ...envs }), + onStdout: (data) => { + const { cleanedData, config } = parseMeteorRspackOutput(data); + if (config && !!config?.devServerUrl) { + logHmrServerStarted(config); + } + if (onCompile && config && (config?.compilationCount || 0) > 0) { + onCompile(cleanedData, config); + + if ( + config?.name?.includes("client") && + !config?.hasErrors && + config?.isRebuild + ) { + getRunLog()?.logClientRestart(); + } + } + if (!cleanedData) return; + if (shouldLogVerbose()) { + logInfo(`[Rspack Client] ${cleanedData}`); + } else { + logCompilationOutput(cleanedData, 'client', config?.statsOverrided); + } + }, + onStderr: (data) => { + const { cleanedData } = parseMeteorRspackOutput(data); + if (!cleanedData) return; + // Check if this is an EADDRINUSE error in development mode (which we want to completely ignore) + if (isMeteorAppDevelopment() && cleanedData.includes('EADDRINUSE')) { + if (shouldLogVerbose()) { + logError(`[Rspack Client Error] ${cleanedData}`); + } else { + logError(stripRspackLabel(cleanedData)); + } + return; + } + // Check if this is actually an informational message (like webpack-dev-server messages) + if (cleanedData.includes('Loopback:') || cleanedData.includes('Project is running at:')) { + if (shouldLogVerbose()) { + logInfo(`[Rspack Client] ${cleanedData}`); + } else { + logRaw(stripRspackLabel(cleanedData)); + } + } else { + // Check if this is the "npm error could not determine executable to run" error + if (cleanedData.includes('npm error could not determine executable to run')) { + const errorMsg = '[Rspack Client Error] Try running "meteor npm install" to ensure rspack is available'; + if (shouldLogVerbose()) { + logError(errorMsg); + } else { + logError('Try running "meteor npm install" to ensure rspack is available'); + } + throw new Error(errorMsg); + } + if (shouldLogVerbose()) { + logError(`[Rspack Client Error] ${cleanedData}`); + } else { + logError(stripRspackLabel(cleanedData)); + } + } + }, + onError: (err) => { + const errorMsg = `Rspack Error: ${err.message}`; + if (shouldLogVerbose()) { + logError(errorMsg); + } else { + logError(err.message); + } + throw new Error(errorMsg); + }, + }); + + // Store the new process in global state + setGlobalState(GLOBAL_STATE_KEYS.CLIENT_PROCESS, newClientProcess); + + return newClientProcess; +} + +/** + * Starts Rspack for server in build --watch mode + * @param {Object} options - Options for server watch + * @param {Function} options.onCompile - Callback function to be called when compilation is complete + * @returns {Object} The server process object + */ +export function startRspackServerWatch(options = {}) { + const { onCompile } = options; + // Get the current server process from global state + const serverProcess = getGlobalState(GLOBAL_STATE_KEYS.SERVER_PROCESS, null); + + // Skip if server process is already running + if (serverProcess && isProcessRunning(serverProcess)) { + return serverProcess; + } + + const appDir = getMeteorAppDir(); + const configFile = getConfigFilePath(); + const { params, envs } = getRspackEnv({ isClient: false, isServer: true }); + const { command, args } = getNpxCommand(['rspack', 'build', '--watch', '--config', configFile, ...params]); + const newServerProcess = spawnProcess( + command, + args, { + cwd: appDir, + env: inheritMeteorToolNodeFlags({ ...process.env, ...envs }), + onStdout: (data) => { + const { cleanedData, config } = parseMeteorRspackOutput(data); + if (onCompile && config && (config?.compilationCount || 0) > 0) { + onCompile(cleanedData, config); + } + if (!cleanedData) return; + if (shouldLogVerbose()) { + logInfo(`[Rspack Server] ${cleanedData}`); + } else { + logCompilationOutput(cleanedData, 'server', config?.statsOverrided); + } + }, + onStderr: (data) => { + const { cleanedData } = parseMeteorRspackOutput(data); + if (!cleanedData) return; + // Check if this is actually an informational message (like webpack-dev-server messages) + if (cleanedData.includes('Project is running at:')) { + if (shouldLogVerbose()) { + logInfo(`[Rspack Server] ${cleanedData}`); + } else { + logRaw(stripRspackLabel(cleanedData)); + } + } else { + // Check if this is the "npm error could not determine executable to run" error + if (cleanedData.includes('npm error could not determine executable to run')) { + const errorMsg = '[Rspack Server Error] Try running "meteor npm install" to ensure rspack is available'; + if (shouldLogVerbose()) { + logError(errorMsg); + } else { + logError('Try running "meteor npm install" to ensure rspack is available'); + } + throw new Error(errorMsg); + } + if (shouldLogVerbose()) { + logError(`[Rspack Server Error] ${cleanedData}`); + } else { + logError(stripRspackLabel(cleanedData)); + } + } + }, + onError: (err) => { + const errorMsg = `Rspack Error: ${err.message}`; + if (shouldLogVerbose()) { + logError(errorMsg); + } else { + logError(err.message); + } + throw new Error(errorMsg); + } + }); + + // Store the new process in global state + setGlobalState(GLOBAL_STATE_KEYS.SERVER_PROCESS, newServerProcess); + + return newServerProcess; +} + +/** + * Runs Rspack build for both client and server without watch mode + * @param {Object} options - Options for the build + * @param {boolean} options.isClient - Whether this is a client build + * @param {boolean} options.isServer - Whether this is a server build + * @param {boolean} options.isTestModule - Whether this is a test module + * @param {Function} options.onCompile - Callback function to be called when compilation is complete + * @param {boolean} options.watch - Whether to run Rspack in watch mode + * @returns {Promise<void>} A promise that resolves when the build is complete + * @throws {Error} If the build process fails + */ +export async function runRspackBuild({ isClient, isServer, isTest, isTestModule, isTestLike, onCompile, watch, label = 'Build' } = {}) { + const appDir = getMeteorAppDir(); + const configFile = getConfigFilePath(); + + const endpoint = isClient ? 'Client' : 'Server'; + // Use a promise to ensure Meteor waits until Rspack finishes + return new Promise((resolve, reject) => { + const { params, envs } = getRspackEnv({ isClient, isServer, isTest, isTestModule, isTestLike }); + const rspackArgs = [ + 'rspack', + 'build', + '--config', + configFile, + ...(watch && ['--watch']) || [], + ...params, + ].filter(Boolean); + const { command, args } = getNpxCommand(rspackArgs); + spawnProcess( + command, + args, + { + cwd: appDir, + env: inheritMeteorToolNodeFlags({ ...process.env, ...envs }), + onStdout: (data) => { + const { cleanedData, config } = parseMeteorRspackOutput(data); + if (onCompile && config && (config?.compilationCount || 0) > 0) { + onCompile(cleanedData, config); + } + if (!cleanedData) return; + if (shouldLogVerbose()) { + logInfo(`[Rspack ${label} ${endpoint}] ${cleanedData}`); + } else { + logCompilationOutput(cleanedData, endpoint.toLowerCase(), config?.statsOverrided); + } + }, + onStderr: (data) => { + const { cleanedData } = parseMeteorRspackOutput(data); + if (!cleanedData) return; + // Check if this is actually an informational message (like webpack-dev-server messages) + if (cleanedData.includes('Project is running at:')) { + if (shouldLogVerbose()) { + logInfo(`[Rspack ${label} ${endpoint}] ${cleanedData}`); + } else { + logRaw(stripRspackLabel(cleanedData)); + } + } else { + // Check if this is the "npm error could not determine executable to run" error + if (cleanedData.includes('npm error could not determine executable to run')) { + const errorMsg = `[Rspack ${label} Error ${endpoint}] Try running "meteor npm install" to ensure rspack is available`; + if (shouldLogVerbose()) { + logError(errorMsg); + } else { + logError(`Try running "meteor npm install" to ensure rspack is available`); + } + throw new Error(errorMsg); + } + if (shouldLogVerbose()) { + logError(`[Rspack ${label} Error ${endpoint}] ${cleanedData}`); + } else { + logError(stripRspackLabel(cleanedData)); + } + } + }, + onExit: (code) => { + if (code === 0) { + resolve(); + } else { + const error = new Error(`Rspack ${label} failed in ${endpoint} with exit code ${code}`); + if (shouldLogVerbose()) { + logError(error.message); + } else { + logError(`Rspack ${label} failed with exit code ${code}`); + } + reject(error); + } + }, + onError: (err) => { + if (shouldLogVerbose()) { + logError(`Rspack ${label} ${endpoint} error: ${err.message}`); + } else { + logError(err.message); + } + reject(err); + } + }); + }); +} + +/** + * Cleans up processes when the plugin is stopped + * Stops any running client and server processes and clears their global state + * @returns {void} + */ +export function cleanup() { + const clientProcess = getGlobalState(GLOBAL_STATE_KEYS.CLIENT_PROCESS, null); + if (clientProcess) { + stopProcess(clientProcess); + setGlobalState(GLOBAL_STATE_KEYS.CLIENT_PROCESS, null); + } + + const serverProcess = getGlobalState(GLOBAL_STATE_KEYS.SERVER_PROCESS, null); + if (serverProcess) { + stopProcess(serverProcess); + setGlobalState(GLOBAL_STATE_KEYS.SERVER_PROCESS, null); + } +} diff --git a/packages/rspack/package.js b/packages/rspack/package.js new file mode 100644 index 0000000000..5be0e1bdca --- /dev/null +++ b/packages/rspack/package.js @@ -0,0 +1,33 @@ +Package.describe({ + summary: "Integrate rspack into the Meteor lifecycle to run the bundler independently", + version: '1.0.0', +}); + +Package.registerBuildPlugin({ + name: 'rspack', + sources: [ + 'lib/constants.js', + 'lib/dependencies.js', + 'lib/build-context.js', + 'lib/processes.js', + 'lib/config.js', + 'rspack_plugin.js', + ], + use: ['modules@0.8.2', 'ecmascript', 'tools-core'], +}); + +Npm.devDepends({ + 'http-proxy-middleware': '3.0.5', +}); + +Package.onUse(function (api) { + api.use('ecmascript', ['client', 'server']); + api.use(['tools-core', 'webapp']); + + api.mainModule('rspack_server.js', 'server'); +}); + +Package.onTest(function (api) { + api.use(['tinytest', 'ecmascript', 'rspack']); + api.addFiles(['rspack_tests.js']); +}); diff --git a/packages/rspack/rspack_plugin.js b/packages/rspack/rspack_plugin.js new file mode 100644 index 0000000000..73c6a6eb5c --- /dev/null +++ b/packages/rspack/rspack_plugin.js @@ -0,0 +1,376 @@ +/** + * @module rspack_plugin + * @description Rspack Plugin for Meteor + * + * This is the main entry point for the Rspack plugin. It orchestrates the integration + * between Rspack and Meteor by: + * 1. Ensuring Rspack and related dependencies are installed + * 2. Setting up the build context directory + * 3. Configuring Meteor settings for Rspack + * 4. Starting Rspack processes based on the Meteor command (run or build) + * 5. Handling cleanup when the plugin is stopped + * + * The plugin uses top-level await to ensure asynchronous operations complete + * before Meteor continues execution. + */ + +// Import modules from lib +const { + GLOBAL_STATE_KEYS, +} = require('./lib/constants'); + +const { + ensureRspackInstalled, + checkReactInstalled, + checkAngularInstalled, + checkTypescriptInstalled, + ensureRspackReactInstalled, +} = require('./lib/dependencies'); + +const { + ensureRspackBuildContextExists, + ensureRspackConfigExists, + cleanBuildContextFiles, +} = require('./lib/build-context'); + +const { + startRspackClientServe, + startRspackServerWatch, + runRspackBuild, + cleanup, + calculateDevServerPort, + calculateRsdoctorClientPort, + calculateRsdoctorServerPort, + getConfigFilePath, + getCustomConfigFilePath, +} = require('./lib/processes'); + +const { + configureMeteorForRspack +} = require('./lib/config'); + +const { + setupCompilationTracking, + waitForFirstCompilation, +} = require('./lib/compilation'); + +const { + getGlobalState, + setGlobalState +} = require('meteor/tools-core/lib/global-state'); + +const { + isMeteorAppRun, + isMeteorAppBuild, + isMeteorAppUpdate, + getMeteorInitialAppEntrypoints, + getMeteorAppEntrypoints, + isMeteorAppTest, + isMeteorAppTestWatch, + isMeteorAppDevelopment, + isMeteorAppProduction, + isMeteorAppDebug, + isMeteorAppConfigModernVerbose, + isMeteorAppNative, + isMeteorBundleVisualizerProject, +} = require('meteor/tools-core/lib/meteor'); + +const { + logInfo, + logError, +} = require('meteor/tools-core/lib/log'); + +const { + getNpxCommand, + getNpmCommand, + getYarnCommand, + isYarnProject, +} = require('meteor/tools-core/lib/npm'); +const { hasMeteorAppConfigAutoInstallDeps } = require("../tools-core/lib/meteor"); + +// Get entry points from Meteor configuration +let initialEntrypoints; +if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest() || isMeteorAppUpdate()) { + initialEntrypoints = getMeteorInitialAppEntrypoints(); + + // Check if mainClient and mainServer exist + if (!initialEntrypoints?.mainServer) { + logError(`\n┌─────────────────────────────────────────────────`); + logError(`│ ❌ Missing Required Entry Points`); + logError(`└─────────────────────────────────────────────────`); + logError(`Your project is missing the required entry points for Rspack.`); + logError(`Please add the following to your package.json file:`); + logError(` +{ + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + } + } +} +`); + logError(`Make sure to replace the paths with your actual entry point files.`); + + throw new Error( + "Missing required entry points. Please add meteor.mainModule.client and meteor.mainModule.server in your package.json file." + ); + } + + setGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS, getMeteorAppEntrypoints()); + + let isYarnProj = process.env.YARN_ENABLED === 'true'; + // Main entry point - using top-level await + try { + // Check if the project is a Yarn project and store the result in environment variable if not already set + if (process.env.YARN_ENABLED === undefined) { + isYarnProj = isYarnProject(); + process.env.YARN_ENABLED = isYarnProj ? 'true' : 'false'; + } + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] Meteor Npx prefix: ${getNpxCommand([])?.prefix}`); + logInfo(`[i] Meteor Npm prefix: ${getNpmCommand([])?.prefix}`); + if (isYarnProj) { + logInfo(`[i] Meteor Yarn prefix: ${getYarnCommand([])?.prefix}`); + } + } + + // Clean build context files only if they haven't been cleaned yet + if (!getGlobalState(GLOBAL_STATE_KEYS.BUILD_CONTEXT_FILES_CLEANED)) { + cleanBuildContextFiles(); + setGlobalState(GLOBAL_STATE_KEYS.BUILD_CONTEXT_FILES_CLEANED, true); + } + + // Auto install deps (by default enabled) + if (hasMeteorAppConfigAutoInstallDeps()) { + // Ensure Rspack is installed + await ensureRspackInstalled(); + } + + // Check if Rspack React is installed + if (checkReactInstalled()) { + // Auto install deps (by default enabled) + if (hasMeteorAppConfigAutoInstallDeps()) { + await ensureRspackReactInstalled(); + } + } + } catch (error) { + logError(`Rspack plugin error: ${error.message}`); + throw error; + } +} + +if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) { + try { + // Check if Angular is installed + checkAngularInstalled(); + + // Check if TypeScript is installed + checkTypescriptInstalled(); + + // Ensure the Rspack build context directory exists + ensureRspackBuildContextExists(); + + // Ensure the rspack.config.js file exists at the project level + ensureRspackConfigExists(); + + // Configure Meteor settings for Rspack + configureMeteorForRspack(); + + // Set native mode flag so the server module can skip dev proxy setup + if (isMeteorAppNative()) { + process.env.RSPACK_NATIVE = 'true'; + } + + // Calculate and set the devServerPort at boot + if (!process.env.RSPACK_DEVSERVER_PORT) { + process.env.RSPACK_DEVSERVER_PORT = calculateDevServerPort(); + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] Rspack DevServer Port: ${process.env.RSPACK_DEVSERVER_PORT}`); + } + } + + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + const configFile = getConfigFilePath(); + logInfo(`[i] Rspack default config: ${configFile}`); + const projectConfigFile = getCustomConfigFilePath(); + logInfo(`[i] Rspack custom config: ${projectConfigFile}`); + } + + // Calculate and set the Rsdoctor client and server ports at boot only if bundle visualizer is enabled + if (isMeteorBundleVisualizerProject()) { + if (!process.env.RSDOCTOR_CLIENT_PORT) { + process.env.RSDOCTOR_CLIENT_PORT = calculateRsdoctorClientPort(); + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] Rsdoctor Client Port: ${process.env.RSDOCTOR_CLIENT_PORT}`); + } + } + + if (!process.env.RSDOCTOR_SERVER_PORT) { + process.env.RSDOCTOR_SERVER_PORT = calculateRsdoctorServerPort(); + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] Rsdoctor Server Port: ${process.env.RSDOCTOR_SERVER_PORT}`); + } + } + } + + // Register cleanup handler + process.on('exit', cleanup); + process.on('SIGINT', () => { + cleanup(); + process.exit(); + }); + + // When running `meteor run` command + if (isMeteorAppRun()) { + // Setup compilation tracking and callbacks + const { + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + onCompileClient, + onCompileServer, + } = setupCompilationTracking(); + + // For 'run' command, start Rspack in appropriate modes with distinct callbacks + if (isMeteorAppDevelopment() && !isMeteorAppNative()) { + if (initialEntrypoints?.mainClient) { + startRspackClientServe({ onCompile: onCompileClient }); + } + if (initialEntrypoints?.mainServer) { + startRspackServerWatch({ onCompile: onCompileServer }); + } + } else if (isMeteorAppProduction() || isMeteorAppNative()) { + if (initialEntrypoints?.mainClient) { + runRspackBuild({ + isClient: true, + isServer: false, + watch: true, + onCompile: onCompileClient, + }); + } + if (initialEntrypoints?.mainServer) { + runRspackBuild({ + isServer: true, + isClient: false, + watch: true, + onCompile: onCompileServer, + }); + } + } + + // Wait for first compilation to complete + const waitTarget = + initialEntrypoints?.mainClient && initialEntrypoints?.mainServer + ? 'both' + : 'server'; + await waitForFirstCompilation( + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + { target: waitTarget }, + ); + + // When running `meteor test` command + } else if (isMeteorAppTest()) { + const initialEntrypoints = getMeteorInitialAppEntrypoints(); + + // Setup compilation tracking and callbacks + const { + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + onCompileClient, + onCompileServer, + } = setupCompilationTracking(); + + // When testModule is specified for client or server, run Rspack considering those files + if (initialEntrypoints?.testClient || initialEntrypoints?.testServer) { + if (initialEntrypoints?.testClient) { + runRspackBuild({ + isTest: true, + isClient: true, + isServer: false, + watch: isMeteorAppTestWatch(), + onCompile: onCompileClient, + label: 'Test', + }); + } + + if (initialEntrypoints?.testServer) { + runRspackBuild({ + isTest: true, + isClient: false, + isServer: true, + watch: isMeteorAppTestWatch(), + onCompile: onCompileServer, + label: 'Test', + }); + } + + // Wait for first compilation to complete + const waitTarget = + initialEntrypoints?.testClient && initialEntrypoints?.testServer + ? 'both' + : 'server'; + await waitForFirstCompilation( + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + { target: waitTarget }, + ); + + // When testModule is specified as a single file or not specified + } else { + if (initialEntrypoints?.testModule) { + runRspackBuild({ + isTest: true, + isTestModule: true, + isClient: true, + isServer: false, + watch: isMeteorAppTestWatch(), + onCompile: onCompileClient, + label: 'Test', + }); + } + runRspackBuild({ + isTest: true, + isTestModule: true, + isClient: false, + isServer: true, + watch: isMeteorAppTestWatch(), + onCompile: onCompileServer, + label: 'Test', + }); + + const waitTarget = initialEntrypoints?.testModule ? 'both' : 'server'; + await waitForFirstCompilation( + clientFirstCompile, + serverFirstCompile, + clientFirstCompilePromise, + serverFirstCompilePromise, + { target: waitTarget } + ); + } + + // When running `meteor build` command + } else if (isMeteorAppBuild()) { + // For 'build' command, run Rspack build without watch mode + // Run client and server builds in parallel and wait for both to complete + const targetsToBuild = [ + initialEntrypoints?.mainClient && + runRspackBuild({ isClient: true, isServer: false }), + initialEntrypoints?.mainServer && + runRspackBuild({ isServer: true, isClient: false }), + ].filter(Boolean); + await Promise.all(targetsToBuild); + } + } catch (error) { + logError(`Rspack plugin error: ${error.message}`); + throw error; + } +} diff --git a/packages/rspack/rspack_server.js b/packages/rspack/rspack_server.js new file mode 100644 index 0000000000..9c55294738 --- /dev/null +++ b/packages/rspack/rspack_server.js @@ -0,0 +1,208 @@ +import { Meteor } from 'meteor/meteor'; +import { WebApp, WebAppInternals } from 'meteor/webapp'; +import path from 'path'; +import { parse as parseUrl } from 'url'; +import { + RSPACK_CHUNKS_CONTEXT, + RSPACK_ASSETS_CONTEXT, + RSPACK_HOT_UPDATE_REGEX, +} from "./lib/constants"; + +// Define constants for both development and production +const rspackChunksContext = process.env.RSPACK_CHUNKS_CONTEXT || RSPACK_CHUNKS_CONTEXT; +const rspackAssetsContext = process.env.RSPACK_ASSETS_CONTEXT || RSPACK_ASSETS_CONTEXT; + +/** + * Regex pattern for rspack bundles + * @constant {RegExp} + */ +const RSPACK_CHUNKS_REGEX = new RegExp( + `^\/${rspackChunksContext}\/(.+)$`, +); + +/** + * Regex pattern for rspack assets + * @constant {RegExp} + */ +const RSPACK_ASSETS_REGEX = new RegExp( + `^\/${rspackAssetsContext}\/(.+)$`, +); + +const shouldEnableDevHMRProxy = + global?.Package?.["tools-core"] != null && + Meteor.isDevelopment && + !process.env.RSPACK_NATIVE; +if (shouldEnableDevHMRProxy) { + const { shuffleString } = require('meteor/tools-core/lib/string'); + const { createProxyMiddleware } = require('http-proxy-middleware'); + + // Target URL for the Rspack dev server + const target = `http://localhost:${process.env.RSPACK_DEVSERVER_PORT}`; + + // Proxy HMR websocket upgrade requests + WebApp.connectHandlers.use('/ws', + createProxyMiddleware( { + target, + ws: true, + logLevel: 'debug' + }) + ); + + // Proxy all dev asset requests under the rspack prefix + WebApp.connectHandlers.use('/__rspack__', + createProxyMiddleware({ + target, + changeOrigin: true, + ws: true, + logLevel: 'debug', + }) + ); + + WebApp.rawConnectHandlers.use((req, res, next) => { + // If this request is already under /__rspack__/, don't redirect it again. + if (req.url.startsWith('/__rspack__/')) { + return next(); + } + + // 1) match ANY URL whose last segment ends with ".hot-update.js" or ".hot-update.json", + // e.g. "/main.ce385971e9f19307.hot-update.js" + // "/ui_pages_tasks_tasks-page_jsx.ce385971e9f19307.hot-update.js" + // "/foo/bar/baz.1234abcd.hot-update.json" + const hotUpdate = req.url.match(RSPACK_HOT_UPDATE_REGEX); + if (hotUpdate) { + // Redirect "/something.hot-update.js" → "/__rspack__/something.hot-update.js" + const target = `/__rspack__/${hotUpdate[1]}`; + res.writeHead(307, { Location: target }); + return res.end(); + } + + // 2) match "/build-chunks/<anything>" + const bundlesMatch = req.url.match(RSPACK_CHUNKS_REGEX); + if (bundlesMatch) { + // Redirect "/bundles/foo.js" → "/__rspack__/build-chunks/foo.js" + const target = `/__rspack__/${rspackChunksContext}/${bundlesMatch[1]}`; + res.writeHead(307, { Location: target }); + return res.end(); + } + + // 3) match "/build-assets/<anything>" + const assetsMatch = req.url.match(RSPACK_ASSETS_REGEX); + if (assetsMatch) { + // Redirect "/build-assets/foo.js" → "/__rspack__/build-assets/foo.js" + const target = `/__rspack__/${rspackAssetsContext}/${assetsMatch[1]}`; + res.writeHead(307, { Location: target }); + return res.end(); + } + + // Otherwise, let it pass through + next(); + }); + + /** + * Force client to reload after Rspack server compilation and restart, which doesn’t happen automatically. + * On each server reload, generate a new client hash once to force Meteor’s client reload. + * After the first reload, apply Meteor's default behavior. + */ + function enableClientReloadOnServerStart() { + Meteor.startup(() => { + const originalCalc = WebApp.calculateClientHashReplaceable; + let hasShuffled = false; + let cachedHash = {}; + let prevRealHash = {}; + WebApp.calculateClientHashReplaceable = function (...args) { + const arch = args[0]; + const realHash = originalCalc.apply(this, args); + if (prevRealHash[arch] && realHash !== prevRealHash[arch]) { + prevRealHash[arch] = realHash; + return realHash; + } + prevRealHash[arch] = realHash; + if (cachedHash[arch] == null) { + cachedHash[arch] = shuffleString(realHash); + hasShuffled = true; + } + return cachedHash[arch]; + }; + }); + } + + // Enable client reload on server startup + enableClientReloadOnServerStart(); +} + +/** + * Register a single rspack static asset with WebAppInternals.staticFilesByArch + * @param {string} arch - The architecture to register the asset for + * @param {string} pathname - The pathname of the asset + * @param {string} filePath - The absolute path to the asset on disk + * @returns {Object} The static file info object + */ +function registerRspackStaticAsset(arch, pathname, filePath) { + // Ensure the architecture exists in staticFilesByArch + if (!WebAppInternals.staticFilesByArch[arch]) { + WebAppInternals.staticFilesByArch[arch] = Object.create(null); + } + + // Get the static files object for this architecture + const staticFiles = WebAppInternals.staticFilesByArch[arch]; + + // Skip if already registered + if (staticFiles[pathname]) { + // Ensure the entry is marked as cacheable + staticFiles[pathname].cacheable = true; + return staticFiles[pathname]; + } + + // Determine file type based on extension + const type = pathname.endsWith(".js") ? "js" : + pathname.endsWith(".css") ? "css" : + pathname.endsWith(".json") ? "json" : undefined; + + // Extract hash from filename (assuming it's the second part after splitting by '.') + const filename = pathname.split("/").pop(); + const hash = filename.split(".")[1]; + + // Register the asset + staticFiles[pathname] = { + absolutePath: filePath, + cacheable: true, // Most rspack assets are cacheable + hash, + type + }; + + return staticFiles[pathname]; +} + +// Store the original staticFilesMiddleware +const originalStaticFilesMiddleware = WebAppInternals.staticFilesMiddleware; + +// Handle rspack assets on-demand to add Meteor's static files headers +WebAppInternals.staticFilesMiddleware = async function(staticFilesByArch, req, res, next) { + const pathname = parseUrl(req.url).pathname; + + try { + // Check if this is a rspack asset request + const chunksMatch = pathname.match(RSPACK_CHUNKS_REGEX); + const assetsMatch = pathname.match(RSPACK_ASSETS_REGEX); + + if (chunksMatch || assetsMatch) { + const cwd = process.cwd(); + const architectures = ["web.browser", "web.browser.legacy", "web.cordova"]; + WebApp.categorizeRequest(req); + + // Try to find the file on disk + const context = chunksMatch ? rspackChunksContext : rspackAssetsContext; + const filename = (chunksMatch ? chunksMatch[1] : assetsMatch[1]); + const filePath = path.join(cwd, context, filename); + + architectures.forEach(archName => { + registerRspackStaticAsset(archName, pathname, filePath); + }); + } + } catch (e) { + console.error(`Error handling rspack asset: ${e.message}`); + } + + // Call the original middleware + return originalStaticFilesMiddleware(staticFilesByArch, req, res, next); +}; diff --git a/packages/rspack/rspack_tests.js b/packages/rspack/rspack_tests.js new file mode 100644 index 0000000000..6dc6f52a00 --- /dev/null +++ b/packages/rspack/rspack_tests.js @@ -0,0 +1 @@ +Tinytest.add('rspack', () => {}); diff --git a/packages/server-render/server-render.d.ts b/packages/server-render/server-render.d.ts index a920e2514f..46c1ca713f 100644 --- a/packages/server-render/server-render.d.ts +++ b/packages/server-render/server-render.d.ts @@ -42,6 +42,7 @@ export type CategorizedRequest = Omit<http.IncomingMessage, 'url'> & { modern: boolean; path: string; url: URL; + cookies?: Record<string, string>; } export interface ServerSink extends ClientSink { diff --git a/packages/shell-server/package.js b/packages/shell-server/package.js index 1dd7551714..885e168309 100644 --- a/packages/shell-server/package.js +++ b/packages/shell-server/package.js @@ -1,8 +1,9 @@ Package.describe({ name: "shell-server", - version: '0.6.2', + version: '0.7.0', summary: "Server-side component of the `meteor shell` command.", - documentation: "README.md" + documentation: "README.md", + devOnly: true, }); Package.onUse(function(api) { diff --git a/packages/standard-minifier-css/package.js b/packages/standard-minifier-css/package.js index fbc6531d43..353a747f45 100644 --- a/packages/standard-minifier-css/package.js +++ b/packages/standard-minifier-css/package.js @@ -1,8 +1,9 @@ Package.describe({ name: 'standard-minifier-css', - version: '1.9.3', + version: '1.10.0', summary: 'Standard css minifier used with Meteor apps by default.', documentation: 'README.md', + devOnly: true, }); Package.registerBuildPlugin({ diff --git a/packages/standard-minifier-js/package.js b/packages/standard-minifier-js/package.js index 6f23b6e939..eccea4f462 100644 --- a/packages/standard-minifier-js/package.js +++ b/packages/standard-minifier-js/package.js @@ -1,8 +1,9 @@ Package.describe({ name: 'standard-minifier-js', - version: '3.1.1', + version: '3.2.0', summary: 'Standard javascript minifiers used with Meteor apps by default.', documentation: 'README.md', + devOnly: true, }); Package.registerBuildPlugin({ @@ -12,7 +13,7 @@ Package.registerBuildPlugin({ 'ecmascript' ], npmDependencies: { - '@meteorjs/swc-core': '1.12.14', + '@meteorjs/swc-core': '1.15.3', 'acorn': '8.10.0', "@babel/runtime": "7.18.9", '@babel/parser': '7.22.7', diff --git a/packages/standard-minifier-js/plugin/minify-js.js b/packages/standard-minifier-js/plugin/minify-js.js index 0fe5ca613f..58bac3f119 100644 --- a/packages/standard-minifier-js/plugin/minify-js.js +++ b/packages/standard-minifier-js/plugin/minify-js.js @@ -49,6 +49,7 @@ export class MeteorMinifier { const NODE_ENV = process.env.NODE_ENV || 'development'; let content = file.getContentsAsString(); + const isLegacyWebArch = file?._arch === 'web.browser.legacy'; return swc.minifySync( content, @@ -60,6 +61,7 @@ export class MeteorMinifier { unused: true, dead_code: true, typeofs: false, + ...(isLegacyWebArch && { defaults: false }), global_defs: { 'process.env.NODE_ENV': NODE_ENV, diff --git a/packages/standard-minifiers/package.js b/packages/standard-minifiers/package.js index 1d533f9a61..acdd37f3c4 100644 --- a/packages/standard-minifiers/package.js +++ b/packages/standard-minifiers/package.js @@ -1,8 +1,9 @@ Package.describe({ name: 'standard-minifiers', - version: '1.1.1', + version: '1.2.0', summary: 'Standard minifiers used with Meteor apps by default.', - documentation: 'README.md' + documentation: 'README.md', + devOnly: true, }); Package.onUse(function(api) { diff --git a/packages/static-html/package.js b/packages/static-html/package.js index ff22f3efa2..8b990e8958 100644 --- a/packages/static-html/package.js +++ b/packages/static-html/package.js @@ -1,8 +1,9 @@ Package.describe({ name: 'static-html', summary: "Define static page content in .html files", - version: '1.4.0', - git: 'https://github.com/meteor/meteor.git' + version: '1.5.0', + git: 'https://github.com/meteor/meteor.git', + devOnly: true, }); Package.registerBuildPlugin({ diff --git a/packages/test-in-browser/diff_match_patch_uncompressed.js b/packages/test-in-browser/diff_match_patch_uncompressed.js deleted file mode 100644 index 4d5542c1d3..0000000000 --- a/packages/test-in-browser/diff_match_patch_uncompressed.js +++ /dev/null @@ -1,2218 +0,0 @@ -/** - * Diff Match and Patch - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Computes the difference between two texts to create a patch. - * Applies the patch onto another text, allowing for errors. - * @author fraser@google.com (Neil Fraser) - */ - -/** - * Class containing the diff, match and patch methods. - * @constructor - */ -var diff_match_patch = function() { - - // Defaults. - // Redefine these in your program to override the defaults. - - // Number of seconds to map a diff before giving up (0 for infinity). - this.Diff_Timeout = 1.0; - // Cost of an empty edit operation in terms of edit characters. - this.Diff_EditCost = 4; - // At what point is no match declared (0.0 = perfection, 1.0 = very loose). - this.Match_Threshold = 0.5; - // How far to search for a match (0 = exact location, 1000+ = broad match). - // A match this many characters away from the expected location will add - // 1.0 to the score (0.0 is a perfect match). - this.Match_Distance = 1000; - // When deleting a large block of text (over ~64 characters), how close do - // the contents have to be to match the expected contents. (0.0 = perfection, - // 1.0 = very loose). Note that Match_Threshold controls how closely the - // end points of a delete need to match. - this.Patch_DeleteThreshold = 0.5; - // Chunk size for context length. - this.Patch_Margin = 4; - - // The number of bits in an int. - this.Match_MaxBits = 32; -}; - - -// DIFF FUNCTIONS - - -/** - * The data structure representing a diff is an array of tuples: - * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] - * which means: delete 'Hello', add 'Goodbye' and keep ' world.' - */ -var DIFF_DELETE = -1; -var DIFF_INSERT = 1; -var DIFF_EQUAL = 0; - -/** - * Class representing one diff tuple. - * ~Attempts to look like a two-element array (which is what this used to be).~ - * Constructor returns an actual two-element array, to allow destructing @JackuB - * See https://github.com/JackuB/diff-match-patch/issues/14 for details - * @param {number} op Operation, one of: DIFF_DELETE, DIFF_INSERT, DIFF_EQUAL. - * @param {string} text Text to be deleted, inserted, or retained. - * @constructor - */ -diff_match_patch.Diff = function(op, text) { - return [op, text]; -}; - -/** - * Find the differences between two texts. Simplifies the problem by stripping - * any common prefix or suffix off the texts before diffing. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean=} opt_checklines Optional speedup flag. If present and false, - * then don't run a line-level diff first to identify the changed areas. - * Defaults to true, which does a faster, slightly less optimal diff. - * @param {number=} opt_deadline Optional time when the diff should be complete - * by. Used internally for recursive calls. Users should set DiffTimeout - * instead. - * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. - */ -diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines, - opt_deadline) { - // Set a deadline by which time the diff must be complete. - if (typeof opt_deadline == 'undefined') { - if (this.Diff_Timeout <= 0) { - opt_deadline = Number.MAX_VALUE; - } else { - opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000; - } - } - var deadline = opt_deadline; - - // Check for null inputs. - if (text1 == null || text2 == null) { - throw new Error('Null input. (diff_main)'); - } - - // Check for equality (speedup). - if (text1 == text2) { - if (text1) { - return [new diff_match_patch.Diff(DIFF_EQUAL, text1)]; - } - return []; - } - - if (typeof opt_checklines == 'undefined') { - opt_checklines = true; - } - var checklines = opt_checklines; - - // Trim off common prefix (speedup). - var commonlength = this.diff_commonPrefix(text1, text2); - var commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = this.diff_commonSuffix(text1, text2); - var commonsuffix = text1.substring(text1.length - commonlength); - text1 = text1.substring(0, text1.length - commonlength); - text2 = text2.substring(0, text2.length - commonlength); - - // Compute the diff on the middle block. - var diffs = this.diff_compute_(text1, text2, checklines, deadline); - - // Restore the prefix and suffix. - if (commonprefix) { - diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, commonprefix)); - } - if (commonsuffix) { - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, commonsuffix)); - } - this.diff_cleanupMerge(diffs); - return diffs; -}; - - -/** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines, - deadline) { - var diffs; - - if (!text1) { - // Just add some text (speedup). - return [new diff_match_patch.Diff(DIFF_INSERT, text2)]; - } - - if (!text2) { - // Just delete some text (speedup). - return [new diff_match_patch.Diff(DIFF_DELETE, text1)]; - } - - var longtext = text1.length > text2.length ? text1 : text2; - var shorttext = text1.length > text2.length ? text2 : text1; - var i = longtext.indexOf(shorttext); - if (i != -1) { - // Shorter text is inside the longer text (speedup). - diffs = [new diff_match_patch.Diff(DIFF_INSERT, longtext.substring(0, i)), - new diff_match_patch.Diff(DIFF_EQUAL, shorttext), - new diff_match_patch.Diff(DIFF_INSERT, - longtext.substring(i + shorttext.length))]; - // Swap insertions for deletions if diff is reversed. - if (text1.length > text2.length) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - - if (shorttext.length == 1) { - // Single character string. - // After the previous speedup, the character can't be an equality. - return [new diff_match_patch.Diff(DIFF_DELETE, text1), - new diff_match_patch.Diff(DIFF_INSERT, text2)]; - } - - // Check to see if the problem can be split in two. - var hm = this.diff_halfMatch_(text1, text2); - if (hm) { - // A half-match was found, sort out the return data. - var text1_a = hm[0]; - var text1_b = hm[1]; - var text2_a = hm[2]; - var text2_b = hm[3]; - var mid_common = hm[4]; - // Send both pairs off for separate processing. - var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline); - var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline); - // Merge the results. - return diffs_a.concat([new diff_match_patch.Diff(DIFF_EQUAL, mid_common)], - diffs_b); - } - - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diff_lineMode_(text1, text2, deadline); - } - - return this.diff_bisect_(text1, text2, deadline); -}; - - -/** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) { - // Scan the text on a line-by-line basis first. - var a = this.diff_linesToChars_(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - var linearray = a.lineArray; - - var diffs = this.diff_main(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diff_charsToLines_(diffs, linearray); - // Eliminate freak matches (e.g. blank lines) - this.diff_cleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); - var pointer = 0; - var count_delete = 0; - var count_insert = 0; - var text_delete = ''; - var text_insert = ''; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - count_insert++; - text_insert += diffs[pointer][1]; - break; - case DIFF_DELETE: - count_delete++; - text_delete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (count_delete >= 1 && count_insert >= 1) { - // Delete the offending records and add the merged ones. - diffs.splice(pointer - count_delete - count_insert, - count_delete + count_insert); - pointer = pointer - count_delete - count_insert; - var subDiff = - this.diff_main(text_delete, text_insert, false, deadline); - for (var j = subDiff.length - 1; j >= 0; j--) { - diffs.splice(pointer, 0, subDiff[j]); - } - pointer = pointer + subDiff.length; - } - count_insert = 0; - count_delete = 0; - text_delete = ''; - text_insert = ''; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; -}; - - -/** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) { - // Cache the text lengths to prevent multiple calls. - var text1_length = text1.length; - var text2_length = text2.length; - var max_d = Math.ceil((text1_length + text2_length) / 2); - var v_offset = max_d; - var v_length = 2 * max_d; - var v1 = new Array(v_length); - var v2 = new Array(v_length); - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (var x = 0; x < v_length; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[v_offset + 1] = 0; - v2[v_offset + 1] = 0; - var delta = text1_length - text2_length; - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - var front = (delta % 2 != 0); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - var k1start = 0; - var k1end = 0; - var k2start = 0; - var k2end = 0; - for (var d = 0; d < max_d; d++) { - // Bail out if deadline is reached. - if ((new Date()).getTime() > deadline) { - break; - } - - // Walk the front path one step. - for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - var k1_offset = v_offset + k1; - var x1; - if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { - x1 = v1[k1_offset + 1]; - } else { - x1 = v1[k1_offset - 1] + 1; - } - var y1 = x1 - k1; - while (x1 < text1_length && y1 < text2_length && - text1.charAt(x1) == text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1_offset] = x1; - if (x1 > text1_length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2_length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - var k2_offset = v_offset + delta - k1; - if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { - // Mirror x2 onto top-left coordinate system. - var x2 = text1_length - v2[k2_offset]; - if (x1 >= x2) { - // Overlap detected. - return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - var k2_offset = v_offset + k2; - var x2; - if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { - x2 = v2[k2_offset + 1]; - } else { - x2 = v2[k2_offset - 1] + 1; - } - var y2 = x2 - k2; - while (x2 < text1_length && y2 < text2_length && - text1.charAt(text1_length - x2 - 1) == - text2.charAt(text2_length - y2 - 1)) { - x2++; - y2++; - } - v2[k2_offset] = x2; - if (x2 > text1_length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2_length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - var k1_offset = v_offset + delta - k2; - if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { - var x1 = v1[k1_offset]; - var y1 = v_offset + x1 - k1_offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1_length - x2; - if (x1 >= x2) { - // Overlap detected. - return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [new diff_match_patch.Diff(DIFF_DELETE, text1), - new diff_match_patch.Diff(DIFF_INSERT, text2)]; -}; - - -/** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y, - deadline) { - var text1a = text1.substring(0, x); - var text2a = text2.substring(0, y); - var text1b = text1.substring(x); - var text2b = text2.substring(y); - - // Compute both diffs serially. - var diffs = this.diff_main(text1a, text2a, false, deadline); - var diffsb = this.diff_main(text1b, text2b, false, deadline); - - return diffs.concat(diffsb); -}; - - -/** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ -diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) { - var lineArray = []; // e.g. lineArray[4] == 'Hello\n' - var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ''; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diff_linesToCharsMunge_(text) { - var chars = ''; - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - var lineStart = 0; - var lineEnd = -1; - // Keeping our own length variable is faster than looking it up. - var lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf('\n', lineStart); - if (lineEnd == -1) { - lineEnd = text.length - 1; - } - var line = text.substring(lineStart, lineEnd + 1); - - if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : - (lineHash[line] !== undefined)) { - chars += String.fromCharCode(lineHash[line]); - } else { - if (lineArrayLength == maxLines) { - // Bail out at 65535 because - // String.fromCharCode(65536) == String.fromCharCode(0) - line = text.substring(lineStart); - lineEnd = text.length; - } - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - lineStart = lineEnd + 1; - } - return chars; - } - // Allocate 2/3rds of the space for text1, the rest for text2. - var maxLines = 40000; - var chars1 = diff_linesToCharsMunge_(text1); - maxLines = 65535; - var chars2 = diff_linesToCharsMunge_(text2); - return {chars1: chars1, chars2: chars2, lineArray: lineArray}; -}; - - -/** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @param {!Array.<string>} lineArray Array of unique strings. - * @private - */ -diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) { - for (var i = 0; i < diffs.length; i++) { - var chars = diffs[i][1]; - var text = []; - for (var j = 0; j < chars.length; j++) { - text[j] = lineArray[chars.charCodeAt(j)]; - } - diffs[i][1] = text.join(''); - } -}; - - -/** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ -diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { - return 0; - } - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - var pointermin = 0; - var pointermax = Math.min(text1.length, text2.length); - var pointermid = pointermax; - var pointerstart = 0; - while (pointermin < pointermid) { - if (text1.substring(pointerstart, pointermid) == - text2.substring(pointerstart, pointermid)) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; -}; - - -/** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ -diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { - // Quick check for common null cases. - if (!text1 || !text2 || - text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { - return 0; - } - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - var pointermin = 0; - var pointermax = Math.min(text1.length, text2.length); - var pointermid = pointermax; - var pointerend = 0; - while (pointermin < pointermid) { - if (text1.substring(text1.length - pointermid, text1.length - pointerend) == - text2.substring(text2.length - pointermid, text2.length - pointerend)) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; -}; - - -/** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ -diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) { - // Cache the text lengths to prevent multiple calls. - var text1_length = text1.length; - var text2_length = text2.length; - // Eliminate the null case. - if (text1_length == 0 || text2_length == 0) { - return 0; - } - // Truncate the longer string. - if (text1_length > text2_length) { - text1 = text1.substring(text1_length - text2_length); - } else if (text1_length < text2_length) { - text2 = text2.substring(0, text1_length); - } - var text_length = Math.min(text1_length, text2_length); - // Quick check for the worst case. - if (text1 == text2) { - return text_length; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: https://neil.fraser.name/news/2010/11/04/ - var best = 0; - var length = 1; - while (true) { - var pattern = text1.substring(text_length - length); - var found = text2.indexOf(pattern); - if (found == -1) { - return best; - } - length += found; - if (found == 0 || text1.substring(text_length - length) == - text2.substring(0, length)) { - best = length; - length++; - } - } -}; - - -/** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.<string>} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ -diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) { - if (this.Diff_Timeout <= 0) { - // Don't risk returning a non-optimal diff if we have unlimited time. - return null; - } - var longtext = text1.length > text2.length ? text1 : text2; - var shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. - } - var dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.<string>} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diff_halfMatchI_(longtext, shorttext, i) { - // Start with a 1/4 length substring at position i as a seed. - var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - var j = -1; - var best_common = ''; - var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; - while ((j = shorttext.indexOf(seed, j + 1)) != -1) { - var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), - shorttext.substring(j)); - var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), - shorttext.substring(0, j)); - if (best_common.length < suffixLength + prefixLength) { - best_common = shorttext.substring(j - suffixLength, j) + - shorttext.substring(j, j + prefixLength); - best_longtext_a = longtext.substring(0, i - suffixLength); - best_longtext_b = longtext.substring(i + prefixLength); - best_shorttext_a = shorttext.substring(0, j - suffixLength); - best_shorttext_b = shorttext.substring(j + prefixLength); - } - } - if (best_common.length * 2 >= longtext.length) { - return [best_longtext_a, best_longtext_b, - best_shorttext_a, best_shorttext_b, best_common]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - var hm1 = diff_halfMatchI_(longtext, shorttext, - Math.ceil(longtext.length / 4)); - // Check again based on the third quarter. - var hm2 = diff_halfMatchI_(longtext, shorttext, - Math.ceil(longtext.length / 2)); - var hm; - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - var text1_a, text1_b, text2_a, text2_b; - if (text1.length > text2.length) { - text1_a = hm[0]; - text1_b = hm[1]; - text2_a = hm[2]; - text2_b = hm[3]; - } else { - text2_a = hm[0]; - text2_b = hm[1]; - text1_a = hm[2]; - text1_b = hm[3]; - } - var mid_common = hm[4]; - return [text1_a, text1_b, text2_a, text2_b, mid_common]; -}; - - -/** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { - var changes = false; - var equalities = []; // Stack of indices where equalities are found. - var equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - var lastEquality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - var pointer = 0; // Index of current position. - // Number of characters that changed prior to the equality. - var length_insertions1 = 0; - var length_deletions1 = 0; - // Number of characters that changed after the equality. - var length_insertions2 = 0; - var length_deletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. - equalities[equalitiesLength++] = pointer; - length_insertions1 = length_insertions2; - length_deletions1 = length_deletions2; - length_insertions2 = 0; - length_deletions2 = 0; - lastEquality = diffs[pointer][1]; - } else { // An insertion or deletion. - if (diffs[pointer][0] == DIFF_INSERT) { - length_insertions2 += diffs[pointer][1].length; - } else { - length_deletions2 += diffs[pointer][1].length; - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastEquality && (lastEquality.length <= - Math.max(length_insertions1, length_deletions1)) && - (lastEquality.length <= Math.max(length_insertions2, - length_deletions2))) { - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, - new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - // Throw away the equality we just deleted. - equalitiesLength--; - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - length_insertions1 = 0; // Reset the counters. - length_deletions1 = 0; - length_insertions2 = 0; - length_deletions2 = 0; - lastEquality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if (changes) { - this.diff_cleanupMerge(diffs); - } - this.diff_cleanupSemanticLossless(diffs); - - // Find any overlaps between deletions and insertions. - // e.g: <del>abcxxx</del><ins>xxxdef</ins> - // -> <del>abc</del>xxx<ins>def</ins> - // e.g: <del>xxxabc</del><ins>defxxx</ins> - // -> <ins>def</ins>xxx<del>abc</del> - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] == DIFF_DELETE && - diffs[pointer][0] == DIFF_INSERT) { - var deletion = diffs[pointer - 1][1]; - var insertion = diffs[pointer][1]; - var overlap_length1 = this.diff_commonOverlap_(deletion, insertion); - var overlap_length2 = this.diff_commonOverlap_(insertion, deletion); - if (overlap_length1 >= overlap_length2) { - if (overlap_length1 >= deletion.length / 2 || - overlap_length1 >= insertion.length / 2) { - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, - insertion.substring(0, overlap_length1))); - diffs[pointer - 1][1] = - deletion.substring(0, deletion.length - overlap_length1); - diffs[pointer + 1][1] = insertion.substring(overlap_length1); - pointer++; - } - } else { - if (overlap_length2 >= deletion.length / 2 || - overlap_length2 >= insertion.length / 2) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, - deletion.substring(0, overlap_length2))); - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = - insertion.substring(0, insertion.length - overlap_length2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = - deletion.substring(overlap_length2); - pointer++; - } - } - pointer++; - } - pointer++; - } -}; - - -/** - * Look for single edits surrounded on both sides by equalities - * which can be shifted sideways to align the edit to a word boundary. - * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { - /** - * Given two strings, compute a score representing whether the internal - * boundary falls on logical boundaries. - * Scores range from 6 (best) to 0 (worst). - * Closure, but does not reference any external variables. - * @param {string} one First string. - * @param {string} two Second string. - * @return {number} The score. - * @private - */ - function diff_cleanupSemanticScore_(one, two) { - if (!one || !two) { - // Edges are the best. - return 6; - } - - // Each port of this function behaves slightly differently due to - // subtle differences in each language's definition of things like - // 'whitespace'. Since this function's purpose is largely cosmetic, - // the choice has been made to use each language's native features - // rather than force total conformity. - var char1 = one.charAt(one.length - 1); - var char2 = two.charAt(0); - var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_); - var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_); - var whitespace1 = nonAlphaNumeric1 && - char1.match(diff_match_patch.whitespaceRegex_); - var whitespace2 = nonAlphaNumeric2 && - char2.match(diff_match_patch.whitespaceRegex_); - var lineBreak1 = whitespace1 && - char1.match(diff_match_patch.linebreakRegex_); - var lineBreak2 = whitespace2 && - char2.match(diff_match_patch.linebreakRegex_); - var blankLine1 = lineBreak1 && - one.match(diff_match_patch.blanklineEndRegex_); - var blankLine2 = lineBreak2 && - two.match(diff_match_patch.blanklineStartRegex_); - - if (blankLine1 || blankLine2) { - // Five points for blank lines. - return 5; - } else if (lineBreak1 || lineBreak2) { - // Four points for line breaks. - return 4; - } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { - // Three points for end of sentences. - return 3; - } else if (whitespace1 || whitespace2) { - // Two points for whitespace. - return 2; - } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { - // One point for non-alphanumeric. - return 1; - } - return 0; - } - - var pointer = 1; - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] == DIFF_EQUAL && - diffs[pointer + 1][0] == DIFF_EQUAL) { - // This is a single edit surrounded by equalities. - var equality1 = diffs[pointer - 1][1]; - var edit = diffs[pointer][1]; - var equality2 = diffs[pointer + 1][1]; - - // First, shift the edit as far left as possible. - var commonOffset = this.diff_commonSuffix(equality1, edit); - if (commonOffset) { - var commonString = edit.substring(edit.length - commonOffset); - equality1 = equality1.substring(0, equality1.length - commonOffset); - edit = commonString + edit.substring(0, edit.length - commonOffset); - equality2 = commonString + equality2; - } - - // Second, step character by character right, looking for the best fit. - var bestEquality1 = equality1; - var bestEdit = edit; - var bestEquality2 = equality2; - var bestScore = diff_cleanupSemanticScore_(equality1, edit) + - diff_cleanupSemanticScore_(edit, equality2); - while (edit.charAt(0) === equality2.charAt(0)) { - equality1 += edit.charAt(0); - edit = edit.substring(1) + equality2.charAt(0); - equality2 = equality2.substring(1); - var score = diff_cleanupSemanticScore_(equality1, edit) + - diff_cleanupSemanticScore_(edit, equality2); - // The >= encourages trailing rather than leading whitespace on edits. - if (score >= bestScore) { - bestScore = score; - bestEquality1 = equality1; - bestEdit = edit; - bestEquality2 = equality2; - } - } - - if (diffs[pointer - 1][1] != bestEquality1) { - // We have an improvement, save it back to the diff. - if (bestEquality1) { - diffs[pointer - 1][1] = bestEquality1; - } else { - diffs.splice(pointer - 1, 1); - pointer--; - } - diffs[pointer][1] = bestEdit; - if (bestEquality2) { - diffs[pointer + 1][1] = bestEquality2; - } else { - diffs.splice(pointer + 1, 1); - pointer--; - } - } - } - pointer++; - } -}; - -// Define some regex patterns for matching boundaries. -diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/; -diff_match_patch.whitespaceRegex_ = /\s/; -diff_match_patch.linebreakRegex_ = /[\r\n]/; -diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/; -diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/; - -/** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { - var changes = false; - var equalities = []; // Stack of indices where equalities are found. - var equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - var lastEquality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - var pointer = 0; // Index of current position. - // Is there an insertion operation before the last equality. - var pre_ins = false; - // Is there a deletion operation before the last equality. - var pre_del = false; - // Is there an insertion operation after the last equality. - var post_ins = false; - // Is there a deletion operation after the last equality. - var post_del = false; - while (pointer < diffs.length) { - if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. - if (diffs[pointer][1].length < this.Diff_EditCost && - (post_ins || post_del)) { - // Candidate found. - equalities[equalitiesLength++] = pointer; - pre_ins = post_ins; - pre_del = post_del; - lastEquality = diffs[pointer][1]; - } else { - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastEquality = null; - } - post_ins = post_del = false; - } else { // An insertion or deletion. - if (diffs[pointer][0] == DIFF_DELETE) { - post_del = true; - } else { - post_ins = true; - } - /* - * Five types to be split: - * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del> - * <ins>A</ins>X<ins>C</ins><del>D</del> - * <ins>A</ins><del>B</del>X<ins>C</ins> - * <ins>A</del>X<ins>C</ins><del>D</del> - * <ins>A</ins><del>B</del>X<del>C</del> - */ - if (lastEquality && ((pre_ins && pre_del && post_ins && post_del) || - ((lastEquality.length < this.Diff_EditCost / 2) && - (pre_ins + pre_del + post_ins + post_del) == 3))) { - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, - new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastEquality = null; - if (pre_ins && pre_del) { - // No changes made which could affect previous entry, keep going. - post_ins = post_del = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? - equalities[equalitiesLength - 1] : -1; - post_ins = post_del = false; - } - changes = true; - } - } - pointer++; - } - - if (changes) { - this.diff_cleanupMerge(diffs); - } -}; - - -/** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { - // Add a dummy entry at the end. - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); - var pointer = 0; - var count_delete = 0; - var count_insert = 0; - var text_delete = ''; - var text_insert = ''; - var commonlength; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - count_insert++; - text_insert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - count_delete++; - text_delete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (count_delete + count_insert > 1) { - if (count_delete !== 0 && count_insert !== 0) { - // Factor out any common prefixies. - commonlength = this.diff_commonPrefix(text_insert, text_delete); - if (commonlength !== 0) { - if ((pointer - count_delete - count_insert) > 0 && - diffs[pointer - count_delete - count_insert - 1][0] == - DIFF_EQUAL) { - diffs[pointer - count_delete - count_insert - 1][1] += - text_insert.substring(0, commonlength); - } else { - diffs.splice(0, 0, new diff_match_patch.Diff(DIFF_EQUAL, - text_insert.substring(0, commonlength))); - pointer++; - } - text_insert = text_insert.substring(commonlength); - text_delete = text_delete.substring(commonlength); - } - // Factor out any common suffixies. - commonlength = this.diff_commonSuffix(text_insert, text_delete); - if (commonlength !== 0) { - diffs[pointer][1] = text_insert.substring(text_insert.length - - commonlength) + diffs[pointer][1]; - text_insert = text_insert.substring(0, text_insert.length - - commonlength); - text_delete = text_delete.substring(0, text_delete.length - - commonlength); - } - } - // Delete the offending records and add the merged ones. - pointer -= count_delete + count_insert; - diffs.splice(pointer, count_delete + count_insert); - if (text_delete.length) { - diffs.splice(pointer, 0, - new diff_match_patch.Diff(DIFF_DELETE, text_delete)); - pointer++; - } - if (text_insert.length) { - diffs.splice(pointer, 0, - new diff_match_patch.Diff(DIFF_INSERT, text_insert)); - pointer++; - } - pointer++; - } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - count_insert = 0; - count_delete = 0; - text_delete = ''; - text_insert = ''; - break; - } - } - if (diffs[diffs.length - 1][1] === '') { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC - var changes = false; - pointer = 1; - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] == DIFF_EQUAL && - diffs[pointer + 1][0] == DIFF_EQUAL) { - // This is a single edit surrounded by equalities. - if (diffs[pointer][1].substring(diffs[pointer][1].length - - diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + - diffs[pointer][1].substring(0, diffs[pointer][1].length - - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == - diffs[pointer + 1][1]) { - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = - diffs[pointer][1].substring(diffs[pointer + 1][1].length) + - diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } - } - pointer++; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diff_cleanupMerge(diffs); - } -}; - - -/** - * loc is a location in text1, compute and return the equivalent location in - * text2. - * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @param {number} loc Location within text1. - * @return {number} Location within text2. - */ -diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { - var chars1 = 0; - var chars2 = 0; - var last_chars1 = 0; - var last_chars2 = 0; - var x; - for (x = 0; x < diffs.length; x++) { - if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. - chars1 += diffs[x][1].length; - } - if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. - chars2 += diffs[x][1].length; - } - if (chars1 > loc) { // Overshot the location. - break; - } - last_chars1 = chars1; - last_chars2 = chars2; - } - // Was the location was deleted? - if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { - return last_chars2; - } - // Add the remaining character length. - return last_chars2 + (loc - last_chars1); -}; - - -/** - * Convert a diff array into a pretty HTML report. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @return {string} HTML representation. - */ -diff_match_patch.prototype.diff_prettyHtml = function(diffs) { - var html = []; - var pattern_amp = /&/g; - var pattern_lt = /</g; - var pattern_gt = />/g; - var pattern_para = /\n/g; - for (var x = 0; x < diffs.length; x++) { - var op = diffs[x][0]; // Operation (insert, delete, equal) - var data = diffs[x][1]; // Text of change. - var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') - .replace(pattern_gt, '>').replace(pattern_para, '¶<br>'); - switch (op) { - case DIFF_INSERT: - html[x] = '<ins style="background:#e6ffe6;">' + text + '</ins>'; - break; - case DIFF_DELETE: - html[x] = '<del style="background:#ffe6e6;">' + text + '</del>'; - break; - case DIFF_EQUAL: - html[x] = '<span>' + text + '</span>'; - break; - } - } - return html.join(''); -}; - - -/** - * Compute and return the source text (all equalities and deletions). - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @return {string} Source text. - */ -diff_match_patch.prototype.diff_text1 = function(diffs) { - var text = []; - for (var x = 0; x < diffs.length; x++) { - if (diffs[x][0] !== DIFF_INSERT) { - text[x] = diffs[x][1]; - } - } - return text.join(''); -}; - - -/** - * Compute and return the destination text (all equalities and insertions). - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @return {string} Destination text. - */ -diff_match_patch.prototype.diff_text2 = function(diffs) { - var text = []; - for (var x = 0; x < diffs.length; x++) { - if (diffs[x][0] !== DIFF_DELETE) { - text[x] = diffs[x][1]; - } - } - return text.join(''); -}; - - -/** - * Compute the Levenshtein distance; the number of inserted, deleted or - * substituted characters. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @return {number} Number of changes. - */ -diff_match_patch.prototype.diff_levenshtein = function(diffs) { - var levenshtein = 0; - var insertions = 0; - var deletions = 0; - for (var x = 0; x < diffs.length; x++) { - var op = diffs[x][0]; - var data = diffs[x][1]; - switch (op) { - case DIFF_INSERT: - insertions += data.length; - break; - case DIFF_DELETE: - deletions += data.length; - break; - case DIFF_EQUAL: - // A deletion and an insertion is one substitution. - levenshtein += Math.max(insertions, deletions); - insertions = 0; - deletions = 0; - break; - } - } - levenshtein += Math.max(insertions, deletions); - return levenshtein; -}; - - -/** - * Crush the diff into an encoded string which describes the operations - * required to transform text1 into text2. - * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. - * Operations are tab-separated. Inserted text is escaped using %xx notation. - * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. - * @return {string} Delta text. - */ -diff_match_patch.prototype.diff_toDelta = function(diffs) { - var text = []; - for (var x = 0; x < diffs.length; x++) { - switch (diffs[x][0]) { - case DIFF_INSERT: - text[x] = '+' + encodeURI(diffs[x][1]); - break; - case DIFF_DELETE: - text[x] = '-' + diffs[x][1].length; - break; - case DIFF_EQUAL: - text[x] = '=' + diffs[x][1].length; - break; - } - } - return text.join('\t').replace(/%20/g, ' '); -}; - - -/** - * Given the original text1, and an encoded string which describes the - * operations required to transform text1 into text2, compute the full diff. - * @param {string} text1 Source string for the diff. - * @param {string} delta Delta text. - * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. - * @throws {!Error} If invalid input. - */ -diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { - var diffs = []; - var diffsLength = 0; // Keeping our own length var is faster in JS. - var pointer = 0; // Cursor in text1 - var tokens = delta.split(/\t/g); - for (var x = 0; x < tokens.length; x++) { - // Each token begins with a one character parameter which specifies the - // operation of this token (delete, insert, equality). - var param = tokens[x].substring(1); - switch (tokens[x].charAt(0)) { - case '+': - try { - diffs[diffsLength++] = - new diff_match_patch.Diff(DIFF_INSERT, decodeURI(param)); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in diff_fromDelta: ' + param); - } - break; - case '-': - // Fall through. - case '=': - var n = parseInt(param, 10); - if (isNaN(n) || n < 0) { - throw new Error('Invalid number in diff_fromDelta: ' + param); - } - var text = text1.substring(pointer, pointer += n); - if (tokens[x].charAt(0) == '=') { - diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_EQUAL, text); - } else { - diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_DELETE, text); - } - break; - default: - // Blank tokens are ok (from a trailing \t). - // Anything else is an error. - if (tokens[x]) { - throw new Error('Invalid diff operation in diff_fromDelta: ' + - tokens[x]); - } - } - } - if (pointer != text1.length) { - throw new Error('Delta length (' + pointer + - ') does not equal source text length (' + text1.length + ').'); - } - return diffs; -}; - - -// MATCH FUNCTIONS - - -/** - * Locate the best instance of 'pattern' in 'text' near 'loc'. - * @param {string} text The text to search. - * @param {string} pattern The pattern to search for. - * @param {number} loc The location to search around. - * @return {number} Best match index or -1. - */ -diff_match_patch.prototype.match_main = function(text, pattern, loc) { - // Check for null inputs. - if (text == null || pattern == null || loc == null) { - throw new Error('Null input. (match_main)'); - } - - loc = Math.max(0, Math.min(loc, text.length)); - if (text == pattern) { - // Shortcut (potentially not guaranteed by the algorithm) - return 0; - } else if (!text.length) { - // Nothing to match. - return -1; - } else if (text.substring(loc, loc + pattern.length) == pattern) { - // Perfect match at the perfect spot! (Includes case of null pattern) - return loc; - } else { - // Do a fuzzy compare. - return this.match_bitap_(text, pattern, loc); - } -}; - - -/** - * Locate the best instance of 'pattern' in 'text' near 'loc' using the - * Bitap algorithm. - * @param {string} text The text to search. - * @param {string} pattern The pattern to search for. - * @param {number} loc The location to search around. - * @return {number} Best match index or -1. - * @private - */ -diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) { - if (pattern.length > this.Match_MaxBits) { - throw new Error('Pattern too long for this browser.'); - } - - // Initialise the alphabet. - var s = this.match_alphabet_(pattern); - - var dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Compute and return the score for a match with e errors and x location. - * Accesses loc and pattern through being a closure. - * @param {number} e Number of errors in match. - * @param {number} x Location of match. - * @return {number} Overall score for match (0.0 = good, 1.0 = bad). - * @private - */ - function match_bitapScore_(e, x) { - var accuracy = e / pattern.length; - var proximity = Math.abs(loc - x); - if (!dmp.Match_Distance) { - // Dodge divide by zero error. - return proximity ? 1.0 : accuracy; - } - return accuracy + (proximity / dmp.Match_Distance); - } - - // Highest score beyond which we give up. - var score_threshold = this.Match_Threshold; - // Is there a nearby exact match? (speedup) - var best_loc = text.indexOf(pattern, loc); - if (best_loc != -1) { - score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); - // What about in the other direction? (speedup) - best_loc = text.lastIndexOf(pattern, loc + pattern.length); - if (best_loc != -1) { - score_threshold = - Math.min(match_bitapScore_(0, best_loc), score_threshold); - } - } - - // Initialise the bit arrays. - var matchmask = 1 << (pattern.length - 1); - best_loc = -1; - - var bin_min, bin_mid; - var bin_max = pattern.length + text.length; - var last_rd; - for (var d = 0; d < pattern.length; d++) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from 'loc' we can stray at this - // error level. - bin_min = 0; - bin_mid = bin_max; - while (bin_min < bin_mid) { - if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { - bin_min = bin_mid; - } else { - bin_max = bin_mid; - } - bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); - } - // Use the result from this iteration as the maximum for the next. - bin_max = bin_mid; - var start = Math.max(1, loc - bin_mid + 1); - var finish = Math.min(loc + bin_mid, text.length) + pattern.length; - - var rd = Array(finish + 2); - rd[finish + 1] = (1 << d) - 1; - for (var j = finish; j >= start; j--) { - // The alphabet (s) is a sparse hash, so the following line generates - // warnings. - var charMatch = s[text.charAt(j - 1)]; - if (d === 0) { // First pass: exact match. - rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; - } else { // Subsequent passes: fuzzy match. - rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | - (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | - last_rd[j + 1]; - } - if (rd[j] & matchmask) { - var score = match_bitapScore_(d, j - 1); - // This match will almost certainly be better than any existing match. - // But check anyway. - if (score <= score_threshold) { - // Told you so. - score_threshold = score; - best_loc = j - 1; - if (best_loc > loc) { - // When passing loc, don't exceed our current distance from loc. - start = Math.max(1, 2 * loc - best_loc); - } else { - // Already passed loc, downhill from here on in. - break; - } - } - } - } - // No hope for a (better) match at greater error levels. - if (match_bitapScore_(d + 1, loc) > score_threshold) { - break; - } - last_rd = rd; - } - return best_loc; -}; - - -/** - * Initialise the alphabet for the Bitap algorithm. - * @param {string} pattern The text to encode. - * @return {!Object} Hash of character locations. - * @private - */ -diff_match_patch.prototype.match_alphabet_ = function(pattern) { - var s = {}; - for (var i = 0; i < pattern.length; i++) { - s[pattern.charAt(i)] = 0; - } - for (var i = 0; i < pattern.length; i++) { - s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); - } - return s; -}; - - -// PATCH FUNCTIONS - - -/** - * Increase the context until it is unique, - * but don't let the pattern expand beyond Match_MaxBits. - * @param {!diff_match_patch.patch_obj} patch The patch to grow. - * @param {string} text Source text. - * @private - */ -diff_match_patch.prototype.patch_addContext_ = function(patch, text) { - if (text.length == 0) { - return; - } - if (patch.start2 === null) { - throw Error('patch not initialized'); - } - var pattern = text.substring(patch.start2, patch.start2 + patch.length1); - var padding = 0; - - // Look for the first and last matches of pattern in text. If two different - // matches are found, increase the pattern length. - while (text.indexOf(pattern) != text.lastIndexOf(pattern) && - pattern.length < this.Match_MaxBits - this.Patch_Margin - - this.Patch_Margin) { - padding += this.Patch_Margin; - pattern = text.substring(patch.start2 - padding, - patch.start2 + patch.length1 + padding); - } - // Add one chunk for good luck. - padding += this.Patch_Margin; - - // Add the prefix. - var prefix = text.substring(patch.start2 - padding, patch.start2); - if (prefix) { - patch.diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, prefix)); - } - // Add the suffix. - var suffix = text.substring(patch.start2 + patch.length1, - patch.start2 + patch.length1 + padding); - if (suffix) { - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, suffix)); - } - - // Roll back the start points. - patch.start1 -= prefix.length; - patch.start2 -= prefix.length; - // Extend the lengths. - patch.length1 += prefix.length + suffix.length; - patch.length2 += prefix.length + suffix.length; -}; - - -/** - * Compute a list of patches to turn text1 into text2. - * Use diffs if provided, otherwise compute it ourselves. - * There are four ways to call this function, depending on what data is - * available to the caller: - * Method 1: - * a = text1, b = text2 - * Method 2: - * a = diffs - * Method 3 (optimal): - * a = text1, b = diffs - * Method 4 (deprecated, use method 3): - * a = text1, b = text2, c = diffs - * - * @param {string|!Array.<!diff_match_patch.Diff>} a text1 (methods 1,3,4) or - * Array of diff tuples for text1 to text2 (method 2). - * @param {string|!Array.<!diff_match_patch.Diff>=} opt_b text2 (methods 1,4) or - * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). - * @param {string|!Array.<!diff_match_patch.Diff>=} opt_c Array of diff tuples - * for text1 to text2 (method 4) or undefined (methods 1,2,3). - * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects. - */ -diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) { - var text1, diffs; - if (typeof a == 'string' && typeof opt_b == 'string' && - typeof opt_c == 'undefined') { - // Method 1: text1, text2 - // Compute diffs from text1 and text2. - text1 = /** @type {string} */(a); - diffs = this.diff_main(text1, /** @type {string} */(opt_b), true); - if (diffs.length > 2) { - this.diff_cleanupSemantic(diffs); - this.diff_cleanupEfficiency(diffs); - } - } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' && - typeof opt_c == 'undefined') { - // Method 2: diffs - // Compute text1 from diffs. - diffs = /** @type {!Array.<!diff_match_patch.Diff>} */(a); - text1 = this.diff_text1(diffs); - } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' && - typeof opt_c == 'undefined') { - // Method 3: text1, diffs - text1 = /** @type {string} */(a); - diffs = /** @type {!Array.<!diff_match_patch.Diff>} */(opt_b); - } else if (typeof a == 'string' && typeof opt_b == 'string' && - opt_c && typeof opt_c == 'object') { - // Method 4: text1, text2, diffs - // text2 is not used. - text1 = /** @type {string} */(a); - diffs = /** @type {!Array.<!diff_match_patch.Diff>} */(opt_c); - } else { - throw new Error('Unknown call format to patch_make.'); - } - - if (diffs.length === 0) { - return []; // Get rid of the null case. - } - var patches = []; - var patch = new diff_match_patch.patch_obj(); - var patchDiffLength = 0; // Keeping our own length var is faster in JS. - var char_count1 = 0; // Number of characters into the text1 string. - var char_count2 = 0; // Number of characters into the text2 string. - // Start with text1 (prepatch_text) and apply the diffs until we arrive at - // text2 (postpatch_text). We recreate the patches one by one to determine - // context info. - var prepatch_text = text1; - var postpatch_text = text1; - for (var x = 0; x < diffs.length; x++) { - var diff_type = diffs[x][0]; - var diff_text = diffs[x][1]; - - if (!patchDiffLength && diff_type !== DIFF_EQUAL) { - // A new patch starts here. - patch.start1 = char_count1; - patch.start2 = char_count2; - } - - switch (diff_type) { - case DIFF_INSERT: - patch.diffs[patchDiffLength++] = diffs[x]; - patch.length2 += diff_text.length; - postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + - postpatch_text.substring(char_count2); - break; - case DIFF_DELETE: - patch.length1 += diff_text.length; - patch.diffs[patchDiffLength++] = diffs[x]; - postpatch_text = postpatch_text.substring(0, char_count2) + - postpatch_text.substring(char_count2 + - diff_text.length); - break; - case DIFF_EQUAL: - if (diff_text.length <= 2 * this.Patch_Margin && - patchDiffLength && diffs.length != x + 1) { - // Small equality inside a patch. - patch.diffs[patchDiffLength++] = diffs[x]; - patch.length1 += diff_text.length; - patch.length2 += diff_text.length; - } else if (diff_text.length >= 2 * this.Patch_Margin) { - // Time for a new patch. - if (patchDiffLength) { - this.patch_addContext_(patch, prepatch_text); - patches.push(patch); - patch = new diff_match_patch.patch_obj(); - patchDiffLength = 0; - // Unlike Unidiff, our patch lists have a rolling context. - // https://github.com/google/diff-match-patch/wiki/Unidiff - // Update prepatch text & pos to reflect the application of the - // just completed patch. - prepatch_text = postpatch_text; - char_count1 = char_count2; - } - } - break; - } - - // Update the current character count. - if (diff_type !== DIFF_INSERT) { - char_count1 += diff_text.length; - } - if (diff_type !== DIFF_DELETE) { - char_count2 += diff_text.length; - } - } - // Pick up the leftover patch if not empty. - if (patchDiffLength) { - this.patch_addContext_(patch, prepatch_text); - patches.push(patch); - } - - return patches; -}; - - -/** - * Given an array of patches, return another array that is identical. - * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects. - * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects. - */ -diff_match_patch.prototype.patch_deepCopy = function(patches) { - // Making deep copies is hard in JavaScript. - var patchesCopy = []; - for (var x = 0; x < patches.length; x++) { - var patch = patches[x]; - var patchCopy = new diff_match_patch.patch_obj(); - patchCopy.diffs = []; - for (var y = 0; y < patch.diffs.length; y++) { - patchCopy.diffs[y] = - new diff_match_patch.Diff(patch.diffs[y][0], patch.diffs[y][1]); - } - patchCopy.start1 = patch.start1; - patchCopy.start2 = patch.start2; - patchCopy.length1 = patch.length1; - patchCopy.length2 = patch.length2; - patchesCopy[x] = patchCopy; - } - return patchesCopy; -}; - - -/** - * Merge a set of patches onto the text. Return a patched text, as well - * as a list of true/false values indicating which patches were applied. - * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects. - * @param {string} text Old text. - * @return {!Array.<string|!Array.<boolean>>} Two element Array, containing the - * new text and an array of boolean values. - */ -diff_match_patch.prototype.patch_apply = function(patches, text) { - if (patches.length == 0) { - return [text, []]; - } - - // Deep copy the patches so that no changes are made to originals. - patches = this.patch_deepCopy(patches); - - var nullPadding = this.patch_addPadding(patches); - text = nullPadding + text + nullPadding; - - this.patch_splitMax(patches); - // delta keeps track of the offset between the expected and actual location - // of the previous patch. If there are patches expected at positions 10 and - // 20, but the first patch was found at 12, delta is 2 and the second patch - // has an effective expected position of 22. - var delta = 0; - var results = []; - for (var x = 0; x < patches.length; x++) { - var expected_loc = patches[x].start2 + delta; - var text1 = this.diff_text1(patches[x].diffs); - var start_loc; - var end_loc = -1; - if (text1.length > this.Match_MaxBits) { - // patch_splitMax will only provide an oversized pattern in the case of - // a monster delete. - start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), - expected_loc); - if (start_loc != -1) { - end_loc = this.match_main(text, - text1.substring(text1.length - this.Match_MaxBits), - expected_loc + text1.length - this.Match_MaxBits); - if (end_loc == -1 || start_loc >= end_loc) { - // Can't find valid trailing context. Drop this patch. - start_loc = -1; - } - } - } else { - start_loc = this.match_main(text, text1, expected_loc); - } - if (start_loc == -1) { - // No match found. :( - results[x] = false; - // Subtract the delta for this failed patch from subsequent patches. - delta -= patches[x].length2 - patches[x].length1; - } else { - // Found a match. :) - results[x] = true; - delta = start_loc - expected_loc; - var text2; - if (end_loc == -1) { - text2 = text.substring(start_loc, start_loc + text1.length); - } else { - text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); - } - if (text1 == text2) { - // Perfect match, just shove the replacement text in. - text = text.substring(0, start_loc) + - this.diff_text2(patches[x].diffs) + - text.substring(start_loc + text1.length); - } else { - // Imperfect match. Run a diff to get a framework of equivalent - // indices. - var diffs = this.diff_main(text1, text2, false); - if (text1.length > this.Match_MaxBits && - this.diff_levenshtein(diffs) / text1.length > - this.Patch_DeleteThreshold) { - // The end points match, but the content is unacceptably bad. - results[x] = false; - } else { - this.diff_cleanupSemanticLossless(diffs); - var index1 = 0; - var index2; - for (var y = 0; y < patches[x].diffs.length; y++) { - var mod = patches[x].diffs[y]; - if (mod[0] !== DIFF_EQUAL) { - index2 = this.diff_xIndex(diffs, index1); - } - if (mod[0] === DIFF_INSERT) { // Insertion - text = text.substring(0, start_loc + index2) + mod[1] + - text.substring(start_loc + index2); - } else if (mod[0] === DIFF_DELETE) { // Deletion - text = text.substring(0, start_loc + index2) + - text.substring(start_loc + this.diff_xIndex(diffs, - index1 + mod[1].length)); - } - if (mod[0] !== DIFF_DELETE) { - index1 += mod[1].length; - } - } - } - } - } - } - // Strip the padding off. - text = text.substring(nullPadding.length, text.length - nullPadding.length); - return [text, results]; -}; - - -/** - * Add some padding on text start and end so that edges can match something. - * Intended to be called only from within patch_apply. - * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects. - * @return {string} The padding string added to each side. - */ -diff_match_patch.prototype.patch_addPadding = function(patches) { - var paddingLength = this.Patch_Margin; - var nullPadding = ''; - for (var x = 1; x <= paddingLength; x++) { - nullPadding += String.fromCharCode(x); - } - - // Bump all the patches forward. - for (var x = 0; x < patches.length; x++) { - patches[x].start1 += paddingLength; - patches[x].start2 += paddingLength; - } - - // Add some padding on start of first diff. - var patch = patches[0]; - var diffs = patch.diffs; - if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { - // Add nullPadding equality. - diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); - patch.start1 -= paddingLength; // Should be 0. - patch.start2 -= paddingLength; // Should be 0. - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs[0][1].length) { - // Grow first equality. - var extraLength = paddingLength - diffs[0][1].length; - diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; - patch.start1 -= extraLength; - patch.start2 -= extraLength; - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - // Add some padding on end of last diff. - patch = patches[patches.length - 1]; - diffs = patch.diffs; - if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { - // Add nullPadding equality. - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs[diffs.length - 1][1].length) { - // Grow last equality. - var extraLength = paddingLength - diffs[diffs.length - 1][1].length; - diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - return nullPadding; -}; - - -/** - * Look through the patches and break up any which are longer than the maximum - * limit of the match algorithm. - * Intended to be called only from within patch_apply. - * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects. - */ -diff_match_patch.prototype.patch_splitMax = function(patches) { - var patch_size = this.Match_MaxBits; - for (var x = 0; x < patches.length; x++) { - if (patches[x].length1 <= patch_size) { - continue; - } - var bigpatch = patches[x]; - // Remove the big old patch. - patches.splice(x--, 1); - var start1 = bigpatch.start1; - var start2 = bigpatch.start2; - var precontext = ''; - while (bigpatch.diffs.length !== 0) { - // Create one of several smaller patches. - var patch = new diff_match_patch.patch_obj(); - var empty = true; - patch.start1 = start1 - precontext.length; - patch.start2 = start2 - precontext.length; - if (precontext !== '') { - patch.length1 = patch.length2 = precontext.length; - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, precontext)); - } - while (bigpatch.diffs.length !== 0 && - patch.length1 < patch_size - this.Patch_Margin) { - var diff_type = bigpatch.diffs[0][0]; - var diff_text = bigpatch.diffs[0][1]; - if (diff_type === DIFF_INSERT) { - // Insertions are harmless. - patch.length2 += diff_text.length; - start2 += diff_text.length; - patch.diffs.push(bigpatch.diffs.shift()); - empty = false; - } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 && - patch.diffs[0][0] == DIFF_EQUAL && - diff_text.length > 2 * patch_size) { - // This is a large deletion. Let it pass in one chunk. - patch.length1 += diff_text.length; - start1 += diff_text.length; - empty = false; - patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); - bigpatch.diffs.shift(); - } else { - // Deletion or equality. Only take as much as we can stomach. - diff_text = diff_text.substring(0, - patch_size - patch.length1 - this.Patch_Margin); - patch.length1 += diff_text.length; - start1 += diff_text.length; - if (diff_type === DIFF_EQUAL) { - patch.length2 += diff_text.length; - start2 += diff_text.length; - } else { - empty = false; - } - patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); - if (diff_text == bigpatch.diffs[0][1]) { - bigpatch.diffs.shift(); - } else { - bigpatch.diffs[0][1] = - bigpatch.diffs[0][1].substring(diff_text.length); - } - } - } - // Compute the head context for the next patch. - precontext = this.diff_text2(patch.diffs); - precontext = - precontext.substring(precontext.length - this.Patch_Margin); - // Append the end context for this patch. - var postcontext = this.diff_text1(bigpatch.diffs) - .substring(0, this.Patch_Margin); - if (postcontext !== '') { - patch.length1 += postcontext.length; - patch.length2 += postcontext.length; - if (patch.diffs.length !== 0 && - patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { - patch.diffs[patch.diffs.length - 1][1] += postcontext; - } else { - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, postcontext)); - } - } - if (!empty) { - patches.splice(++x, 0, patch); - } - } - } -}; - - -/** - * Take a list of patches and return a textual representation. - * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects. - * @return {string} Text representation of patches. - */ -diff_match_patch.prototype.patch_toText = function(patches) { - var text = []; - for (var x = 0; x < patches.length; x++) { - text[x] = patches[x]; - } - return text.join(''); -}; - - -/** - * Parse a textual representation of patches and return a list of Patch objects. - * @param {string} textline Text representation of patches. - * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -diff_match_patch.prototype.patch_fromText = function(textline) { - var patches = []; - if (!textline) { - return patches; - } - var text = textline.split('\n'); - var textPointer = 0; - var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; - while (textPointer < text.length) { - var m = text[textPointer].match(patchHeader); - if (!m) { - throw new Error('Invalid patch string: ' + text[textPointer]); - } - var patch = new diff_match_patch.patch_obj(); - patches.push(patch); - patch.start1 = parseInt(m[1], 10); - if (m[2] === '') { - patch.start1--; - patch.length1 = 1; - } else if (m[2] == '0') { - patch.length1 = 0; - } else { - patch.start1--; - patch.length1 = parseInt(m[2], 10); - } - - patch.start2 = parseInt(m[3], 10); - if (m[4] === '') { - patch.start2--; - patch.length2 = 1; - } else if (m[4] == '0') { - patch.length2 = 0; - } else { - patch.start2--; - patch.length2 = parseInt(m[4], 10); - } - textPointer++; - - while (textPointer < text.length) { - var sign = text[textPointer].charAt(0); - try { - var line = decodeURI(text[textPointer].substring(1)); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in patch_fromText: ' + line); - } - if (sign == '-') { - // Deletion. - patch.diffs.push(new diff_match_patch.Diff(DIFF_DELETE, line)); - } else if (sign == '+') { - // Insertion. - patch.diffs.push(new diff_match_patch.Diff(DIFF_INSERT, line)); - } else if (sign == ' ') { - // Minor equality. - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, line)); - } else if (sign == '@') { - // Start of next patch. - break; - } else if (sign === '') { - // Blank line? Whatever. - } else { - // WTF? - throw new Error('Invalid patch mode "' + sign + '" in: ' + line); - } - textPointer++; - } - } - return patches; -}; - - -/** - * Class representing one patch operation. - * @constructor - */ -diff_match_patch.patch_obj = function() { - /** @type {!Array.<!diff_match_patch.Diff>} */ - this.diffs = []; - /** @type {?number} */ - this.start1 = null; - /** @type {?number} */ - this.start2 = null; - /** @type {number} */ - this.length1 = 0; - /** @type {number} */ - this.length2 = 0; -}; - - -/** - * Emulate GNU diff's format. - * Header: @@ -382,8 +481,9 @@ - * Indices are printed as 1-based, not 0-based. - * @return {string} The GNU diff string. - */ -diff_match_patch.patch_obj.prototype.toString = function() { - var coords1, coords2; - if (this.length1 === 0) { - coords1 = this.start1 + ',0'; - } else if (this.length1 == 1) { - coords1 = this.start1 + 1; - } else { - coords1 = (this.start1 + 1) + ',' + this.length1; - } - if (this.length2 === 0) { - coords2 = this.start2 + ',0'; - } else if (this.length2 == 1) { - coords2 = this.start2 + 1; - } else { - coords2 = (this.start2 + 1) + ',' + this.length2; - } - var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; - var op; - // Escape the body of the patch with %xx notation. - for (var x = 0; x < this.diffs.length; x++) { - switch (this.diffs[x][0]) { - case DIFF_INSERT: - op = '+'; - break; - case DIFF_DELETE: - op = '-'; - break; - case DIFF_EQUAL: - op = ' '; - break; - } - text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; - } - return text.join('').replace(/%20/g, ' '); -}; - - -// The following export code was added by @ForbesLindesay -module.exports = diff_match_patch; -module.exports['diff_match_patch'] = diff_match_patch; -module.exports['DIFF_DELETE'] = DIFF_DELETE; -module.exports['DIFF_INSERT'] = DIFF_INSERT; -module.exports['DIFF_EQUAL'] = DIFF_EQUAL; \ No newline at end of file diff --git a/packages/test-in-browser/driver.html b/packages/test-in-browser/driver.html index 52bd3b8aad..a8f976998b 100644 --- a/packages/test-in-browser/driver.html +++ b/packages/test-in-browser/driver.html @@ -44,12 +44,15 @@ <ul class="navbar-nav mr-auto"> {{#each groupPaths}} <li class="nav-item"><span class="nav-link"> - </span></li> - <li class="nav-item"><a class="nav-link" href="#">{{name}}</a></li> + <li class="nav-item"><a class="nav-link group" href="#">{{name}}</a></li> {{/each}} </ul> <form class="navbar-form pull-right"> <span id="current-client-test"></span> + {{#if isFiltered}} + <button class="btn btn-secondary run-all">Run All Tests</button> + {{/if}} <button class="btn btn-primary rerun"> {{#if rerunScheduled}} Rerun scheduled... diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index d14a6fecde..e469fa7fa5 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -1,9 +1,13 @@ //// //// Setup //// -import { diff_match_patch } from './diff_match_patch_uncompressed' +import { diffChars } from 'diff' import 'bootstrap/dist/css/bootstrap.min.css'; +const arraysEqual = (a, b) => { + return a.length === b.length && a.every((v, i) => v === b[i]); +} + // dependency for the count of tests running/passed/failed, etc. drives // the navbar and the like. var countDep = new Tracker.Dependency; @@ -43,12 +47,118 @@ window.onerror = (message, source, line) => { Session.set("uncaughtErrors", Array.from(uncaughtErrors)); }; -Session.setDefault("groupPath", ["tinytest"]); +var getGroupPathFromURL = function() { + var pathname = window.location.pathname; + var match = pathname.match(/^\/group\/(.+)$/); + if (match) { + try { + return JSON.parse(decodeURIComponent(match[1])); + } catch (e) { + console.warn('Invalid group path in URL:', match[1]); + } + } + return ["tinytest"]; +}; + +var setGroupPathInURL = function(groupPath, pushState = true) { + var newURL = '/'; + + if (!arraysEqual(groupPath, ['tinytest'])) { + newURL = '/group/' + encodeURIComponent(JSON.stringify(groupPath)); + } + + var historyState = { groupPath: groupPath }; + + if (pushState) { + window.history.pushState(historyState, '', newURL); + } else { + window.history.replaceState(historyState, '', newURL); + } +}; + +// Initialize group path from URL, fallback to default +var initialGroupPath = getGroupPathFromURL(); +Session.setDefault("groupPath", initialGroupPath); Session.set("rerunScheduled", false); +// Safeguards for rapid navigation +var isNavigating = false; +var lastNavigationTime = 0; +var navigationDebounceMs = 200; // Minimum time between navigations + +// Handle browser back/forward navigation +window.addEventListener('popstate', function(event) { + var now = Date.now(); + + // Safeguard 1: Prevent overlapping navigations + if (isNavigating) { + console.log('Navigation already in progress, ignoring'); + return; + } + + // Safeguard 2: Debounce rapid successive calls + if (now - lastNavigationTime < navigationDebounceMs) { + console.log('Navigation too rapid, ignoring'); + return; + } + + var newGroupPath = getGroupPathFromURL(); + var currentGroupPath = Session.get("groupPath"); + + if (!arraysEqual(newGroupPath, currentGroupPath)) { + // Set navigation flag + isNavigating = true; + lastNavigationTime = now; + + // Emulate the EXACT same sequence as changeToPath + + // 1. URL is already changed by browser, but ensure it's correct + setGroupPathInURL(newGroupPath, false); // replaceState, don't create new entry + + // 2. Update session state (SAME as changeToPath) + Session.set("groupPath", newGroupPath); + Session.set("rerunScheduled", true); + + // 3. Clean reload (SAME as changeToPath) + Reload._reload(); + } +}); + // This function is exported. It's called on client startup by the // bundle generated by `meteor test` or `meteor test-packages`. runTests = function () { + // Reset navigation safeguards when app starts + isNavigating = false; + + // Reset all test state before starting + running = true; + totalCount = 0; + passedCount = 0; + failedCount = 0; + failedTests = []; + resultTree = []; + + // Reset dependencies to trigger UI updates + countDep.changed(); + topLevelGroupsDep.changed(); + + // Get current group path from URL (in case of refresh) + var currentGroupPath = getGroupPathFromURL(); + Session.set("groupPath", currentGroupPath); + + // Only update URL if it's actually different from what we expect + var expectedURL; + if (currentGroupPath && currentGroupPath.length > 0 && JSON.stringify(currentGroupPath) !== JSON.stringify(["tinytest"])) { + expectedURL = '/group/' + encodeURIComponent(JSON.stringify(currentGroupPath)); + } else { + expectedURL = '/'; + } + + // Only do replaceState if the URL doesn't match what we expect + if (window.location.pathname !== expectedURL) { + setGroupPathInURL(currentGroupPath, false); + } + document.body.innerHTML = ""; document.head.title = "Tests"; @@ -62,7 +172,7 @@ runTests = function () { Tracker.flush(); Meteor.connection._unsubscribeAll(); - }, Session.get("groupPath")); + }, currentGroupPath); }; @@ -322,6 +432,10 @@ Template.progressBar.helpers({ //// Template - groupNav var changeToPath = function (path) { + // Update URL with new group path (pushState creates new history entry) + setGroupPathInURL(path, true); + + // Update session to trigger UI updates Session.set("groupPath", path); Session.set("rerunScheduled", true); // pretend there's just been a hot code push @@ -340,6 +454,10 @@ Template.groupNav.helpers({ }, rerunScheduled: function () { return Session.get("rerunScheduled"); + }, + isFiltered: function () { + var groupPath = Session.get("groupPath"); + return groupPath.length > 1 || groupPath[0] !== "tinytest"; } }); @@ -350,6 +468,9 @@ Template.groupNav.events({ 'click .rerun': function () { Session.set("rerunScheduled", true); Reload._reload(); + }, + 'click .run-all': function () { + changeToPath(["tinytest"]); } }); @@ -507,10 +628,13 @@ Template.event.events({ // e.g. doDiff('abc', 'bcd') => [[-1, 'a'], [0, 'bc'], [1, 'd']] var doDiff = function (str1, str2) { - var D = new diff_match_patch(); - var pieces = D.diff_main(str1, str2, false); - D.diff_cleanupSemantic(pieces); - return pieces; + const diff = diffChars(str1, str2); + + return diff.map(part => { + if (part.added) return [1, part.value]; + if (part.removed) return [-1, part.value]; + return [0, part.value]; + }); }; Template.event.helpers({ diff --git a/packages/test-in-browser/package.js b/packages/test-in-browser/package.js index 2f721e5b1e..1c9d96834a 100644 --- a/packages/test-in-browser/package.js +++ b/packages/test-in-browser/package.js @@ -1,11 +1,12 @@ Package.describe({ summary: "Run tests interactively in the browser", - version: '1.4.0', + version: '1.5.0', documentation: null }); Npm.depends({ 'bootstrap': '4.3.1', + 'diff': '8.0.2' }); Package.onUse(function (api) { diff --git a/packages/test-in-console/driver.js b/packages/test-in-console/driver.js index 4f66667d0d..a4533f1256 100644 --- a/packages/test-in-console/driver.js +++ b/packages/test-in-console/driver.js @@ -210,7 +210,9 @@ runTests = function () { // Also log xUnit output xunit('<testsuite errors="" failures="" name="meteor" skips="" tests="" time="">'); - resultSet.forEach(function (result, name) { + Object.keys(resultSet).forEach(function (name) { + let result = resultSet[name]; + var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client"); var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client"); var time = ""; diff --git a/packages/test-in-console/package.js b/packages/test-in-console/package.js index ebf1b6eff9..fe96041fd6 100644 --- a/packages/test-in-console/package.js +++ b/packages/test-in-console/package.js @@ -6,6 +6,7 @@ Package.describe({ Package.onUse(function(api) { api.use(['tinytest', 'random', 'ejson', 'check', 'ecmascript']); api.use('fetch', 'server'); + api.use('jquery', 'client'); api.export('TEST_STATUS', 'client'); diff --git a/packages/test-in-console/puppeteer_runner.js b/packages/test-in-console/puppeteer_runner.js index 991bbd4631..7065341278 100644 --- a/packages/test-in-console/puppeteer_runner.js +++ b/packages/test-in-console/puppeteer_runner.js @@ -16,7 +16,7 @@ async function runNextUrl(browser) { if (text.includes('Permissions policy violation')) { return; } - if (msg._text !== undefined) console.log(msg._text); + if (text) console.log(text); else { testNumber++; const currentClientTest = diff --git a/packages/test-in-console/run.sh b/packages/test-in-console/run.sh index 5a88e9065d..5bdfb466eb 100755 --- a/packages/test-in-console/run.sh +++ b/packages/test-in-console/run.sh @@ -15,6 +15,7 @@ export PATH=$METEOR_HOME:$PATH export URL='http://127.0.0.1:4096/' export METEOR_PACKAGE_DIRS='packages/deprecated' +export METEOR_NO_DEPRECATION=true exec 3< <(./meteor test-packages --driver-package test-in-console -p 4096 --exclude ${TEST_PACKAGES_EXCLUDE:-''} $1) EXEC_PID=$! diff --git a/packages/tinytest/README.md b/packages/tinytest/README.md index d93c5f1222..514ae883a7 100644 --- a/packages/tinytest/README.md +++ b/packages/tinytest/README.md @@ -272,7 +272,13 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t ### Assertions without optional fail messages -`test.throws(func, expected);` +`test.throws(func, expected[, message]);` + +`test.throwsAsync(func, expected[, message]);` + +`test.doesNotThrows(func[, failureMessage]);` + +`test.doesNotThrowsAsync(func[, failureMessage]);` `expected` can be: @@ -281,6 +287,8 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t - `regexp`: pass if the exception message passes the regexp. - `function`: call the function as a predicate with the exception. +`doesNotThrows` and `doesNotThrowsAsync` assert that the function does not throw. If the function throws, the assertion fails. The optional `failureMessage` is only used to annotate the failure. + Note: Node's `assert.throws` also accepts a constructor to test whether the error is of the expected class. But since JavaScript can't distinguish between constructors and plain functions and Node's `assert.throws` also accepts a predicate function, if the error fails the `instanceof` test with the constructor then the constructor is then treated as a predicate and called (!) The upshot is, if you want to test whether an error is of a particular class, use a predicate function. diff --git a/packages/tinytest/tinytest.js b/packages/tinytest/tinytest.js index b66097aafb..f8c29bab74 100644 --- a/packages/tinytest/tinytest.js +++ b/packages/tinytest/tinytest.js @@ -244,6 +244,19 @@ export class TestCaseResults { // The upshot is, if you want to test whether an error is of a // particular class, use a predicate function. // + /** + * Assert that `f` throws. + * + * `expected` can be: + * - undefined: accept any exception. + * - string: pass if the string is a substring of the exception message. + * - regexp: pass if the exception message passes the regexp. + * - function: call the function as a predicate with the exception. + * + * @param {Function} f + * @param {*} expected + * @param {String} message + */ throws(f, expected, message) { let actual; const predicate = this._guessPredicate(expected); @@ -258,10 +271,34 @@ export class TestCaseResults { } /** - * Same as throw, but accepts an async function as a parameter. - * @param f - * @param expected - * @param message + * Assert that `f` does not throw. + * @param {Function} f + * @param {String} failureMessage + */ + doesNotThrows(f, failureMessage) { + let actual; + + try { + f(); + } catch (exception) { + actual = exception; + } + + if (!actual) { + this.ok(); + } else { + this.fail({ + type: "throws", + message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""), + }); + } + } + + /** + * Same as `throws`, but accepts an async function as a parameter. + * @param {Function} f + * @param {*} expected + * @param {String} message * @returns {Promise<void>} */ async throwsAsync(f, expected, message) { @@ -276,6 +313,31 @@ export class TestCaseResults { this._assertActual(actual, predicate, message); } + /** + * Same as `doesNotThrows`, but accepts an async function as a parameter. + * @param {Function} f + * @param {String} failureMessage + * @returns {Promise<void>} + */ + async doesNotThrowsAsync(f, failureMessage) { + let actual; + + try { + await f(); + } catch (exception) { + actual = exception; + } + + if (!actual) { + this.ok(); + } else { + this.fail({ + type: "throws", + message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""), + }); + } + } + isTrue(v, msg) { if (v) this.ok(); diff --git a/packages/tools-core/README.md b/packages/tools-core/README.md new file mode 100644 index 0000000000..c3e636c63a --- /dev/null +++ b/packages/tools-core/README.md @@ -0,0 +1,5 @@ +# tools-core + +The tools-core package exposes helpers for managing modern tools in Meteor, providing modules for npm, log, process management, and so on; and exporting them from a Meteor package rather than being directly tied to the Meteor tool. + +These helpers will be useful to integrate a modern bundler like Rspack and a native solution like CapacitorJS. diff --git a/packages/tools-core/lib/git.js b/packages/tools-core/lib/git.js new file mode 100644 index 0000000000..51021ce95b --- /dev/null +++ b/packages/tools-core/lib/git.js @@ -0,0 +1,126 @@ +import fs from 'fs'; +import path from 'path'; +import { logError, logProgress, logSuccess } from './log'; + +/** + * Checks if the given directory is a git repository + * @param {string} dir - Directory to check + * @returns {boolean} - True if the directory is a git repository + */ +export function isGitRepository(dir) { + try { + const gitDir = path.join(dir, '.git'); + return fs.existsSync(gitDir) && fs.statSync(gitDir).isDirectory(); + } catch (error) { + return false; + } +} + +/** + * Checks if a .gitignore file exists in the given directory + * @param {string} dir - Directory to check + * @returns {boolean} - True if .gitignore exists + */ +export function gitignoreExists(dir) { + try { + const gitignorePath = path.join(dir, '.gitignore'); + return fs.existsSync(gitignorePath); + } catch (error) { + return false; + } +} + +/** + * Creates a .gitignore file in the given directory if it doesn't exist + * @param {string} dir - Directory where to create .gitignore + * @param {string[]} [initialEntries=[]] - Initial entries to add to the .gitignore file + * @returns {boolean} - True if .gitignore was created or already exists + */ +export function ensureGitignoreExists(dir, initialEntries = []) { + const gitignorePath = path.join(dir, '.gitignore'); + + if (!gitignoreExists(dir)) { + try { + const content = initialEntries.length > 0 ? initialEntries.join('\n') + '\n' : ''; + fs.writeFileSync(gitignorePath, content, 'utf8'); + return true; + } catch (error) { + logError(`=> Failed to create .gitignore: ${error.message}`); + return false; + } + } + + return true; +} + +/** + * Checks if specific entries exist in the .gitignore file + * @param {string} dir - Directory containing the .gitignore file + * @param {string[]} entries - Entries to check + * @returns {string[]} - Entries that don't exist in the .gitignore file + */ +export function getMissingGitignoreEntries(dir, entries) { + if (!gitignoreExists(dir)) { + return entries; + } + + try { + const gitignorePath = path.join(dir, '.gitignore'); + const content = fs.readFileSync(gitignorePath, 'utf8'); + const lines = content.split('\n').map(line => line.trim()); + + return entries.filter(entry => !lines.includes(entry)); + } catch (error) { + logError(`=> Failed to read .gitignore: ${error.message}`); + return entries; + } +} + +/** + * Adds entries to the .gitignore file if they don't exist + * @param {string} dir - Directory containing the .gitignore file + * @param {string[]} entries - Entries to add + * @param {string} [context] - Optional context to add as a comment before the entries + * @returns {boolean} - True if entries were added successfully + */ +export function addGitignoreEntries(dir, entries, context = '') { + // Ensure .gitignore exists + if (!ensureGitignoreExists(dir)) { + return false; + } + + // Get entries that don't exist + const missingEntries = getMissingGitignoreEntries(dir, entries); + + if (missingEntries.length === 0) { + return true; // All entries already exist + } + + logProgress(`=> Adding gitignore entries${context ? ` for ${context}` : ''}: ${missingEntries.join(', ')}`); + + try { + const gitignorePath = path.join(dir, '.gitignore'); + let content = ''; + + if (fs.existsSync(gitignorePath)) { + content = fs.readFileSync(gitignorePath, 'utf8'); + // Ensure there's a newline at the end if the file is not empty + if (content.length > 0 && !content.endsWith('\n')) { + content += '\n'; + } + } + + // Add context as a comment if provided + if (context) { + content += `\n# ${context}\n`; + } + content += missingEntries.join('\n') + '\n'; + fs.writeFileSync(gitignorePath, content, 'utf8'); + + logSuccess(`=> Added gitignore entries${context ? ` for ${context}` : ''}`); + return true; + } catch (error) { + logError(`=> Failed to add gitignore entries${context ? ` for ${context}` : ''}: ${error.message}`); + return false; + } +} diff --git a/packages/tools-core/lib/global-state.js b/packages/tools-core/lib/global-state.js new file mode 100644 index 0000000000..94a6a5c56c --- /dev/null +++ b/packages/tools-core/lib/global-state.js @@ -0,0 +1,45 @@ +/** + * Global state management for Meteor packages. + * This module provides a way to store and retrieve global state that persists across file changes. + */ + +/** + * Gets a value from the global state. + * @param {string} key - The key to retrieve. + * @param {any} defaultValue - The default value to return if the key doesn't exist. + * @returns {any} The value associated with the key, or the default value if not found. + */ +export function getGlobalState(key, defaultValue) { + return Package.meteor?.global?.[key] !== undefined + ? Package.meteor.global.persistentState[key] + : defaultValue; +} + +/** + * Sets a value in the global state. + * @param {string} key - The key to set. + * @param {any} value - The value to associate with the key. + */ +export function setGlobalState(key, value) { + // Create a namespace for our global state if it doesn't exist + if (!Package?.meteor.global.persistentState) { + Package.meteor.global.persistentState = {}; + } + + Package.meteor.global.persistentState[key] = value; +} + +/** + * Removes a key from the global state. + * @param {string} key - The key to remove. + */ +export function removeGlobalState(key) { + delete Package.meteor.global.persistentState[key]; +} + +/** + * Clears all keys from the global state. + */ +export function clearGlobalState() { + Package.meteor.global.persistentState = {}; +} diff --git a/packages/tools-core/lib/ignore.js b/packages/tools-core/lib/ignore.js new file mode 100644 index 0000000000..611cb3344b --- /dev/null +++ b/packages/tools-core/lib/ignore.js @@ -0,0 +1,87 @@ +/** + * Build gitignore-style "unignore" patterns for specific files/folders. + * + * Rules: + * - Files: !a/ !a/b/ !a/b/c.txt + * - Folders (must end with '/'): + * !a/ !a/b/ !a/b/c/ !a/b/c/** + * + * @param {string[]} inputPaths Paths to keep. Use '/' for dirs (e.g. 'assets/public/'). + * @param {Object} [options] + * @param {boolean} [options.includeAllAncestors=true] If false, only include the immediate parent dir. + * @param {boolean} [options.includeGlobForDirs=true] Emit '**' for directories. + * @param {number} [options.skipLevel=0] Skip this many levels from the beginning. + * @returns {string[]} Negation patterns, in correct order. + */ +export function buildUnignorePatterns(inputPaths, { + includeAllAncestors = true, + includeGlobForDirs = true, + skipLevel = 0, +} = {}) { + const out = []; + const seen = new Set(); + + const push = (p) => { + if (!seen.has(p)) { + seen.add(p); + out.push(p); + } + }; + + for (let raw of inputPaths) { + if (!raw || typeof raw !== 'string') continue; + + // Normalize: forward slashes, drop leading './', collapse double slashes + let anchored = raw.startsWith('/'); + let p = raw.replace(/\\/g, '/') + .replace(/^\.\/+/, '') + .replace(/\/{2,}/g, '/'); + + // detect dir by trailing slash + const isDir = p.endsWith('/'); + // strip leading + trailing slashes for splitting, but remember anchoring + const core = p.replace(/^\/+/, '').replace(/\/+$/, ''); + if (!core) continue; + + const parts = core.split('/'); + + // Process based on skipLevel + if (skipLevel >= parts.length) { + // Skip everything if skipLevel is greater than or equal to the number of parts + continue; + } + + // Ancestors (top-down) + if (includeAllAncestors) { + // Start from skipLevel + 1 to skip the specified number of levels + const startLevel = Math.max(1, skipLevel + 1); + for (let i = startLevel; i <= parts.length - 1; i++) { + const anc = (anchored ? '/' : '') + parts.slice(0, i).join('/') + '/'; + push('!' + anc); + } + } else if (parts.length > 1) { + // Only immediate parent + // For minimal mode with skipLevel, we need to check if the parent is at a level we should skip + if (skipLevel < parts.length - 1) { + // Check if the parent's level is greater than skipLevel + const parentLevel = parts.length - 1; + if (parentLevel > skipLevel) { + const parent = (anchored ? '/' : '') + parts.slice(0, parts.length - 1).join('/') + '/'; + push('!' + parent); + } + } + } + + // Add the file/directory pattern + if (isDir) { + const dir = (anchored ? '/' : '') + parts.join('/') + '/'; + push('!' + dir); + if (includeGlobForDirs) push('!' + dir + '**'); + } else { + const file = (anchored ? '/' : '') + parts.join('/'); + push('!' + file); + } + } + + return out; +} diff --git a/packages/tools-core/lib/log.js b/packages/tools-core/lib/log.js new file mode 100644 index 0000000000..6225d0da06 --- /dev/null +++ b/packages/tools-core/lib/log.js @@ -0,0 +1,79 @@ +// Check if colors should be disabled +const shouldDisableColors = !!process.env.METEOR_DISABLE_COLORS; + +// Minimum message length for consistent log formatting +const MIN_MESSAGE_LENGTH = 80; + +// ANSI color codes +const colors = { + reset: shouldDisableColors ? "" : "\x1b[0m", + blue: shouldDisableColors ? "" : "\x1b[34m", + red: shouldDisableColors ? "" : "\x1b[31m", + purple: shouldDisableColors ? "" : "\x1b[35m", + green: shouldDisableColors ? "" : "\x1b[32m", + cyan: shouldDisableColors ? "" : "\x1b[36m", +}; + +/** + * Pad a message to ensure it has a minimum length + * @param {string} message - The message to pad + * @param {number} minLength - The minimum length (default: MIN_MESSAGE_LENGTH) + * @returns {string} The padded message + */ +export function padMessage(message, minLength = MIN_MESSAGE_LENGTH) { + if (message.length >= minLength) { + return message; + } + return message.padEnd(minLength); +} + +/** + * Log a progress message in blue + * @param {string} message - The message to log + */ +export function logProgress(message) { + console.log(`${colors.blue}${padMessage(message)}${colors.reset}`); +} + +/** + * Log an error message in red + * @param {string} message - The message to log + */ +export function logError(message) { + console.error(`${colors.red}${padMessage(message)}${colors.reset}`); +} + +/** + * Log an info message in cyan + * @param {string} message - The message to log + */ +export function logInfo(message) { + console.log(`${colors.cyan}${padMessage(message)}${colors.reset}`); +} + +/** + * Log a raw message without any color + * @param {string} message - The message to log + */ +export function logRaw(message) { + console.log(padMessage(message)); +} + +/** + * Log a success message in green + * @param {string} message - The message to log + */ +export function logSuccess(message) { + console.log(`${colors.green}${padMessage(message)}${colors.reset}`); +} + +/** + * Get the runLogInstance from the Plugin object if it exists + * @returns {Object|undefined} The runLogInstance or undefined + */ +export function getRunLog() { + if (typeof Plugin !== 'undefined') { + return Plugin.runLogInstance; + } + return undefined; +} diff --git a/packages/tools-core/lib/meteor.js b/packages/tools-core/lib/meteor.js new file mode 100644 index 0000000000..c603101f29 --- /dev/null +++ b/packages/tools-core/lib/meteor.js @@ -0,0 +1,542 @@ +const fs = require('fs'); +const path = require('path'); + +const { logError } = require("./log"); + +// Normalize a path to always use forward slashes (POSIX style). +// Module identifiers must use '/' regardless of OS. +const toPosix = (p) => p.replace(/\\/g, '/'); + +/** + * Returns the current working directory of the Meteor application. + * @returns {string} The absolute path to the Meteor application directory. + */ +export function getMeteorAppDir() { + return process.cwd(); +} + +/** + * Reads and parses the package.json file of the Meteor application. + * @returns {Object} The parsed content of the package.json file. + */ +export function getMeteorAppPackageJson() { + return JSON.parse( + fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8') + ); +} + +/** + * Retrieves the Meteor configuration from the application's package.json. + * @returns {Object|undefined} The Meteor configuration object or undefined if not found. + */ +export function getMeteorAppConfig() { + return typeof Plugin?.getMeteorConfig === 'function' + ? Plugin.getMeteorConfig() + : getMeteorAppPackageJson()?.meteor; +} + +/** + * Get Meteor's app port + * @returns {false|*} + */ +export function getMeteorAppPort() { + return Package?.meteor?.global?.currentCommand?.options?.['port'] || process.env.PORT || '3000'; +} + +/** + * Retrieves the modern configuration from the application's package.json. + * @returns {Object|undefined} The modern configuration object or undefined if not found. + */ +export function getMeteorAppConfigModern() { + return getMeteorAppConfig()?.modern; +} + +/** + * Retrieves the verbose flag from the application's package.json. + * @returns {boolean|undefined} The verbose flag or undefined if not found. + */ +export function isMeteorAppConfigModernVerbose() { + return getMeteorAppConfigModern()?.verbose || + getMeteorAppConfigModern()?.transpiler?.verbose || false; +} + +/** + * Retrieves the auto install deps flag from the app's package.json. + * @returns {Boolean|*} + */ +export function hasMeteorAppConfigAutoInstallDeps() { + const { autoInstallDeps = true } = getMeteorAppConfig() || {}; + return !!autoInstallDeps; +} + +/** + * Retrieves the entry points for the Meteor application from the configuration. + * Uses Plugin.getMeteorConfig() if available, otherwise falls back to getMeteorAppConfig(). + * @returns {Object} An object containing the main and test entry points for client and server. + * @returns {string|undefined} mainClient - The client main module path. + * @returns {string|undefined} mainServer - The server main module path. + * @returns {string|undefined} testClient - The client test module path. + * @returns {string|undefined} testServer - The server test module path. + */ +export function getMeteorAppEntrypoints() { + const meteorConfig = getMeteorAppConfig(); + return { + mainClient: meteorConfig?.mainModule?.client, + mainServer: meteorConfig?.mainModule?.server, + testClient: meteorConfig?.testModule?.client || meteorConfig?.testModule, + testServer: meteorConfig?.testModule?.server || meteorConfig?.testModule, + }; +} + +/** + * Retrieves the initial entry points for the Meteor application from the package.json. + * @returns {Object} An object containing the main and test entry points for client and server. + * @returns {string|undefined} mainClient - The client main module path. + * @returns {string|undefined} mainClientHtml - The client main html path. + * @returns {string|undefined} mainServer - The server main module path. + * @returns {string|undefined} testClient - The client test module path. + * @returns {string|undefined} testServer - The server test module path. + */ +export function getMeteorInitialAppEntrypoints() { + const meteorConfig = getMeteorAppPackageJson()?.meteor; + const mainClient = meteorConfig?.mainModule?.client; + + let mainClientHtml; + if (mainClient) { + const clientDir = path.dirname(mainClient); + const clientBasename = path.basename(mainClient, path.extname(mainClient)); + const htmlPath = path.join( + getMeteorAppDir(), + clientDir, + `${clientBasename}.html` + ); + + if (fs.existsSync(htmlPath)) { + mainClientHtml = toPosix(path.join(clientDir, `${clientBasename}.html`)); + } else { + // Find first html in entry folder + const files = fs.readdirSync(path.join(getMeteorAppDir(), clientDir)); + const htmlFile = files.find((file) => path.extname(file) === ".html"); + if (htmlFile) { + mainClientHtml = toPosix(path.join(clientDir, htmlFile)); + } + } + } + + return { + mainClient, + mainClientHtml, + mainServer: meteorConfig?.mainModule?.server, + ...(meteorConfig?.testModule?.client && { + testClient: meteorConfig?.testModule?.client, + }), + ...(meteorConfig?.testModule?.server && { + testServer: meteorConfig?.testModule?.server, + }), + ...(!meteorConfig?.testModule?.client && + !meteorConfig?.testModule?.server && { + testModule: meteorConfig?.testModule, + }), + }; +} + +/** + * Checks if the current Meteor project is configured as test module. + * @returns {boolean} + */ +export function isMeteorAppTestModule() { + return getMeteorInitialAppEntrypoints().testModule != null; +} + +/** + * Sets the Meteor application entry points in environment variables. + * @param {Object} options - The entry points configuration object. + * @param {string} [options.mainClient] - The client main module path. + * @param {string} [options.mainServer] - The server main module path. + * @param {string} [options.testModule] - The test module path. + * @param {string} [options.testClient] - The client test module path. + * @param {string} [options.testServer] - The server test module path. + */ +export function setMeteorAppEntrypoints({ + mainClient, + mainServer, + testModule, + testClient, + testServer, +}) { + if (mainClient) { + process.env.METEOR_CONFIG_CLIENT = mainClient; + } + if (mainServer) { + process.env.METEOR_CONFIG_SERVER = mainServer; + } + if (testModule) { + process.env.METEOR_CONFIG_TEST = testModule; + } else { + if (testClient) { + process.env.METEOR_CONFIG_TEST_CLIENT = testClient; + } + if (testServer) { + process.env.METEOR_CONFIG_TEST_SERVER = testServer; + } + } + global.reinitializeMeteorConfig?.(); +} + +/** + * Sets patterns to be ignored by the Meteor application in the environment variable. + * Appends the new ignore pattern to any existing ones. + * @param {string} ignore - The pattern to be ignored. + */ +export function setMeteorAppIgnore(ignore) { + process.env.METEOR_IGNORE = `${process.env.METEOR_IGNORE || ''} ${ignore}`.trim(); +} + +/** + * Checks if the current Meteor command is 'run'. + * @returns {boolean} True if the current command is 'run', false otherwise. + */ +export function isMeteorAppRun() { + return Package?.meteor?.global?.currentCommand?.name === 'run'; +} + +/** + * Checks if the current Meteor command is 'build'. + * @returns {boolean} True if the current command is 'build', false otherwise. + */ +export function isMeteorAppBuild() { + return ['build', 'deploy'].includes(Package?.meteor?.global?.currentCommand?.name); +} + +/** + * Checks if the current Meteor command is 'update'. + * @returns {boolean} True if the current command is 'update', false otherwise. + */ +export function isMeteorAppUpdate() { + return Package?.meteor?.global?.currentCommand?.name === 'update'; +} + +/** + * Checks if the current Meteor command is 'test'. + * @returns {boolean} True if the current command is 'test', false otherwise. + */ +export function isMeteorAppTest() { + return Package?.meteor?.global?.currentCommand?.name === 'test'; +} + +/** + * Checks if the current Meteor command is 'test' and is running in full app mode. + * @returns {false|*} + */ +export function isMeteorAppTestFullApp() { + return isMeteorAppTest() && !!Package?.meteor?.global?.currentCommand?.options?.['full-app']; +} + +/** + * Checks if the current Meteor command is 'test' and is running in watch mode. + * @returns {boolean} True if the current command is 'test' and is running in watch mode, false otherwise. + */ +export function isMeteorAppTestWatch() { + return isMeteorAppTest() && !Package?.meteor?.global?.currentCommand?.options?.once; +} + +/** + * Check if the current Meteor current command is running Android. + * @returns {boolean} + */ +export function isMeteorAppNativeAndroid() { + return Package?.meteor?.global?.currentCommand?.options?.args?.some(_arg => + ['android', 'android-device'].includes(_arg) + ); +} + +/** + * Check if the current Meteor current command is running iOS. + * @returns {boolean} + */ +export function isMeteorAppNativeIos() { + return Package?.meteor?.global?.currentCommand?.options?.args?.some(_arg => + ['ios', 'ios-device'].includes(_arg) + ); +} + +/** + * Checks if the current Meteor command is running native. + * @returns {boolean} + */ +export function isMeteorAppNative() { + return isMeteorAppNativeAndroid() || isMeteorAppNativeIos(); +} + +/** + * Checks if the Meteor application is running in development mode. + * @returns {boolean} True if the application is in development mode, false otherwise. + */ +export function isMeteorAppDevelopment() { + if (process.env.NODE_ENV) { + return process.env.NODE_ENV !== 'production'; + } + return Package.meteor?.Meteor.isDevelopment && !isMeteorAppBuild(); +} + +/** + * Checks if the Meteor application is running in production mode. + * @returns {boolean} True if the application is in production mode, false otherwise. + */ +export function isMeteorAppProduction() { + if (process.env.NODE_ENV) { + return process.env.NODE_ENV === 'production'; + } + return Package.meteor?.Meteor.isProduction || isMeteorAppBuild(); +} + +/** + * Checks if the Meteor application is running in debug mode. + * @returns {boolean} True if the application is in debug mode, false otherwise. + */ +export function isMeteorAppDebug() { + return Package.meteor?.Meteor.isDebug || ( + !!process.env.NODE_INSPECTOR_IPC || + !!process.env.VSCODE_INSPECTOR_OPTIONS || + Object.keys(global.currentCommand?.options || {}).some(function(_arg) { + return ['inspect', 'debug', 'brk'].includes(_arg); + }) + ); +} + +/** + * Checks if the Meteor application is running with METEOR_PROFILE enabled. + * @returns {boolean} True if METEOR_PROFILE is set, false otherwise. + */ +export function isMeteorAppProfile() { + return !!process.env.METEOR_PROFILE; +} + +/** + * Sets a custom script URL for the Meteor application in the environment variable. + * @param {string} scriptUrl - The URL of the custom script. + */ +export function setMeteorAppCustomScriptUrl(scriptUrl) { + process.env.METEOR_APP_CUSTOM_SCRIPT_URL = scriptUrl; +} + +/** + * Retrieves a list of all packages installed in the Meteor application. + * @returns {string[]} An array of package names. + */ +export function getMeteorAppPackages() { + return Object.keys(Package?.meteor?.global?.packageVersionMap || {}); +} + +/** + * Gets all files and folders from the root level of the Meteor application. + * @param {Object} options - Options for getting files and folders. + * @param {boolean} [options.recursive=true] - Whether to scan directories recursively. + * @param {Array<string>} [options.ignore=[]] - Patterns to ignore (e.g., ['node_modules', '.git']). + * @param {boolean} [options.includeStats=false] - Whether to include file/folder stats in the result. + * @param {string} [options.startPath] - Custom start path (defaults to Meteor app root). + * @returns {Object} An object with 'files' and 'directories' arrays containing paths relative to the root. + */ +export function getMeteorAppFilesAndFolders(options = {}) { + const { + recursive = true, + ignore = ['node_modules', '.git', '.meteor/local'], + includeStats = false, + startPath = getMeteorAppDir() + } = options; + + // Helper function to check if a path should be ignored + const shouldIgnore = (itemPath) => { + const relativePath = path.relative(getMeteorAppDir(), itemPath); + return ignore.some(pattern => { + if (pattern.endsWith('/**')) { + const dirPattern = pattern.slice(0, -3); + return relativePath === dirPattern || relativePath.startsWith(`${dirPattern}/`); + } + return relativePath === pattern || relativePath.startsWith(`${pattern}/`); + }); + }; + + // Helper function to recursively scan directories + const scanDirectory = (dirPath) => { + const result = { + files: [], + directories: [] + }; + + if (shouldIgnore(dirPath)) { + return result; + } + + try { + const items = fs.readdirSync(dirPath); + + for (const item of items) { + const itemPath = path.join(dirPath, item); + + // Skip if the item should be ignored + if (shouldIgnore(itemPath)) { + continue; + } + + try { + const stats = fs.statSync(itemPath); + const relativePath = path.relative(getMeteorAppDir(), itemPath); + + if (stats.isDirectory()) { + // Add directory to the result + result.directories.push( + includeStats ? { path: relativePath, stats } : relativePath + ); + + // Recursively scan subdirectories if recursive option is true + if (recursive) { + const subResult = scanDirectory(itemPath); + result.files.push(...subResult.files); + result.directories.push(...subResult.directories); + } + } else if (stats.isFile()) { + // Add file to the result + result.files.push( + includeStats ? { path: relativePath, stats } : relativePath + ); + } + } catch (error) { + // Skip items that can't be accessed + logError(`=> Failed to access ${itemPath}: ${error.message}`); + } + } + } catch (error) { + logError(`=> Failed to read directory ${dirPath}: ${error.message}`); + } + + return result; + }; + + // Start the scan from the specified path + return scanDirectory(startPath); +} + +/** + * Requires a module relative to the Meteor tools directory. + * @param {string} filePath - The path of the file to require, relative to the Meteor tools directory. + * @returns {Object} The exported module from the required file. + */ +export function getMeteorToolsRequire(filePath) { + const mainModule = global.process.mainModule; + const absPath = mainModule.filename.split(path.sep).slice(0, -1).join(path.sep); + return mainModule.require(path.resolve(absPath, filePath)); +} + +/** + * Checks if the Meteor application is a Blaze project. + * @returns {boolean} True if the application is a Blaze project, false otherwise. + */ +export function isMeteorBlazeProject() { + return getMeteorAppPackages().includes('blaze') || getMeteorAppPackages().includes('blaze-html-templates'); +} + +/** + * Checks if the Meteor application is a Blaze Hot project. + * @returns {boolean} True if the application is a Blaze Hot project, false otherwise. + */ +export function isMeteorBlazeHotProject() { + return isMeteorBlazeProject() && getMeteorAppPackages().includes('blaze-hot'); +} + +/** + * Checks if the Meteor application is a Coffeescript project. + * @returns {boolean} + */ +export function isMeteorCoffeescriptProject() { + return getMeteorAppPackages().includes('coffeescript'); +} + +/** + * Checks if the Meteor application is a Less project. + * @returns {boolean} True if the application has the 'less' package, false otherwise. + */ +export function isMeteorLessProject() { + return getMeteorAppPackages().includes('less'); +} + +/** + * Checks if the Meteor application is a SCSS project. + * @returns {boolean} True if the application has any package containing 'scss', false otherwise. + */ +export function isMeteorScssProject() { + return getMeteorAppPackages().some(pkg => pkg.includes('scss')); +} + +/** + * Checks if the Meteor application is a Bundle Visualizer project. + * @returns {boolean} + */ +export function isMeteorBundleVisualizerProject() { + return getMeteorAppPackages().includes('bundle-visualizer'); +} + +/** + * Checks if the Meteor application is a Typescript project. + * @returns {boolean} True if the application is a Typescript project, false otherwise. + */ +export function isMeteorTypescriptProject() { + return getMeteorAppPackages().includes('typescript'); +} + +/** + * Checks if the current Meteor command is 'test-packages'. + * @returns {boolean} True if the current command is 'test-packages', false otherwise. + */ +export function isMeteorPackagesTest() { + return Package?.meteor?.global?.currentCommand?.name === 'test-packages'; +} + +/** + * Gets the package directories from the environment variables. + * @returns {string[]} + */ +export function getMeteorEnvPackageDirs() { + function packageDirsFromEnvVar(envVar, delimiter = path.delimiter) { + return process.env[envVar] && process.env[envVar].split(delimiter) || []; + } + return [ + // METEOR_PACKAGE_DIRS should use the arch-specific delimiter + ...(packageDirsFromEnvVar('METEOR_PACKAGE_DIRS', path.delimiter || ':')), + // PACKAGE_DIRS (deprecated) always used ':' separator (yes, even Windows) + ...(packageDirsFromEnvVar('PACKAGE_DIRS', ':')), + ]; +} + +/** + * Spreads Meteor's TOOL_NODE_FLAGS to NODE_OPTIONS for proper inheritance + * of Meteor-specific tool environment process variables. + * Only spreads if TOOL_NODE_FLAGS_INHERIT is truthy (enabled by default). + * @param {Object} env - The current environment variables + * @returns {Object} The updated environment variables with NODE_OPTIONS + */ +export function inheritMeteorToolNodeFlags(env = {}) { + const toolFlags = env.TOOL_NODE_FLAGS; + if (!toolFlags) { + return env; + } + + // Check if spreading is enabled (default: true) + // Only disable if TOOL_NODE_FLAGS_INHERIT is explicitly set to a falsy value + // Treat "0" as falsy for this specific case + const shouldSpread = env.TOOL_NODE_FLAGS_INHERIT !== undefined + ? (env.TOOL_NODE_FLAGS_INHERIT !== "0" && !!env.TOOL_NODE_FLAGS_INHERIT) + : true; + + if (!shouldSpread) { + return env; + } + + return { + ...env, + NODE_OPTIONS: [toolFlags, env.NODE_OPTIONS] + .filter(Boolean) + .map(s => s.trim()) + .join(' '), + }; +} diff --git a/packages/tools-core/lib/npm.js b/packages/tools-core/lib/npm.js new file mode 100644 index 0000000000..66462675f3 --- /dev/null +++ b/packages/tools-core/lib/npm.js @@ -0,0 +1,487 @@ +const fs = require('fs'); +const path = require('path'); +const { spawnProcess } = require('./process'); + +/** + * Gets the path to a Node.js binary using Plugin.getCurrentNodeBinDir() if available, + * otherwise returns null. + * + * @param {string} binaryName - The name of the binary (e.g., 'npm', 'npx', 'node') + * @returns {string|null} The path to the specified binary, or null if not available + */ +export function getNodeBinaryPath(binaryName) { + try { + // Try to access Plugin.getCurrentNodeBinDir() + if (typeof Plugin !== 'undefined' && + typeof Plugin.getCurrentNodeBinDir === 'function' && + Plugin.getCurrentNodeBinDir()) { + return path.join(Plugin.getCurrentNodeBinDir(), binaryName); + } + + // If we're in a context where we can directly access the function + if (typeof getCurrentNodeBinDir === 'function') { + return path.join(getCurrentNodeBinDir(), binaryName); + } + + return null; + } catch (e) { + // If any error occurs, return null + return null; + } +} + +/** + * Checks if a npm dependency exists in the project. + * First checks optimistically in node_modules folder, then checks package.json. + * + * @param {string} dependency - The npm dependency name to check + * @param {Object} [options] - Options for the check + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @param {boolean} [options.checkNodeModules] - Whether to check in node_modules first (defaults to false) + * @returns {boolean} True if the dependency exists, false otherwise + */ +export function checkNpmDependencyExists(dependency, options = {}) { + const cwd = options.cwd || process.cwd(); + + // First, optimistically check if the dependency exists in node_modules + if (options.checkNodeModules) { + const nodeModulesPath = path.join(cwd, 'node_modules', dependency); + try { + if (fs.existsSync(nodeModulesPath)) { + // Check if it has a package.json to confirm it's a valid package + const packageJsonPath = path.join(nodeModulesPath, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + return true; + } + } + } catch (error) { + // If there's an error checking the file system, continue to the fallback method + } + } + + // Fallback: Check package.json directly instead of using `npm ls` + try { + const packageJsonPath = path.join(cwd, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Check if the dependency is listed in any of the dependency sections + return !!( + (packageJson.dependencies && packageJson.dependencies[dependency]) || + (packageJson.devDependencies && packageJson.devDependencies[dependency]) || + (packageJson.optionalDependencies && packageJson.optionalDependencies[dependency]) || + (packageJson.peerDependencies && packageJson.peerDependencies[dependency]) + ); + } + } catch (error) { + // If there's an error reading or parsing package.json, return false + return false; + } + + // If we've reached this point, the dependency was not found + return false; +} + +/** + * Checks if a npm binary exists in the project. + * Looks for the binary in the node_modules/.bin directory. + * + * @param {string} binary - The npm binary name to check + * @param {Object} [options] - Options for the check + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @returns {boolean} True if the binary exists, false otherwise + */ +export function checkNpmBinaryExists(binary, options = {}) { + const cwd = options.cwd || process.cwd(); + const binaryPath = path.join(cwd, 'node_modules', '.bin', binary); + + try { + // Check if the binary file exists and is executable + const stats = fs.statSync(binaryPath); + return stats.isFile() && (stats.mode & 0o111); // Check if executable bit is set + } catch (error) { + return false; + } +} + +/** + * Builds npm install arguments based on options and dependencies + * + * @param {string|string[]} dependencies - The npm dependency or dependencies to install + * @param {Object} [options] - Options for the installation + * @param {boolean} [options.dev=false] - If true, install as a dev dependency + * @param {boolean} [options.exact=false] - If true, install with exact version + * @param {boolean} [options.isMeteorCommand=false] - If true, prepends 'npm' to the args for meteor command + * @returns {string[]} Array of arguments for the npm install command + */ +function buildNpmInstallArgs(dependencies, options = {}) { + const args = options.isMeteorCommand ? ['npm', 'install'] : ['install']; + + // Add flags based on options + if (options.dev) { + args.push('--save-dev'); + } + + if (options.exact) { + args.push('--save-exact'); + } + + // Add dependencies to the command + if (Array.isArray(dependencies)) { + args.push(...dependencies); + } else { + args.push(dependencies); + } + + return args; +} + +/** + * Builds yarn install arguments based on options and dependencies + * + * @param {string|string[]} dependencies - The npm dependency or dependencies to install + * @param {Object} [options] - Options for the installation + * @param {boolean} [options.dev=false] - If true, install as a dev dependency + * @param {boolean} [options.exact=false] - If true, install with exact version + * @returns {string[]} Array of arguments for the yarn add command + */ +function buildYarnInstallArgs(dependencies, options = {}) { + const args = ['add']; + + // Add flags based on options + if (options.dev) { + args.push('--dev'); + } + + if (options.exact) { + args.push('--exact'); + } + + // Add dependencies to the command + if (Array.isArray(dependencies)) { + args.push(...dependencies); + } else { + args.push(dependencies); + } + + return args; +} + +/** + * Executes a command and returns a promise that resolves to true if successful + * + * @param {string} command - The command to execute + * @param {string[]} args - The arguments for the command + * @param {Object} options - Options for the spawn process + * @param {string} options.cwd - Current working directory + * @returns {Promise<boolean>} A promise that resolves to true if command succeeded, false otherwise + */ +function executeCommand(command, args, options) { + return new Promise((resolve) => { + spawnProcess(command, args, { + cwd: options.cwd, + onExit: (code) => { + resolve(code === 0); + }, + onError: () => { + resolve(false); + } + }); + }); +} + +/** + * Installs a npm dependency using direct npm binary if available, otherwise falls back to `meteor npm install`. + * If yarn option is true, uses yarn instead. + * + * @param {string|string[]} dependencies - The npm dependency or dependencies to install + * @param {Object} [options] - Options for the installation + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @param {boolean} [options.dev=false] - If true, install as a dev dependency + * @param {boolean} [options.exact=false] - If true, install with exact version + * @param {boolean} [options.yarn=false] - If true, use yarn instead of npm + * @returns {Promise<boolean>} A promise that resolves to true if installation succeeded, false otherwise + */ +export function installNpmDependency(dependencies, options = {}) { + const cwd = options.cwd || process.cwd(); + + // If yarn option is true, use yarn + if (options.yarn) { + const { command, args: baseArgs } = getYarnCommand([]); + const args = buildYarnInstallArgs(dependencies, options); + return executeCommand(command, [...baseArgs, ...args], { cwd }); + } + + // Try to get the npm binary path + const npmBinaryPath = getNodeBinaryPath('npm'); + + // If we have a direct path to npm, use it + if (npmBinaryPath && fs.existsSync(npmBinaryPath)) { + const args = buildNpmInstallArgs(dependencies, options); + return executeCommand(npmBinaryPath, args, { cwd }); + } + + // Fall back to the current method using 'meteor npm install' + const args = buildNpmInstallArgs(dependencies, { ...options, isMeteorCommand: true }); + return executeCommand('meteor', args, { cwd }); +} + + +/** + * Checks if a specific npm dependency version meets a semver condition. + * First checks in node_modules if checkNodeModules is true, then checks project's package.json. + * + * @param {string} dependency - The npm dependency name to check + * @param {Object} [options] - Options for the check + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @param {string} [options.versionRequirement] - The version requirement to check against (e.g., '6.0.0') + * @param {string} [options.semverCondition='gte'] - The semver condition to use (e.g., 'gte', 'lt', 'eq') + * @param {boolean} [options.checkNodeModules] - Whether to check in node_modules first (defaults to false) + * @param {boolean} [options.existenceOnly] - If true, only checks if the dependency exists without version validation + * @returns {boolean} True if the dependency version meets the condition (or exists if existenceOnly is true), false otherwise + */ +export function checkNpmDependencyVersion(dependency, options = {}) { + const semver = require('semver'); + const cwd = options.cwd || process.cwd(); + const versionRequirement = options.versionRequirement; + const semverCondition = options.semverCondition || 'gte'; + + if (!dependency) { + throw new Error('Dependency name must be specified'); + } + + // If existenceOnly is true, delegate to checkNpmDependencyExists + if (options.existenceOnly) { + return checkNpmDependencyExists(dependency, { + cwd, + checkNodeModules: options.checkNodeModules + }); + } + + if (!versionRequirement) { + throw new Error('Version requirement must be specified'); + } + + if (!semver[semverCondition]) { + throw new Error(`Invalid semver condition: ${semverCondition}`); + } + + // First, check in node_modules if the option is enabled + if (options.checkNodeModules) { + const nodeModulesPath = path.join(cwd, 'node_modules', dependency, 'package.json'); + try { + if (fs.existsSync(nodeModulesPath)) { + const packageJson = JSON.parse(fs.readFileSync(nodeModulesPath, 'utf8')); + if (packageJson.version) { + return semver[semverCondition](packageJson.version, versionRequirement); + } + } + } catch (error) { + // If there's an error reading the package.json, continue to the fallback method + } + } + + // Fallback: Check project's package.json directly + try { + const packageJsonPath = path.join(cwd, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Check all dependency sections for the package and its version + const sections = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']; + + for (const section of sections) { + if (packageJson[section] && packageJson[section][dependency]) { + const versionString = packageJson[section][dependency]; + // Extract the version number from the version string (removing ^ or ~ if present) + const version = versionString.replace(/^[\^~]/, ''); + return semver[semverCondition](version, versionRequirement); + } + } + } + } catch (error) { + // If there's an error reading or parsing package.json, return false + return false; + } + + // If we've reached this point, the dependency version couldn't be determined + return false; +} + +/** + * Gets the npm command and arguments + * @param {string[]} args - The arguments to pass to npm + * @returns {Object} An object with command, args, and base properties + */ +export function getNpmCommand(args) { + // Try to get the npm binary path + const npmBinaryPath = getNodeBinaryPath('npm'); + + // If we have a direct path to npm, use it + if (npmBinaryPath && fs.existsSync(npmBinaryPath)) { + return { + command: npmBinaryPath, + args: args, + prefix: `${npmBinaryPath}`, + }; + } + + // Fall back to the current method using 'meteor npm' + return { + command: 'meteor', + args: ['npm', ...args], + prefix: `meteor npm`, + }; +} + +/** + * Gets the npx command and arguments + * @param {string[]} args - The arguments to pass to npx + * @returns {Object} An object with command, args, and base properties + */ +export function getNpxCommand(args) { + // Try to get the npx binary path + const npxBinaryPath = getNodeBinaryPath('npx'); + + // If we have a direct path to npx, use it + if (npxBinaryPath && fs.existsSync(npxBinaryPath)) { + return { + command: npxBinaryPath, + args: args, + prefix: `${npxBinaryPath}`, + }; + } + + // Fall back to the current method using 'meteor npx' + return { + command: 'meteor', + args: ['npx', ...args], + prefix: `meteor npx`, + }; +} + +/** + * Checks if the current project is a Yarn project. + * Looks for yarn.lock file in the current working directory and checks packageManager in package.json. + * + * @param {Object} [options] - Options for the check + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @returns {boolean} True if it's a Yarn project, false otherwise + */ +export function isYarnProject(options = {}) { + const cwd = options.cwd || process.cwd(); + + // Check if yarn.lock exists + const yarnLockPath = path.join(cwd, 'yarn.lock'); + if (fs.existsSync(yarnLockPath)) { + return true; + } + + // Check packageManager field in package.json + try { + const packageJsonPath = path.join(cwd, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Check if packageManager contains "yarn" + if (packageJson.packageManager && packageJson.packageManager.includes('yarn')) { + return true; + } + } + } catch (error) { + // If there's an error reading or parsing package.json, continue + } + + return false; +} + +/** + * Gets the yarn command and arguments + * @param {string[]} args - The arguments to pass to yarn + * @returns {Object} An object with command, args, and base properties + */ +export function getYarnCommand(args) { + // Try to get the yarn binary path + const yarnBinaryPath = getNodeBinaryPath('yarn'); + + // If we have a direct path to yarn, use it + if (yarnBinaryPath && fs.existsSync(yarnBinaryPath)) { + return { + command: yarnBinaryPath, + args, + prefix: `${yarnBinaryPath}`, + }; + } + + // Fall back to using 'yarn' directly + return { + command: 'yarn', + args, + prefix: `yarn`, + }; +} + +/** + * Gets the path to the monorepo root by checking for common monorepo indicators. + * Traverses up the directory tree until it finds a monorepo indicator or reaches the root. + * + * @param {Object} [options] - Options for the detection + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @returns {string|null} Path to the monorepo root if found, null otherwise + */ +export function getMonorepoPath(options = {}) { + const cwd = options.cwd || process.cwd(); + let currentDir = cwd; + + // Function to check if directory has monorepo indicators + const hasMonorepoIndicators = (dir) => { + try { + // Check for npm/yarn workspaces in package.json + const packageJsonPath = path.join(dir, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.workspaces) { + return true; + } + } + + // Check for Lerna + const lernaJsonPath = path.join(dir, 'lerna.json'); + if (fs.existsSync(lernaJsonPath)) { + return true; + } + + // Check for pnpm workspaces + const pnpmWorkspacePath = path.join(dir, 'pnpm-workspace.yaml'); + if (fs.existsSync(pnpmWorkspacePath)) { + return true; + } + + return false; + } catch (error) { + return false; + } + }; + + // Traverse up the directory tree + while (currentDir !== path.dirname(currentDir)) { // Stop when we reach the root directory + if (hasMonorepoIndicators(currentDir)) { + return currentDir; + } + currentDir = path.dirname(currentDir); + } + + // Check the root directory as well + return hasMonorepoIndicators(currentDir) ? currentDir : null; +} + +/** + * Detects if a directory is within a monorepo by checking for common monorepo indicators. + * + * @param {Object} [options] - Options for the detection + * @param {string} [options.cwd] - Current working directory (defaults to process.cwd()) + * @returns {boolean} True if the directory is within a monorepo, false otherwise + */ +export function isMonorepo(options = {}) { + return getMonorepoPath(options) !== null; +} diff --git a/packages/tools-core/lib/process.js b/packages/tools-core/lib/process.js new file mode 100644 index 0000000000..211870eee7 --- /dev/null +++ b/packages/tools-core/lib/process.js @@ -0,0 +1,218 @@ +const { spawn } = require('child_process'); +const net = require('net'); +const { logError } = require('./log'); + +/** + * Spawns a new OS process with the given command and arguments. + * Streams output with original styling and handles errors and exit events. + * Always preserves raw output formatting (colors, progress bars, etc.) and + * provides decoded string data to callbacks for logic/checking/logging. + * + * @param {string} command - The command to run + * @param {string[]} args - Arguments to pass to the command + * @param {Object} options - Options for the spawned process + * @param {Object} [options.env] - Environment variables to merge with process.env + * @param {string} [options.cwd] - Current working directory + * @param {boolean} [options.detached] - Whether to run the process detached from the parent + * @param {Function} [options.onStdout] - Callback for stdout data (receives decoded string) + * @param {Function} [options.onStderr] - Callback for stderr data (receives decoded string) + * @param {Function} [options.onExit] - Callback when process exits + * @param {Function} [options.onError] - Callback when process encounters an error + * @returns {Object} The spawned process with additional utility methods + */ +export function spawnProcess(command, args, options = {}) { + const proc = spawn(command, args, { + env: { ...process.env, ...(options.env || {}), FORCE_COLOR: '1', TERM: 'xterm-256color' }, + cwd: options.cwd || process.cwd(), + stdio: ['pipe', 'pipe', 'pipe'], + detached: options.detached || false, + ...(process.platform === 'win32' && { shell: true }), + }); + + // Add a reference to track if the process is running + proc.isRunning = true; + + // Handle stdout + proc.stdout.on('data', (buf) => { + if (options.onStdout) { + options.onStdout(buf.toString()); + } + }); + + // Handle stderr + proc.stderr.on('data', (buf) => { + if (options.onStderr) { + options.onStderr(buf.toString()); + } + }); + + // Handle process exit + proc.on('close', (code, signal) => { + proc.isRunning = false; + if (options.onExit) options.onExit(code, signal); + }); + + // Handle process errors + proc.on('error', (err) => { + proc.isRunning = false; + if (options.onError) options.onError(err); + else logError(`=> Process error: ${err.message}`); + }); + + // This happens sometimes when we write to stdin after the app + // is dead. If we don't register a handler, we get a top level + // exception and the whole app dies. + proc.stdin.on('error', () => {}); + + if (options.detached) proc.unref(); + return proc; +} + +/** + * Stops a running process. + * + * @param {Object} proc - The process to stop + * @param {Object} [options] - Options for stopping the process + * @param {string} [options.signal='SIGTERM'] - The signal to send to the process + * @param {number} [options.timeout=5000] - Timeout in ms before forcing kill with SIGKILL + * @returns {Promise<void>} A promise that resolves when the process is stopped + */ +export function stopProcess(proc, options = {}) { + if (!proc || !proc.pid || !isProcessRunning(proc)) { + return Promise.resolve(); + } + + const signal = options.signal || 'SIGTERM'; + const timeout = options.timeout || 5000; + + return new Promise((resolve) => { + // Set a timeout to force kill if the process doesn't exit gracefully + const forceKillTimeout = setTimeout(() => { + if (isProcessRunning(proc)) { + proc.kill('SIGKILL'); + } + }, timeout); + + // Listen for the process to exit + proc.on('close', () => { + clearTimeout(forceKillTimeout); + proc.isRunning = false; + resolve(); + }); + + // Send the signal to terminate the process + proc.kill(signal); + }); +} + +/** + * Checks if a process is running. + * + * @param {Object} proc - The process to check + * @returns {boolean} True if the process is running, false otherwise + */ +export function isProcessRunning(proc) { + if (!proc || !proc.pid) { + return false; + } + + // If we've been tracking the process state with our isRunning property + if (proc.isRunning === false) { + return false; + } + + // Try to send signal 0 to the process, which doesn't actually send a signal + // but checks if the process exists + try { + process.kill(proc.pid, 0); + return true; + } catch (e) { + return false; + } +} + +/** + * Checks if a port is available. + * + * @param {number} port - The port to check + * @param {string} [host='127.0.0.1'] - The host to check + * @returns {Promise<boolean>} A promise that resolves to true if the port is available, false otherwise + */ +export function isPortAvailable(port, host = '127.0.0.1') { + return new Promise((resolve) => { + const server = net.createServer(); + + server.once('error', (err) => { + if (err.code === 'EADDRINUSE') { + resolve(false); + } else { + // For other errors, we'll assume the port is not available + resolve(false); + } + }); + + server.once('listening', () => { + // Close the server and resolve with true (port is available) + server.close(() => { + resolve(true); + }); + }); + + server.listen(port, host); + }); +} + +/** + * Waits for a port to become available or unavailable. + * + * @param {number} port - The port to check + * @param {Object} [options] - Options for waiting + * @param {string} [options.host='127.0.0.1'] - The host to check + * @param {boolean} [options.waitUntilAvailable=false] - If true, wait until port is available; if false, wait until port is in use + * @param {number} [options.timeout=30000] - Timeout in ms + * @param {number} [options.interval=500] - Interval between checks in ms + * @returns {Promise<boolean>} A promise that resolves to true if the condition is met, false if timed out + */ +export function waitForPort(port, options = {}) { + const host = options.host || '127.0.0.1'; + const waitUntilAvailable = options.waitUntilAvailable || false; + const timeout = options.timeout || 30000; + const interval = options.interval || 500; + + const startTime = Date.now(); + + return new Promise((resolve) => { + let timeoutId = null; + + const check = async () => { + // Check if we've exceeded the timeout + if (Date.now() - startTime > timeout) { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + resolve(false); + return; + } + + const isAvailable = await isPortAvailable(port, host); + + // If we're waiting for the port to be available and it is, or + // if we're waiting for the port to be in use and it's not available + if ((waitUntilAvailable && isAvailable) || (!waitUntilAvailable && !isAvailable)) { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + resolve(true); + return; + } + + // Schedule the next check + timeoutId = setTimeout(check, interval); + }; + + // Start checking + check(); + }); +} diff --git a/packages/tools-core/lib/string.js b/packages/tools-core/lib/string.js new file mode 100644 index 0000000000..f401c86548 --- /dev/null +++ b/packages/tools-core/lib/string.js @@ -0,0 +1,58 @@ +/** + * Capitalizes the first letter of the given string. + * + * @param {string} str – The input string. + * @returns {string} – The string with its first character uppercased. + */ +export function capitalizeFirstLetter(str) { + if (typeof str !== 'string' || str.length === 0) { + return ''; + } + return str.charAt(0).toUpperCase() + str.slice(1); +} + +/** + * Shuffles the elements of the given array. + * @param arr + * @returns {*} + */ +function shuffleArray(arr) { + for (let i = arr.length - 1; i > 0; --i) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +/** + * Shuffles the characters of the given string. + * @param str + * @returns {string} + */ +export function shuffleString(str) { + return shuffleArray(str.split('')).join(''); +} + +/** + * Join an array of strings into a human-readable list. + * + * @param {string[]} items - The items to join. + * @param {object} [opts] + * @param {string} [opts.separator=', '] - Separator between items (except last). + * @param {string} [opts.lastSeparator=' and '] - Text to insert before the last item. + * @returns {string} + */ +export function joinWithAnd( + items, + { separator = ', ', lastSeparator = ' and ' } = {}, +) { + const len = items.length; + if (len === 0) return ''; + if (len === 1) return items[0]; + if (len === 2) return items[0] + lastSeparator + items[1]; + return items + .slice(0, -1) + .reduce((acc, item, idx) => { + return acc + (idx === 0 ? '' : separator) + item; + }, '') + lastSeparator + items[len - 1]; +} diff --git a/packages/tools-core/package.js b/packages/tools-core/package.js new file mode 100644 index 0000000000..c12ebf8b89 --- /dev/null +++ b/packages/tools-core/package.js @@ -0,0 +1,22 @@ +Package.describe({ + summary: "Helpers for managing modern tools in Meteor", + version: '1.0.0', + devOnly: true, +}); + +Package.onUse(function (api) { + api.use('ecmascript', ['client', 'server']); + + api.mainModule('tools-core.js', 'server'); +}); + +Package.onTest(function (api) { + api.use(['ecmascript', 'tinytest']); + api.use('tools-core'); + + // Add test files for each lib/ module + // This structure allows easy addition of tests for other lib/ categories + api.addFiles([ + 'tests/meteor_tests.js', + ], 'server'); +}); diff --git a/packages/tools-core/tests/meteor_tests.js b/packages/tools-core/tests/meteor_tests.js new file mode 100644 index 0000000000..2615172da9 --- /dev/null +++ b/packages/tools-core/tests/meteor_tests.js @@ -0,0 +1,213 @@ +import { inheritMeteorToolNodeFlags } from "../lib/meteor.js"; + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - no TOOL_NODE_FLAGS", + function (test) { + const env = { + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "true", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result, + env, + "Should return input unchanged when no TOOL_NODE_FLAGS" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - default behavior (inherit enabled)", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.TOOL_NODE_FLAGS, + "--max-old-space-size=4096", + "TOOL_NODE_FLAGS should be preserved" + ); + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "NODE_OPTIONS should contain both flags" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit explicitly enabled", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "true", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "NODE_OPTIONS should contain both flags when explicitly enabled" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit explicitly enabled with truthy value", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "1", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "NODE_OPTIONS should contain both flags with truthy string" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit disabled with empty string", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--inspect", + "NODE_OPTIONS should remain unchanged when inherit disabled with empty string" + ); + test.equal( + result.TOOL_NODE_FLAGS, + "--max-old-space-size=4096", + "TOOL_NODE_FLAGS should be preserved" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit disabled with false", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: false, + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--inspect", + "NODE_OPTIONS should remain unchanged when inherit disabled with false" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit disabled with zero", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "0", + }; + const result = inheritMeteorToolNodeFlags(env); + console.log("--> (meteor_tests.js-Line: 128)\n result: ", result); + + test.equal( + result.NODE_OPTIONS, + "--inspect", + 'NODE_OPTIONS should remain unchanged when inherit disabled with "0"' + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - no existing NODE_OPTIONS", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096", + "NODE_OPTIONS should be set to TOOL_NODE_FLAGS when no existing NODE_OPTIONS" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - whitespace handling", + function (test) { + const env = { + TOOL_NODE_FLAGS: " --max-old-space-size=4096 ", + NODE_OPTIONS: " --inspect ", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "Should handle whitespace correctly" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - multiple flags", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096 --expose-gc", + NODE_OPTIONS: "--inspect --trace-warnings", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --expose-gc --inspect --trace-warnings", + "Should handle multiple flags correctly" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - empty environment", + function (test) { + const env = {}; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result, + env, + "Should return input unchanged for empty environment" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - undefined environment", + function (test) { + const result = inheritMeteorToolNodeFlags(); + + test.equal( + Object.keys(result).length, + 0, + "Should return empty object for undefined input" + ); + } +); diff --git a/packages/tools-core/tools-core.js b/packages/tools-core/tools-core.js new file mode 100644 index 0000000000..108d4e35c5 --- /dev/null +++ b/packages/tools-core/tools-core.js @@ -0,0 +1,8 @@ +export * from './lib/log'; +export * from './lib/meteor'; +export * from './lib/npm'; +export * from './lib/process'; +export * from './lib/global-state'; +export * from './lib/git'; +export * from './lib/string'; +export * from './lib/ignore'; diff --git a/packages/twitter-config-ui/twitter_configure.html b/packages/twitter-config-ui/twitter_configure.html index 85b2f3c2d1..b3fb304d65 100644 --- a/packages/twitter-config-ui/twitter_configure.html +++ b/packages/twitter-config-ui/twitter_configure.html @@ -4,22 +4,28 @@ </p> <ol> <li> - Visit <a href="https://developer.twitter.com/en/portal/projects/new" target="_blank">https://developer.twitter.com/en/portal/projects/new</a> + Visit <a href="https://developer.x.com/en/portal/dashboard" target="_blank">https://developer.x.com/en/portal/dashboard</a> and sign in. </li> <li> - Select "Add project". + Create a new project and app (or select an existing one). </li> <li> - Save the API keys. + In your app settings, click on "Set up" under "User authentication settings". </li> <li> - Once you create your project, click "Set up" under "User authentication settings" + Enable "OAuth 1.0a" (required for Meteor). </li> <li> - Set Callback URI to: <span class="url">{{siteUrl}}_oauth/twitter</span> + Set "Callback URI / Redirect URL" to: <span class="url">{{siteUrl}}_oauth/twitter</span> </li> <li> - Set Website to: <span class="url">{{siteUrl}}</span> + Set "Website URL" to: <span class="url">{{siteUrl}}</span> + </li> + <li> + Click "Save". + </li> + <li> + Go to the "Keys and tokens" tab and note down your "API Key" (Consumer Key) and "API Key Secret" (Consumer Secret). </li> </ol> </template> diff --git a/packages/typescript/package.js b/packages/typescript/package.js index 2a4372edf4..2b75566208 100644 --- a/packages/typescript/package.js +++ b/packages/typescript/package.js @@ -1,9 +1,10 @@ Package.describe({ name: 'typescript', - version: '5.6.6', + version: '5.9.3', summary: 'Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files', documentation: 'README.md', + devOnly: true, }); Package.registerBuildPlugin({ diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 1765555052..6a2bb3d3ff 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Serves a Meteor app over HTTP", - version: "2.0.7", + version: "2.1.0", }); Npm.depends({ diff --git a/packages/webapp/socket_file_tests.js b/packages/webapp/socket_file_tests.js index 2dbde1927f..856fe7329e 100644 --- a/packages/webapp/socket_file_tests.js +++ b/packages/webapp/socket_file_tests.js @@ -26,6 +26,49 @@ const isMacOS = () => { return platform() === 'darwin'; }; +const getGroupNameForGid = (gid) => { + try { + const data = readFileSync('/etc/group', 'utf8'); + const line = data + .trim() + .split('\n') + .find((groupLine) => { + const [, , groupGid] = groupLine.trim().split(':'); + return Number(groupGid) === gid; + }); + + if (!line) return null; + const [name] = line.trim().split(':'); + return name || null; + } catch { + return null; + } +}; + +const getWritableGroupName = () => { + const { gid, uid } = userInfo(); + const gidsToTry = new Set(); + + if (typeof gid === 'number') { + gidsToTry.add(gid); + } + + if (typeof process.getgroups === 'function') { + process.getgroups().forEach((groupId) => gidsToTry.add(groupId)); + } + + for (const groupId of gidsToTry) { + const groupName = getGroupNameForGid(groupId); + if (groupName) { + return groupName; + } + } + + if (Boolean(process.env.TRAVIS)) return 'travis'; + if (isMacOS()) return 'staff'; + return uid === 0 ? 'root' : null; +}; + const removeTestSocketFile = () => { try { unlinkSync(testSocketFile); @@ -131,9 +174,17 @@ testAsyncMulti( }, async (test) => { // use UNIX_SOCKET_PATH and UNIX_SOCKET_GROUP + const groupToUse = getWritableGroupName(); + + if (!groupToUse) { + // Skip when no writable group could be determined for the current user. + // test.isTrue(true); + test.fail(`fail test: no writable group could be determined for the current user.`); + return; + } + const { httpServer, server } = prepareServer(); - const groupToUse = Boolean(process.env.TRAVIS) && 'travis' || (isMacOS() ? 'staff' : 'root'); process.env.UNIX_SOCKET_PATH = testSocketFile; process.env.UNIX_SOCKET_GROUP = groupToUse; process.env.UNIX_SOCKET_PERMISSIONS = '777'; diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 124e32c9d6..ad76808974 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -437,7 +437,7 @@ WebApp.addRuntimeConfigHook = function(callback) { return runtimeConfig.hooks.register(callback); }; -async function getBoilerplateAsync(request, arch) { +async function getBoilerplateAsync(request, arch, response) { let boilerplate = boilerplateByArch[arch]; await runtimeConfig.hooks.forEachAsync(async hook => { const meteorRuntimeConfig = await hook({ @@ -470,7 +470,7 @@ async function getBoilerplateAsync(request, arch) { promise = promise .then(() => { const callback = boilerplateDataCallbacks[key]; - return callback(request, data, arch); + return callback(request, data, arch, response); }) .then(result => { // Callbacks should return false if they did not make any changes. @@ -1222,6 +1222,7 @@ async function runWebAppServer() { } var request = WebApp.categorizeRequest(req); + var response = res; if (request.url.query && request.url.query['meteor_css_resource']) { // In this case, we're requesting a CSS resource in the meteor-specific @@ -1281,7 +1282,7 @@ async function runWebAppServer() { // Promise that will be resolved when the program is unpaused. await WebApp.clientPrograms[arch].paused; - return getBoilerplateAsync(request, arch) + return getBoilerplateAsync(request, arch, response) .then(({ stream, statusCode, headers: newHeaders }) => { if (!statusCode) { statusCode = res.statusCode ? res.statusCode : 200; @@ -1293,10 +1294,12 @@ async function runWebAppServer() { res.writeHead(statusCode, headers); - stream.pipe(res, { - // End the response when the stream ends. - end: true, - }); + if (!disableBoilerplateResponse) { + stream.pipe(res, { + // End the response when the stream ends. + end: true, + }); + } }) .catch(error => { Log.error('Error running template: ' + error.stack); @@ -1551,6 +1554,11 @@ WebAppInternals.addStaticJs = function(contents) { additionalStaticJs['/' + sha1(contents) + '.js'] = contents; }; +var disableBoilerplateResponse = false; +WebAppInternals.disableBoilerplateResponse = function() { + disableBoilerplateResponse = true; +} + // Exported for tests WebAppInternals.getBoilerplate = getBoilerplate; WebAppInternals.additionalStaticJs = additionalStaticJs; diff --git a/packages/weibo-config-ui/weibo_configure.html b/packages/weibo-config-ui/weibo_configure.html index e48ff8e4e9..b956b8e407 100644 --- a/packages/weibo-config-ui/weibo_configure.html +++ b/packages/weibo-config-ui/weibo_configure.html @@ -1,25 +1,31 @@ <template name="configureLoginServiceDialogForWeibo"> + <p> + <strong>Note:</strong> Weibo is currently deprecated and the team is looking for maintainers for this package. + </p> <p> First, you'll need to register your app on Weibo. Follow these steps: </p> <ol> <li> - Visit <a href="http://open.weibo.com/development" target="_blank">http://open.weibo.com/development</a> (Google Chrome's automatic translation works well here) + Visit <a href="https://open.weibo.com/apps" target="_blank">https://open.weibo.com/apps</a> and sign in (Google Chrome's automatic translation works well here). </li> <li> - Click the orange "立即创建微连接" button + Click "创建应用" (Create Application) and select "网页应用" (Web Application). </li> <li> - Select the third option, APP / 网页应用 (Web Applications) + Complete the registration form with your app details. </li> <li> - Complete the registration process. An email confirmation will be send. No SMS confirmation required. + Complete the email verification process. </li> <li> - Open 应用信息 (Application) -> 高级信息 (Advanced Information) + In your app dashboard, go to "应用信息" > "高级信息" (Application Info > Advanced Information). </li> <li> - Set OAuth2.0 授权回调页 (authorized callback page) to: <span class="url">{{siteUrl}}_oauth/weibo</span> + Set "OAuth2.0 授权回调页" (OAuth2.0 Redirect URI) to: <span class="url">{{siteUrl}}_oauth/weibo</span> + </li> + <li> + In "应用信息" > "基本信息" (Application Info > Basic Information), note down your "App Key" (Client ID) and "App Secret" (Client Secret). </li> </ol> </template> diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index 2432e8857d..a7bdfd5d81 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "3.3.1-rc.2", + "version": "3.4-rc.4", "recommended": false, "official": false, "description": "Meteor experimental release" diff --git a/scripts/admin/meteor-release-official.json b/scripts/admin/meteor-release-official.json index 7b89d5c0a6..45e025b42e 100644 --- a/scripts/admin/meteor-release-official.json +++ b/scripts/admin/meteor-release-official.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "3.3.2", + "version": "3.4", "recommended": false, "official": true, "description": "The Official Meteor Distribution" diff --git a/scripts/build-dev-bundle-common.sh b/scripts/build-dev-bundle-common.sh index 6f06d676a6..00a2d7df8b 100644 --- a/scripts/build-dev-bundle-common.sh +++ b/scripts/build-dev-bundle-common.sh @@ -5,10 +5,10 @@ set -u UNAME=$(uname) ARCH=$(uname -m) -NODE_VERSION=22.18.0 +NODE_VERSION=22.22.1 MONGO_VERSION_64BIT=7.0.16 MONGO_VERSION_32BIT=3.2.22 -NPM_VERSION=10.9.3 +NPM_VERSION=10.9.4 if [ "$UNAME" == "Linux" ] ; then diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index b8043936a5..e2170b9774 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -10,10 +10,11 @@ var packageJson = { dependencies: { // Explicit dependency because we are replacing it with a bundled version // and we want to make sure there are no dependencies on a higher version - npm: "10.9.3", + npm: "10.9.4", "node-gyp": "10.2.0", + "node-gyp-build": "4.8.4", "@mapbox/node-pre-gyp": "1.0.11", - typescript: "5.6.3", + typescript: "5.9.3", "@meteorjs/babel": "7.20.0", // Keep the versions of these packages consistent with the versions // found in dev-bundle-server-package.js. @@ -49,7 +50,7 @@ var packageJson = { "moment": "2.30.1", "rimraf": "2.6.2", "glob": "7.1.6", - ignore: "5.3.2", + ignore: "7.0.5", // XXX: When we update this, see if it fixes this Github issue: // https://github.com/jgm/CommonMark/issues/276 . If it does, remove the // workaround from the tool. diff --git a/scripts/windows/ci/install.ps1 b/scripts/windows/ci/install.ps1 index 3624e4c11d..07cccb0c50 100644 --- a/scripts/windows/ci/install.ps1 +++ b/scripts/windows/ci/install.ps1 @@ -4,6 +4,9 @@ If ($env:PLATFORM -Match '^x86|x64$') { $env:PLATFORM = "windows_${env:PLATFORM}" } +# Check if we're running in a CI environment +$isCI = $env:GITHUB_ACTIONS -eq "true" + $dirCheckout = (Get-Item $PSScriptRoot).parent.parent.parent.FullName $meteorBat = Join-Path $dirCheckout 'meteor.bat' @@ -15,7 +18,8 @@ Write-Host "Updating submodules recursively..." -ForegroundColor Magenta # Appveyor suggests -q flag for 'git submodule...' https://goo.gl/4TFAHm & git.exe -C "$dirCheckout" submodule -q update --init --recursive -If ($LASTEXITCODE -ne 0) { +# Only throw locally +If ($LASTEXITCODE -ne 0 -and -not $isCI) { throw "Updating submodules failed." } @@ -25,25 +29,28 @@ If ($LASTEXITCODE -ne 0) { throw "'meteor npm install' failed." } +# Only `meteor --get-ready` get-ready locally to have better control on CI # The `meteor --get-ready` command is susceptible to EPERM errors, so # we attempt it three times. -$attempt = 3 -$success = $false -while ($attempt -gt 0 -and -not $success) { +If (-not $isCI) { + $attempt = 3 + $success = $false + while ($attempt -gt 0 -and -not $success) { - Write-Host "Running 'meteor --get-ready'..." -ForegroundColor Magenta - # By redirecting error to host, we avoid a shocking/false error color, - # since --get-ready and --version can print (anything) to STDERR and - # PowerShell will interpret that as something being terribly wrong. - & "$meteorBat" --get-ready + Write-Host "Running 'meteor --get-ready'..." -ForegroundColor Magenta + # By redirecting error to host, we avoid a shocking/false error color, + # since --get-ready and --version can print (anything) to STDERR and + # PowerShell will interpret that as something being terribly wrong. + & "$meteorBat" --get-ready - If ($LASTEXITCODE -eq 0) { - $success = $true - } else { - $attempt-- + If ($LASTEXITCODE -eq 0) { + $success = $true + } else { + $attempt-- + } + } + + If ($LASTEXITCODE -ne 0) { + throw "Running .\meteor --get-ready failed three times." } } - -If ($LASTEXITCODE -ne 0) { - throw "Running .\meteor --get-ready failed three times." -} diff --git a/tools/cli/commands-packages.js b/tools/cli/commands-packages.js index 408654581b..395c26ba36 100644 --- a/tools/cli/commands-packages.js +++ b/tools/cli/commands-packages.js @@ -7,6 +7,8 @@ var config = require('../meteor-services/config.js'); var utils = require('../utils/utils.js'); var httpHelpers = require('../utils/http-helpers.js'); var compiler = require('../isobuild/compiler.js'); +var PackageSource = require('../isobuild/package-source.js'); +var archinfo = require('../utils/archinfo'); var catalog = require('../packaging/catalog/catalog.js'); var catalogRemote = require('../packaging/catalog/catalog-remote.js'); var isopack = require('../isobuild/isopack.js'); @@ -1771,13 +1773,33 @@ async function getLaterReleaseVersions(releaseTrack, releaseVersion) { releaseTrack, orderKey); } +async function compileMeteorApp(options) { + var projectContext = new projectContextModule.ProjectContext({ + projectDir: options.appDir, + serverArchitectures: [archinfo.host()], + allowIncompatibleUpdate: options["allow-incompatible-update"], + }); + await main.captureAndExit("=> Errors prevented the build:", async () => await projectContext.prepareProjectForBuild()); + await buildmessage.capture({ + title: "updating the application" + }, async function () { + var packageSource = new PackageSource(); + packageSource.initFromAppDir(projectContext, [/~$/, /^\.#/, /^(\.meteor\/|\.git\/|Thumbs\.db|\.DS_Store\/?|Icon\r|ehthumbs\.db|\..*\.sw.|#.*#)$/]); + await compiler.compile(packageSource, { + packageMap: projectContext.packageMap, + isopackCache: projectContext.isopackCache, + }); + }); +} + main.registerCommand({ name: 'update', options: { patch: { type: Boolean }, "packages-only": { type: Boolean }, "allow-incompatible-update": { type: Boolean }, - "all-packages": { type: Boolean } + "all-packages": { type: Boolean }, + npm: { type: Boolean }, }, // We have to be able to work without a release, since 'meteor // update' is how you fix apps that don't have a release. @@ -1809,6 +1831,12 @@ main.registerCommand({ return 1; } + // Compile the app to resolve NPM dependencies bump coming from plugins + if (options["npm"]) { + await compileMeteorApp(options); + return 0; + } + var releaseUpdateStatus = await maybeUpdateRelease(options); // If we encountered an error and cannot proceed, return. if (releaseUpdateStatus !== 0) { diff --git a/tools/cli/commands.js b/tools/cli/commands.js index bb4bbea6cc..88716546f4 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -690,6 +690,7 @@ function getExamplesJSON(){ const DEFAULT_SKELETON = "react"; export const AVAILABLE_SKELETONS = [ "apollo", + "babel", "bare", "blaze", "full", @@ -701,6 +702,9 @@ export const AVAILABLE_SKELETONS = [ "tailwind", "chakra-ui", "solid", + "legacy", + "coffeescript", + "angular" ]; const SKELETON_INFO = { @@ -715,8 +719,11 @@ const SKELETON_INFO = { "svelte": "To create a basic Svelte app", "tailwind": "To create an app using React and Tailwind", "chakra-ui": "To create an app Chakra UI and React", - "solid": "To create a basic Solid app" -} + "solid": "To create a basic Solid app", + "coffeescript": "To create a basic CoffeeScript app", + "babel": "To create a React app with Babel support", + "angular": "To create a basic Angular app", +}; main.registerCommand({ name: 'create', @@ -726,6 +733,7 @@ main.registerCommand({ list: { type: Boolean }, example: { type: String }, package: { type: Boolean }, + babel: { type: Boolean }, bare: { type: Boolean }, minimal: { type: Boolean }, full: { type: Boolean }, @@ -737,7 +745,10 @@ main.registerCommand({ svelte: { type: Boolean }, tailwind: { type: Boolean }, 'chakra-ui': { type: Boolean }, + coffeescript: { type: Boolean }, solid: { type: Boolean }, + angular: { type: Boolean }, + legacy: { type: Boolean }, prototype: { type: Boolean }, from: { type: String }, }, @@ -840,7 +851,7 @@ main.registerCommand({ return transform(f); }, transformContents: async function (contents, f) { - if (/(\.html|\.[jt]sx?|\.css)/.test(f)) { + if (/(\.html|\.[jt]sx?|\.css|\.coffee)/.test(f)) { return Buffer.from(await transform(contents.toString())); } else { return contents; @@ -1225,7 +1236,7 @@ main.registerCommand({ return Buffer.from(contents.toString().replace(/~prototype~/g, "")); } } - if (/(\.html|\.[jt]sx?|\.css)/.test(f)) { + if (/(\.html|\.[jt]sx?|\.css|\.coffee)/.test(f)) { return Buffer.from(transform(contents.toString())); } else { return contents; @@ -1854,47 +1865,76 @@ main.registerCommand({ "MONGO_URL will NOT be reset."); } - const resetMeteorNmCachePromise = options['skip-cache'] ? Promise.resolve() : files.rm_recursive_async( + // Always clean the default .meteor/local directory to prevent regressions. + // When METEOR_LOCAL_DIR is set, also clean the custom local directory. + const defaultLocalRelative = files.pathJoin('.meteor', 'local'); + const customLocalRelative = process.env.METEOR_LOCAL_DIR || null; + const localDirs = [defaultLocalRelative]; + if (customLocalRelative && customLocalRelative !== defaultLocalRelative) { + localDirs.push(customLocalRelative); + } + + const resetMeteorNpmCachePromise = options['skip-cache'] ? Promise.resolve() : files.rm_recursive_async( files.pathJoin(options.appDir, "node_modules", ".cache", "meteor") ); + const rspackHelpers = require('../tool-env/rspack.js'); + const rspackAppContexts = rspackHelpers.getRspackAppContexts(options.appDir); + const resetRspackPromises = rspackAppContexts.map((contextPath) => files.rm_recursive_async( + contextPath + )); + if (options.db) { // XXX detect the case where Meteor is running the app, but // MONGO_URL was set, so we don't see a Mongo process var findMongoPort = require('../runners/run-mongo.js').findMongoPort; - var isRunning = !! await findMongoPort(files.pathJoin(options.appDir, ".meteor", "local", "db")); - if (isRunning) { - Console.error("reset: Meteor is running."); - Console.error(); - Console.error( - "This command does not work while Meteor is running your application.", - "Exit the running Meteor development server."); - return 1; + // Check all local dirs for a running Mongo instance + for (const localRelative of localDirs) { + const localDir = files.pathResolve(options.appDir, localRelative); + var isRunning = !! await findMongoPort(files.pathJoin(localDir, "db")); + if (isRunning) { + Console.error("reset: Meteor is running."); + Console.error(); + Console.error( + "This command does not work while Meteor is running your application.", + "Exit the running Meteor development server."); + return 1; + } } await Promise.all([ - files.rm_recursive_async( - files.pathJoin(options.appDir, ".meteor", "local") + ...localDirs.map((rel) => + files.rm_recursive_async(files.pathResolve(options.appDir, rel)) ), - resetMeteorNmCachePromise, + resetMeteorNpmCachePromise, + ...resetRspackPromises, ]); Console.info("Project reset."); return; } - var allExceptDb = files.getPathsInDir(files.pathJoin('.meteor', 'local'), { - cwd: options.appDir, - maxDepth: 1, - }).filter(function (path) { - return !path.includes('.meteor/local/db'); + // Collect all paths inside each local dir except db + var allExceptDb = localDirs.flatMap((rel) => { + try { + return files.getPathsInDir(rel, { + cwd: options.appDir, + maxDepth: 1, + }).filter(function (p) { + return !p.includes('/db'); + }); + } catch (e) { + // Directory may not exist (e.g. default dir when only custom is used) + return []; + } }); var allRemovePromises = [ ...allExceptDb.map((_path) => files.rm_recursive_async(files.pathJoin(options.appDir, _path)) ), - resetMeteorNmCachePromise + resetMeteorNpmCachePromise, + ...resetRspackPromises, ]; await Promise.all(allRemovePromises); Console.info("Project reset."); @@ -3423,7 +3463,7 @@ const setupBenchmarkSuite = async (profilingPath) => { process.env.GIT_TERMINAL_PROMPT = 0; const repoUrl = "https://github.com/meteor/performance"; - const branch = "v3.3.0"; + const branch = "v3.4.0"; let tarFailed = false; diff --git a/tools/cli/help.txt b/tools/cli/help.txt index 96e611feca..811b4ec872 100644 --- a/tools/cli/help.txt +++ b/tools/cli/help.txt @@ -153,7 +153,7 @@ Options: >>> create Create a new project. -Usage: meteor create [--release <release>] [--bare|--minimal|--full|--react|--vue|--apollo|--svelte|--blaze|--tailwind|--chakra-ui|--solid] <path> +Usage: meteor create [--release <release>] [--bare|--minimal|--full|--react|--vue|--apollo|--svelte|--blaze|--tailwind|--chakra-ui|--solid|--babel|--coffeescript|--angular] <path> meteor create [--release <release>] --example <example_name> [<path>] meteor create [--release <release>] --from <git_url> [<path>] meteor create --list @@ -195,7 +195,10 @@ Options: --blaze Create a basic blaze-based app. --tailwind Create a basic react-based app, with tailwind configured. --chakra-ui Create a basic react-based app, with chakra-ui configured. + --coffeescript Create a basic coffescript app, with react. + --babel Create a React app with Babel support. --solid Create a basic solid-based app. + --angular Create a basic Angular app. --prototype Create a prototype app with the insecure & autopublish packages. Can be used along with other app commands @@ -961,6 +964,10 @@ Options: constraints. --no-lint don't run linters on the published package and its local dependencies before publishing + --release <version> Specify the release of Meteor to resolve top-level version + conflicts. This option is particularly useful during the migration + process when dealing with packages that have conflicting version + requirements. >>> publish-for-arch diff --git a/tools/cli/main.js b/tools/cli/main.js index b73e2b7868..d72744a72f 100644 --- a/tools/cli/main.js +++ b/tools/cli/main.js @@ -2,6 +2,10 @@ var showRequireProfile = ('METEOR_PROFILE_REQUIRE' in process.env); if (showRequireProfile) { require('../tool-env/profile-require.js').start(); } +const { initMeteorConfig } = require('../tool-env/meteor-config'); + +// Initialize meteorConfig globally +initMeteorConfig(); var assert = require("assert"); var _ = require('underscore'); @@ -287,16 +291,12 @@ main.captureAndExit = async function (header, title, f) { // NB: files required up to this point may not define commands -const { initMeteorConfig } = require('../tool-env/meteor-config'); require('./commands.js'); require('./commands-packages.js'); require('./commands-packages-query.js'); require('./commands-cordova.js'); require('./commands-aliases.js'); -// Initialize meteorConfig globally -initMeteorConfig(); - /////////////////////////////////////////////////////////////////////////////// // Record all the top-level commands as JSON /////////////////////////////////////////////////////////////////////////////// diff --git a/tools/e2e-tests/README.md b/tools/e2e-tests/README.md new file mode 100644 index 0000000000..de681740c9 --- /dev/null +++ b/tools/e2e-tests/README.md @@ -0,0 +1,20 @@ +# E2E Tests + +Isolated Jest + Playwright environment for end-to-end testing Meteor skeletons and bundler integrations. + +The repo root `node_modules/` is used to build the dev bundle, which becomes the Meteor tool itself. Installing test deps (jest, playwright, swc, cheerio, semver, underscore) there could pull in incompatible transitive versions (e.g. lru-cache v10 vs v5) and silently break the dev bundle build or a published Meteor release. This subfolder keeps test dependencies fully isolated so they never affect how Meteor is built or shipped. + +Tests create real Meteor projects, start dev servers, and assert behavior in a headless Chromium browser. + +All commands below should be run from the repo root: + +```sh +# Install dependencies (first time) +npm run install:e2e + +# Run all E2E tests +npm run test:e2e + +# Run a specific suite +npm run test:e2e -- --testPathPattern skeleton +``` diff --git a/tools/e2e-tests/apps/babel/.babelrc b/tools/e2e-tests/apps/babel/.babelrc new file mode 100644 index 0000000000..283dab1090 --- /dev/null +++ b/tools/e2e-tests/apps/babel/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + ["module-resolver", { + "alias": { + "@ui": "./imports/ui", + "@api": "./imports/api" + } + }] + ] +} diff --git a/tools/e2e-tests/apps/babel/.gitignore b/tools/e2e-tests/apps/babel/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/babel/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/babel/.meteor/.gitignore b/tools/e2e-tests/apps/babel/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/babel/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/babel/.meteor/.id b/tools/e2e-tests/apps/babel/.meteor/.id new file mode 100644 index 0000000000..1a116bada7 --- /dev/null +++ b/tools/e2e-tests/apps/babel/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +yhlwvsq0co1.ensn1rspode diff --git a/tools/e2e-tests/apps/babel/.meteor/packages b/tools/e2e-tests/apps/babel/.meteor/packages new file mode 100644 index 0000000000..b652eec546 --- /dev/null +++ b/tools/e2e-tests/apps/babel/.meteor/packages @@ -0,0 +1,22 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +static-html # Define static page content in .html files +apollo # Basic Apollo integration for Meteor apps +compat:graphql # Import .graphql files diff --git a/tools/e2e-tests/apps/babel/.meteor/platforms b/tools/e2e-tests/apps/babel/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/babel/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/babel/.meteor/release b/tools/e2e-tests/apps/babel/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/babel/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/babel/.meteor/versions b/tools/e2e-tests/apps/babel/.meteor/versions new file mode 100644 index 0000000000..d6422e9d89 --- /dev/null +++ b/tools/e2e-tests/apps/babel/.meteor/versions @@ -0,0 +1,73 @@ +accounts-base@3.1.1 +allow-deny@2.1.0 +apollo@5.0.1-beta.0 +autoupdate@2.0.1 +babel-compiler@7.12.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.4.4 +compat:graphql@2.0.0 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-rate-limiter@1.2.2 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +localstorage@1.2.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3 +minimongo@2.0.3 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +rate-limit@1.1.2 +react-fast-refresh@0.2.9 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +static-html@1.4.0 +static-html-tools@1.0.0 +tracker@1.3.4 +typescript@5.6.5 +url@1.3.5 +webapp@2.0.7 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/babel/.swcrc b/tools/e2e-tests/apps/babel/.swcrc new file mode 100644 index 0000000000..769668cf03 --- /dev/null +++ b/tools/e2e-tests/apps/babel/.swcrc @@ -0,0 +1,9 @@ +{ + "jsc": { + "baseUrl": "./", + "paths": { + "@ui/*": ["imports/ui/*"], + "@api/*": ["imports/api/*"] + } + } +} diff --git a/tools/e2e-tests/apps/babel/client/main.css b/tools/e2e-tests/apps/babel/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/babel/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/babel/client/main.html b/tools/e2e-tests/apps/babel/client/main.html new file mode 100644 index 0000000000..a7f7a8075e --- /dev/null +++ b/tools/e2e-tests/apps/babel/client/main.html @@ -0,0 +1,8 @@ +<head> + <title>babel + + + + +
+ diff --git a/tools/e2e-tests/apps/babel/client/main.jsx b/tools/e2e-tests/apps/babel/client/main.jsx new file mode 100644 index 0000000000..7e4d0bf2fa --- /dev/null +++ b/tools/e2e-tests/apps/babel/client/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Meteor } from 'meteor/meteor'; +import { App } from '@ui/App'; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); + const root = createRoot(container); + root.render(); +}); diff --git a/tools/e2e-tests/apps/babel/imports/api/links.js b/tools/e2e-tests/apps/babel/imports/api/links.js new file mode 100644 index 0000000000..050c508eae --- /dev/null +++ b/tools/e2e-tests/apps/babel/imports/api/links.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/babel/imports/apollo/schema.graphql b/tools/e2e-tests/apps/babel/imports/apollo/schema.graphql new file mode 100644 index 0000000000..d081209cbc --- /dev/null +++ b/tools/e2e-tests/apps/babel/imports/apollo/schema.graphql @@ -0,0 +1,10 @@ +type Link { + _id: ID! + title: String + url: String +} + +type Query { + getLink (id: ID!): Link + getLinks: [Link] +} diff --git a/tools/e2e-tests/apps/babel/imports/ui/App.jsx b/tools/e2e-tests/apps/babel/imports/ui/App.jsx new file mode 100644 index 0000000000..b6d8114398 --- /dev/null +++ b/tools/e2e-tests/apps/babel/imports/ui/App.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { InMemoryCache, ApolloProvider, ApolloClient, ApolloLink, HttpLink } from '@apollo/client'; +// import { MeteorAccountsLink } from 'meteor/apollo' +import { Hello } from './Hello.jsx'; +import { Info } from './Info.jsx'; + +const cache = new InMemoryCache().restore(window.__APOLLO_STATE__); + +const link = ApolloLink.from([ + // MeteorAccountsLink(), + new HttpLink({ + uri: '/graphql' + }) +]); + +const client = new ApolloClient({ + uri: '/graphql', + cache, + link, +}); + +export const App = () => ( + +
+

Welcome to Meteor! ☄

+ + +
+
+); diff --git a/tools/static-assets/skel-react/imports/ui/Hello.jsx b/tools/e2e-tests/apps/babel/imports/ui/Hello.jsx similarity index 100% rename from tools/static-assets/skel-react/imports/ui/Hello.jsx rename to tools/e2e-tests/apps/babel/imports/ui/Hello.jsx diff --git a/tools/e2e-tests/apps/babel/imports/ui/Info.jsx b/tools/e2e-tests/apps/babel/imports/ui/Info.jsx new file mode 100644 index 0000000000..7d97da3514 --- /dev/null +++ b/tools/e2e-tests/apps/babel/imports/ui/Info.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useQuery, gql } from '@apollo/client'; + +const GET_LINKS = gql` + { + links: getLinks { + _id + title + url + } + } +`; + +export const Info = () => { + const { loading, error, data } = useQuery(GET_LINKS); + + if (loading) return

Loading...

; + if (error) return

Error ⁉️

; + + return ( +
+

Learn Meteor!

+ +
+ ); +}; diff --git a/tools/e2e-tests/apps/babel/package.json b/tools/e2e-tests/apps/babel/package.json new file mode 100644 index 0000000000..5ef1951a8d --- /dev/null +++ b/tools/e2e-tests/apps/babel/package.json @@ -0,0 +1,36 @@ +{ + "name": "babel", + "private": true, + "scripts": { + "start": "meteor run --open", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@apollo/client": "^3.9.2", + "@apollo/server": "^4.10.0", + "@babel/runtime": "^7.23.9", + "@swc/helpers": "^0.5.17", + "graphql": "^16.8.1", + "meteor-node-stubs": "^1.2.12", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@babel/preset-env": "^7.28.3", + "@babel/preset-react": "^7.23.3", + "@graphql-tools/webpack-loader": "^7.0.0", + "babel-loader": "^9.1.3", + "babel-plugin-module-resolver": "^5.0.0", + "playwright": "1.58.0" + }, + "meteor": { + "mainModule": { + "client": "client/main.jsx", + "server": "server/main.js" + }, + "testModule": "tests/main.js", + "modern": true + } +} diff --git a/tools/e2e-tests/apps/babel/rspack.config.mjs b/tools/e2e-tests/apps/babel/rspack.config.mjs new file mode 100644 index 0000000000..c127da8f22 --- /dev/null +++ b/tools/e2e-tests/apps/babel/rspack.config.mjs @@ -0,0 +1,32 @@ +import { defineConfig } from '@meteorjs/rspack'; + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +export default defineConfig(Meteor => { + return { + module: { + rules: [ + { + test: /\.(js|jsx)$/i, + exclude: /node_modules|\.meteor\/local/, + use: { + loader: 'babel-loader', + }, + }, + { + test: /\.(graphql|gql)$/i, + exclude: /node_modules/, + loader: '@graphql-tools/webpack-loader', + }, + ], + }, + }; +}); diff --git a/tools/e2e-tests/apps/babel/server/apollo.js b/tools/e2e-tests/apps/babel/server/apollo.js new file mode 100644 index 0000000000..eeee0be6bf --- /dev/null +++ b/tools/e2e-tests/apps/babel/server/apollo.js @@ -0,0 +1,35 @@ +import { ApolloServer } from '@apollo/server'; +import { WebApp, WebAppInternals } from 'meteor/webapp'; +import { getUser } from 'meteor/apollo'; +import { LinksCollection } from '/imports/api/links'; +import typeDefs from '/imports/apollo/schema.graphql'; +import { expressMiddleware } from '@apollo/server/express4'; + +const express = WebAppInternals.NpmModules.express.module; + +const resolvers = { + Query: { + getLink: async (obj, { id }) => LinksCollection.findOneAsync(id), + getLinks: async () => LinksCollection.find().fetchAsync() + } +}; + +const context = async ({ req }) => ({ + user: await getUser(req.headers.authorization) +}); + +const server = new ApolloServer({ + cache: 'bounded', + typeDefs, + resolvers, +}); + +export async function startApolloServer() { + await server.start(); + + WebApp.handlers.use( + '/graphql', // Configure the path as you want. + express.json(), + expressMiddleware(server, { context }) // From `@apollo/server/express4` + ); +} diff --git a/tools/e2e-tests/apps/babel/server/main.js b/tools/e2e-tests/apps/babel/server/main.js new file mode 100644 index 0000000000..5e1e1f4aad --- /dev/null +++ b/tools/e2e-tests/apps/babel/server/main.js @@ -0,0 +1,38 @@ +import { Meteor } from 'meteor/meteor'; +import { LinksCollection } from '@api/links'; +import { startApolloServer } from './apollo'; + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +try { + startApolloServer().then(); +} catch (e) { + console.error(e.reason); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } +}); diff --git a/tools/e2e-tests/apps/babel/tests/main.js b/tools/e2e-tests/apps/babel/tests/main.js new file mode 100644 index 0000000000..c8ce52ada5 --- /dev/null +++ b/tools/e2e-tests/apps/babel/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("babel", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "babel"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/e2e-tests/apps/blaze/.gitignore b/tools/e2e-tests/apps/blaze/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/blaze/.meteor/.gitignore b/tools/e2e-tests/apps/blaze/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/blaze/.meteor/.id b/tools/e2e-tests/apps/blaze/.meteor/.id new file mode 100644 index 0000000000..cd2b2544c6 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +gxrt55bwv37g.sjs635lnddn9 diff --git a/tools/e2e-tests/apps/blaze/.meteor/packages b/tools/e2e-tests/apps/blaze/.meteor/packages new file mode 100644 index 0000000000..0572acd7d9 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.meteor/packages @@ -0,0 +1,25 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +blaze-html-templates # Compile .html files into Meteor Blaze views +jquery # Wrapper package for npm-installed jquery +reactive-var # Reactive variable for tracker +tracker # Meteor's client-side reactive programming library + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command + + + +hot-module-replacement # Update code in development without reloading the page +blaze-hot # Update files using Blaze's API with HMR diff --git a/tools/e2e-tests/apps/blaze/.meteor/platforms b/tools/e2e-tests/apps/blaze/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/blaze/.meteor/release b/tools/e2e-tests/apps/blaze/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/blaze/.meteor/versions b/tools/e2e-tests/apps/blaze/.meteor/versions new file mode 100644 index 0000000000..f5bf0b0733 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/.meteor/versions @@ -0,0 +1,79 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +blaze@3.0.2 +blaze-hot@2.0.0 +blaze-html-templates@3.0.0 +blaze-tools@2.0.0 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +caching-html-compiler@2.0.0 +callback-hook@1.6.1 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +html-tools@2.0.0 +htmljs@2.0.1 +id-map@1.2.0 +inter-process-messaging@0.1.2 +jquery@3.0.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3 +minimongo@2.0.3 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0 +observe-sequence@2.0.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +spacebars@2.0.0 +spacebars-compiler@2.0.0 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +templating@1.4.4 +templating-compiler@2.0.0 +templating-runtime@2.0.1 +templating-tools@2.0.0 +tracker@1.3.4 +typescript@5.6.5 +webapp@2.0.7 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/blaze/client/main.css b/tools/e2e-tests/apps/blaze/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/blaze/client/main.html b/tools/e2e-tests/apps/blaze/client/main.html new file mode 100644 index 0000000000..c351a27472 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/client/main.html @@ -0,0 +1,26 @@ + + blaze + + + + +

Welcome to Meteor!

+ + {{> hello}} + {{> info}} + + + + + diff --git a/tools/e2e-tests/apps/blaze/client/main.js b/tools/e2e-tests/apps/blaze/client/main.js new file mode 100644 index 0000000000..ecb3282a2f --- /dev/null +++ b/tools/e2e-tests/apps/blaze/client/main.js @@ -0,0 +1,22 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; + +import './main.html'; + +Template.hello.onCreated(function helloOnCreated() { + // counter starts at 0 + this.counter = new ReactiveVar(0); +}); + +Template.hello.helpers({ + counter() { + return Template.instance().counter.get(); + }, +}); + +Template.hello.events({ + 'click button'(event, instance) { + // increment the counter when button is clicked + instance.counter.set(instance.counter.get() + 1); + }, +}); diff --git a/tools/e2e-tests/apps/blaze/package.json b/tools/e2e-tests/apps/blaze/package.json new file mode 100644 index 0000000000..5187e38e2e --- /dev/null +++ b/tools/e2e-tests/apps/blaze/package.json @@ -0,0 +1,27 @@ +{ + "name": "blaze", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "jquery": "^3.7.1", + "meteor-node-stubs": "^1.2.12" + }, + "devDependencies": { + "playwright": "1.58.0" + }, + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + }, + "testModule": "tests/main.js", + "modern": true + } +} diff --git a/tools/e2e-tests/apps/blaze/server/main.js b/tools/e2e-tests/apps/blaze/server/main.js new file mode 100644 index 0000000000..31a9e0e2d6 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/server/main.js @@ -0,0 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + +Meteor.startup(() => { + // code to run on server at startup +}); diff --git a/tools/e2e-tests/apps/blaze/tests/main.js b/tools/e2e-tests/apps/blaze/tests/main.js new file mode 100644 index 0000000000..12284bb0a5 --- /dev/null +++ b/tools/e2e-tests/apps/blaze/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("blaze", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "blaze"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/e2e-tests/apps/coffeescript/.gitignore b/tools/e2e-tests/apps/coffeescript/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/coffeescript/.meteor/.gitignore b/tools/e2e-tests/apps/coffeescript/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/coffeescript/.meteor/.id b/tools/e2e-tests/apps/coffeescript/.meteor/.id new file mode 100644 index 0000000000..248d80b9ca --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +lv2ipazth09.ysbw5dwq9qvl diff --git a/tools/e2e-tests/apps/coffeescript/.meteor/packages b/tools/e2e-tests/apps/coffeescript/.meteor/packages new file mode 100644 index 0000000000..1cfb3a8ef5 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.meteor/packages @@ -0,0 +1,23 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + + +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data +coffeescript # Enable CoffeeScript syntax in .coffee files diff --git a/tools/e2e-tests/apps/coffeescript/.meteor/platforms b/tools/e2e-tests/apps/coffeescript/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/coffeescript/.meteor/release b/tools/e2e-tests/apps/coffeescript/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/coffeescript/.meteor/versions b/tools/e2e-tests/apps/coffeescript/.meteor/versions new file mode 100644 index 0000000000..5644ad4d7f --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1-rc331.2 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1-rc331.2 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12-rc331.2 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3-rc331.2 +minimongo@2.0.3-rc331.2 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3-rc331.2 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3-rc331.2 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0-rc331.2 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +react-meteor-data@4.0.0 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1-rc331.2 +static-html@1.4.0 +static-html-tools@1.0.0 +tracker@1.3.4 +typescript@5.6.5-rc331.2 +webapp@2.0.7 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/tools/e2e-tests/apps/coffeescript/client/main.coffee b/tools/e2e-tests/apps/coffeescript/client/main.coffee new file mode 100644 index 0000000000..406b81b26c --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/client/main.coffee @@ -0,0 +1,9 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { Meteor } from 'meteor/meteor' +import { App } from '/imports/ui/App.coffee' + +Meteor.startup -> + container = document.getElementById('react-target') + root = createRoot(container) + root.render() diff --git a/tools/e2e-tests/apps/coffeescript/client/main.css b/tools/e2e-tests/apps/coffeescript/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/coffeescript/client/main.html b/tools/e2e-tests/apps/coffeescript/client/main.html new file mode 100644 index 0000000000..b11cc28692 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/client/main.html @@ -0,0 +1,8 @@ + + coffeescript + + + + +
+ diff --git a/tools/e2e-tests/apps/coffeescript/imports/api/links.coffee b/tools/e2e-tests/apps/coffeescript/imports/api/links.coffee new file mode 100644 index 0000000000..95c5ed8cfc --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/imports/api/links.coffee @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo' + +export LinksCollection = new Mongo.Collection('links') diff --git a/tools/e2e-tests/apps/coffeescript/imports/ui/App.coffee b/tools/e2e-tests/apps/coffeescript/imports/ui/App.coffee new file mode 100644 index 0000000000..f6cf0f2b01 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/imports/ui/App.coffee @@ -0,0 +1,10 @@ +import React from 'react' +import { Hello } from './Hello.coffee' +import { Info } from './Info.coffee' + +export App = -> +
+

Welcome to Meteor!

+ + +
diff --git a/tools/e2e-tests/apps/coffeescript/imports/ui/Hello.coffee b/tools/e2e-tests/apps/coffeescript/imports/ui/Hello.coffee new file mode 100644 index 0000000000..ab48a1e4f2 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/imports/ui/Hello.coffee @@ -0,0 +1,12 @@ +import React, { useState } from 'react' + +export Hello = -> + [counter, setCounter] = useState(0) + + increment = -> + setCounter(counter + 1) + +
+ +

You've pressed the button {counter} times.

+
diff --git a/tools/e2e-tests/apps/coffeescript/imports/ui/Info.coffee b/tools/e2e-tests/apps/coffeescript/imports/ui/Info.coffee new file mode 100644 index 0000000000..062d35eee2 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/imports/ui/Info.coffee @@ -0,0 +1,19 @@ +import React from 'react' +import { useFind, useSubscribe } from 'meteor/react-meteor-data' +import { LinksCollection } from '../api/links.coffee' + +export Info = -> + isLoading = useSubscribe('links') + links = useFind(-> LinksCollection.find()) + + if isLoading() + return
Loading...
+ +
+

Learn Meteor!

+ +
diff --git a/tools/e2e-tests/apps/coffeescript/package.json b/tools/e2e-tests/apps/coffeescript/package.json new file mode 100644 index 0000000000..b41b8f8595 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/package.json @@ -0,0 +1,31 @@ +{ + "name": "react", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "playwright": "1.58.0", + "coffee-loader": "^5.0.0", + "coffeescript": "^2.7.0", + "swc-loader": "^0.2.6" + }, + "meteor": { + "mainModule": { + "client": "client/main.coffee", + "server": "server/main.coffee" + }, + "testModule": "tests/main.coffee", + "modern": true + } +} diff --git a/tools/e2e-tests/apps/coffeescript/rspack.config.js b/tools/e2e-tests/apps/coffeescript/rspack.config.js new file mode 100644 index 0000000000..ca59f202e2 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/rspack.config.js @@ -0,0 +1,36 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + module: { + rules: [ + { + test: /\.coffee$/i, + use: [ + { + loader: 'swc-loader', + // perserve SWC config in the Meteor project level + options: Meteor.swcConfigOptions, + }, + { + loader: 'coffee-loader', + }, + ], + }, + ], + }, + resolve: { + extensions: ['.coffee'], + }, + }; +}); diff --git a/tools/e2e-tests/apps/coffeescript/server/main.coffee b/tools/e2e-tests/apps/coffeescript/server/main.coffee new file mode 100644 index 0000000000..35d653b1a7 --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/server/main.coffee @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor' +import { LinksCollection } from '/imports/api/links.coffee' + +insertLink = ({ title, url }) -> + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }) + +Meteor.startup -> + # If the Links collection is empty, add some data. + if await LinksCollection.find().countAsync() is 0 + await insertLink + title: 'Do the Tutorial' + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html' + + await insertLink + title: 'Follow the Guide' + url: 'https://guide.meteor.com' + + await insertLink + title: 'Read the Docs' + url: 'https://docs.meteor.com' + + await insertLink + title: 'Discussions' + url: 'https://forums.meteor.com' + + # We publish the entire Links collection to all clients. + # In order to be fetched in real-time to the clients + Meteor.publish "links", -> + LinksCollection.find() diff --git a/tools/e2e-tests/apps/coffeescript/tests/main.coffee b/tools/e2e-tests/apps/coffeescript/tests/main.coffee new file mode 100644 index 0000000000..fe5d2c4b1b --- /dev/null +++ b/tools/e2e-tests/apps/coffeescript/tests/main.coffee @@ -0,0 +1,14 @@ +import assert from "assert" + +describe "react", -> + it "package.json has correct name", -> + { name } = await import("../package.json") + assert.strictEqual(name, "react") + + if Meteor.isClient + it "client is not server", -> + assert.strictEqual(Meteor.isServer, false) + + if Meteor.isServer + it "server is not client", -> + assert.strictEqual(Meteor.isClient, false) diff --git a/tools/e2e-tests/apps/full-blaze/.gitignore b/tools/e2e-tests/apps/full-blaze/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/full-blaze/.meteor/.gitignore b/tools/e2e-tests/apps/full-blaze/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/full-blaze/.meteor/.id b/tools/e2e-tests/apps/full-blaze/.meteor/.id new file mode 100644 index 0000000000..25db0d1fb3 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +xu86p52uskh.jouz4tf2p07f diff --git a/tools/e2e-tests/apps/full-blaze/.meteor/packages b/tools/e2e-tests/apps/full-blaze/.meteor/packages new file mode 100644 index 0000000000..eacddcc6ff --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.meteor/packages @@ -0,0 +1,23 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +blaze-html-templates # Compile .html files into Meteor Blaze views +jquery # Wrapper package for npm-installed jquery +reactive-var # Reactive variable for tracker +tracker # Meteor's client-side reactive programming library + + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +less +ostrio:flow-router-extra # FlowRouter is a very simple router for Meteor diff --git a/tools/e2e-tests/apps/full-blaze/.meteor/platforms b/tools/e2e-tests/apps/full-blaze/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/full-blaze/.meteor/release b/tools/e2e-tests/apps/full-blaze/.meteor/release new file mode 100644 index 0000000000..c86c3f3551 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.meteor/release @@ -0,0 +1 @@ +none \ No newline at end of file diff --git a/tools/e2e-tests/apps/full-blaze/.meteor/versions b/tools/e2e-tests/apps/full-blaze/.meteor/versions new file mode 100644 index 0000000000..8ac5ec1b56 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/.meteor/versions @@ -0,0 +1,83 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.0 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +blaze@3.0.2 +blaze-html-templates@3.0.0 +blaze-tools@2.0.0 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +caching-html-compiler@2.0.0 +callback-hook@1.6.0 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.11 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +html-tools@2.0.0 +htmljs@2.0.1 +http@1.0.1 +id-map@1.2.0 +inter-process-messaging@0.1.2 +johanbrook:publication-collector@1.1.0 +jquery@3.0.2 +launch-screen@2.0.1 +less@4.1.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +meteortesting:browser-tests@1.6.0 +minifier-css@2.0.1 +minifier-js@3.0.2 +minimongo@2.0.2 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.2 +modules@0.20.3 +modules-runtime@0.13.2 +mongo@2.1.2 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.10.2 +observe-sequence@2.0.0 +ordered-dict@1.2.0 +ostrio:flow-router-extra@3.12.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-dict@1.3.2 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +spacebars@2.0.0 +spacebars-compiler@2.0.0 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.0 +templating@1.4.4 +templating-compiler@2.0.0 +templating-runtime@2.0.1 +templating-tools@2.0.0 +tracker@1.3.4 +typescript@5.6.4 +underscore@1.6.4 +webapp@2.0.7 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/full-blaze/client/head.html b/tools/e2e-tests/apps/full-blaze/client/head.html new file mode 100644 index 0000000000..6fa033587b --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/client/head.html @@ -0,0 +1,3 @@ + + full-blaze + diff --git a/tools/e2e-tests/apps/full-blaze/client/main.js b/tools/e2e-tests/apps/full-blaze/client/main.js new file mode 100644 index 0000000000..f5384b43a6 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/client/main.js @@ -0,0 +1,4 @@ +// Client entry point, imports all client code + +import '/imports/startup/client'; +import '/imports/startup/both'; diff --git a/tools/e2e-tests/apps/full-blaze/client/main.less b/tools/e2e-tests/apps/full-blaze/client/main.less new file mode 100644 index 0000000000..fee4b2d78c --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/client/main.less @@ -0,0 +1,6 @@ +@import "{}/imports/ui/stylesheets/not-found.less"; + +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/full-blaze/imports/api/links/links.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/links.js new file mode 100644 index 0000000000..7c700cf2c6 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/api/links/links.js @@ -0,0 +1,5 @@ +// Definition of the links collection + +import { Mongo } from 'meteor/mongo'; + +export const Links = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/full-blaze/imports/api/links/links.tests.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/links.tests.js new file mode 100644 index 0000000000..183b07b3ae --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/api/links/links.tests.js @@ -0,0 +1,24 @@ +// Tests for the behavior of the links collection +// +// https://guide.meteor.com/testing.html + +import { Meteor } from 'meteor/meteor'; +import { assert } from 'chai'; +import { Links } from './links.js'; + +if (Meteor.isServer) { + describe('links collection', function () { + it('insert correctly', async function () { + const linkId = await Links.insertAsync({ + title: 'meteor homepage', + url: 'https://www.meteor.com', + }); + const added = Links.find({ _id: linkId }); + const collectionName = added._getCollectionName(); + const count = await added.countAsync(); + + assert.equal(collectionName, 'links'); + assert.equal(count, 1); + }); + }); +} diff --git a/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.js new file mode 100644 index 0000000000..cb3280e9c1 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.js @@ -0,0 +1,18 @@ +// Methods related to links + +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { Links } from './links.js'; + +Meteor.methods({ + 'links.insert': async function(title, url) { + check(url, String); + check(title, String); + + return await Links.insertAsync({ + url, + title, + createdAt: new Date(), + }); + }, +}); diff --git a/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.tests.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.tests.js new file mode 100644 index 0000000000..69908d1582 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.tests.js @@ -0,0 +1,24 @@ +// Tests for links methods +// +// https://guide.meteor.com/testing.html + +import { Meteor } from 'meteor/meteor'; +import { assert } from 'chai'; +import { Links } from './links.js'; +import './methods.js'; + +if (Meteor.isServer) { + describe('links methods', function () { + beforeEach(async function () { + await Links.removeAsync({}); + }); + + it('can add a new link', async function () { + const addLink = Meteor.server.method_handlers['links.insert']; + + await addLink.apply({}, ['meteor.com', 'https://www.meteor.com']); + + assert.equal(await Links.find().countAsync(), 1); + }); + }); +} diff --git a/tools/static-assets/skel-full/imports/api/links/server/publications.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/server/publications.js similarity index 100% rename from tools/static-assets/skel-full/imports/api/links/server/publications.js rename to tools/e2e-tests/apps/full-blaze/imports/api/links/server/publications.js diff --git a/tools/e2e-tests/apps/full-blaze/imports/startup/both/index.js b/tools/e2e-tests/apps/full-blaze/imports/startup/both/index.js new file mode 100644 index 0000000000..7297b2916e --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/startup/both/index.js @@ -0,0 +1,2 @@ +// Import modules used by both client and server through a single index entry point +// e.g. useraccounts configuration file. diff --git a/tools/e2e-tests/apps/full-blaze/imports/startup/client/index.js b/tools/e2e-tests/apps/full-blaze/imports/startup/client/index.js new file mode 100644 index 0000000000..fd8ca6b7bd --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/startup/client/index.js @@ -0,0 +1,3 @@ +// Import client startup through a single index entry point + +import './routes.js'; diff --git a/tools/e2e-tests/apps/full-blaze/imports/startup/client/routes.js b/tools/e2e-tests/apps/full-blaze/imports/startup/client/routes.js new file mode 100644 index 0000000000..b87a5c5c7f --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/startup/client/routes.js @@ -0,0 +1,20 @@ +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +// Import needed templates +import '@ui/layouts/body/body.js'; +import '@ui/pages/home/home.js'; +import '@ui/pages/not-found/not-found.js'; + +// Set up all routes in the app +FlowRouter.route('/', { + name: 'App.home', + action() { + this.render('App_body', 'App_home'); + }, +}); + +FlowRouter.notFound = { + action() { + this.render('App_body', 'App_notFound'); + }, +}; diff --git a/tools/e2e-tests/apps/full-blaze/imports/startup/server/fixtures.js b/tools/e2e-tests/apps/full-blaze/imports/startup/server/fixtures.js new file mode 100644 index 0000000000..f3473aa4f8 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/startup/server/fixtures.js @@ -0,0 +1,33 @@ +// Fill the DB with example data on startup + +import { Meteor } from 'meteor/meteor'; +import { Links } from '../../api/links/links.js'; + +async function insertLink({ title, url }) { + await Links.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await Links.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://www.meteor.com/tutorials/react/creating-an-app', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } +}); diff --git a/tools/e2e-tests/apps/full-blaze/imports/startup/server/index.js b/tools/e2e-tests/apps/full-blaze/imports/startup/server/index.js new file mode 100644 index 0000000000..4b8e0a0865 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/startup/server/index.js @@ -0,0 +1,4 @@ +// Import server startup through a single index entry point + +import './fixtures.js'; +import './register-api.js'; diff --git a/tools/e2e-tests/apps/full-blaze/imports/startup/server/register-api.js b/tools/e2e-tests/apps/full-blaze/imports/startup/server/register-api.js new file mode 100644 index 0000000000..002fa1b8d8 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/startup/server/register-api.js @@ -0,0 +1,4 @@ +// Register your apis here + +import '@api/links/methods.js'; +import '@api/links/server/publications.js'; diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.html b/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.html new file mode 100644 index 0000000000..334719ec90 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.html @@ -0,0 +1,5 @@ + diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.js b/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.js new file mode 100644 index 0000000000..e4aff73a79 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.js @@ -0,0 +1,19 @@ +import './hello.html'; + +Template.hello.onCreated(function helloOnCreated() { + // counter starts at 0 + this.counter = new ReactiveVar(0); +}); + +Template.hello.helpers({ + counter() { + return Template.instance().counter.get(); + }, +}); + +Template.hello.events({ + 'click button'(event, instance) { + // increment the counter when button is clicked + instance.counter.set(instance.counter.get() + 1); + }, +}); diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.html b/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.html new file mode 100644 index 0000000000..3045bc1068 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.html @@ -0,0 +1,15 @@ + diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.js b/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.js new file mode 100644 index 0000000000..5db4caec09 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.js @@ -0,0 +1,32 @@ +import { Links } from '@api/links/links.js'; +import { Meteor } from 'meteor/meteor'; +import './info.html'; + +Template.info.onCreated(function () { + Meteor.subscribe('links.all'); +}); + +Template.info.helpers({ + links() { + return Links.find({}); + }, +}); + +Template.info.events({ + 'submit .info-link-add'(event) { + event.preventDefault(); + + const target = event.target; + const title = target.title; + const url = target.url; + + Meteor.callAsync('links.insert', title.value, url.value) + .then(() => { + title.value = ''; + url.value = ''; + }) + .catch((error) => { + alert(error.error || error.message); + }); + }, +}); diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.html b/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.html new file mode 100644 index 0000000000..40e3cd9a99 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.html @@ -0,0 +1,3 @@ + diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.js b/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.js new file mode 100644 index 0000000000..2da6810f1f --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.js @@ -0,0 +1 @@ +import './body.html'; diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.html b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.html new file mode 100644 index 0000000000..8b2e47142f --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.html @@ -0,0 +1,4 @@ + diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.js b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.js new file mode 100644 index 0000000000..40668735f9 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.js @@ -0,0 +1,4 @@ +import './home.html'; + +import '@ui/components/hello/hello.js'; +import '@ui/components/info/info.js'; diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html new file mode 100644 index 0000000000..9d685a96ec --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html @@ -0,0 +1,11 @@ + diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js new file mode 100644 index 0000000000..f252259132 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js @@ -0,0 +1 @@ +import './not-found.html'; diff --git a/tools/e2e-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less b/tools/e2e-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less new file mode 100644 index 0000000000..e84ffe0c93 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less @@ -0,0 +1,35 @@ +#not-found { + width: 700px; + margin: 0 auto; + .not-found-image { + width: 25%; + float: left; + } + .not-found-title { + width: 70%; + float: right; + background: url(/img/bg-footer.svg); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + min-height: 400px; + h1 { + font-size: 30px; + color: #1f2128; + margin-bottom: 20px; + margin-top: 155px; + } + a.gotohomepage { + background-color: #de4f4f; + color: #fff; + width: 180px; + line-height: 40px; + display: block; + text-align: center; + font-size: 14px; + text-decoration: none; + text-transform: uppercase; + height: 40px; + } + } +} diff --git a/tools/e2e-tests/apps/full-blaze/package.json b/tools/e2e-tests/apps/full-blaze/package.json new file mode 100644 index 0000000000..26624c6b6b --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/package.json @@ -0,0 +1,25 @@ +{ + "name": "meteor-app", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha" + }, + "dependencies": { + "@babel/runtime": "^7.27.6", + "@swc/helpers": "^0.5.17", + "jquery": "^3.7.1", + "meteor-node-stubs": "^1.2.1" + }, + "meteor": { + "modern": true, + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + } + }, + "devDependencies": { + "chai": "^4.2.0", + "playwright": "^1.54.2" + } +} diff --git a/tools/e2e-tests/apps/full-blaze/private/README.md b/tools/e2e-tests/apps/full-blaze/private/README.md new file mode 100644 index 0000000000..2071259f55 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/private/README.md @@ -0,0 +1,3 @@ +**private folder** + +All files inside a top-level directory called `private/` are only accessible from server code and can be loaded via the [`Assets`](http://docs.meteor.com/#/full/assets_getText) API. This can be used for private data files and any files that are in your project directory that you don't want to be accessible from the outside. diff --git a/tools/e2e-tests/apps/full-blaze/public/img/404.svg b/tools/e2e-tests/apps/full-blaze/public/img/404.svg new file mode 100644 index 0000000000..d2b9a01bc0 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/public/img/404.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + . + . + . + . + + + + + . + . + + + . + . + + + . + . + . + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/e2e-tests/apps/full-blaze/public/img/bg-footer.svg b/tools/e2e-tests/apps/full-blaze/public/img/bg-footer.svg new file mode 100644 index 0000000000..9169356de7 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/public/img/bg-footer.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/tools/e2e-tests/apps/full-blaze/rspack.config.js b/tools/e2e-tests/apps/full-blaze/rspack.config.js new file mode 100644 index 0000000000..1d1e902def --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/rspack.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from '@meteorjs/rspack'; + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +export default defineConfig(Meteor => { + return {}; +}); diff --git a/tools/e2e-tests/apps/full-blaze/server/main.js b/tools/e2e-tests/apps/full-blaze/server/main.js new file mode 100644 index 0000000000..68cc7679b6 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/server/main.js @@ -0,0 +1,4 @@ +// Server entry point, imports all server code + +import '/imports/startup/server'; +import '/imports/startup/both'; diff --git a/tools/e2e-tests/apps/full-blaze/swc.config.js b/tools/e2e-tests/apps/full-blaze/swc.config.js new file mode 100644 index 0000000000..d331278f7b --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/swc.config.js @@ -0,0 +1,10 @@ +export default { + jsc: { + baseUrl: ".", + paths: { + "@api/*": ["imports/api/*"], + "@startup/*": ["imports/startup/*"], + "@ui/*": ["imports/ui/*"] + } + } +}; diff --git a/tools/e2e-tests/apps/full-blaze/tests/main.js b/tools/e2e-tests/apps/full-blaze/tests/main.js new file mode 100644 index 0000000000..0154b1a2a9 --- /dev/null +++ b/tools/e2e-tests/apps/full-blaze/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("full-blaze", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "meteor-app"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/e2e-tests/apps/monorepo/.npmrc b/tools/e2e-tests/apps/monorepo/.npmrc new file mode 100644 index 0000000000..217a694955 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/.npmrc @@ -0,0 +1 @@ +install-strategy=nested diff --git a/tools/e2e-tests/apps/monorepo/app/.meteor/packages b/tools/e2e-tests/apps/monorepo/app/.meteor/packages new file mode 100644 index 0000000000..80b136d881 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/.meteor/packages @@ -0,0 +1,22 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + + +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data diff --git a/tools/e2e-tests/apps/monorepo/app/.meteor/platforms b/tools/e2e-tests/apps/monorepo/app/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/monorepo/app/.meteor/release b/tools/e2e-tests/apps/monorepo/app/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/monorepo/app/.meteorignore b/tools/e2e-tests/apps/monorepo/app/.meteorignore new file mode 100644 index 0000000000..b8463671b0 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/.meteorignore @@ -0,0 +1 @@ +**/ignore* diff --git a/tools/e2e-tests/apps/monorepo/app/.swcrc b/tools/e2e-tests/apps/monorepo/app/.swcrc new file mode 100644 index 0000000000..68b72e1154 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/.swcrc @@ -0,0 +1,8 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true + } + } +} diff --git a/tools/e2e-tests/apps/monorepo/app/client/client.test.js b/tools/e2e-tests/apps/monorepo/app/client/client.test.js new file mode 100644 index 0000000000..0ba68b2daa --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/client/client.test.js @@ -0,0 +1,9 @@ +import assert from "assert"; +import { Meteor } from "meteor/meteor"; + +describe("run client test", () => { + it("Runs a client test", async () => { + console.log("Client test ran"); + assert(Meteor.isClient); + }); +}); diff --git a/tools/e2e-tests/apps/monorepo/app/client/main.css b/tools/e2e-tests/apps/monorepo/app/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/monorepo/app/client/main.html b/tools/e2e-tests/apps/monorepo/app/client/main.html new file mode 100644 index 0000000000..0d1a6cada5 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/client/main.html @@ -0,0 +1,8 @@ + + monorepo + + + + +
+ diff --git a/tools/e2e-tests/apps/monorepo/app/client/main.jsx b/tools/e2e-tests/apps/monorepo/app/client/main.jsx new file mode 100644 index 0000000000..d2e380f93c --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/client/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Meteor } from 'meteor/meteor'; +import { App } from '/imports/ui/App'; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); + const root = createRoot(container); + root.render(); +}); diff --git a/tools/e2e-tests/apps/monorepo/app/ignored/ignore.test.js b/tools/e2e-tests/apps/monorepo/app/ignored/ignore.test.js new file mode 100644 index 0000000000..6e10e42476 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/ignored/ignore.test.js @@ -0,0 +1 @@ +throw new Error("This file should be ignored"); diff --git a/tools/e2e-tests/apps/monorepo/app/imports/api/links.js b/tools/e2e-tests/apps/monorepo/app/imports/api/links.js new file mode 100644 index 0000000000..a3e3364a2e --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/imports/api/links.js @@ -0,0 +1,8 @@ +import { Mongo } from 'meteor/mongo'; +import { Buffer } from "node:buffer"; + +// Don't trigger an error on using node modules +// as ignored to enable shared client/server code +console.log("Buffer loaded: ", !!Buffer); + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/monorepo/app/imports/emails/TestEmail.jsx b/tools/e2e-tests/apps/monorepo/app/imports/emails/TestEmail.jsx new file mode 100644 index 0000000000..4d2a6c491c --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/imports/emails/TestEmail.jsx @@ -0,0 +1,15 @@ +import { Button, Html } from '@react-email/components'; +import * as React from 'react'; + +export default function TestEmail() { + return ( + + + + ); +} diff --git a/tools/e2e-tests/apps/monorepo/app/imports/ui/App.jsx b/tools/e2e-tests/apps/monorepo/app/imports/ui/App.jsx new file mode 100644 index 0000000000..6f7340caf9 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/imports/ui/App.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Hello } from './Hello.jsx'; +import { Info } from './Info.jsx'; + +export const App = () => ( +
+

Welcome to Meteor!

+ + +
+); diff --git a/tools/e2e-tests/apps/monorepo/app/imports/ui/Hello.jsx b/tools/e2e-tests/apps/monorepo/app/imports/ui/Hello.jsx new file mode 100644 index 0000000000..15e0f185ac --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/imports/ui/Hello.jsx @@ -0,0 +1,16 @@ +import React, { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+ +

You've pressed the button {counter} times.

+
+ ); +}; diff --git a/tools/e2e-tests/apps/monorepo/app/imports/ui/Info.jsx b/tools/e2e-tests/apps/monorepo/app/imports/ui/Info.jsx new file mode 100644 index 0000000000..a9a7a45cfe --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/imports/ui/Info.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useFind, useSubscribe } from 'meteor/react-meteor-data'; +import { LinksCollection } from '../api/links'; + +export const Info = () => { + const isLoading = useSubscribe('links'); + const links = useFind(() => LinksCollection.find()); + + if(isLoading()) { + return
Loading...
; + } + + return ( +
+

Learn Meteor!

+ +
+ ); +}; diff --git a/tools/e2e-tests/apps/monorepo/app/package.json b/tools/e2e-tests/apps/monorepo/app/package.json new file mode 100644 index 0000000000..e1d0a1ed71 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/package.json @@ -0,0 +1,33 @@ +{ + "name": "app", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@react-email/components": "0.5.3", + "@swc/helpers": "^0.5.17", + "grubba-rpc": "^0.12.8", + "meteor-node-stubs": "^1.2.12", + "pino": "^9.6.0", + "pino-pretty": "^13.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "thread-stream": "^3.1.0", + "zod": "^3.24.4" + }, + "devDependencies": { + "playwright": "1.58.0" + }, + "meteor": { + "mainModule": { + "client": "client/main.jsx", + "server": "server/main.js" + }, + "modern": true + } +} diff --git a/tools/e2e-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js b/tools/e2e-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js new file mode 100644 index 0000000000..0e8abd4037 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js @@ -0,0 +1,14 @@ +// CustomConsoleLogPlugin.js +class CustomConsoleLogPlugin { + apply(compiler) { + compiler.hooks.beforeRun.tap('CustomConsoleLogPlugin', (compilation) => { + console.log('👉 [CustomConsoleLogPlugin] Build is starting...'); + }); + + compiler.hooks.done.tap('CustomConsoleLogPlugin', (stats) => { + console.log('✅ [CustomConsoleLogPlugin] Build finished successfully!'); + }); + } +} + +module.exports = CustomConsoleLogPlugin; diff --git a/tools/e2e-tests/apps/monorepo/app/public/1x1.png b/tools/e2e-tests/apps/monorepo/app/public/1x1.png new file mode 100644 index 0000000000..1914264c08 Binary files /dev/null and b/tools/e2e-tests/apps/monorepo/app/public/1x1.png differ diff --git a/tools/e2e-tests/apps/monorepo/app/public/docs/text.md b/tools/e2e-tests/apps/monorepo/app/public/docs/text.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/e2e-tests/apps/monorepo/app/public/images/1x1.png b/tools/e2e-tests/apps/monorepo/app/public/images/1x1.png new file mode 100644 index 0000000000..1914264c08 Binary files /dev/null and b/tools/e2e-tests/apps/monorepo/app/public/images/1x1.png differ diff --git a/tools/e2e-tests/apps/monorepo/app/rspack.config.cjs b/tools/e2e-tests/apps/monorepo/app/rspack.config.cjs new file mode 100644 index 0000000000..b2e81958db --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/rspack.config.cjs @@ -0,0 +1,18 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.compileWithMeteor(["thread-stream"]), + ...Meteor.compileWithRspack(["grubba-rpc"]), + }; +}); diff --git a/tools/e2e-tests/apps/monorepo/app/rspack.config.override.cjs b/tools/e2e-tests/apps/monorepo/app/rspack.config.override.cjs new file mode 100644 index 0000000000..a4f160b843 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/rspack.config.override.cjs @@ -0,0 +1,10 @@ +const { defineConfig } = require("@meteorjs/rspack"); +const CustomConsoleLogPlugin = require('./plugins/CustomConsoleLogPlugin') + +module.exports = defineConfig((Meteor) => { + return { + plugins: [ + new CustomConsoleLogPlugin() + ], + }; +}); diff --git a/tools/e2e-tests/apps/monorepo/app/server/main.js b/tools/e2e-tests/apps/monorepo/app/server/main.js new file mode 100644 index 0000000000..00d7d58f57 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/server/main.js @@ -0,0 +1,64 @@ +import { Meteor } from 'meteor/meteor'; +import pino from 'pino'; +import { createClient } from 'grubba-rpc'; + +import { LinksCollection } from '/imports/api/links'; +import { TestEmail } from '/imports/emails/TestEmail'; + +console.log('-> TestEmail loaded', !!TestEmail); + +const logger = pino({ + transport: { + target: "pino-pretty", + options: { + colorize: true, + levelFirst: true, + translateTime: "UTC:yyyy-mm-dd HH:MM:ss.l o", + ignore: "pid,hostname" + } + }, + browser: { asObject: true } +}); + +// Issue with thread-stream "Cannot find module '/_build/main-dev/lib/worker.js'" +// Ensure `Meteor.compileWithMeteor(["thread-stream"])` works +console.log("logger loaded", !!logger); + +// Issue with npm deps that require compilation as not transpiled +// Ensure `Meteor.compileWithRspack(["grubba-rpc"])` works +console.log("grubba-rpc's createClient", !!createClient); + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/e2e-tests/apps/monorepo/app/server/server.test.js b/tools/e2e-tests/apps/monorepo/app/server/server.test.js new file mode 100644 index 0000000000..8e35e97367 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/server/server.test.js @@ -0,0 +1,10 @@ +import assert from "assert"; +import { Meteor } from "meteor/meteor"; + +describe("run server test", () => { + it("Runs a server test", async () => { + console.log("Server test ran"); + assert(Meteor.isServer); + assert(Meteor.isTest); + }); +}); diff --git a/tools/e2e-tests/apps/monorepo/app/tests/ignore-test.test.js b/tools/e2e-tests/apps/monorepo/app/tests/ignore-test.test.js new file mode 100644 index 0000000000..6e10e42476 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/tests/ignore-test.test.js @@ -0,0 +1 @@ +throw new Error("This file should be ignored"); diff --git a/tools/e2e-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js b/tools/e2e-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js new file mode 100644 index 0000000000..6e10e42476 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js @@ -0,0 +1 @@ +throw new Error("This file should be ignored"); diff --git a/tools/e2e-tests/apps/monorepo/app/tests/main.test.js b/tools/e2e-tests/apps/monorepo/app/tests/main.test.js new file mode 100644 index 0000000000..685c69de3f --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/app/tests/main.test.js @@ -0,0 +1,25 @@ +import assert from "assert"; + +describe("monorepo", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "app"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } + + it("is test", function () { + assert.strictEqual(Meteor.isTest, true); + assert.strictEqual(Meteor.isAppTest, false); + }); +}); diff --git a/tools/e2e-tests/apps/monorepo/package.json b/tools/e2e-tests/apps/monorepo/package.json new file mode 100644 index 0000000000..f2422900c5 --- /dev/null +++ b/tools/e2e-tests/apps/monorepo/package.json @@ -0,0 +1,10 @@ +{ + "name": "monorepo", + "private": true, + "workspaces": [ + "app" + ], + "engines": { + "node": ">=22.0.0" + } +} diff --git a/tools/e2e-tests/apps/react-router/.gitignore b/tools/e2e-tests/apps/react-router/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/react-router/.meteor/.gitignore b/tools/e2e-tests/apps/react-router/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/react-router/.meteor/.id b/tools/e2e-tests/apps/react-router/.meteor/.id new file mode 100644 index 0000000000..248d80b9ca --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +lv2ipazth09.ysbw5dwq9qvl diff --git a/tools/e2e-tests/apps/react-router/.meteor/packages b/tools/e2e-tests/apps/react-router/.meteor/packages new file mode 100644 index 0000000000..b2e3a1c15e --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteor/packages @@ -0,0 +1,24 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + + +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data +default-package +custom-package diff --git a/tools/e2e-tests/apps/react-router/.meteor/platforms b/tools/e2e-tests/apps/react-router/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/react-router/.meteor/release b/tools/e2e-tests/apps/react-router/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/react-router/.meteor/versions b/tools/e2e-tests/apps/react-router/.meteor/versions new file mode 100644 index 0000000000..5644ad4d7f --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1-rc331.2 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1-rc331.2 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12-rc331.2 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3-rc331.2 +minimongo@2.0.3-rc331.2 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3-rc331.2 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3-rc331.2 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0-rc331.2 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +react-meteor-data@4.0.0 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1-rc331.2 +static-html@1.4.0 +static-html-tools@1.0.0 +tracker@1.3.4 +typescript@5.6.5-rc331.2 +webapp@2.0.7 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/tools/e2e-tests/apps/react-router/.meteorignore b/tools/e2e-tests/apps/react-router/.meteorignore new file mode 100644 index 0000000000..269a4e9cc4 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteorignore @@ -0,0 +1,16 @@ +# Folder combination patterns +react-router* +tests/ignored/react-router*-ignored +tests/ignored/folder-to-ignored/ +tests/ignored/file-to-ignored.app-test.js +**/glob-ignored/*.app-test.js +tests/ignored/prefix-*-ignored +tests/ignored/*-ignored-suffix + +# File combination patterns +tests/ignored/specific-file-ignored.app-test.js +tests/ignored/*.temp-ignored.app-test.js +**/unit/*.app-test.js +tests/ignored/integration/*-ignored.app-test.js +tests/ignored/test-*-ignored.app-test.js +!important.app-test.js diff --git a/tools/e2e-tests/apps/react-router/babel.config.js b/tools/e2e-tests/apps/react-router/babel.config.js new file mode 100644 index 0000000000..69119e468d --- /dev/null +++ b/tools/e2e-tests/apps/react-router/babel.config.js @@ -0,0 +1,17 @@ +const ReactCompilerConfig = { + target: '18', +}; + +module.exports = function (api) { + // required when exporting a function + api.cache(true); // cache forever; or api.cache.using(() => process.env.NODE_ENV) + + console.log('babel.config.js: babel-plugin-react-compiler'); + + return { + plugins: [ + ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! + '@babel/plugin-syntax-jsx', + ], + }; +}; diff --git a/tools/e2e-tests/apps/react-router/client/client.app-test.js b/tools/e2e-tests/apps/react-router/client/client.app-test.js new file mode 100644 index 0000000000..e856117b4d --- /dev/null +++ b/tools/e2e-tests/apps/react-router/client/client.app-test.js @@ -0,0 +1,13 @@ +import assert from "assert"; +import { Meteor } from "meteor/meteor"; + +describe("Run integration tests", () => { + it("Runs integration tests", async () => { + console.log("Integration tests ran"); + assert(Meteor.isAppTest); + const result = await Meteor.callAsync("test.method"); + assert(Meteor.isClient, "IS CLIENT"); + + assert.deepEqual(result, { isAppTestInitial: true, isAppTestNow: true }); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/client/main.css b/tools/e2e-tests/apps/react-router/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/react-router/client/main.html b/tools/e2e-tests/apps/react-router/client/main.html new file mode 100644 index 0000000000..b3d1adaf6b --- /dev/null +++ b/tools/e2e-tests/apps/react-router/client/main.html @@ -0,0 +1,8 @@ + + react-router + + + + +
+ diff --git a/tools/e2e-tests/apps/react-router/client/main.jsx b/tools/e2e-tests/apps/react-router/client/main.jsx new file mode 100644 index 0000000000..d216179947 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/client/main.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Meteor } from 'meteor/meteor'; +import { App } from '/imports/ui/App'; +import '@helper/alias'; +import ReactAlias from '@react/alias'; + +console.log('@react/alias loaded', ReactAlias.version); + +let root; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); // your container id + if (!root) { + root = createRoot(container); // create once + } + root.render(); +}); diff --git a/tools/e2e-tests/apps/react-router/important.app-test.js b/tools/e2e-tests/apps/react-router/important.app-test.js new file mode 100644 index 0000000000..a9cb841908 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/important.app-test.js @@ -0,0 +1,7 @@ +import assert from "assert"; + +describe("!important.app-test.js pattern", () => { + it("should run as it's not ignored (negation)", () => { + assert(true, "should run"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/imports/api/links.js b/tools/e2e-tests/apps/react-router/imports/api/links.js new file mode 100644 index 0000000000..050c508eae --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/api/links.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/react-router/imports/helpers/alias.js b/tools/e2e-tests/apps/react-router/imports/helpers/alias.js new file mode 100644 index 0000000000..a2274fa1d4 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/helpers/alias.js @@ -0,0 +1 @@ +console.log('@helper/alias loaded'); diff --git a/tools/e2e-tests/apps/react-router/imports/ui/App.jsx b/tools/e2e-tests/apps/react-router/imports/ui/App.jsx new file mode 100644 index 0000000000..d5cb7c95e6 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/ui/App.jsx @@ -0,0 +1,42 @@ +import React, { Suspense, lazy } from "react"; +import { + createBrowserRouter, + RouterProvider, + Route, + createRoutesFromElements, +} from "react-router-dom"; +import "./Global.less"; + +// Dynamically import components +const Home = lazy(() => + import(/* webpackPrefetch: true */ "./Home.jsx").then((module) => ({ default: module.Home })) +); +const NotFound = lazy(() => + import("./NotFound.jsx").then((module) => ({ default: module.NotFound })) +); + +// Create router with routes +const router = createBrowserRouter( + createRoutesFromElements( + <> + Loading...}> + + + } + /> + Loading...}> + + + } + /> + + ) +); + +export const App = () => ; diff --git a/tools/e2e-tests/apps/react-router/imports/ui/Global.less b/tools/e2e-tests/apps/react-router/imports/ui/Global.less new file mode 100644 index 0000000000..2c0c9d93e3 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/ui/Global.less @@ -0,0 +1,3 @@ +body { + white-space: break-spaces; +} diff --git a/tools/e2e-tests/apps/react-router/imports/ui/Hello.jsx b/tools/e2e-tests/apps/react-router/imports/ui/Hello.jsx new file mode 100644 index 0000000000..15e0f185ac --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/ui/Hello.jsx @@ -0,0 +1,16 @@ +import React, { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+ +

You've pressed the button {counter} times.

+
+ ); +}; diff --git a/tools/e2e-tests/apps/react-router/imports/ui/Home.jsx b/tools/e2e-tests/apps/react-router/imports/ui/Home.jsx new file mode 100644 index 0000000000..f92bfef612 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/ui/Home.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Hello } from './Hello.jsx'; +import { Info } from './Info.jsx'; + +export const Home = () => ( +
+

Welcome to Meteor!

+ + +
+); diff --git a/tools/e2e-tests/apps/react-router/imports/ui/Info.jsx b/tools/e2e-tests/apps/react-router/imports/ui/Info.jsx new file mode 100644 index 0000000000..a9a7a45cfe --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/ui/Info.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useFind, useSubscribe } from 'meteor/react-meteor-data'; +import { LinksCollection } from '../api/links'; + +export const Info = () => { + const isLoading = useSubscribe('links'); + const links = useFind(() => LinksCollection.find()); + + if(isLoading()) { + return
Loading...
; + } + + return ( +
+

Learn Meteor!

+ +
+ ); +}; diff --git a/tools/e2e-tests/apps/react-router/imports/ui/NotFound.jsx b/tools/e2e-tests/apps/react-router/imports/ui/NotFound.jsx new file mode 100644 index 0000000000..9a98780879 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/imports/ui/NotFound.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +export const NotFound = () => ( +
+

404 - Page Not Found

+

The page you are looking for does not exist.

+ Go back to home +
+); diff --git a/tools/e2e-tests/apps/react-router/my-packages/custom-package/custom-package.js b/tools/e2e-tests/apps/react-router/my-packages/custom-package/custom-package.js new file mode 100644 index 0000000000..da16b7d6ad --- /dev/null +++ b/tools/e2e-tests/apps/react-router/my-packages/custom-package/custom-package.js @@ -0,0 +1 @@ +console.log('custom-package loaded'); diff --git a/tools/e2e-tests/apps/react-router/my-packages/custom-package/package.js b/tools/e2e-tests/apps/react-router/my-packages/custom-package/package.js new file mode 100644 index 0000000000..5b9cc7197f --- /dev/null +++ b/tools/e2e-tests/apps/react-router/my-packages/custom-package/package.js @@ -0,0 +1,11 @@ +Package.describe({ + name: 'custom-package', + summary: 'Custom package for react-router', + version: '1.0.0', +}); + +Package.onUse(function (api) { + api.use('ecmascript', ['client', 'server']); + + api.mainModule('custom-package.js', ['client', 'server']); +}); diff --git a/tools/e2e-tests/apps/react-router/package.json b/tools/e2e-tests/apps/react-router/package.json new file mode 100644 index 0000000000..ca94b83b1a --- /dev/null +++ b/tools/e2e-tests/apps/react-router/package.json @@ -0,0 +1,42 @@ +{ + "name": "react", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@modelcontextprotocol/sdk": "^1.17.3", + "@swc/helpers": "^0.5.17", + "bcrypt": "^6.0.0", + "meteor-node-stubs": "^1.2.12", + "react": "^18.3.1", + "react-compiler-runtime": "^19.1.0-rc.2", + "react-dom": "^18.3.1", + "react-router-dom": "^7.0.0", + "s3mini": "^0.4.0" + }, + "devDependencies": { + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/preset-env": "^7.23.5", + "@babel/preset-react": "^7.23.3", + "babel-plugin-react-compiler": "^19.1.0-rc.2", + "babel-loader": "^9.1.3", + "less": "^4.4.0", + "less-loader": "^12.3.0", + "playwright": "1.58.0", + "puppeteer": "^24.31.0", + "typescript": "^5.9.2" + }, + "meteor": { + "mainModule": { + "client": "client/main.jsx", + "server": "server/main.js" + }, + "modules": ["styles/module.css"], + "modern": true + } +} diff --git a/tools/e2e-tests/apps/react-router/packages/default-package/default-package.js b/tools/e2e-tests/apps/react-router/packages/default-package/default-package.js new file mode 100644 index 0000000000..7df3b40483 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/packages/default-package/default-package.js @@ -0,0 +1 @@ +console.log('default-package loaded'); diff --git a/tools/e2e-tests/apps/react-router/packages/default-package/package.js b/tools/e2e-tests/apps/react-router/packages/default-package/package.js new file mode 100644 index 0000000000..fe8726ff2b --- /dev/null +++ b/tools/e2e-tests/apps/react-router/packages/default-package/package.js @@ -0,0 +1,11 @@ +Package.describe({ + name: 'default-package', + summary: 'Default package for react-router', + version: '1.0.0', +}); + +Package.onUse(function (api) { + api.use('ecmascript', ['client', 'server']); + + api.mainModule('default-package.js', ['client', 'server']); +}); diff --git a/tools/e2e-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js b/tools/e2e-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js new file mode 100644 index 0000000000..0e8abd4037 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js @@ -0,0 +1,14 @@ +// CustomConsoleLogPlugin.js +class CustomConsoleLogPlugin { + apply(compiler) { + compiler.hooks.beforeRun.tap('CustomConsoleLogPlugin', (compilation) => { + console.log('👉 [CustomConsoleLogPlugin] Build is starting...'); + }); + + compiler.hooks.done.tap('CustomConsoleLogPlugin', (stats) => { + console.log('✅ [CustomConsoleLogPlugin] Build finished successfully!'); + }); + } +} + +module.exports = CustomConsoleLogPlugin; diff --git a/tools/e2e-tests/apps/react-router/public/1x1.png b/tools/e2e-tests/apps/react-router/public/1x1.png new file mode 100644 index 0000000000..1914264c08 Binary files /dev/null and b/tools/e2e-tests/apps/react-router/public/1x1.png differ diff --git a/tools/e2e-tests/apps/react-router/public/docs/text.md b/tools/e2e-tests/apps/react-router/public/docs/text.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/e2e-tests/apps/react-router/public/images/1x1.png b/tools/e2e-tests/apps/react-router/public/images/1x1.png new file mode 100644 index 0000000000..1914264c08 Binary files /dev/null and b/tools/e2e-tests/apps/react-router/public/images/1x1.png differ diff --git a/tools/e2e-tests/apps/react-router/react-router-wxyz-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/react-router-wxyz-ignored/ignore.app-test.js new file mode 100644 index 0000000000..bf67b299e7 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/react-router-wxyz-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("react-router*-ignored pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/rspack.config.js b/tools/e2e-tests/apps/react-router/rspack.config.js new file mode 100644 index 0000000000..45f50b6250 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/rspack.config.js @@ -0,0 +1,71 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + resolve: { + alias: { + '@helper/alias': '/imports/helpers/alias.js', + '@react/alias': '/node_modules/react', + }, + extensions: ['.jsx'], + }, + module: { + rules: [ + { + test: /\.jsx$/, + use: [ + { + loader: 'builtin:swc-loader', + options: { jsc: { parser: { syntax: 'ecmascript', jsx: true } } }, + }, + { loader: 'babel-loader' }, + ], + type: 'javascript/auto', + }, + { + test: /\.less$/, + use: [ + { + loader: 'less-loader', + }, + ], + type: 'css/auto', + }, + ], + }, + ...(Meteor.isClient && { + optimization: { + splitChunks: { + chunks: 'all', + cacheGroups: { + reactRouter: { + test: /[\\/]node_modules[\\/](react-router|react-router-dom)[\\/]/, + name: 'react-router', + priority: 40, + enforce: true, + }, + vendor: { test: /node_modules/, name: 'vendors' }, + }, + }, + }, + }), + plugins: [ + Meteor.HtmlRspackPlugin({ + title: 'react-router', + meta: { + 'theme-color': '#4285f4', + }, + }), + ], + }; +}); diff --git a/tools/e2e-tests/apps/react-router/rspack.config.override.js b/tools/e2e-tests/apps/react-router/rspack.config.override.js new file mode 100644 index 0000000000..a4f160b843 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/rspack.config.override.js @@ -0,0 +1,10 @@ +const { defineConfig } = require("@meteorjs/rspack"); +const CustomConsoleLogPlugin = require('./plugins/CustomConsoleLogPlugin') + +module.exports = defineConfig((Meteor) => { + return { + plugins: [ + new CustomConsoleLogPlugin() + ], + }; +}); diff --git a/tools/e2e-tests/apps/react-router/server/browser-tests/browser.app-test.js b/tools/e2e-tests/apps/react-router/server/browser-tests/browser.app-test.js new file mode 100644 index 0000000000..169893b3cc --- /dev/null +++ b/tools/e2e-tests/apps/react-router/server/browser-tests/browser.app-test.js @@ -0,0 +1,51 @@ +import puppeteer from "puppeteer"; + +const width = 200; +const height = 150; + +describe("Run browser tests", () => { + it("Runs browser based integration tests", async () => { + console.log("STARTING BROWSER TEST"); + + const props = { + headless: true, + timeout: 15_000, + defaultViewport: { width, height }, + args: [ + `--window-size=${width},${height}`, + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + "--single-process", + "--no-zygote", + "--no-first-run", + "--no-default-browser-check", + "--disable-background-networking", + "--disable-background-timer-throttling", + "--disable-breakpad", + "--disable-component-update", + "--disable-sync", + "--metrics-recording-only", + "--mute-audio", + "--enable-unsafe-swiftshader", + ], + }; + + if (process.env.CHROME_COMMAND_LOCATION) { + props.executablePath = process.env.CHROME_COMMAND_LOCATION; + } + + const browser = await puppeteer.launch(props); + console.log("LAUNCHED BROWSER"); + + const page = await browser.newPage(); + console.log("NEW PAGE"); + + await page.goto(process.env.ROOT_URL); + console.log("OPEN PAGE", process.env.ROOT_URL); + + await page.waitForSelector("#react-target"); + await browser.close(); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/server/main.js b/tools/e2e-tests/apps/react-router/server/main.js new file mode 100644 index 0000000000..dea9ec8d94 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/server/main.js @@ -0,0 +1,60 @@ +import { Meteor } from 'meteor/meteor'; +import { S3mini } from "s3mini"; +import { LinksCollection } from '/imports/api/links'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import '@helper/alias'; +import ReactAlias from '@react/alias'; +import './resolve-extensions/first'; +import { TypescriptEnabled } from './ts/helpers'; +import bcrypt from "bcrypt"; + +console.log('@react/alias loaded', ReactAlias.version); +console.log('TypescriptEnabled', TypescriptEnabled); +console.log("bcrypt loaded", !!bcrypt); + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); + +console.log("--> S3mini imported: ", !!S3mini); +console.log("--> StreamableHTTPClientTransport imported: ", !!StreamableHTTPClientTransport); + +if (Meteor.isAppTest) { + let isAppTest = Meteor.isAppTest; + Meteor.methods({ + "test.method": () => { + return { isAppTestInitial: isAppTest, isAppTestNow: Meteor.isAppTest }; + } + }); +} diff --git a/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.jsx b/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.jsx new file mode 100644 index 0000000000..f4a34cd479 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.jsx @@ -0,0 +1 @@ +console.log('first.jsx loaded'); diff --git a/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.tsx b/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.tsx new file mode 100644 index 0000000000..fafcfe4cf4 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.tsx @@ -0,0 +1 @@ +console.log('first.tsx loaded'); diff --git a/tools/e2e-tests/apps/react-router/server/server.app-test.js b/tools/e2e-tests/apps/react-router/server/server.app-test.js new file mode 100644 index 0000000000..02748ab59d --- /dev/null +++ b/tools/e2e-tests/apps/react-router/server/server.app-test.js @@ -0,0 +1,13 @@ +import assert from "assert"; +import { Meteor } from "meteor/meteor"; + +describe("Run integration tests", () => { + it("Runs integration tests", async () => { + console.log("Integration tests ran"); + assert(Meteor.isAppTest); + const result = await Meteor.callAsync("test.method"); + assert(Meteor.isServer, "IS SERVER"); + + assert.deepEqual(result, { isAppTestInitial: true, isAppTestNow: true }); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/server/ts/helpers.ts b/tools/e2e-tests/apps/react-router/server/ts/helpers.ts new file mode 100644 index 0000000000..67a82d6f6c --- /dev/null +++ b/tools/e2e-tests/apps/react-router/server/ts/helpers.ts @@ -0,0 +1 @@ +export const TypescriptEnabled: boolean = true; diff --git a/tools/e2e-tests/apps/react-router/styles/module.css b/tools/e2e-tests/apps/react-router/styles/module.css new file mode 100644 index 0000000000..2b1f0dafba --- /dev/null +++ b/tools/e2e-tests/apps/react-router/styles/module.css @@ -0,0 +1,3 @@ +body { + align-content: center; +} diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/file-to-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/file-to-ignored.app-test.js new file mode 100644 index 0000000000..c67d43d5d9 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/file-to-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/file-to-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/folder-to-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/folder-to-ignored/ignore.app-test.js new file mode 100644 index 0000000000..5878c51755 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/folder-to-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/folder-to-ignored/ pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/integration/test-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/integration/test-ignored.app-test.js new file mode 100644 index 0000000000..6c85dda4f6 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/integration/test-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/integration/*-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/prefix-test-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/prefix-test-ignored/ignore.app-test.js new file mode 100644 index 0000000000..a8a1c30c5a --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/prefix-test-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/prefix-*-ignored pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/react-router-wxyz-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/react-router-wxyz-ignored/ignore.app-test.js new file mode 100644 index 0000000000..217570f7b1 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/react-router-wxyz-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/react-router*-ignored pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/some-test-ignored-suffix/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/some-test-ignored-suffix/ignore.app-test.js new file mode 100644 index 0000000000..ad7167d7c5 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/some-test-ignored-suffix/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/*-ignored-suffix pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/some/nested/glob-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/some/nested/glob-ignored/ignore.app-test.js new file mode 100644 index 0000000000..403124d0b5 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/some/nested/glob-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/some/nested/glob-ignored/ignore.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/some/unit/test-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/some/unit/test-ignored.app-test.js new file mode 100644 index 0000000000..3d8cab2346 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/some/unit/test-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/some/unit/test-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/specific-file-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/specific-file-ignored.app-test.js new file mode 100644 index 0000000000..4eda02edf5 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/specific-file-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/specific-file-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/test-example-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/test-example-ignored.app-test.js new file mode 100644 index 0000000000..9fdbbda403 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/test-example-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/test-*-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/test.temp-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/test.temp-ignored.app-test.js new file mode 100644 index 0000000000..72d4d3d0e1 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/test.temp-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/*.temp-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/main.app-test.js b/tools/e2e-tests/apps/react-router/tests/main.app-test.js new file mode 100644 index 0000000000..92679b43e3 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/main.app-test.js @@ -0,0 +1,57 @@ +import assert from "assert"; + +// Define todos publication, method, and collection to test full-app mode +export const Todos = new Mongo.Collection('todos'); + +if (Meteor.isServer) { + Meteor.publish('todos', function () { + return Todos.find(); + }); +} + +Meteor.methods({ + 'todos.add'(text) { + return Todos.insert({ text, createdAt: new Date() }); + }, +}); + +describe("react-router", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "react"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + + it('registers the todos.add method in full-app mode', function () { + // Methods only exist if app startup code runs + assert.strictEqual( + typeof Meteor.server.method_handlers['todos.add'], + 'function', + 'Expected todos.add method to be registered' + ); + }); + + it('registers the todos publication in full-app mode', function () { + assert.strictEqual( + typeof Meteor.server.publish_handlers['todos'], + 'function', + 'Expected todos publication to be registered' + ); + }); + } + + it("is app test", function () { + assert.strictEqual(Meteor.isAppTest, true); + assert.strictEqual(Meteor.isTest, false); + }); +}); diff --git a/tools/e2e-tests/apps/react/.gitignore b/tools/e2e-tests/apps/react/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/react/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/react/.meteor/.gitignore b/tools/e2e-tests/apps/react/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/react/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/react/.meteor/.id b/tools/e2e-tests/apps/react/.meteor/.id new file mode 100644 index 0000000000..248d80b9ca --- /dev/null +++ b/tools/e2e-tests/apps/react/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +lv2ipazth09.ysbw5dwq9qvl diff --git a/tools/e2e-tests/apps/react/.meteor/packages b/tools/e2e-tests/apps/react/.meteor/packages new file mode 100644 index 0000000000..80b136d881 --- /dev/null +++ b/tools/e2e-tests/apps/react/.meteor/packages @@ -0,0 +1,22 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + + +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data diff --git a/tools/e2e-tests/apps/react/.meteor/platforms b/tools/e2e-tests/apps/react/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/react/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/react/.meteor/release b/tools/e2e-tests/apps/react/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/react/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/react/.meteor/versions b/tools/e2e-tests/apps/react/.meteor/versions new file mode 100644 index 0000000000..5644ad4d7f --- /dev/null +++ b/tools/e2e-tests/apps/react/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1-rc331.2 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1-rc331.2 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12-rc331.2 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3-rc331.2 +minimongo@2.0.3-rc331.2 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3-rc331.2 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3-rc331.2 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0-rc331.2 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +react-meteor-data@4.0.0 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1-rc331.2 +static-html@1.4.0 +static-html-tools@1.0.0 +tracker@1.3.4 +typescript@5.6.5-rc331.2 +webapp@2.0.7 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/tools/e2e-tests/apps/react/client/main.html b/tools/e2e-tests/apps/react/client/main.html new file mode 100644 index 0000000000..e08221b6e1 --- /dev/null +++ b/tools/e2e-tests/apps/react/client/main.html @@ -0,0 +1,8 @@ + + react + + + + +
+ diff --git a/tools/e2e-tests/apps/react/client/main.jsx b/tools/e2e-tests/apps/react/client/main.jsx new file mode 100644 index 0000000000..d2e380f93c --- /dev/null +++ b/tools/e2e-tests/apps/react/client/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Meteor } from 'meteor/meteor'; +import { App } from '/imports/ui/App'; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); + const root = createRoot(container); + root.render(); +}); diff --git a/tools/e2e-tests/apps/react/imports/api/links.js b/tools/e2e-tests/apps/react/imports/api/links.js new file mode 100644 index 0000000000..050c508eae --- /dev/null +++ b/tools/e2e-tests/apps/react/imports/api/links.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/react/imports/ui/App.jsx b/tools/e2e-tests/apps/react/imports/ui/App.jsx new file mode 100644 index 0000000000..4c36afc5b9 --- /dev/null +++ b/tools/e2e-tests/apps/react/imports/ui/App.jsx @@ -0,0 +1,17 @@ +import imagePublic from '@public/1x1-public.jpg'; +import React from 'react'; + +import './main.css'; +import { Hello } from './Hello.jsx'; +import { Info } from './Info.jsx'; +import imageGenerated from './images/1x1-js.png'; + +export const App = () => ( +
+

Welcome to Meteor!

+ + + 1x1 pixel imported in JS + 1x1 pixel imported in JS +
+); diff --git a/tools/e2e-tests/apps/react/imports/ui/Hello.jsx b/tools/e2e-tests/apps/react/imports/ui/Hello.jsx new file mode 100644 index 0000000000..15e0f185ac --- /dev/null +++ b/tools/e2e-tests/apps/react/imports/ui/Hello.jsx @@ -0,0 +1,16 @@ +import React, { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+ +

You've pressed the button {counter} times.

+
+ ); +}; diff --git a/tools/e2e-tests/apps/react/imports/ui/Info.jsx b/tools/e2e-tests/apps/react/imports/ui/Info.jsx new file mode 100644 index 0000000000..a9a7a45cfe --- /dev/null +++ b/tools/e2e-tests/apps/react/imports/ui/Info.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useFind, useSubscribe } from 'meteor/react-meteor-data'; +import { LinksCollection } from '../api/links'; + +export const Info = () => { + const isLoading = useSubscribe('links'); + const links = useFind(() => LinksCollection.find()); + + if(isLoading()) { + return
Loading...
; + } + + return ( +
+

Learn Meteor!

+ +
+ ); +}; diff --git a/tools/e2e-tests/apps/react/imports/ui/images/1x1-js.png b/tools/e2e-tests/apps/react/imports/ui/images/1x1-js.png new file mode 100644 index 0000000000..1914264c08 Binary files /dev/null and b/tools/e2e-tests/apps/react/imports/ui/images/1x1-js.png differ diff --git a/tools/e2e-tests/apps/react/imports/ui/main.css b/tools/e2e-tests/apps/react/imports/ui/main.css new file mode 100644 index 0000000000..84e8f18582 --- /dev/null +++ b/tools/e2e-tests/apps/react/imports/ui/main.css @@ -0,0 +1,9 @@ +body { + padding: 10px; + font-family: sans-serif; + background-image: url('/1x1-css.png'); +} + +h1 { + background-image: url('@public/1x1-public.jpg'); +} diff --git a/tools/e2e-tests/apps/react/package.json b/tools/e2e-tests/apps/react/package.json new file mode 100644 index 0000000000..748641428a --- /dev/null +++ b/tools/e2e-tests/apps/react/package.json @@ -0,0 +1,29 @@ +{ + "name": "react", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "unplugin": "^2.3.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "playwright": "1.58.0" + }, + "meteor": { + "mainModule": { + "client": "client/main.jsx", + "server": "server/main.js" + }, + "testModule": "tests/main.js", + "modern": true + } +} diff --git a/tools/e2e-tests/apps/react/plugins/CustomConsoleLogPlugin.js b/tools/e2e-tests/apps/react/plugins/CustomConsoleLogPlugin.js new file mode 100644 index 0000000000..0e8abd4037 --- /dev/null +++ b/tools/e2e-tests/apps/react/plugins/CustomConsoleLogPlugin.js @@ -0,0 +1,14 @@ +// CustomConsoleLogPlugin.js +class CustomConsoleLogPlugin { + apply(compiler) { + compiler.hooks.beforeRun.tap('CustomConsoleLogPlugin', (compilation) => { + console.log('👉 [CustomConsoleLogPlugin] Build is starting...'); + }); + + compiler.hooks.done.tap('CustomConsoleLogPlugin', (stats) => { + console.log('✅ [CustomConsoleLogPlugin] Build finished successfully!'); + }); + } +} + +module.exports = CustomConsoleLogPlugin; diff --git a/tools/e2e-tests/apps/react/plugins/demo-unplugin.js b/tools/e2e-tests/apps/react/plugins/demo-unplugin.js new file mode 100644 index 0000000000..e570040b16 --- /dev/null +++ b/tools/e2e-tests/apps/react/plugins/demo-unplugin.js @@ -0,0 +1,30 @@ +const { createUnplugin } = require('unplugin'); + +const demoUnplugin = createUnplugin(() => { + console.log('[demo-unplugin][factory-created]'); + return { + name: 'demo-unplugin', + transformInclude(id) { + // Only process app source files, skip node_modules and .meteor + if (id.includes('node_modules') || id.includes('.meteor')) { + return false; + } + const ok = + id.endsWith('.tsx') || + id.endsWith('.ts') || + id.endsWith('.jsx') || + id.endsWith('.js'); + + if (ok) { + console.log('[demo-unplugin][transformInclude]', id, '=> true'); + } + return ok; + }, + transform(code, id) { + console.log('[demo-unplugin][transform-enter]', id); + return { code, map: null }; + }, + }; +}); + +module.exports = { demoRspackPlugin: demoUnplugin.rspack }; diff --git a/tools/e2e-tests/apps/react/public/1x1-css.png b/tools/e2e-tests/apps/react/public/1x1-css.png new file mode 100644 index 0000000000..1914264c08 Binary files /dev/null and b/tools/e2e-tests/apps/react/public/1x1-css.png differ diff --git a/tools/e2e-tests/apps/react/public/1x1-public.jpg b/tools/e2e-tests/apps/react/public/1x1-public.jpg new file mode 100644 index 0000000000..87fcedf154 Binary files /dev/null and b/tools/e2e-tests/apps/react/public/1x1-public.jpg differ diff --git a/tools/e2e-tests/apps/react/rspack.config.cjs b/tools/e2e-tests/apps/react/rspack.config.cjs new file mode 100644 index 0000000000..91a774dcdb --- /dev/null +++ b/tools/e2e-tests/apps/react/rspack.config.cjs @@ -0,0 +1,45 @@ +const { defineConfig } = require('@meteorjs/rspack'); +const path = require('path'); +const CustomConsoleLogPlugin = require("./plugins/CustomConsoleLogPlugin"); +const { demoRspackPlugin } = require("./plugins/demo-unplugin"); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + const disabledPluginMatches = Meteor.isRun + ? ['CustomConsoleLogPlugin'] + : Meteor.isTest + ? /CustomConsoleLogPlugin/i + : p => p?.constructor?.name === 'CustomConsoleLogPlugin'; + return { + ...Meteor.disablePlugins(disabledPluginMatches), + resolve: { + alias: { + "@public": path.resolve(__dirname, "public"), + }, + }, + module: { + rules: [ + // ✅ Images + { + test: /\.(png|jpe?g|gif|webp|avif)$/i, + type: "asset/resource", // emits a file, returns URL string + }, + // (optional) SVGs + { + test: /\.svg$/i, + type: "asset/resource", + }, + ], + }, + plugins: [new CustomConsoleLogPlugin(), demoRspackPlugin()], + }; +}); diff --git a/tools/e2e-tests/apps/react/server/main.js b/tools/e2e-tests/apps/react/server/main.js new file mode 100644 index 0000000000..49452ad352 --- /dev/null +++ b/tools/e2e-tests/apps/react/server/main.js @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { LinksCollection } from '/imports/api/links'; + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/e2e-tests/apps/react/tests/main.js b/tools/e2e-tests/apps/react/tests/main.js new file mode 100644 index 0000000000..23214c3f8f --- /dev/null +++ b/tools/e2e-tests/apps/react/tests/main.js @@ -0,0 +1,25 @@ +import assert from "assert"; + +describe("react", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "react"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } + + it("is test", function () { + assert.strictEqual(Meteor.isTest, true); + assert.strictEqual(Meteor.isAppTest, false); + }); +}); diff --git a/tools/e2e-tests/apps/server-only/.meteor/.gitignore b/tools/e2e-tests/apps/server-only/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/server-only/.meteor/.id b/tools/e2e-tests/apps/server-only/.meteor/.id new file mode 100644 index 0000000000..33386e121f --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +l2d6s2mpmf9d.hefjva6hrzxq diff --git a/tools/e2e-tests/apps/server-only/.meteor/packages b/tools/e2e-tests/apps/server-only/.meteor/packages new file mode 100644 index 0000000000..0743c87631 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/packages @@ -0,0 +1,20 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base@1.5.2 # Packages every Meteor app needs to have +mobile-experience@1.1.2 # Packages for a great mobile UX +mongo@2.2.0 # The database Meteor supports right now +static-html@1.5.0 # Define static page content in .html files +reactive-var@1.0.13 # Reactive variable for tracker +tracker@1.3.4 # Meteor's client-side reactive programming library + +standard-minifier-css@1.10.0 # CSS minifier run for production mode +standard-minifier-js@3.2.0 # JS minifier run for production mode +es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers +ecmascript@0.17.0 # Enable ECMAScript2015+ syntax in app code +typescript@5.9.3 # Enable TypeScript syntax in .ts and .tsx modules +shell-server@0.7.0 # Server-side component of the `meteor shell` command +rspack diff --git a/tools/e2e-tests/apps/server-only/.meteor/platforms b/tools/e2e-tests/apps/server-only/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/server-only/.meteor/release b/tools/e2e-tests/apps/server-only/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/server-only/.meteor/versions b/tools/e2e-tests/apps/server-only/.meteor/versions new file mode 100644 index 0000000000..7e24fc06e4 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/versions @@ -0,0 +1,66 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.13.0 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.1.0 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.5.0 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.17.0 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.2.0 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.1.0 +minimongo@2.0.5 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +mongo@2.2.0 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.1 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.3.0 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +rspack@1.0.0 +shell-server@0.7.0 +socket-stream-client@0.6.1 +standard-minifier-css@1.10.0 +standard-minifier-js@3.2.0 +static-html@1.5.0 +static-html-tools@1.0.0 +tools-core@1.0.0 +tracker@1.3.4 +typescript@5.9.3 +webapp@2.1.0 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/server-only/package.json b/tools/e2e-tests/apps/server-only/package.json new file mode 100644 index 0000000000..2b9704d1f7 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/package.json @@ -0,0 +1,17 @@ +{ + "name": "server-only", + "private": true, + "scripts": { + "start": "meteor run" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "meteor-node-stubs": "^1.2.12" + }, + "meteor": { + "mainModule": { + "server": "server/main.js" + }, + "modern": true + } +} diff --git a/tools/e2e-tests/apps/server-only/rspack.config.js b/tools/e2e-tests/apps/server-only/rspack.config.js new file mode 100644 index 0000000000..a6434b4e45 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/rspack.config.js @@ -0,0 +1,5 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(() => { + return {}; +}); diff --git a/tools/e2e-tests/apps/server-only/server/main.js b/tools/e2e-tests/apps/server-only/server/main.js new file mode 100644 index 0000000000..f24ca7f09f --- /dev/null +++ b/tools/e2e-tests/apps/server-only/server/main.js @@ -0,0 +1 @@ +console.log('server/main.js loaded'); \ No newline at end of file diff --git a/tools/e2e-tests/apps/solid/.gitignore b/tools/e2e-tests/apps/solid/.gitignore new file mode 100644 index 0000000000..0d45e6c875 --- /dev/null +++ b/tools/e2e-tests/apps/solid/.gitignore @@ -0,0 +1,7 @@ +node_modules/ + +# Meteor-Rspack build context directories +_build +public/build-chunks +public/build-assets +private/build-assets diff --git a/tools/e2e-tests/apps/solid/.meteor/.gitignore b/tools/e2e-tests/apps/solid/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/solid/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/solid/.meteor/.id b/tools/e2e-tests/apps/solid/.meteor/.id new file mode 100644 index 0000000000..1dff39fea2 --- /dev/null +++ b/tools/e2e-tests/apps/solid/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +l9l5u78nay5.axumzntivb1 diff --git a/tools/e2e-tests/apps/solid/.meteor/packages b/tools/e2e-tests/apps/solid/.meteor/packages new file mode 100644 index 0000000000..c311636dc7 --- /dev/null +++ b/tools/e2e-tests/apps/solid/.meteor/packages @@ -0,0 +1,22 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + + +static-html # Define static page content in .html files +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/e2e-tests/apps/solid/.meteor/platforms b/tools/e2e-tests/apps/solid/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/solid/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/solid/.meteor/release b/tools/e2e-tests/apps/solid/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/solid/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/solid/.meteor/versions b/tools/e2e-tests/apps/solid/.meteor/versions new file mode 100644 index 0000000000..f4b6296c13 --- /dev/null +++ b/tools/e2e-tests/apps/solid/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3 +minimongo@2.0.3 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +rspack@1.0.0 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +static-html@1.4.0 +static-html-tools@1.0.0 +tools-core@1.0.0 +tracker@1.3.4 +typescript@5.6.5 +webapp@2.0.7 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/solid/client/main.html b/tools/e2e-tests/apps/solid/client/main.html new file mode 100644 index 0000000000..e999571103 --- /dev/null +++ b/tools/e2e-tests/apps/solid/client/main.html @@ -0,0 +1,10 @@ + + solid + + + + + +
+ + diff --git a/tools/e2e-tests/apps/solid/client/main.js b/tools/e2e-tests/apps/solid/client/main.js new file mode 100644 index 0000000000..44bcc64d55 --- /dev/null +++ b/tools/e2e-tests/apps/solid/client/main.js @@ -0,0 +1 @@ +import '../imports/ui/main'; \ No newline at end of file diff --git a/tools/e2e-tests/apps/solid/imports/api/links.js b/tools/e2e-tests/apps/solid/imports/api/links.js new file mode 100644 index 0000000000..ffe1b58fbe --- /dev/null +++ b/tools/e2e-tests/apps/solid/imports/api/links.js @@ -0,0 +1,4 @@ +import { Mongo } from 'meteor/mongo'; + +export const LinksCollection = new Mongo.Collection('links'); + diff --git a/tools/e2e-tests/apps/solid/imports/ui/App.jsx b/tools/e2e-tests/apps/solid/imports/ui/App.jsx new file mode 100644 index 0000000000..53c80e4498 --- /dev/null +++ b/tools/e2e-tests/apps/solid/imports/ui/App.jsx @@ -0,0 +1,12 @@ +import { Hello } from "./Hello"; +import { Info } from "./Info"; + +export const App = () => ( +
+

Welcome to Meteor!

+ + +
+); + + diff --git a/tools/e2e-tests/apps/solid/imports/ui/Hello.jsx b/tools/e2e-tests/apps/solid/imports/ui/Hello.jsx new file mode 100644 index 0000000000..c23767f44d --- /dev/null +++ b/tools/e2e-tests/apps/solid/imports/ui/Hello.jsx @@ -0,0 +1,16 @@ +import { createSignal } from "solid-js"; + +export const Hello = () => { + const [counter, setCounter] = createSignal(0); + + const increment = () => { + setCounter(counter() + 1); + }; + + return ( +
+ +

You've pressed the button {counter()} times.

+
+ ); +} diff --git a/tools/e2e-tests/apps/solid/imports/ui/Info.jsx b/tools/e2e-tests/apps/solid/imports/ui/Info.jsx new file mode 100644 index 0000000000..ef228ac43c --- /dev/null +++ b/tools/e2e-tests/apps/solid/imports/ui/Info.jsx @@ -0,0 +1,37 @@ +import { LinksCollection } from "../api/links"; +import { createSignal, For, Show } from "solid-js"; +import { Tracker } from "meteor/tracker"; +import { Meteor } from "meteor/meteor"; + +export const Info = () => { + const subscription = Meteor.subscribe("links"); + const [isReady, setIsReady] = createSignal(subscription.ready()); + const [links, setLinks] = createSignal([]); + + Tracker.autorun(async () => { + setIsReady(subscription.ready()); + setLinks(await LinksCollection.find().fetchAsync()); + }); + + return ( + Loading...} + > +
+

Learn Meteor!

+ +
+
+ ); +}; diff --git a/tools/e2e-tests/apps/solid/imports/ui/main.css b/tools/e2e-tests/apps/solid/imports/ui/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/solid/imports/ui/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/solid/imports/ui/main.jsx b/tools/e2e-tests/apps/solid/imports/ui/main.jsx new file mode 100644 index 0000000000..044f1aee69 --- /dev/null +++ b/tools/e2e-tests/apps/solid/imports/ui/main.jsx @@ -0,0 +1,9 @@ +/* @refresh reload */ +import { render } from 'solid-js/web'; +import { App } from './App'; +import { Meteor } from "meteor/meteor"; +import './main.css'; + +Meteor.startup(() => { + render(() => , document.getElementById('root')); +}) diff --git a/tools/e2e-tests/apps/solid/package.json b/tools/e2e-tests/apps/solid/package.json new file mode 100644 index 0000000000..5ce93de638 --- /dev/null +++ b/tools/e2e-tests/apps/solid/package.json @@ -0,0 +1,34 @@ +{ + "name": "solid", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "picocolors": "^1.1.1" + }, + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + }, + "testModule": "tests/main.js", + "modern": true + }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rspack/cli": "^1.4.8", + "@rspack/core": "^1.4.8", + "babel-loader": "10.0.0", + "babel-preset-solid": "^1.8.15", + "playwright": "1.58.0", + "solid-js": "^1.9.4", + "solid-refresh": "0.7.5" + } +} diff --git a/tools/e2e-tests/apps/solid/rspack.config.js b/tools/e2e-tests/apps/solid/rspack.config.js new file mode 100644 index 0000000000..2a399ac99c --- /dev/null +++ b/tools/e2e-tests/apps/solid/rspack.config.js @@ -0,0 +1,38 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.isClient && { + module: { + rules: [ + { + test: /\.jsx$/, + use: [ + { + loader: 'babel-loader', + options: { + presets: [['solid']], + plugins: ['solid-refresh/babel'], + }, + }, + ], + }, + { + test: /\.svg$/, + type: 'asset/resource', + }, + ], + }, + } + }; +}); diff --git a/tools/e2e-tests/apps/solid/server/main.js b/tools/e2e-tests/apps/solid/server/main.js new file mode 100644 index 0000000000..3eb4465cdb --- /dev/null +++ b/tools/e2e-tests/apps/solid/server/main.js @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { LinksCollection } from '/imports/api/links'; + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://www.solidjs.com/tutorial/introduction_basics', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish('links', function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/e2e-tests/apps/solid/tests/main.js b/tools/e2e-tests/apps/solid/tests/main.js new file mode 100644 index 0000000000..45b2f2b5b9 --- /dev/null +++ b/tools/e2e-tests/apps/solid/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("solid", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "solid"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/e2e-tests/apps/svelte/.gitignore b/tools/e2e-tests/apps/svelte/.gitignore new file mode 100644 index 0000000000..0d45e6c875 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.gitignore @@ -0,0 +1,7 @@ +node_modules/ + +# Meteor-Rspack build context directories +_build +public/build-chunks +public/build-assets +private/build-assets diff --git a/tools/e2e-tests/apps/svelte/.meteor/.gitignore b/tools/e2e-tests/apps/svelte/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/svelte/.meteor/.id b/tools/e2e-tests/apps/svelte/.meteor/.id new file mode 100644 index 0000000000..46b9400e59 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +v15vu450mxwm.h0z81cg7agugm diff --git a/tools/e2e-tests/apps/svelte/.meteor/packages b/tools/e2e-tests/apps/svelte/.meteor/packages new file mode 100644 index 0000000000..a8fc0b318e --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.meteor/packages @@ -0,0 +1,23 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command + + +static-html # Define static page content in .html files +# zodern:melte # Meteor package to allow us to create files with the .svelte extension +hot-module-replacement # Update client in development without reloading the page +zodern:types # Enable types from meteor/atmosphere packages +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/e2e-tests/apps/svelte/.meteor/platforms b/tools/e2e-tests/apps/svelte/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/svelte/.meteor/release b/tools/e2e-tests/apps/svelte/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/svelte/.meteor/versions b/tools/e2e-tests/apps/svelte/.meteor/versions new file mode 100644 index 0000000000..65dd067371 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3 +minimongo@2.0.3 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +rspack@1.0.0 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +static-html@1.4.0 +static-html-tools@1.0.0 +tools-core@1.0.0 +tracker@1.3.4 +typescript@5.6.5 +webapp@2.0.7 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/tools/e2e-tests/apps/svelte/client/main.css b/tools/e2e-tests/apps/svelte/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/svelte/client/main.html b/tools/e2e-tests/apps/svelte/client/main.html new file mode 100644 index 0000000000..64bc00425d --- /dev/null +++ b/tools/e2e-tests/apps/svelte/client/main.html @@ -0,0 +1,8 @@ + + svelte + + + + +
+ diff --git a/tools/e2e-tests/apps/svelte/client/main.js b/tools/e2e-tests/apps/svelte/client/main.js new file mode 100644 index 0000000000..0ee13b10d2 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/client/main.js @@ -0,0 +1,27 @@ +// client/main.js +import { Meteor } from 'meteor/meteor'; +import App from '../imports/ui/App.svelte'; +import { mount, unmount } from 'svelte'; + +let app; // will hold the mounted instance + +Meteor.startup(() => { + const target = document.getElementById('app'); + + // (Re)mount + app = mount(App, { target }); + + // Clean up on HMR so we don't double-mount + if (import.meta.webpackHot) { + import.meta.webpackHot.accept(); + import.meta.webpackHot.dispose(() => { + if (app) { + // pass the instance you got from mount() + unmount(app, { outro: false }); // set outro:true if you want transitions + app = null; + } + // optional: clear target to be extra safe + target.innerHTML = ''; + }); + } +}); diff --git a/tools/e2e-tests/apps/svelte/imports/api/links.js b/tools/e2e-tests/apps/svelte/imports/api/links.js new file mode 100644 index 0000000000..050c508eae --- /dev/null +++ b/tools/e2e-tests/apps/svelte/imports/api/links.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/svelte/imports/ui/App.svelte b/tools/e2e-tests/apps/svelte/imports/ui/App.svelte new file mode 100644 index 0000000000..f011b8b2c9 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/imports/ui/App.svelte @@ -0,0 +1,53 @@ + + +
+

Welcome to Meteor!

+ +

You've pressed the button {counter} times.

+ +

Learn Meteor!

+ {#if subIsReady} +
    + {#each links as link (link._id)} +
  • {link.title}
  • + {/each} +
+ {:else} +
Loading ...
+ {/if} +
diff --git a/tools/e2e-tests/apps/svelte/package.json b/tools/e2e-tests/apps/svelte/package.json new file mode 100644 index 0000000000..cba0d4e7a0 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/package.json @@ -0,0 +1,41 @@ +{ + "name": "svelte", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12" + }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rspack/cli": "^1.4.8", + "@rspack/core": "^1.4.8", + "playwright": "1.58.0", + "postcss-load-config": "^5.1.0", + "svelte": "^5.38.2", + "svelte-check": "^4.3.1", + "svelte-loader": "^3.2.4", + "svelte-preprocess": "^6.0.3" + }, + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + }, + "nodeModules": { + "recompile": { + "svelte": [ + "legacy" + ] + } + }, + "testModule": "tests/main.js", + "modern": true + } +} diff --git a/tools/e2e-tests/apps/svelte/rspack.config.js b/tools/e2e-tests/apps/svelte/rspack.config.js new file mode 100644 index 0000000000..3161c3db9c --- /dev/null +++ b/tools/e2e-tests/apps/svelte/rspack.config.js @@ -0,0 +1,41 @@ +const { defineConfig } = require('@meteorjs/rspack'); +const path = require('path'); +const sveltePreprocess = require('svelte-preprocess'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.isClient && { + resolve: { + extensions: [".mjs", ".js", ".ts", ".svelte", ".json"], + mainFields: ["svelte", "browser", "module", "main"], + conditionNames: ["svelte", "browser", "import", "module", "default"], + }, + module: { + rules: [ + { + test: /\.svelte$/, + use: [{ + loader: 'svelte-loader', + options: { + compilerOptions: { dev: !Meteor.isProduction }, + emitCss: Meteor.isProduction, + hotReload: !Meteor.isProduction, + preprocess: sveltePreprocess({ sourceMap: !Meteor.isProduction, postcss: true }), + }, + }], + }, + ], + }, + }, + }; +}); diff --git a/tools/e2e-tests/apps/svelte/server/main.js b/tools/e2e-tests/apps/svelte/server/main.js new file mode 100644 index 0000000000..886520b487 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/server/main.js @@ -0,0 +1,35 @@ +import { Meteor } from 'meteor/meteor'; +import { LinksCollection } from '/imports/api/links'; + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.publish('links.all', function publishLinksAll() { + return LinksCollection.find(); +}) + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://svelte-tutorial.meteor.com/', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } +}); diff --git a/tools/e2e-tests/apps/svelte/tests/main.js b/tools/e2e-tests/apps/svelte/tests/main.js new file mode 100644 index 0000000000..bf652ef61d --- /dev/null +++ b/tools/e2e-tests/apps/svelte/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("svelte", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "svelte"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/e2e-tests/apps/svelte/tsconfig.json b/tools/e2e-tests/apps/svelte/tsconfig.json new file mode 100644 index 0000000000..7f0c17ad00 --- /dev/null +++ b/tools/e2e-tests/apps/svelte/tsconfig.json @@ -0,0 +1,20 @@ +{ + // see https://guide.meteor.com/build-tool.html#typescript for a config example + "compilerOptions": { + "allowSyntheticDefaultImports": true, // to be able to import eg meteor/mongo + "baseUrl": ".", // required by "paths" + "module": "esNext", // required by "preserveValueImports" + "moduleResolution": "node", // required by zodern:types (not documented) + "paths": { + "/*": ["*"], // support absolute /imports/* with a leading '/' + // support Meteor/Atmospehere packages, required by zodern:types + "meteor/*": [ + "node_modules/@types/meteor/*", + ".meteor/local/types/packages.d.ts" + ] + }, + "preserveSymlinks": true, // required by zodern:types + "preserveValueImports": true // otherwise TS will remove imported components + }, + "exclude": ["./.meteor/**", "!./.meteor/local/types", "./packages/**"] // this may solve VS Code Svelte plugin warnings +} diff --git a/tools/e2e-tests/apps/typescript/.gitignore b/tools/e2e-tests/apps/typescript/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/e2e-tests/apps/typescript/.meteor/.gitignore b/tools/e2e-tests/apps/typescript/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/typescript/.meteor/.id b/tools/e2e-tests/apps/typescript/.meteor/.id new file mode 100644 index 0000000000..e9e2dc15a7 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +k0367lnvnclor.z6cmuyuro69u diff --git a/tools/e2e-tests/apps/typescript/.meteor/packages b/tools/e2e-tests/apps/typescript/.meteor/packages new file mode 100644 index 0000000000..da38d886c4 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.meteor/packages @@ -0,0 +1,23 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +fourseven:scss@5.0.0-rc.1 # Enable SCSS syntax in .scss using Meteor +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data +zodern:types # Pull in type declarations from other Meteor packages diff --git a/tools/e2e-tests/apps/typescript/.meteor/platforms b/tools/e2e-tests/apps/typescript/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/typescript/.meteor/release b/tools/e2e-tests/apps/typescript/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/typescript/.meteor/versions b/tools/e2e-tests/apps/typescript/.meteor/versions new file mode 100644 index 0000000000..ee3f71c234 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3 +minimongo@2.0.3 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +react-meteor-data@4.0.0 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +static-html@1.4.0 +static-html-tools@1.0.0 +tracker@1.3.4 +typescript@5.6.5 +webapp@2.0.7 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/tools/e2e-tests/apps/typescript/client/main.html b/tools/e2e-tests/apps/typescript/client/main.html new file mode 100644 index 0000000000..7526f5d325 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/client/main.html @@ -0,0 +1,8 @@ + + typescript + + + + +
+ diff --git a/tools/e2e-tests/apps/typescript/client/main.scss b/tools/e2e-tests/apps/typescript/client/main.scss new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/client/main.scss @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/typescript/client/main.tsx b/tools/e2e-tests/apps/typescript/client/main.tsx new file mode 100644 index 0000000000..4eb40f49d1 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/client/main.tsx @@ -0,0 +1,9 @@ +import { createRoot } from 'react-dom/client'; +import { Meteor } from 'meteor/meteor'; +import { App } from '@ui/App'; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); + const root = createRoot(container!); + root.render(); +}); diff --git a/tools/e2e-tests/apps/typescript/imports/api/links.ts b/tools/e2e-tests/apps/typescript/imports/api/links.ts new file mode 100644 index 0000000000..ec0bf4631f --- /dev/null +++ b/tools/e2e-tests/apps/typescript/imports/api/links.ts @@ -0,0 +1,10 @@ +import { Mongo } from 'meteor/mongo'; + +export interface Link { + _id?: string; + title: string; + url: string; + createdAt: Date; +} + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/e2e-tests/apps/typescript/imports/ui/App.tsx b/tools/e2e-tests/apps/typescript/imports/ui/App.tsx new file mode 100644 index 0000000000..94679c5547 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/imports/ui/App.tsx @@ -0,0 +1,11 @@ +import './Global.scss'; +import { Hello } from './Hello'; +import { Info } from './Info'; + +export const App = () => ( +
+

Welcome to Meteor!

+ + +
+); diff --git a/tools/e2e-tests/apps/typescript/imports/ui/Global.scss b/tools/e2e-tests/apps/typescript/imports/ui/Global.scss new file mode 100644 index 0000000000..2c0c9d93e3 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/imports/ui/Global.scss @@ -0,0 +1,3 @@ +body { + white-space: break-spaces; +} diff --git a/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx b/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx new file mode 100644 index 0000000000..527d5af607 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+ +

You've pressed the button {counter} times.

+
+ ); +}; diff --git a/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx b/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx new file mode 100644 index 0000000000..809fbc6716 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx @@ -0,0 +1,26 @@ +import { useFind, useSubscribe } from "meteor/react-meteor-data"; +import { LinksCollection, Link } from "../api/links"; + +export const Info = () => { + const isLoading = useSubscribe("links"); + const links = useFind(() => LinksCollection.find()); + + if (isLoading()) { + return
Loading...
; + } + + const makeLink = (link: Link) => { + return ( +
  • + { link.title } +
  • + ); + } + + return ( +
    +

    Learn Meteor!

    +
      { links.map(makeLink) }
    +
    + ); +}; diff --git a/tools/e2e-tests/apps/typescript/package.json b/tools/e2e-tests/apps/typescript/package.json new file mode 100644 index 0000000000..f0359b6094 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/package.json @@ -0,0 +1,46 @@ +{ + "name": "typescript", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@swc/core": "^1.15.18", + "@types/meteor": "^2.9.9", + "@types/mocha": "^8.2.3", + "@types/node": "^22.10.6", + "@types/react": "^18.2.5", + "@types/react-dom": "^18.2.4", + "assert": "^2.1.0", + "playwright": "1.58.0", + "sass": "^1.92.1", + "sass-embedded": "^1.92.1", + "sass-loader": "^16.0.5", + "ts-checker-rspack-plugin": "1.1.6", + "typescript": "^5.4.5" + }, + "meteor": { + "mainModule": { + "client": "client/main.tsx", + "server": "server/main.ts" + }, + "testModule": { + "client": "tests/client.ts", + "server": "tests/server.ts" + }, + "modern": true, + "buildContext": "build", + "assetsContext": "assets", + "chunksContext": "chunks" + } +} diff --git a/tools/e2e-tests/apps/typescript/rspack.config.ts b/tools/e2e-tests/apps/typescript/rspack.config.ts new file mode 100644 index 0000000000..f4be271f64 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/rspack.config.ts @@ -0,0 +1,48 @@ +import { defineConfig } from '@meteorjs/rspack'; +import { createRequire } from 'node:module'; +import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin'; + +const require = createRequire(import.meta.url); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +export default defineConfig(Meteor => { + return { + ...Meteor.enablePortableBuild(), + ...Meteor.extendSwcConfig({ + jsc: { + baseUrl: process.cwd(), + paths: { + '@ui/*': ['imports/ui/*'], + '@api/*': ['imports/api/*'], + }, + }, + }), + module: { + rules: [ + { + test: /\.scss$/i, + use: [ + { + loader: 'sass-loader', + options: { + api: 'modern-compiler', + implementation: require.resolve('sass-embedded'), + }, + }, + ], + type: 'css/auto', + }, + ], + }, + plugins: [new TsCheckerRspackPlugin()], + }; +}); diff --git a/tools/e2e-tests/apps/typescript/server/main.ts b/tools/e2e-tests/apps/typescript/server/main.ts new file mode 100644 index 0000000000..3655f77b23 --- /dev/null +++ b/tools/e2e-tests/apps/typescript/server/main.ts @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { Link, LinksCollection } from '@api/links'; + +async function insertLink({ title, url }: Pick) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/e2e-tests/apps/typescript/swc.config.ts b/tools/e2e-tests/apps/typescript/swc.config.ts new file mode 100644 index 0000000000..699a33a40d --- /dev/null +++ b/tools/e2e-tests/apps/typescript/swc.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "@swc/core"; + +const config: Config = { + jsc: { + transform: { + react: { + runtime: "automatic", + }, + }, + }, +}; + +export default config; diff --git a/tools/e2e-tests/apps/typescript/tests/client.ts b/tools/e2e-tests/apps/typescript/tests/client.ts new file mode 100644 index 0000000000..cbb880302a --- /dev/null +++ b/tools/e2e-tests/apps/typescript/tests/client.ts @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; +import assert from 'assert'; + +describe('typescript', function () { + it('client is not server', function () { + assert.strictEqual(Meteor.isServer, false); + }); +}); diff --git a/tools/e2e-tests/apps/typescript/tests/server.ts b/tools/e2e-tests/apps/typescript/tests/server.ts new file mode 100644 index 0000000000..4e9e93ba3b --- /dev/null +++ b/tools/e2e-tests/apps/typescript/tests/server.ts @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; +import assert from 'assert'; + +describe('typescript', function () { + it('package.json has correct name', async function () { + const { name } = await import('../package.json'); + assert.strictEqual(name, 'typescript'); + }); + + it('server is not client', function () { + assert.strictEqual(Meteor.isClient, false); + }); +}); diff --git a/tools/e2e-tests/apps/typescript/tsconfig.json b/tools/e2e-tests/apps/typescript/tsconfig.json new file mode 100644 index 0000000000..5924c795ef --- /dev/null +++ b/tools/e2e-tests/apps/typescript/tsconfig.json @@ -0,0 +1,54 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es2018", + "module": "esNext", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "jsx": "preserve", + "incremental": true, + "composite": true, + "tsBuildInfoFile": ".tsbuildinfo", + "noEmit": true, + + /* Strict Type-Checking Options */ + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "baseUrl": ".", + "paths": { + /* Support absolute /imports/* with a leading '/' */ + "/*": ["*"], + /* Test aliases */ + "@ui/*": ["imports/ui/*"], + "@api/*": ["imports/api/*"], + /* Pull in type declarations for Meteor packages from either zodern:types or @types/meteor packages */ + "meteor/*": [ + "node_modules/@types/meteor/*", + ".meteor/local/types/packages.d.ts" + ] + }, + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node", "mocha"], + "esModuleInterop": true, + "preserveSymlinks": true + }, + "exclude": [ + "./.meteor/**", + "./packages/**", + "./build/**", + "./public/chunks/**", + "./public/assets/**", + "./private/assets/**" + ] +} diff --git a/tools/e2e-tests/apps/vue/.gitignore b/tools/e2e-tests/apps/vue/.gitignore new file mode 100644 index 0000000000..0d45e6c875 --- /dev/null +++ b/tools/e2e-tests/apps/vue/.gitignore @@ -0,0 +1,7 @@ +node_modules/ + +# Meteor-Rspack build context directories +_build +public/build-chunks +public/build-assets +private/build-assets diff --git a/tools/e2e-tests/apps/vue/.meteor/.gitignore b/tools/e2e-tests/apps/vue/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/e2e-tests/apps/vue/.meteor/.id b/tools/e2e-tests/apps/vue/.meteor/.id new file mode 100644 index 0000000000..6b1243a5d2 --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +wqcp16e9g1lm.z6pria236oki diff --git a/tools/e2e-tests/apps/vue/.meteor/packages b/tools/e2e-tests/apps/vue/.meteor/packages new file mode 100644 index 0000000000..62fa592f9d --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/packages @@ -0,0 +1,21 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +static-html # Define static page content in .html files +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/e2e-tests/apps/vue/.meteor/platforms b/tools/e2e-tests/apps/vue/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/vue/.meteor/release b/tools/e2e-tests/apps/vue/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/e2e-tests/apps/vue/.meteor/versions b/tools/e2e-tests/apps/vue/.meteor/versions new file mode 100644 index 0000000000..f4b6296c13 --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/versions @@ -0,0 +1,68 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.1 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.12 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +hot-module-replacement@0.5.4 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.3 +minimongo@2.0.3 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +modules-runtime-hot@0.14.3 +mongo@2.1.3 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.0 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +rspack@1.0.0 +shell-server@0.6.1 +socket-stream-client@0.6.1 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +static-html@1.4.0 +static-html-tools@1.0.0 +tools-core@1.0.0 +tracker@1.3.4 +typescript@5.6.5 +webapp@2.0.7 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/vue/.meteorignore b/tools/e2e-tests/apps/vue/.meteorignore new file mode 100644 index 0000000000..21d63e9485 --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteorignore @@ -0,0 +1,2 @@ +# Ignore client/main.css file so that is processed by Rspack import +client/main.css diff --git a/tools/e2e-tests/apps/vue/client/main.css b/tools/e2e-tests/apps/vue/client/main.css new file mode 100644 index 0000000000..d61a8dd67c --- /dev/null +++ b/tools/e2e-tests/apps/vue/client/main.css @@ -0,0 +1,3 @@ +body { + padding: 10px; +} diff --git a/tools/e2e-tests/apps/vue/client/main.html b/tools/e2e-tests/apps/vue/client/main.html new file mode 100644 index 0000000000..973ce29281 --- /dev/null +++ b/tools/e2e-tests/apps/vue/client/main.html @@ -0,0 +1,9 @@ + + vue + + + + + +
    + diff --git a/tools/e2e-tests/apps/vue/client/main.js b/tools/e2e-tests/apps/vue/client/main.js new file mode 100644 index 0000000000..64512a8d55 --- /dev/null +++ b/tools/e2e-tests/apps/vue/client/main.js @@ -0,0 +1,2 @@ +import './main.css'; +import '../imports/ui/main'; diff --git a/tools/e2e-tests/apps/vue/client/meteor.css b/tools/e2e-tests/apps/vue/client/meteor.css new file mode 100644 index 0000000000..853ac29a7c --- /dev/null +++ b/tools/e2e-tests/apps/vue/client/meteor.css @@ -0,0 +1,3 @@ +body { + font-family: sans-serif; +} diff --git a/tools/e2e-tests/apps/vue/imports/api/links.js b/tools/e2e-tests/apps/vue/imports/api/links.js new file mode 100644 index 0000000000..4e98fcca62 --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/api/links.js @@ -0,0 +1,10 @@ +import { Meteor } from 'meteor/meteor' +import { Mongo } from 'meteor/mongo' + +export const LinksCollection = new Mongo.Collection('links') + +if (Meteor.isServer) { + Meteor.publish('links', function () { + return LinksCollection.find({}) + }) +} diff --git a/tools/e2e-tests/apps/vue/imports/ui/App.vue b/tools/e2e-tests/apps/vue/imports/ui/App.vue new file mode 100644 index 0000000000..19a68a1ea1 --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/App.vue @@ -0,0 +1,10 @@ + + + diff --git a/tools/e2e-tests/apps/vue/imports/ui/components/AppMenu.vue b/tools/e2e-tests/apps/vue/imports/ui/components/AppMenu.vue new file mode 100644 index 0000000000..5b1997efec --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/components/AppMenu.vue @@ -0,0 +1,6 @@ + diff --git a/tools/e2e-tests/apps/vue/imports/ui/components/Hello.vue b/tools/e2e-tests/apps/vue/imports/ui/components/Hello.vue new file mode 100644 index 0000000000..ebe691f4d2 --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/components/Hello.vue @@ -0,0 +1,16 @@ + + + diff --git a/tools/e2e-tests/apps/vue/imports/ui/components/Info.vue b/tools/e2e-tests/apps/vue/imports/ui/components/Info.vue new file mode 100644 index 0000000000..5a17339c53 --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/components/Info.vue @@ -0,0 +1,16 @@ + + + diff --git a/tools/e2e-tests/apps/vue/imports/ui/main.css b/tools/e2e-tests/apps/vue/imports/ui/main.css new file mode 100644 index 0000000000..f1d8c73cdc --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/main.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/tools/e2e-tests/apps/vue/imports/ui/main.js b/tools/e2e-tests/apps/vue/imports/ui/main.js new file mode 100644 index 0000000000..f568d94ebf --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/main.js @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor' +import { createApp } from 'vue' +import { VueMeteor } from 'vue-meteor-tracker' + +import './main.css' +import App from './App.vue' +import { router } from './router' + +Meteor.startup(() => { + const app = createApp(App) + app.use(router) + app.use(VueMeteor) + app.mount('#app') +}) diff --git a/tools/e2e-tests/apps/vue/imports/ui/router.js b/tools/e2e-tests/apps/vue/imports/ui/router.js new file mode 100644 index 0000000000..00ed2a51e1 --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/router.js @@ -0,0 +1,19 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Home from './views/Home.vue' +import About from './views/About.vue' + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'home', + component: Home, + }, + { + path: '/about', + name: 'about', + component: About, + }, + ], +}) diff --git a/tools/e2e-tests/apps/vue/imports/ui/views/About.vue b/tools/e2e-tests/apps/vue/imports/ui/views/About.vue new file mode 100644 index 0000000000..d1ba384f1b --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/views/About.vue @@ -0,0 +1,5 @@ + diff --git a/tools/e2e-tests/apps/vue/imports/ui/views/Home.vue b/tools/e2e-tests/apps/vue/imports/ui/views/Home.vue new file mode 100644 index 0000000000..38e3688bee --- /dev/null +++ b/tools/e2e-tests/apps/vue/imports/ui/views/Home.vue @@ -0,0 +1,10 @@ + + + diff --git a/tools/e2e-tests/apps/vue/package.json b/tools/e2e-tests/apps/vue/package.json new file mode 100644 index 0000000000..31c39838ad --- /dev/null +++ b/tools/e2e-tests/apps/vue/package.json @@ -0,0 +1,39 @@ +{ + "name": "vue", + "private": true, + "scripts": { + "start": "meteor run", + "build": "meteor build ../output/vue --directory", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "vue": "^3.3.9", + "vue-meteor-tracker": "^3.0.0-beta.7", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rspack/cli": "^1.4.8", + "@rspack/core": "^1.4.8", + "@tailwindcss/postcss": "^4.1.12", + "@types/meteor": "^2.9.7", + "postcss": "^8.5.6", + "postcss-loader": "^8.1.1", + "playwright": "1.58.0", + "tailwindcss": "^4.1.12", + "vue-loader": "^17.4.2" + }, + "meteor": { + "modern": true, + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + }, + "testModule": "tests/main.js" + } +} diff --git a/tools/e2e-tests/apps/vue/postcss.config.js b/tools/e2e-tests/apps/vue/postcss.config.js new file mode 100644 index 0000000000..c2ddf74822 --- /dev/null +++ b/tools/e2e-tests/apps/vue/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/tools/e2e-tests/apps/vue/rspack.config.js b/tools/e2e-tests/apps/vue/rspack.config.js new file mode 100644 index 0000000000..9f7fb1cd8f --- /dev/null +++ b/tools/e2e-tests/apps/vue/rspack.config.js @@ -0,0 +1,37 @@ +const { defineConfig } = require('@meteorjs/rspack'); +const { VueLoaderPlugin } = require('vue-loader'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.isClient && { + plugins: [new VueLoaderPlugin()], + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + // Note, for the majority of features to be available, make sure this option is `true` + experimentalInlineMatchResource: true, + }, + }, + { + test: /\.css$/, + use: ["postcss-loader"], + type: "css", + }, + ], + }, + }, + }; +}); diff --git a/tools/static-assets/skel-solid/server/entry-meteor.js b/tools/e2e-tests/apps/vue/server/entry-meteor.js similarity index 100% rename from tools/static-assets/skel-solid/server/entry-meteor.js rename to tools/e2e-tests/apps/vue/server/entry-meteor.js diff --git a/tools/e2e-tests/apps/vue/server/main.js b/tools/e2e-tests/apps/vue/server/main.js new file mode 100644 index 0000000000..e5ef48eccc --- /dev/null +++ b/tools/e2e-tests/apps/vue/server/main.js @@ -0,0 +1,31 @@ +import { Meteor } from 'meteor/meteor' +import { LinksCollection } from '/imports/api/links' + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }) +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if ((await LinksCollection.find().countAsync()) === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://vuejs.org/guide/quick-start.html', + }) + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }) + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }) + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }) + } +}) diff --git a/tools/e2e-tests/apps/vue/tests/main.js b/tools/e2e-tests/apps/vue/tests/main.js new file mode 100644 index 0000000000..6bdfc61292 --- /dev/null +++ b/tools/e2e-tests/apps/vue/tests/main.js @@ -0,0 +1,20 @@ +import assert from 'assert' + +describe('vue', function () { + it('package.json has correct name', async function () { + const { name } = await import('../package.json') + assert.strictEqual(name, 'vue') + }) + + if (Meteor.isClient) { + it('client is not server', function () { + assert.strictEqual(Meteor.isServer, false) + }) + } + + if (Meteor.isServer) { + it('server is not client', function () { + assert.strictEqual(Meteor.isClient, false) + }) + } +}) diff --git a/tools/e2e-tests/assertions.js b/tools/e2e-tests/assertions.js new file mode 100644 index 0000000000..5abaad4daf --- /dev/null +++ b/tools/e2e-tests/assertions.js @@ -0,0 +1,351 @@ +/** + * This file contains assertion helpers for testing Meteor applications. + */ + +import fs from 'fs-extra'; +import path from 'path'; +import { wait } from "./helpers"; + +/** + * Helper function to assert that a Meteor app is running correctly + * @param {number} port - Port where the app is running + * @param {Object} options - Options for the assertion + * @param {string} options.title - Expected content in the title + * @param {string} options.h1 - Expected content in the h1 element + * @returns {Promise} + */ +export async function assertMeteorApp(port, options = {}) { + // Extract options with default values + const { title: inTitle, h1: inH1 = "Welcome to Meteor!" } = options; + + // Navigate to the app + await page.goto(`http://localhost:${port}`); + + // Check the title if specified + if (inTitle) { + const title = await page.title(); + expect(title).toMatch(new RegExp(inTitle)); + console.log(`✅ Title: ${title}`); + } + + // Check for static content if specified + if (inH1) { + await page.waitForSelector('h1'); + const h1Text = await page.$eval('h1', el => el.textContent); + expect(h1Text).toMatch(new RegExp(inH1)); + console.log(`✅ H1: ${h1Text}`); + } +} + +/** + * Helper function to assert that a Meteor React app is running correctly + * @param {number} port - Port where the app is running + * @param {Object} options - Options for the assertion + * @param {string} options.title - Expected content in the title (default: "react") + * @param {string} options.h1 - Expected content in the h1 element (default: "Welcome to Meteor!") + * @returns {Promise} + */ +export async function assertMeteorReactApp(port, options = {}) { + const reactOptions = { + title: "react", + h1: "Welcome to Meteor!", + ...options + }; + await assertMeteorApp(port, reactOptions); +} + +/** + * Helper function to assert that a Meteor app is using Rspack + * @param {number} port - Port where the app is running + * @returns {Promise} + */ +export async function assertRspackScriptTag(port, shoudlExist = true) { + // Navigate to the app + await page.goto(`http://localhost:${port}`); + + // Get all script tags + const scriptTags = await page.$$eval('script', scripts => + scripts.map(script => script.getAttribute('src')) + ); + + // Check if any script tag has __rspack__ in its path + const hasRspackScript = scriptTags.some(src => src && src.includes('__rspack__')); + expect(hasRspackScript).toBe(shoudlExist); +} + +/** + * Helper function to assert that a single file exists and optionally contains specific content + * @param {string} tempDir - Path to the temporary directory + * @param {string} filePath - File path relative to tempDir + * @param {Object} options - Additional options + * @param {string} options.content - Content to check for + * @param {boolean} options.exactMatch - Whether the content should be an exact match (default: false) + * @param {number} options.timeout - Maximum time to wait in milliseconds (default: 5000) + * @param {number} options.checkInterval - Interval between checks in milliseconds (default: 100) + * @returns {Promise} + */ +export async function assertFileExist(tempDir, filePath, options = {}) { + const { content, exactMatch = false, timeout = 5000, checkInterval = 100 } = options; + const fullPath = path.join(tempDir, filePath); + + const startTime = Date.now(); + + // Function to check file existence and content + const checkFile = async () => { + // Check if file exists + const fileExists = await fs.pathExists(fullPath); + + if (!fileExists) { + // If file doesn't exist and we haven't exceeded the timeout, wait and retry + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + return checkFile(); + } + // If we've exceeded the timeout, fail the test + throw new Error(`Expected file to exist but it was not found: ${fullPath}`); + return false; + } + + // If content check is requested + if (content) { + // Read the file content + const fileContent = await fs.readFile(fullPath, 'utf8'); + + if (exactMatch) { + // Check for exact match + if (fileContent !== content) { + // If content doesn't match and we haven't exceeded the timeout, wait and retry + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + return checkFile(); + } + // If we've exceeded the timeout, fail the test + expect(fileContent).toBe(content); + return false; + } + } else { + // Check if file includes the specified content + if (!fileContent.includes(content)) { + // If content doesn't include the specified content and we haven't exceeded the timeout, wait and retry + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + return checkFile(); + } + // If we've exceeded the timeout, fail the test + expect(fileContent).toContain(content); + return false; + } + } + } + + return true; + }; + + // Start checking + await checkFile(); +} + +/** + * Helper function to assert that a path does NOT exist + * Retries until the path is gone or the timeout is exceeded + * @param {string} basePath - Base directory path + * @param {string} relPath - Relative path from basePath to check + * @param {Object} options - Additional options + * @param {number} options.timeout - Maximum time to wait in milliseconds (default: 5000) + * @param {number} options.checkInterval - Interval between checks in milliseconds (default: 100) + * @returns {Promise} + */ +export async function assertPathNotExist(basePath, relPath, options = {}) { + const { timeout = 5000, checkInterval = 100 } = options; + const fullPath = path.join(basePath, relPath); + const startTime = Date.now(); + + const check = async () => { + const exists = await fs.pathExists(fullPath); + if (exists && Date.now() - startTime < timeout) { + await new Promise(r => setTimeout(r, checkInterval)); + return check(); + } + if (exists) { + const stat = await fs.stat(fullPath); + const isDir = stat.isDirectory(); + let contents = ''; + if (isDir) { + const entries = await fs.readdir(fullPath); + contents = ` (contains: ${entries.join(', ')})`; + } + console.error(`assertPathNotExist FAILED: ${relPath} still exists at ${fullPath} [${isDir ? 'dir' : 'file'}, ${stat.size} bytes]${contents}`); + } + expect(exists).toBe(false); + }; + + await check(); +} + +/** + * Helper function to evaluate JavaScript code in the browser console and assert the result + * @param {string} code - JavaScript code to evaluate in the browser console + * @param {any} expectedResult - Expected result of the evaluation + * @param {Object} options - Additional options + * @param {boolean} options.exactMatch - Whether to check for exact equality (default: true) + * @param {number} options.timeout - Maximum time to wait in milliseconds (default: 5000) + * @param {number} options.checkInterval - Interval between checks in milliseconds (default: 100) + * @returns {Promise} - A promise that resolves with the evaluation result + */ +export async function assertConsoleEval(code, expectedResult, options = {}) { + const { exactMatch = true, timeout = 5000, checkInterval = 100 } = options; + + console.log(`Evaluating code in browser console: ${code}`); + + const startTime = Date.now(); + + // Function to evaluate code and check result + const evaluateAndCheck = async () => { + try { + // Evaluate the code in the browser context + const result = await page.evaluate(code); + + if (exactMatch) { + // Check for exact match + if (JSON.stringify(result) !== JSON.stringify(expectedResult)) { + // If result doesn't match and we haven't exceeded the timeout, wait and retry + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + return evaluateAndCheck(); + } + // If we've exceeded the timeout, fail the test + expect(result).toEqual(expectedResult); + return null; + } + } else { + // Check for partial match (contains) + const resultStr = JSON.stringify(result); + const expectedStr = JSON.stringify(expectedResult); + + if (!resultStr.includes(expectedStr)) { + // If result doesn't include the expected result and we haven't exceeded the timeout, wait and retry + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + return evaluateAndCheck(); + } + // If we've exceeded the timeout, fail the test + expect(resultStr).toContain(expectedStr); + return null; + } + } + + console.log(`✅ Console evaluation successful. Result: ${JSON.stringify(result)}`); + return result; + } catch (error) { + // If evaluation fails and we haven't exceeded the timeout, wait and retry + if (Date.now() - startTime < timeout) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + return evaluateAndCheck(); + } + // If we've exceeded the timeout, fail the test + throw new Error(`Error evaluating code in console: ${error.message}`); + } + }; + + // Start evaluating + return await evaluateAndCheck(); +} + +/** + * Helper function to assert that an element has the expected CSS styles + * @param {string} selector - CSS selector string (e.g., 'body', '.my-class') or a string representing a DOM element (e.g., 'document.body') + * @param {Object} expectedStyles - Expected CSS styles as key-value pairs + * @param {Object} options - Additional options for assertConsoleEval + * @returns {Promise} - A promise that resolves with the computed styles + */ +export async function assertStyles(selector, expectedStyles, options = {}) { + // Determine if the selector is a CSS selector or a DOM element reference + const isCssSelector = selector.startsWith('.') || + selector.startsWith('#') || + selector.startsWith('[') || + selector === 'body' || + selector.includes(' '); + + console.log(`Asserting styles for ${selector}: ${JSON.stringify(expectedStyles)}`); + + // Create a JavaScript code string that evaluates the computed styles + const code = ` + (() => { + let element; + + // Handle the selector based on its type + ${isCssSelector + ? `element = document.querySelector('${selector}'); + if (!element) { + throw new Error('Element not found with selector: ${selector}'); + }` + : `element = ${selector}; + if (!element) { + throw new Error('Element not found: ${selector}'); + }` + } + + const computedStyle = getComputedStyle(element); + const result = {}; + ${Object.keys(expectedStyles).map(prop => + `result['${prop}'] = computedStyle.getPropertyValue('${prop}');` + ).join('\n')} + return result; + })() + `; + + // Use assertConsoleEval to evaluate the code and check the result + return assertConsoleEval(code, expectedStyles, { + exactMatch: false, + ...options + }); +} + +/** + * Helper function to assert that the body element has the expected CSS styles + * @param {Object} expectedStyles - Expected CSS styles as key-value pairs + * @param {Object} options - Additional options for assertConsoleEval + * @returns {Promise} - A promise that resolves with the computed styles + */ +export async function assertBodyStyles(expectedStyles, options = {}) { + // Use assertStyles with document.body as the selector + return assertStyles('document.body', expectedStyles, options); +} + +/** + * Helper function to assert that meta tags have the expected content + * @param {Object} expectedMetaTags - Expected meta tag properties and values as key-value pairs + * @param {Object} options - Additional options for assertConsoleEval + * @returns {Promise} - A promise that resolves with the meta tag values + * + * @example + * // Assert that the theme-color meta tag has the expected value + * await assertMetaTags({ + * 'theme-color': '#4285f4' + * }); + */ +export async function assertMetaTags(expectedMetaTags, options = {}) { + console.log(`Asserting meta tags: ${JSON.stringify(expectedMetaTags)}`); + + // Create a JavaScript code string that evaluates the meta tags + const code = ` + (() => { + const result = {}; + ${Object.keys(expectedMetaTags).map(prop => + `// Try to find meta tag by property or name attribute + let metaTag = document.querySelector('meta[property="${prop}"]'); + if (!metaTag) { + metaTag = document.querySelector('meta[name="${prop}"]'); + } + result['${prop}'] = metaTag ? metaTag.getAttribute('content') : null;` + ).join('\n')} + return result; + })() + `; + + // Use assertConsoleEval to evaluate the code and check the result + return await assertConsoleEval(code, expectedMetaTags, { + exactMatch: true, // We want exact matches for meta tag content + ...options + }); +} diff --git a/tools/e2e-tests/babel.config.js b/tools/e2e-tests/babel.config.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/e2e-tests/babel.test.js b/tools/e2e-tests/babel.test.js new file mode 100644 index 0000000000..4565410d1f --- /dev/null +++ b/tools/e2e-tests/babel.test.js @@ -0,0 +1,86 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Babel App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'babel', + port: 3122, + filePaths: { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.js' + }, + configFile: 'rspack.config.mjs', + skipEnvCheck: true, + // Test custom NODE_ENV compilation + env: { + meteorRun: { NODE_ENV: 'development' }, + meteorRunProduction: { NODE_ENV: 'production' }, + meteorTest: { NODE_ENV: 'development' }, + meteorTestOnce: { NODE_ENV: 'test' }, + meteorBuild: { NODE_ENV: 'development' }, + }, + customAssertions: { + afterRun: async ({ result }) => { + await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProduction: async ({ result }) => { + await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: production/); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/ + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/ + ); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterTest: async ({ result }) => { + await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); + }, + afterTestOnce: async ({ result }) => { + await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); + }, + afterBuild: async ({ result }) => { + await assertFileExtensionModuleRules(result.outputLines); + // Force development mode on build + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); + }, + } + })); +}); + +/** + * Helper function to assert that output contains expected file extension moduel rules + * @param {string[]} outputLines - Array of output lines to check + * @returns {Promise} + */ +export async function assertFileExtensionModuleRules(outputLines) { + // Check for custom and residual rules + await waitForMeteorOutput(outputLines, '/\\.(js|jsx)$/i'); + await waitForMeteorOutput(outputLines, '/\\.(tsx|ts|mts|cts|mjs|cjs)$/i'); + await waitForMeteorOutput(outputLines, '/\\.(graphql|gql)$/i'); + await waitForMeteorOutput(outputLines, '/\\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i', { negate: true }); +} diff --git a/tools/e2e-tests/blaze.test.js b/tools/e2e-tests/blaze.test.js new file mode 100644 index 0000000000..3730ad01c7 --- /dev/null +++ b/tools/e2e-tests/blaze.test.js @@ -0,0 +1,57 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('BasicBlaze App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'blaze', + port: 3122, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js' + }, + customAssertions: { + afterRun: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled as incompatible with Blaze + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterRunProduction: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled as incompatible with Blaze + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterTest: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterTestOnce: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterBuild: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + } + })); +}); + +/** + * Helper function to wait for Blaze environment output from both Rspack Client and Server + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @returns {Promise} - A promise that resolves when blaze envs are enabled + */ +export async function waitForBlazeEnvs(outputLines, options = {}) { + await waitForMeteorOutput( + outputLines, + /.*isBlazeEnabled:.*true.*/, + options + ); +} diff --git a/tools/e2e-tests/coffeescript.test.js b/tools/e2e-tests/coffeescript.test.js new file mode 100644 index 0000000000..872a1176e6 --- /dev/null +++ b/tools/e2e-tests/coffeescript.test.js @@ -0,0 +1,33 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('CoffeeScript App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'coffeescript', + port: 3132, + filePaths: { + client: 'client/main.coffee', + server: 'server/main.coffee', + test: 'tests/main.coffee' + }, + customUpdates: { + devClient: (message) => `console.log "${message}" if Meteor.isDevelopment`, + devServer: (message) => `console.log "${message}" if Meteor.isDevelopment`, + prodClient: (message) => `console.log "${message}" if Meteor.isProduction`, + prodServer: (message) => `console.log "${message}" if Meteor.isProduction`, + test: (message) => `console.log "${message}"` + }, + customAssertions: { + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + } + })); +}); diff --git a/tools/e2e-tests/full-blaze.test.js b/tools/e2e-tests/full-blaze.test.js new file mode 100644 index 0000000000..ae79cd25b8 --- /dev/null +++ b/tools/e2e-tests/full-blaze.test.js @@ -0,0 +1,57 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Full Blaze App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'full-blaze', + port: 3122, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'imports/api/links/methods.tests.js' + }, + customAssertions: { + afterRun: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled as incompatible with Blaze + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/, { negate: true }); + }, + afterRunProduction: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterTest: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterTestOnce: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + afterBuild: async ({ result }) => { + await waitForBlazeEnvs(result.outputLines); + }, + } + })); +}); + +/** + * Helper function to wait for Blaze environment output from both Rspack Client and Server + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @returns {Promise} - A promise that resolves when blaze envs are enabled + */ +export async function waitForBlazeEnvs(outputLines, options = {}) { + await waitForMeteorOutput( + outputLines, + /.*isBlazeEnabled:.*true.*/, + options + ); +} diff --git a/tools/e2e-tests/helpers.js b/tools/e2e-tests/helpers.js new file mode 100644 index 0000000000..dd62124f30 --- /dev/null +++ b/tools/e2e-tests/helpers.js @@ -0,0 +1,754 @@ +const execa = require('execa'); +const waitOn = require('wait-on'); +const path = require('path'); +const fs = require('fs-extra'); +const os = require('os'); +const rimraf = require('rimraf'); + +// Get the absolute path to the meteor executable +const REPO_ROOT = path.resolve(__dirname, '../..'); +const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor'); + +/** + * Helper function to set up a Meteor app in a temporary directory + * Copies the app and runs npm install + * @param {string} appName - Name of the app in the apps directory + * @param {Object} options - Additional options + * @param {boolean} options.isMonorepo - Whether the app is a monorepo + * @returns {string} - Path to the temporary directory containing the app + */ +export async function setupMeteorApp(appName, options = {}) { + const { isMonorepo = false } = options; + + // Create a unique temporary directory + const randomSuffix = Math.random().toString(36).substring(2, 10); + const tempDir = path.join(os.tmpdir(), `meteortest-${appName}-${randomSuffix}`); + + // Source app directory + const sourceAppDir = path.join(__dirname, 'apps', appName); + console.log(`Source app directory: ${sourceAppDir}`); + console.log(`Temporary directory: ${tempDir}`); + + try { + // Create the destination directory if it doesn't exist + if (!fs.existsSync(tempDir)) { + await fs.mkdir(tempDir, { recursive: true }); + } + + // Use fs-extra's copy method with recursive option + await fs.copy(sourceAppDir, tempDir, { + dereference: true, + preserveTimestamps: true, + overwrite: true + }); + console.log(`Copied app to temporary directory: ${tempDir}`); + } catch (err) { + console.error('Error during copy:', err); + } + + if (isMonorepo) { + // For monorepo, install dependencies at both root and app level + console.log('Running npm install at root level...'); + await execa.command('npm install', { + cwd: tempDir, + stdio: 'inherit', + shell: true, + }); + + console.log('Running npm install at app level...'); + await execa.command('npm install', { + cwd: path.join(tempDir, 'app'), + stdio: 'inherit', + shell: true, + }); + } else { + // For regular apps, just install at the root + console.log('Running npm install...'); + await execa.command('npm install', { + cwd: tempDir, + stdio: 'inherit', + shell: true, + }); + } + + return { tempDir }; +} + +/** + * Helper function to run a Meteor app + * @param {string} tempDir - Path to the directory containing the app + * @param {number} port - Port to run the app on + * @param {Object} options - Additional options + * @param {string|RegExp} options.waitForOutput - Output pattern to wait for + * @param {Object} options.waitOptions - Options for waitForMeteorOutput + * @param {string[]} options.commandOptions - Additional command line options for the run command (e.g. ['--production']) + * @param {boolean} options.isMonorepo - Whether the app is a monorepo + * @returns {Object} - The meteor process and output lines + */ +export async function runMeteorApp(tempDir, port, options = {}) { + const { isMonorepo = false, env = {} } = options; + + // Start Meteor CLI in dev mode + console.log(`Starting Meteor app on port ${port}...`); + + // Determine if we need to capture output + const captureOutput = !!options.waitForOutput; + + // Combine port option with any additional command options + const args = ['--port', port.toString()]; + if (options.commandOptions && Array.isArray(options.commandOptions)) { + args.push(...options.commandOptions); + } + + // For monorepo, run the meteor command from the app subdirectory + const appDir = isMonorepo ? path.join(tempDir, 'app') : tempDir; + + // Run the meteor command + const { meteorProcess, outputLines } = await runMeteorCommand( + 'run', + args, + appDir, + { + captureOutput, + execaOptions: { env: { ...process.env, ...env } } + } + ); + + // If a specific output pattern is requested, wait for it + if (options.waitForOutput) { + await waitForMeteorOutput( + outputLines, + options.waitForOutput, + options + ); + } + + // Wait for server to be up + if (!options.skipWaitOn) { + console.log(`Waiting for app to be available on port ${port}...`); + await waitOn({ + resources: [`http-get://localhost:${port}`], + timeout: 90000 + }); + } + + return { meteorProcess, outputLines }; +} + +/** + * Helper function to kill a Meteor process + * @param {Object} meteorProcess - The Meteor process to kill + * @returns {Promise} + */ +export async function killMeteorProcess(meteorProcess) { + if (meteorProcess) { + try { + await meteorProcess.kill('SIGKILL'); + console.log('Successfully killed meteor process'); + } catch (err) { + console.log(`Error killing meteor process: ${err.message}`); + } + } +} + +/** + * Kills any process running on the specified port(s) + * @param {number|number[]} port - The port or array of ports to kill processes on + * @returns {Promise} + */ +export async function killProcessByPort(port) { + // If port is an array, kill processes on each port + if (Array.isArray(port)) { + console.log(`Killing processes on multiple ports: ${port.join(', ')}...`); + // Process each port sequentially + for (const singlePort of port) { + await killSingleProcessByPort(singlePort); + } + return; + } + + // Handle single port case + await killSingleProcessByPort(port); +} + +/** + * Helper function to kill a process on a single port + * @param {number} port - The port to kill processes on + * @returns {Promise} + * @private + */ +async function killSingleProcessByPort(port) { + try { + // Different commands based on OS + const command = process.platform === 'win32' + ? `FOR /F "tokens=5" %a in ('netstat -ano ^| find "LISTENING" ^| find ":${port}"') do taskkill /F /PID %a` + : `lsof -i :${port} -t | grep -v ^${process.pid}$ | xargs -r kill -9`; + + console.log(`Killing process on port ${port}...`); + try { + // Use { reject: false } to prevent execa from throwing on non-zero exit codes + const result = await execa.command(command, { shell: true, reject: false }); + if (result.failed) { + // It's okay if this fails because there might not be a process on that port + console.log(`No process found on port ${port} or command returned non-zero exit code`); + } else { + console.log(`Successfully killed process on port ${port}`); + } + } catch (err) { + // This catch block will only be reached for operational errors, not for command failures + console.log(`Error executing kill command: ${err.message}`); + } + console.log(`Successfully ensured no process is running on port ${port}`); + } catch (error) { + // This should never be reached with the inner try/catch, but keeping as a safety net + console.error(`Error killing process on port ${port}:`, error); + } +} + +/** + * Helper function to run any Meteor command + * @param {string} command - The Meteor command to run (e.g., 'run', 'build', 'test') + * @param {string[]} args - Additional arguments for the command + * @param {string} cwd - Working directory where the command should be executed + * @param {Object} options - Additional options + * @param {Object} options.execaOptions - Additional options for execa + * @param {boolean} options.captureOutput - Whether to capture the command's output + * @param {boolean} options.checkExitCode - Whether to automatically check the exit code and throw an error if it's not 0 + * @returns {Object} - The meteor process and output lines if capturing output, and processResult if checkExitCode is true + */ +export async function runMeteorCommand(command, args = [], cwd, options = {}) { + console.log(`Running Meteor command: ${command} ${args.join(' ')}...`); + + const { captureOutput = false, checkExitCode = false, execaOptions: extraExecaOptions = {}, env = {} } = options; + + const execaOptions = { + cwd, + env: { + ...process.env, + ...env + }, + ...extraExecaOptions + }; + + // If we're capturing output, set up stdio accordingly + if (captureOutput) { + execaOptions.stdio = ['inherit', 'pipe', 'pipe']; + } else { + execaOptions.stdio = 'inherit'; + } + + const meteorProcess = execa(METEOR_EXECUTABLE, [command, ...args], execaOptions); + + // If we're capturing output, set up the output collection + let outputLines = []; + if (captureOutput && meteorProcess.stdout && meteorProcess.stderr) { + meteorProcess.stdout.on('data', (data) => { + const lines = data.toString().split('\n').filter(line => line.trim()); + outputLines.push(...lines); + // Still log to console for visibility + process.stdout.write(data); + }); + + meteorProcess.stderr.on('data', (data) => { + const lines = data.toString().split('\n').filter(line => line.trim()); + outputLines.push(...lines); + // Still log to console for visibility + process.stderr.write(data); + }); + } + + // If we're checking the exit code, wait for the process to complete and check it + let processResult; + if (checkExitCode) { + processResult = await new Promise((resolve) => { + meteorProcess.on('exit', (code) => { + resolve({ code, outputLines }); + }); + }); + + // Check if the command was successful + if (processResult.code !== 0) { + throw new Error(`Meteor command '${command}' failed with code ${processResult.code}${captureOutput ? `:\n${processResult.outputLines.join('\n')}` : ''}`); + } + } + + return { meteorProcess, outputLines, processResult }; +} + +/** + * Helper function to create a new Meteor app with a specific example + * @param {string} appName - Name of the new app + * @param {string} example - Example to use (e.g., 'react', 'vue') + * @param {Object} options - Additional options for execa + * @returns {Object} - The path to the new app, the meteor process, and the process result + */ +export async function createMeteorApp(appName, example, options = {}) { + // Create a unique temporary directory that will be the app directory directly + const randomSuffix = Math.random().toString(36).substring(2, 10); + const tempAppName= `meteortest-${appName}-${randomSuffix}`; + const tempDir = path.join(os.tmpdir(), tempAppName); + + console.log(`Creating new Meteor app '${appName}' with example '${example}' in ${tempDir}...`); + + // Create the destination directory if it doesn't exist + if (!fs.existsSync(tempDir)) { + await fs.mkdir(tempDir, { recursive: true }); + } + + // Run 'meteor create --react myapp' command + const args = ['create']; + + // Add example option if provided + if (example) { + args.push(`--${example}`); + } + + // Add app name + args.push(tempAppName); + + // Run the command in the temporary directory + const { meteorProcess, processResult } = await runMeteorCommand(args[0], args.slice(1), os.tmpdir(), { + execaOptions: options, + checkExitCode: true + }); + + return { tempDir, meteorProcess, processResult }; +} + +/** + * Helper function to clean up a temporary directory + * @param {string} tempDir - Path to the temporary directory to clean up + * @returns {Promise} + */ +export async function cleanupTempDir(tempDir) { + if (tempDir) { + try { + rimraf.sync(tempDir, { disableGlob: true, maxRetries: 5, retryDelay: 500 }); + console.log(`Removed temporary directory: ${tempDir}`); + } catch (err) { + // Implement async removal as a fallback + return new Promise((resolve, reject) => { + rimraf(tempDir, { disableGlob: true, maxRetries: 5, retryDelay: 500 }, (error) => { + if (error) { + console.error(`Async removal also failed: ${error}`); + reject(error); + } else { + console.log(`Removed temporary directory: ${tempDir}`); + resolve(); + } + }); + }); + } + } +} + +/** + * Helper function to wait for a specific number of milliseconds + * @param {number} ms - The number of milliseconds to wait + * @returns {Promise} - A promise that resolves after the specified time + */ +export async function wait(ms) { + console.log(`Waiting for ${ms} milliseconds...`); + return new Promise(resolve => { + setTimeout(() => { + console.log(`Finished waiting for ${ms} milliseconds`); + resolve(); + }, ms); + }); +} + +/** + * Helper function to wait for specific output from a Meteor process + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {string|RegExp} pattern - String or RegExp pattern to wait for + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @param {boolean} options.negate - If true, wait until the pattern is NOT found in any output line + * @returns {Promise} - A promise that resolves with the matched line + */ +export async function waitForMeteorOutput(outputLines, pattern, options = {}) { + const timeout = options.timeout || 90000; // Default 1 minute timeout + const checkInterval = options.checkInterval || 100; // Check every 100ms by default + const negate = options.negate || false; // Default is to check for presence, not absence + + console.log(`Waiting for output ${negate ? 'NOT ' : ''}matching: ${pattern}`); + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + // Function to check for the pattern in the output lines + const checkForPattern = () => { + // Check if we've exceeded the timeout + if (Date.now() - startTime > timeout) { + reject(new Error(`Timeout waiting for output ${negate ? 'NOT ' : ''}matching: ${pattern}`)); + return; + } + + if (negate) { + // In negation mode, we need to check all lines and make sure none match + // If we've processed all lines and none match, we can resolve + if (outputLines.length > 0) { + let allLinesPass = true; + for (const line of outputLines) { + const matches = (typeof pattern === 'string' && line.includes(pattern)) || + (pattern instanceof RegExp && pattern.test(line)); + if (matches) { + allLinesPass = false; + break; + } + } + if (allLinesPass) { + console.log(`Confirmed no output matching: ${pattern}`); + resolve(null); + return; + } + } + } else { + // Check each line for the pattern (original behavior) + for (const line of outputLines) { + if (typeof pattern === 'string' && line.includes(pattern)) { + console.log(`Found output matching string: ${pattern}`); + resolve(line); + return; + } else if (pattern instanceof RegExp && pattern.test(line)) { + console.log(`Found output matching regex: ${pattern}`); + resolve(line); + return; + } + } + } + + // If we didn't find a match, check again after the interval + setTimeout(checkForPattern, checkInterval); + }; + + // Start checking + checkForPattern(); + }); +} + +/** + * Helper function to replace specific text within a file in a temporary directory + * This is useful for triggering file change detection in tests + * @param {string} tempDir - Path to the temporary directory + * @param {string} filePath - Path to the file relative to tempDir + * @param {Object} options - Additional options + * @param {string} options.searchText - Text to search for in the file + * @param {string} options.replaceText - Text to replace the searchText with + * @param {boolean} options.createIfNotExists - Create the file if it doesn't exist (default: true) + * @returns {Promise} - A promise that resolves when the file has been updated + */ +export async function replaceFileContent(tempDir, filePath, options = {}) { + const { searchText, replaceText, createIfNotExists = true } = options; + const fullPath = path.join(tempDir, filePath); + + console.log(`Replacing text in file: ${fullPath}`); + + try { + // Check if file exists + const fileExists = await fs.pathExists(fullPath); + + if (!fileExists) { + if (!createIfNotExists) { + throw new Error(`File does not exist: ${fullPath}`); + } + // Create directory structure if it doesn't exist + await fs.ensureDir(path.dirname(fullPath)); + // Create an empty file + await fs.writeFile(fullPath, '', 'utf8'); + } else { + // Read the existing content + const content = await fs.readFile(fullPath, 'utf8'); + + // Replace the specified text + const newContent = content.replace(searchText, replaceText); + + // Write the modified content back to the file + await fs.writeFile(fullPath, newContent, 'utf8'); + } + + console.log(`Successfully replaced text in file: ${fullPath}`); + } catch (err) { + console.error(`Error replacing text in file ${fullPath}:`, err); + throw err; + } +} + +/** + * Helper function to append content to a file in a temporary directory + * This is useful for adding code to files during tests + * @param {string} tempDir - Path to the temporary directory + * @param {string} filePath - Path to the file relative to tempDir + * @param {string} content - Content to append to the file + * @param {Object} options - Additional options + * @param {boolean} options.createIfNotExists - Create the file if it doesn't exist (default: true) + * @param {string} options.separator - Separator to add before the appended content (default: '\n') + * @returns {Promise} - A promise that resolves when the file has been updated + */ +export async function appendFileContent(tempDir, filePath, options = {}) { + const { createIfNotExists = true, separator = '\n', content = '' } = options; + const fullPath = path.join(tempDir, filePath); + + console.log(`Appending content to file: ${fullPath}`); + + try { + // Check if file exists + const fileExists = await fs.pathExists(fullPath); + + if (!fileExists) { + if (!createIfNotExists) { + throw new Error(`File does not exist: ${fullPath}`); + } + // Create directory structure if it doesn't exist + await fs.ensureDir(path.dirname(fullPath)); + // Create the file with the content + await fs.writeFile(fullPath, content, 'utf8'); + } else { + // Read the existing content + const existingContent = await fs.readFile(fullPath, 'utf8'); + + // Append the new content with a separator + const newContent = existingContent + separator + content; + + // Write the modified content back to the file + await fs.writeFile(fullPath, newContent, 'utf8'); + } + + console.log(`Successfully appended content to file: ${fullPath}`); + } catch (err) { + console.error(`Error appending content to file ${fullPath}:`, err); + throw err; + } +} + +/** + * Helper function to run Meteor tests with the meteortesting:mocha driver package + * @param {string} tempDir - Path to the directory containing the app + * @param {number} port - Port to run the tests on + * @param {Object} options - Additional options + * @param {string|RegExp} options.waitForOutput - Output pattern to wait for + * @param {Object} options.waitOptions - Options for waitForMeteorOutput + * @param {string[]} options.commandOptions - Additional command line options for the test command + * @param {boolean} options.testClient - Whether to enable client-side tests with a browser driver + * @param {boolean} options.checkTestResults - Whether to check test results and propagate failures to Jest + * @param {boolean} options.isMonorepo - Whether the app is a monorepo + * @returns {Object} - The meteor process and output lines + */ +export async function runMeteorTests(tempDir, port, options = {}) { + const { isMonorepo = false, env = {} } = options; + + // Start Meteor tests + console.log(`Starting Meteor tests on port ${port}...`); + + // Determine if we need to capture output + const captureOutput = !!options.waitForOutput || !!options.checkTestResults; + + // Combine base options with any additional command options + const args = ['--port', port.toString(), '--driver-package', 'meteortesting:mocha']; + if (options.commandOptions && Array.isArray(options.commandOptions)) { + args.push(...options.commandOptions); + } + + // For monorepo, run the meteor command from the app subdirectory + const appDir = isMonorepo ? path.join(tempDir, 'app') : tempDir; + + // Run the meteor test command + const { meteorProcess, outputLines, processResult } = await runMeteorCommand( + 'test', + args, + appDir, + { + execaOptions: { + env: { + ...process.env, + ...(options.testClient ? { TEST_BROWSER_DRIVER: 'playwright' } : { TEST_CLIENT: 0 }), + ...env, + } + }, + captureOutput, + checkExitCode: options.checkTestResults // Automatically check exit code if checkTestResults is true + } + ); + + // If a specific output pattern is requested, wait for it + if (options.waitForOutput) { + await waitForMeteorOutput( + outputLines, + options.waitForOutput, + options + ); + } + + return { meteorProcess, outputLines, processResult }; +} + +/** + * Helper function to wait for a console message matching a pattern + * @param {string|RegExp} pattern - Pattern to match in console messages + * @param {Object} options - Additional options + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @param {boolean} options.negate - If true, wait until the pattern is NOT found in any console message + * @param {boolean} options.returnAllLogs - If true, returns an object with both the matching message and all collected logs + * @param {boolean} options.collectAllLogs - If true, collects all logs for the specified timeout period without waiting for a pattern match + * @returns {Promise} - Returns the matching message or an object with message and allLogs if returnAllLogs is true + */ +export async function waitForPlaywrightConsole(pattern, options = {}) { + const timeout = options.timeout || 30000; // Default 30 seconds timeout + const checkInterval = options.checkInterval || 100; // Check every 100ms by default + const negate = options.negate || false; // Default is to check for presence, not absence + const returnAllLogs = options.returnAllLogs || false; // Default is to return just the matching message + const collectAllLogs = options.collectAllLogs || false; // Default is to wait for a pattern match + + if (collectAllLogs) { + console.log(`Collecting all console logs for ${timeout}ms`); + } else { + console.log(`Waiting for console message ${negate ? 'NOT ' : ''}matching: ${pattern}`); + } + + // Array to collect console messages + const consoleMessages = []; + + // Create a named listener function so we can remove it later + const consoleListener = (msg) => { + const text = msg.text(); + consoleMessages.push(text); + console.log(`Browser console: ${text}`); + }; + + // Set up console message listener + page.on('console', consoleListener); + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + // If we're just collecting all logs, set a timeout to resolve after the specified time + if (collectAllLogs) { + setTimeout(() => { + console.log(`Collected ${consoleMessages.length} console logs in ${timeout}ms`); + page.removeListener('console', consoleListener); + resolve({ message: null, allLogs: [...consoleMessages] }); + }, timeout); + return; + } + + // Function to check for the pattern in the console messages + const checkForPattern = () => { + // Check if we've exceeded the timeout + if (Date.now() - startTime > timeout) { + // Remove the listener before rejecting + page.removeListener('console', consoleListener); + reject(new Error(`Timeout waiting for console message ${negate ? 'NOT ' : ''}matching: ${pattern}`)); + return; + } + + if (negate) { + // In negation mode, we need to check all messages and make sure none match + // If we've received messages and none match, we can resolve + if (consoleMessages.length > 0) { + let allMessagesPass = true; + for (const message of consoleMessages) { + const matches = (typeof pattern === 'string' && message.includes(pattern)) || + (pattern instanceof RegExp && pattern.test(message)); + if (matches) { + allMessagesPass = false; + break; + } + } + if (allMessagesPass) { + console.log(`Confirmed no console message matching: ${pattern}`); + // Remove the listener before resolving + page.removeListener('console', consoleListener); + if (returnAllLogs) { + resolve({ message: null, allLogs: [...consoleMessages] }); + } else { + resolve(null); + } + return; + } + } + } else { + // Check each message for the pattern (original behavior) + for (const message of consoleMessages) { + if (typeof pattern === 'string' && message.includes(pattern)) { + console.log(`Found console message matching string: ${pattern}`); + // Remove the listener before resolving + page.removeListener('console', consoleListener); + if (returnAllLogs) { + resolve({ message, allLogs: [...consoleMessages] }); + } else { + resolve(message); + } + return; + } else if (pattern instanceof RegExp && pattern.test(message)) { + console.log(`Found console message matching regex: ${pattern}`); + // Remove the listener before resolving + page.removeListener('console', consoleListener); + if (returnAllLogs) { + resolve({ message, allLogs: [...consoleMessages] }); + } else { + resolve(message); + } + return; + } + } + } + + // If we didn't find a match, check again after the interval + setTimeout(checkForPattern, checkInterval); + }; + + // Start checking + checkForPattern(); + }); +} + +/** + * Helper function to build a Meteor app using 'meteor build' + * @param {string} tempDir - Path to the directory containing the app + * @param {Object} options - Additional options + * @param {string[]} options.commandOptions - Additional command line options for the build command + * @param {boolean} options.captureOutput - Whether to capture the command's output + * @param {boolean} options.isMonorepo - Whether the app is a monorepo + * @returns {Object} - The build output directory and the meteor process result + */ +export async function buildMeteorApp(tempDir, options = {}) { + const { isMonorepo = false, env = {} } = options; + + // Create a unique temporary directory for the build output + const randomSuffix = Math.random().toString(36).substring(2, 10); + const buildOutputDir = path.join(os.tmpdir(), `meteor-build-${randomSuffix}`); + + console.log(`Building Meteor app from ${tempDir} to ${buildOutputDir}...`); + + // Create the build output directory if it doesn't exist + if (!fs.existsSync(buildOutputDir)) { + await fs.mkdir(buildOutputDir, { recursive: true }); + } + + // Combine base options with any additional command options + const args = [buildOutputDir]; + if (options.commandOptions && Array.isArray(options.commandOptions)) { + args.push(...options.commandOptions); + } + + // For monorepo, run the meteor command from the app subdirectory + const appDir = isMonorepo ? path.join(tempDir, 'app') : tempDir; + + // Run the meteor build command with automatic exit code checking + const result = await runMeteorCommand( + 'build', + args, + appDir, + { + execaOptions: { ...(options.execaOptions || {}), env: { ...process.env, ...(options.execaOptions?.env || {}), ...env } }, + captureOutput: options.captureOutput !== undefined ? options.captureOutput : true, + checkExitCode: true // Automatically check exit code + } + ); + + console.log(`Successfully built Meteor app to ${buildOutputDir}`); + + return { buildOutputDir, processResult: result.processResult }; +} diff --git a/tools/e2e-tests/jest.config.js b/tools/e2e-tests/jest.config.js new file mode 100644 index 0000000000..f625e86ac5 --- /dev/null +++ b/tools/e2e-tests/jest.config.js @@ -0,0 +1,33 @@ +module.exports = { + preset: 'jest-playwright-preset', + rootDir: __dirname, + testMatch: ["**/*.test.js"], + testPathIgnorePatterns: ["/apps/"], + setupFilesAfterEnv: ['/jest.setup.js'], + verbose: true, + // Increase timeout for CLI operations + testTimeout: 60_000, + // Transform ES modules in node_modules + transformIgnorePatterns: [ + "/node_modules/(?!(execa|wait-on|is-docker|is-stream|human-signals|merge-stream|npm-run-path|onetime|mimic-fn|strip-final-newline|path-key|shebug-command|shebug-regex)/)" + ], + transform: { + "^.+\\.js$": ["@swc/jest", { + jsc: { + parser: { syntax: "ecmascript" }, + target: "es2022", + }, + module: { type: "commonjs" }, + }], + }, + // Playwright configuration + globals: { + 'jest-playwright': { + browsers: ['chromium'], + launchOptions: { + headless: true, + } + } + }, + maxWorkers: 1, +}; diff --git a/tools/e2e-tests/jest.setup.js b/tools/e2e-tests/jest.setup.js new file mode 100644 index 0000000000..8b3f830a1f --- /dev/null +++ b/tools/e2e-tests/jest.setup.js @@ -0,0 +1,23 @@ +// jest.setup.js +import chalk from 'chalk'; + +const isCI = process.env.GITHUB_ACTIONS === "true"; +if (isCI) { + jest.retryTimes(2); + console.log('Set 2 retries on Jest level'); +} + +// Clear NODE_ENV so meteor commands don't inherit any value from the test runner environment +process.env.NODE_ENV = ''; + +// Set fixed ports for all tests +process.env.RSPACK_DEVSERVER_PORT = '8080'; +process.env.RSDOCTOR_CLIENT_PORT = '8888'; +process.env.RSDOCTOR_SERVER_PORT = '8889'; + +// This runs before each test +beforeEach(() => { + const name = expect.getState().currentTestName; + // e.g. a bright cyan arrow and test name + console.log(chalk.cyan(`▶ ${name}`)); +}); diff --git a/tools/e2e-tests/monorepo.test.js b/tools/e2e-tests/monorepo.test.js new file mode 100644 index 0000000000..27ae70ac87 --- /dev/null +++ b/tools/e2e-tests/monorepo.test.js @@ -0,0 +1,48 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Monorepo App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'monorepo', + port: 3133, + isMonorepo: true, + filePaths: { + client: 'app/client/main.jsx', + server: 'app/server/main.js', + test: 'app/tests/main.test.js' + }, + checkBundleFilePaths: [ + 'programs/web.browser/app/1x1.png', + 'programs/web.browser/app/images/1x1.png', + 'programs/web.browser/app/docs/text.md', + 'programs/web.browser.legacy/app/1x1.png', + 'programs/web.browser.legacy/app/images/1x1.png', + 'programs/web.browser.legacy/app/docs/text.md', + ], + configFile: 'rspack.config.cjs', + customAssertions: { + afterRun: async ({ result }) => { + // Check custom plugin gets loaded from rspack.config.override.cjs file + await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check custom plugin gets loaded from rspack.config.override.cjs file + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterTest: async ({ result }) => { + // Check custom plugin gets loaded from rspack.config.override.cjs file + await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/); + }, + afterBuild: async ({ result }) => { + // Check custom plugin gets loaded from rspack.config.override.cjs file + await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/); + }, + } + })); +}); diff --git a/tools/e2e-tests/package-lock.json b/tools/e2e-tests/package-lock.json new file mode 100644 index 0000000000..341c3bf556 --- /dev/null +++ b/tools/e2e-tests/package-lock.json @@ -0,0 +1,5855 @@ +{ + "name": "meteor-e2e-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meteor-e2e-tests", + "version": "1.0.0", + "devDependencies": { + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", + "cheerio": "^1.0.0-rc.12", + "execa": "^5.1.1", + "fs-extra": "^11.3.1", + "jest": "^29.0.0", + "jest-playwright-preset": "^3.0.1", + "playwright": "^1.58.0", + "semver": "^7.7.4", + "underscore": "^1.13.8", + "wait-on": "^7.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", + "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/jest": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", + "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^30.0.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/wait-on": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz", + "integrity": "sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.211", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect-playwright": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.8.0.tgz", + "integrity": "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/find-file-up": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", + "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", + "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-process": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.11.tgz", + "integrity": "sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "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-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "license": "ISC", + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-playwright-preset": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jest-playwright-preset/-/jest-playwright-preset-3.0.1.tgz", + "integrity": "sha512-tHqv+JUmheNMZpmH7XyT5CAMHr3ExTUIY9baMPzcJiLYPvCaPTwig9YvuGGnXV2n+Epmch0Ld4429g6py0nq0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect-playwright": "^0.8.0", + "jest-process-manager": "^0.3.1", + "nyc": "^15.1.0", + "playwright-core": ">=1.2.0", + "rimraf": "^3.0.2", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "jest": "^29.3.1", + "jest-circus": "^29.3.1", + "jest-environment-node": "^29.3.1", + "jest-runner": "^29.3.1" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-process-manager": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/jest-process-manager/-/jest-process-manager-0.3.1.tgz", + "integrity": "sha512-x9W54UgZ7IkzUHgXtnI1x4GKOVjxtwW0CA/7yGbTHtT/YhENO0Lic2yfVyC/gekn7OIEMcQmy0L1r9WLQABfqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/wait-on": "^5.2.0", + "chalk": "^4.1.0", + "cwd": "^0.10.0", + "exit": "^0.1.2", + "find-process": "^1.4.4", + "prompts": "^2.4.1", + "signal-exit": "^3.0.3", + "spawnd": "^5.0.0", + "tree-kill": "^1.2.2", + "wait-on": "^5.3.0" + } + }, + "node_modules/jest-process-manager/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/jest-process-manager/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/jest-process-manager/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/jest-process-manager/node_modules/wait-on": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-5.3.0.tgz", + "integrity": "sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.21.1", + "joi": "^17.3.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^6.6.3" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "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/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/spawnd": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-5.0.0.tgz", + "integrity": "sha512-28+AJr82moMVWolQvlAIv3JcYDkjkFTEmfDc503wxrF5l2rQ3dFz6DpbXp3kD4zmgGGldfM4xM4v1sFj/ZaIOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "exit": "^0.1.2", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "wait-port": "^0.2.9" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", + "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wait-port": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", + "integrity": "sha512-kIzjWcr6ykl7WFbZd0TMae8xovwqcqbx6FM9l+7agOgUByhzdjfzZBPK2CPufldTOMxbUivss//Sh9MFawmPRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "commander": "^3.0.2", + "debug": "^4.1.1" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wait-port/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wait-port/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wait-port/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/wait-port/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wait-port/node_modules/commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "dev": true, + "license": "MIT" + }, + "node_modules/wait-port/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wait-port/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/wait-port/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tools/e2e-tests/package.json b/tools/e2e-tests/package.json new file mode 100644 index 0000000000..1893ece2e4 --- /dev/null +++ b/tools/e2e-tests/package.json @@ -0,0 +1,21 @@ +{ + "name": "meteor-e2e-tests", + "version": "1.0.0", + "description": "Isolated Jest + Playwright environment for Meteor E2E tests", + "scripts": { + "test": "jest --config jest.config.js" + }, + "devDependencies": { + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", + "cheerio": "^1.0.0-rc.12", + "execa": "^5.1.1", + "fs-extra": "^11.3.1", + "jest": "^29.0.0", + "jest-playwright-preset": "^3.0.1", + "playwright": "^1.58.0", + "semver": "^7.7.4", + "underscore": "^1.13.8", + "wait-on": "^7.0.0" + } +} diff --git a/tools/e2e-tests/react-router.test.js b/tools/e2e-tests/react-router.test.js new file mode 100644 index 0000000000..6434019a3d --- /dev/null +++ b/tools/e2e-tests/react-router.test.js @@ -0,0 +1,151 @@ +import { waitForMeteorOutput } from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; +import { assertBodyStyles, assertMetaTags } from "./assertions"; + +describe('R.Router App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'react-router', + port: 3142, + filePaths: { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.app-test.js', + }, + testFullApp: true, + checkBundleFilePaths: [ + 'programs/web.browser/app/1x1.png', + 'programs/web.browser/app/images/1x1.png', + 'programs/web.browser/app/docs/text.md', + 'programs/web.browser.legacy/app/1x1.png', + 'programs/web.browser.legacy/app/images/1x1.png', + 'programs/web.browser.legacy/app/docs/text.md', + ], + beforeAllBehavior: async () => { + process.env.METEOR_PACKAGE_DIRS = './my-packages'; + }, + afterAllBehavior: async () => { + process.env.METEOR_PACKAGE_DIRS = ''; + }, + customAssertions: { + afterInit: async ({ result }) => { + await waitForMeteorOutput(result.outputLines, /.*babel-plugin-react-compiler.*/); + }, + afterRun: async ({ result, port }) => { + await waitForReactEnvs(result.outputLines, { isTsxEnabled: true }); + // negated as cached output (babel.config.js) + await waitForMeteorOutput(result.outputLines, /.*babel-plugin-react-compiler.*/, { negate: true }); + await assert404Page(port); + // Less styles support + await assertBodyStyles({ + 'white-space': 'break-spaces', + }); + // Meteor modules config + await assertBodyStyles({ + 'align-content': 'center', + }); + // Custom html rspack plugin options + await assertMetaTags({ + 'theme-color': '#4285f4', + }); + // default-package loading + await waitForMeteorOutput(result.outputLines, /.*default-package loaded.*/); + // custom-package loading + await waitForMeteorOutput(result.outputLines, /.*custom-package loaded.*/); + // resolve.extensions loading + await waitForMeteorOutput(result.outputLines, /.*first\.jsx loaded.*/); + // Check custom plugin gets loaded from rspack.config.override.js file + await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProduction: async ({ result, port }) => { + await waitForReactEnvs(result.outputLines, { isTsxEnabled: true }); + await waitForMeteorOutput(result.outputLines, /.*babel-plugin-react-compiler.*/); + await assert404Page(port, { isProductionMode: true }); + // Less styles support + await assertBodyStyles({ + 'white-space': 'break-spaces', + }); + // Meteor modules config + await assertBodyStyles({ + 'align-content': 'center', + }); + // Custom html rspack plugin options + await assertMetaTags({ + 'theme-color': '#4285f4', + }); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterTest: async ({ result }) => { + await waitForReactEnvs(result.outputLines); + // Check custom plugin gets loaded from rspack.config.override.js file + await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/); + }, + afterTestOnce: async ({ result }) => { + await waitForReactEnvs(result.outputLines); + }, + afterBuild: async ({ result }) => { + await waitForReactEnvs(result.outputLines, { isTsxEnabled: true }); + await waitForMeteorOutput(result.outputLines, /.*babel-plugin-react-compiler.*/); + // Check custom plugin gets loaded from rspack.config.override.js file + await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/); + }, + } + })); +}); + +/** + * Helper function to wait for React environment output from both Rspack Client and Server + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @returns {Promise} - A promise that resolves when react envs are enabled + */ +export async function waitForReactEnvs(outputLines, options = {}) { + await waitForMeteorOutput( + outputLines, + /.*isReactEnabled:.*true.*/, + options + ); + if (options.isTsxEnabled) { + await waitForMeteorOutput( + outputLines, + /.*isTsxEnabled:.*true.*/, + options + ); + } +} + +/** + * Helper function to assert that the 404 page is working correctly + * @param {number} port - Port where the app is running + * @param {Object} options - Options for the assertion + * @param {boolean} options.isProductionMode - Whether the app is running in production mode + * @returns {Promise} - A promise that resolves when the assertion is complete + */ +async function assert404Page(port, options = {}) { + const { isProductionMode = false } = options; + const modeText = isProductionMode ? 'in production mode' : ''; + + // Test 404 page + console.log(`Testing 404 page${modeText ? ' ' + modeText : ''}...`); + await page.goto(`http://localhost:${port}/not-found`); + + // Check for 404 message + await page.waitForSelector('h1'); + const notFoundText = await page.$eval('h1', el => el.textContent); + expect(notFoundText).toBe('404 - Page Not Found'); + + // Check for additional text + await page.waitForSelector('p'); + const paragraphText = await page.$eval('p', el => el.textContent); + expect(paragraphText).toBe('The page you are looking for does not exist.'); + + console.log(`✅ 404 page test passed${modeText ? ' ' + modeText : ''}`); +} diff --git a/tools/e2e-tests/react.test.js b/tools/e2e-tests/react.test.js new file mode 100644 index 0000000000..2f13cdfa46 --- /dev/null +++ b/tools/e2e-tests/react.test.js @@ -0,0 +1,287 @@ +import { + killProcessByPort, + cleanupTempDir, + killMeteorProcess, + createMeteorApp, + runMeteorApp, + waitForMeteorOutput, waitForPlaywrightConsole +} from "./helpers"; +import { testMeteorBundler, testMeteorRspackBundler } from './test-helpers'; +import fs from 'fs-extra'; +import path from 'path'; +import { assertMeteorReactApp, assertConsoleEval, assertFileExist } from "./assertions"; + +describe('React App Bundling /', () => { + + // TODO: Create one test aside for all skeletons + describe.skip('Meteor Creator /', () => { + const PORT = 3100; + + beforeAll(async () => { + // Ensure any process on the port is killed + await killProcessByPort(PORT); + }); + + test('"meteor create" / should create a new Meteor react app', async () => { + // Create a new Meteor app with --react example + const result = await createMeteorApp('react', 'react'); + const newAppTempDir = result.tempDir; + const newAppMeteorProcess = result.meteorProcess; + + // Wait for the process to complete + await newAppMeteorProcess; + + // Check if the app directory exists + const appDirExists = await fs.pathExists(newAppTempDir); + expect(appDirExists).toBe(true); + + // Check if package.json exists and contains react + const packageJsonPath = path.join(newAppTempDir, 'package.json'); + const packageJsonExists = await fs.pathExists(packageJsonPath); + expect(packageJsonExists).toBe(true); + + const packageJson = await fs.readJson(packageJsonPath); + expect(packageJson.dependencies).toHaveProperty('react'); + expect(packageJson.dependencies).toHaveProperty('react-dom'); + + // Run the newly created app + let runAppProcess = (await runMeteorApp(newAppTempDir, PORT))?.meteorProcess; + + // Assert that the Meteor React app is running correctly + await assertMeteorReactApp(PORT); + + // Kill the meteor processes + await killMeteorProcess(runAppProcess); + if (newAppMeteorProcess) { + await killMeteorProcess(newAppMeteorProcess); + } + + // Ensure any process on the port is killed + await killProcessByPort(PORT); + + // Clean up the temporary directory + await cleanupTempDir(newAppTempDir); + }); + }); + + describe( + "Meteor+Rspack Bundler /", + testMeteorRspackBundler({ + appName: "react", + port: 3102, + filePaths: { + client: "client/main.jsx", + server: "server/main.js", + test: "tests/main.js", + }, + configFile: "rspack.config.cjs", + buildDir: "_build-local-custom", + env: { METEOR_LOCAL_DIR: ".meteor/local-custom" }, + customAssertions: { + afterInit: async ({ result }) => { + // Verify unplugin transform hook is called on first run (fresh cache) + await waitForMeteorOutput( + result.outputLines, + /.*\[demo-unplugin\]\[transform-enter\].*/ + ); + }, + afterRun: async ({ result, tempDir }) => { + const appDir = tempDir; // testMeteorRspackBundler uses tempDir as appDir if not monorepo + + const localDir = path.join(appDir, ".meteor", "local-custom"); + const buildDir = path.join(appDir, "_build-local-custom"); + + expect(await fs.pathExists(buildDir)).toBe(true); + expect(await fs.pathExists(localDir)).toBe(true); + + await assertFileExist(appDir, '.gitignore', { content: '.meteor/local-custom' }); + + await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + + // Check if images exist and return 200 status code + await assertImagesExistAndLoad(); + + // Check custom plugin is disabled with Meteor.disablePlugins + // Use specific log prefix to avoid matching the filename in buildDependencies + await waitForMeteorOutput( + result.outputLines, + /.*\[CustomConsoleLogPlugin\].*/, + { negate: true } + ); + + // Verify unplugin factory is still created on second run (with cache) + // This confirms the plugin is loaded and active even when rspack uses + // cached transform results (#14031 regression test) + await waitForMeteorOutput( + result.outputLines, + /.*\[demo-unplugin\]\[factory-created\].*/ + ); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput( + allConsoleLogs, + /.*HMR.*Updated modules:.*/ + ); + }, + afterRunProduction: async ({ result }) => { + await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + + // Check if images exist and return 200 status code + await assertImagesExistAndLoad(); + + // Check custom plugin is disabled with Meteor.disablePlugins + // Use specific log prefix to avoid matching the filename in buildDependencies + await waitForMeteorOutput( + result.outputLines, + /.*\[CustomConsoleLogPlugin\].*/, + { negate: true } + ); + + // Verify demo-unplugin.js is tracked in rspack buildDependencies (#14031) + await waitForMeteorOutput( + result.outputLines, + /.*plugins\/demo-unplugin\.js.*/ + ); + + // Verify unplugin transform hook fires in production (separate cache version) + await waitForMeteorOutput( + result.outputLines, + /.*\[demo-unplugin\]\[transform-enter\].*/ + ); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput( + allConsoleLogs, + /.*HMR.*Updated modules:*/, + { negate: true } + ); + }, + afterTest: async ({ result }) => { + await waitForReactEnvs(result.outputLines); + + // Check custom plugin is disabled with Meteor.disablePlugins + // Use specific log prefix to avoid matching the filename in buildDependencies + await waitForMeteorOutput( + result.outputLines, + /.*\[CustomConsoleLogPlugin\].*/, + { negate: true } + ); + }, + afterTestOnce: async ({ result }) => { + await waitForReactEnvs(result.outputLines); + + // Check custom plugin is disabled with Meteor.disablePlugins + // Use specific log prefix to avoid matching the filename in buildDependencies + await waitForMeteorOutput( + result.outputLines, + /.*\[CustomConsoleLogPlugin\].*/, + { negate: true } + ); + }, + afterBuild: async ({ result }) => { + await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + + // Check custom plugin is disabled with Meteor.disablePlugins + // Use specific log prefix to avoid matching the filename in buildDependencies + await waitForMeteorOutput( + result.outputLines, + /.*\[CustomConsoleLogPlugin\].*/, + { negate: true } + ); + }, + }, + }) + ); +}); + +/** + * Helper function to wait for React environment output from both Rspack Client and Server + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @returns {Promise} - A promise that resolves when react envs are enabled + */ +export async function waitForReactEnvs(outputLines, options = {}) { + await waitForMeteorOutput( + outputLines, + /.*isReactEnabled:.*true.*/, + options + ); + if (options.isJsxEnabled) { + await waitForMeteorOutput( + outputLines, + /.*isJsxEnabled:.*true.*/, + options + ); + } +} + +/** + * Helper function to assert that images exist in the DOM and return 200 status code when fetched + * @returns {Promise} - A promise that resolves when images are verified + */ +export async function assertImagesExistAndLoad() { + await assertConsoleEval(` + (async () => { + // Get the image elements + const imageGenerated = document.getElementById('image-generated'); + const imagePublic = document.getElementById('image-public'); + + // Check if images exist + if (!imageGenerated || !imagePublic) { + return { success: false, error: 'Images not found in the DOM' }; + } + + // Function to check if an image URL returns 200 + const checkImageStatus = async (url) => { + try { + const response = await fetch(url); + return { + url, + status: response.status, + ok: response.ok + }; + } catch (error) { + return { + url, + status: 0, + ok: false, + error: error.message + }; + } + }; + + // Function to extract URL from background-image CSS property + const extractUrlFromBackgroundImage = (backgroundImage) => { + // Extract the URL from the background-image property (format: url("...")) + const urlMatch = backgroundImage.match(/url\\(['"]?([^'"\\)]+)['"]?\\)/); + return urlMatch ? urlMatch[1] : null; + }; + + // Get body's background-image + const bodyStyle = getComputedStyle(document.body); + const backgroundImage = bodyStyle.backgroundImage; + const backgroundImageUrl = extractUrlFromBackgroundImage(backgroundImage); + + // Check both images and background image + const generatedResult = await checkImageStatus(imageGenerated.src); + const publicResult = await checkImageStatus(imagePublic.src); + + if (!backgroundImageUrl) { + return { + ok: false, + error: 'No background image URL found on body element' + }; + } + + const backgroundResult = await checkImageStatus(backgroundImageUrl); + + return { + success: generatedResult.ok && publicResult.ok && backgroundResult.ok, + }; + })() + `, { success: true }, { exactMatch: false }); +} diff --git a/tools/e2e-tests/scripts/create-app.js b/tools/e2e-tests/scripts/create-app.js new file mode 100644 index 0000000000..fc11504356 --- /dev/null +++ b/tools/e2e-tests/scripts/create-app.js @@ -0,0 +1,483 @@ +#!/usr/bin/env node + +/** + * Script to create a Meteor test app for manual testing without automatic cleanup. + * + * Sources apps from: + * - tools/e2e-tests/apps/ (use --app flag) + * - meteor create -- (use --skeleton flag) + * + * Usage: + * npm run create-app:e2e -- --app react + * npm run create-app:e2e -- --app react --output ./dist/my-react-app + * npm run create-app:e2e -- --app monorepo --monorepo + * npm run create-app:e2e -- --skeleton react + * npm run create-app:e2e -- --skeleton react --output ./my-apps/custom-name + */ + +const path = require('path'); +const fs = require('fs-extra'); +const execa = require('execa'); +const { linkLocalRspack } = require('./link-rspack'); + +const REPO_ROOT = path.resolve(__dirname, '../../..'); +const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor'); +const E2E_TESTS_DIR = path.join(__dirname, '..'); +const APPS_DIR = path.join(E2E_TESTS_DIR, 'apps'); +const DEFAULT_OUTPUT_DIR = path.join(REPO_ROOT, 'dist'); + +// ANSI color helpers +const c = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + red: '\x1b[31m', + magenta: '\x1b[35m', +}; +const log = { + step: (msg) => console.log(`${c.cyan}>${c.reset} ${msg}`), + success: (msg) => console.log(`${c.green}>${c.reset} ${msg}`), + info: (msg) => console.log(`${c.blue}>${c.reset} ${msg}`), + warn: (msg) => console.log(`${c.yellow}>${c.reset} ${msg}`), + error: (msg) => console.error(`${c.red}>${c.reset} ${msg}`), + detail: (msg) => console.log(` ${c.dim}${msg}${c.reset}`), +}; + +function parseArgs(argv) { + const args = { monorepo: false }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--app') { + args.app = argv[++i]; + } else if (argv[i] === '--skeleton') { + args.skeleton = argv[++i]; + } else if (argv[i] === '--output') { + args.output = argv[++i]; + } else if (argv[i] === '--monorepo') { + args.monorepo = true; + } else if (argv[i] === '--force' || argv[i] === '-f') { + args.force = true; + } else if (argv[i] === '--help' || argv[i] === '-h') { + args.help = true; + } + } + return args; +} + +function printHelp() { + const availableApps = fs.existsSync(APPS_DIR) + ? fs.readdirSync(APPS_DIR).join(', ') + : '(none found)'; + + console.log(` +${c.bold}Usage:${c.reset} npm run create-app:e2e -- [options] + +${c.bold}Options:${c.reset} + ${c.cyan}--app${c.reset} Copy an existing app from tools/e2e-tests/apps/ + ${c.cyan}--skeleton${c.reset} Create a new app via "meteor create --" + ${c.cyan}--output${c.reset} Full destination path for the app (default: ./dist/) + ${c.cyan}--monorepo${c.reset} Treat the app as a monorepo (runs npm install at both root and app/ levels) + ${c.cyan}--force${c.reset}, ${c.cyan}-f${c.reset} Remove the destination directory if it already exists before creating the app + ${c.cyan}--help${c.reset}, ${c.cyan}-h${c.reset} Show this help message + +${c.bold}Available apps:${c.reset} ${c.green}${availableApps}${c.reset} + +${c.bold}Examples:${c.reset} + ${c.dim}npm run create-app:e2e -- --app react${c.reset} + ${c.dim}npm run create-app:e2e -- --app react --output ./dist/my-react-app${c.reset} + ${c.dim}npm run create-app:e2e -- --app monorepo --monorepo${c.reset} + ${c.dim}npm run create-app:e2e -- --skeleton react${c.reset} + ${c.dim}npm run create-app:e2e -- --skeleton react --output ./my-apps/custom-name${c.reset} +`); +} + +/** + * Find a test-helper function call block (e.g., testMeteorSkeleton({ skeletonName: 'react', ... })) + * that contains a matching name key/value, and return the content of its options object. + */ +function findTestHelperBlock(content, fnName, nameKey, nameValue) { + let searchStart = 0; + + while (searchStart < content.length) { + const fnIdx = content.indexOf(fnName + '(', searchStart); + if (fnIdx === -1) return null; + + const braceIdx = content.indexOf('{', fnIdx + fnName.length); + if (braceIdx === -1) return null; + + // Find matching closing brace + let depth = 0; + let endIdx = -1; + for (let i = braceIdx; i < content.length; i++) { + if (content[i] === '{') depth++; + else if (content[i] === '}') { + depth--; + if (depth === 0) { + endIdx = i; + break; + } + } + } + + if (endIdx !== -1) { + const block = content.substring(braceIdx, endIdx + 1); + const namePattern = new RegExp(`${nameKey}:\\s*['"]${nameValue}['"]`); + if (namePattern.test(block)) { + return block; + } + } + + searchStart = endIdx !== -1 ? endIdx + 1 : fnIdx + 1; + } + + return null; +} + +/** + * Parse environment variable patterns from a code string. + * Matches: + * - process.env.KEY = 'value' (non-empty string values only) + * - env: { KEY: 'value', ... } + * Only includes envs that have an actual non-empty value. + */ +function parseEnvVars(code) { + const envVars = {}; + + // Pattern 1: process.env.KEY = 'value' or "value" (non-empty values only) + const processEnvRegex = /process\.env\.(\w+)\s*=\s*['"]([^'"]+)['"]/g; + let match; + while ((match = processEnvRegex.exec(code)) !== null) { + envVars[match[1]] = match[2]; + } + + // Pattern 2: env: { KEY: 'value', ... } + const envObjRegex = /\benv:\s*\{([^}]+)\}/g; + while ((match = envObjRegex.exec(code)) !== null) { + const envContent = match[1]; + const kvRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]/g; + let kvMatch; + while ((kvMatch = kvRegex.exec(envContent)) !== null) { + envVars[kvMatch[1]] = kvMatch[2]; + } + } + + return envVars; +} + +/** + * Read the corresponding test file for an app or skeleton and extract + * environment variables that the tests set (so the manually created app + * behaves the same way). + * + * For --app : reads tools/e2e-tests/.test.js (whole file) + * For --skeleton : reads tools/e2e-tests/skeleton.test.js and + * scopes to the testMeteorSkeleton({ skeletonName: '' }) block. + */ +function extractEnvVarsFromTestFile(sourceName, isApp) { + const testFile = isApp + ? path.join(E2E_TESTS_DIR, `${sourceName}.test.js`) + : path.join(E2E_TESTS_DIR, 'skeleton.test.js'); + + if (!fs.existsSync(testFile)) return {}; + + const content = fs.readFileSync(testFile, 'utf8'); + let scope; + + if (isApp) { + scope = content; + } else { + scope = findTestHelperBlock(content, 'testMeteorSkeleton', 'skeletonName', sourceName); + if (!scope) return {}; + } + + return parseEnvVars(scope); +} + +/** + * Build a shell env prefix string from an env vars object. + * e.g., { METEOR_LOCAL_DIR: '.meteor/local-custom' } => "METEOR_LOCAL_DIR=.meteor/local-custom" + */ +function buildEnvPrefix(envVars) { + const entries = Object.entries(envVars); + if (entries.length === 0) return ''; + return entries.map(([key, value]) => `${key}=${value}`).join(' '); +} + +/** + * Replace bare `meteor` command occurrences in a script string with the full + * checkout path. Matches `meteor` only as a standalone command word: + * - not preceded by `/` or a word character (avoids already-resolved paths + * and things like `something-meteor`) + * - not followed by a word character (avoids `meteor-node-stubs` etc.) + */ +function rewriteMeteorCmd(scriptValue, meteorExecutable) { + return scriptValue.replace(/(? { + const inv = n === 'start' ? 'npm start' : `npm run ${n}`; + return Math.max(max, inv.length); + }, 0); + + const line = `${c.dim}${'─'.repeat(55)}${c.reset}`; + + console.log(''); + console.log(line); + console.log(` ${c.green}${c.bold}App ready at:${c.reset} ${c.cyan}${destDir}${c.reset}`); + console.log(line); + console.log(''); + console.log(` ${c.yellow}cd ${destDir}${c.reset}`); + console.log(''); + console.log(` ${c.bold}Run commands${c.reset} ${c.dim}(meteor checkout binary):${c.reset}`); + console.log(` ${c.dim}${m} run${c.reset}`); + console.log(` ${c.dim}${m} run --production${c.reset}`); + + if (hasTestModule) { + console.log(` ${c.dim}${m} test --driver-package meteortesting:mocha${c.reset}`); + console.log(` ${c.dim}${m} test --once --driver-package meteortesting:mocha${c.reset}`); + if (hasClient) { + console.log(` ${c.dim}${m} test --full-app --driver-package meteortesting:mocha${c.reset}`); + console.log(` ${c.dim}${m} test --full-app --once --driver-package meteortesting:mocha${c.reset}`); + } + } + + console.log(` ${c.dim}${m} build ./_build --directory${c.reset}`); + console.log(''); + console.log(` ${c.bold}npm scripts${c.reset} ${c.dim}(run from the app directory):${c.reset}`); + + for (const [name, cmd] of Object.entries(scripts)) { + const invocation = name === 'start' ? 'npm start' : `npm run ${name}`; + const padding = ' '.repeat(maxLen - invocation.length); + console.log(` ${c.cyan}${invocation}${c.reset}${padding} ${c.dim}# ${cmd}${c.reset}`); + } + + console.log(line); + console.log(''); +} + +async function setupFromApp(appName, destDir, { isMonorepo = false, force = false } = {}) { + const sourceDir = path.join(APPS_DIR, appName); + + if (!fs.existsSync(sourceDir)) { + const available = fs.readdirSync(APPS_DIR).join(', '); + throw new Error( + `App '${appName}' not found in tools/e2e-tests/apps/\nAvailable apps: ${available}` + ); + } + + if (fs.existsSync(destDir)) { + if (force) { + log.warn(`Removing existing destination: ${c.cyan}${destDir}${c.reset}`); + await fs.remove(destDir); + } else { + log.error(`Destination already exists: ${c.cyan}${destDir}${c.reset}`); + log.detail('Remove it first or use --force to replace it.'); + process.exit(1); + } + } + + await fs.ensureDir(path.dirname(destDir)); + + log.step(`Copying app ${c.bold}${appName}${c.reset} to ${c.cyan}${destDir}${c.reset}`); + await fs.copy(sourceDir, destDir, { + dereference: true, + preserveTimestamps: true, + overwrite: true, + }); + log.success('Copy complete.'); + + const appPackageJsonPath = isMonorepo + ? path.join(destDir, 'app', 'package.json') + : path.join(destDir, 'package.json'); + + const envVars = extractEnvVarsFromTestFile(appName, true); + if (Object.keys(envVars).length > 0) { + log.detail(`env from test file: ${c.magenta}${Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join(' ')}${c.reset}`); + } + + const meteorAppDir = isMonorepo ? path.join(destDir, 'app') : destDir; + const execEnv = Object.keys(envVars).length > 0 ? { env: { ...process.env, ...envVars } } : {}; + + log.step('Adding rspack package...'); + await execa(METEOR_EXECUTABLE, ['add', 'rspack'], { + cwd: meteorAppDir, + stdio: 'inherit', + ...execEnv, + }); + + log.step('Linking local @meteorjs/rspack...'); + await linkLocalRspack(meteorAppDir, { env: envVars }); + + if (isMonorepo) { + log.step('Running meteor npm install at root level...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { cwd: destDir, stdio: 'inherit', ...execEnv }); + log.step('Running meteor npm install at app level...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { + cwd: path.join(destDir, 'app'), + stdio: 'inherit', + ...execEnv, + }); + } else { + log.step('Running meteor npm install...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { cwd: destDir, stdio: 'inherit', ...execEnv }); + } + + if (fs.existsSync(appPackageJsonPath)) { + log.step('Injecting npm scripts into package.json...'); + await injectNpmScripts(appPackageJsonPath, envVars); + } + + return { destDir, appPackageJsonPath }; +} + +async function setupFromSkeleton(skeletonName, destDir, { force = false } = {}) { + if (fs.existsSync(destDir)) { + if (force) { + log.warn(`Removing existing destination: ${c.cyan}${destDir}${c.reset}`); + await fs.remove(destDir); + } else { + log.error(`Destination already exists: ${c.cyan}${destDir}${c.reset}`); + log.detail('Remove it first or use --force to replace it.'); + process.exit(1); + } + } + + const parentDir = path.dirname(destDir); + const appDirName = path.basename(destDir); + + await fs.ensureDir(parentDir); + + log.step(`Creating Meteor app ${c.bold}${appDirName}${c.reset} via ${c.dim}meteor create --${skeletonName}${c.reset}`); + await execa(METEOR_EXECUTABLE, ['create', `--${skeletonName}`, appDirName], { + cwd: parentDir, + stdio: 'inherit', + }); + + const appPackageJsonPath = path.join(destDir, 'package.json'); + + const envVars = extractEnvVarsFromTestFile(skeletonName, false); + if (Object.keys(envVars).length > 0) { + log.detail(`env from test file: ${c.magenta}${Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join(' ')}${c.reset}`); + } + const execEnv = Object.keys(envVars).length > 0 ? { env: { ...process.env, ...envVars } } : {}; + + log.step('Adding rspack package...'); + await execa(METEOR_EXECUTABLE, ['add', 'rspack'], { + cwd: destDir, + stdio: 'inherit', + ...execEnv, + }); + + log.step('Linking local @meteorjs/rspack...'); + await linkLocalRspack(destDir, { env: envVars }); + + log.step('Running meteor npm install...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { cwd: destDir, stdio: 'inherit', ...execEnv }); + + if (fs.existsSync(appPackageJsonPath)) { + log.step('Injecting npm scripts into package.json...'); + await injectNpmScripts(appPackageJsonPath, envVars); + } + + return { destDir, appPackageJsonPath }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printHelp(); + process.exit(0); + } + + if (!args.app && !args.skeleton) { + log.error('You must provide --app or --skeleton '); + printHelp(); + process.exit(1); + } + + if (args.app && args.skeleton) { + log.error('--app and --skeleton are mutually exclusive'); + process.exit(1); + } + + const sourceName = args.app || args.skeleton; + + // --output is the full destination path; if omitted, default to ./dist/ + const destDir = args.output + ? path.resolve(REPO_ROOT, args.output) + : path.join(DEFAULT_OUTPUT_DIR, sourceName); + + let result; + if (args.app) { + result = await setupFromApp(args.app, destDir, { isMonorepo: args.monorepo, force: args.force }); + } else { + result = await setupFromSkeleton(args.skeleton, destDir, { force: args.force }); + } + + printCommandSummary(result.destDir, result.appPackageJsonPath); +} + +main().catch(err => { + log.error(err.message); + process.exit(1); +}); diff --git a/tools/e2e-tests/scripts/link-rspack.js b/tools/e2e-tests/scripts/link-rspack.js new file mode 100644 index 0000000000..5e8b6bbb97 --- /dev/null +++ b/tools/e2e-tests/scripts/link-rspack.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +/** + * Links the local npm-packages/meteor-rspack into a Meteor app so it runs + * against the latest dev version. + * + * Steps: + * 1. Run `meteor update --npm` in the app + * 2. Install the matching @rspack/core and @rspack/cli versions into the + * local meteor-rspack package (read from packages/rspack/lib/constants.js) + * 3. Install `ignore-loader` in the app + * 4. `npm link` the local meteor-rspack into the app + * + */ + +const path = require('path'); +const fs = require('fs'); +const execa = require('execa'); + +const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); +const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor'); +const RSPACK_PACKAGE_DIR = path.join(REPO_ROOT, 'npm-packages', 'meteor-rspack'); +const CONSTANTS_PATH = path.join(REPO_ROOT, 'packages', 'rspack', 'lib', 'constants.js'); + +async function linkLocalRspack(appDir, { env } = {}) { + const execOpts = env ? { env: { ...process.env, ...env } } : {}; + + console.log(`Running meteor update --npm in ${appDir}...`); + await execa(METEOR_EXECUTABLE, ['update', '--npm'], { + cwd: appDir, + stdio: 'inherit', + ...execOpts, + }); + + const constantsContent = fs.readFileSync(CONSTANTS_PATH, 'utf8'); + const rspackVersionMatch = constantsContent.match( + /DEFAULT_RSPACK_VERSION\s*=\s*['"]([^'"]+)['"]/ + ); + const rspackVersion = rspackVersionMatch?.[1]; + if (rspackVersion) { + console.log(`Installing @rspack/core@${rspackVersion} and @rspack/cli@${rspackVersion}...`); + await execa( + 'npm', + [ + 'install', + `@rspack/core@${rspackVersion}`, + `@rspack/cli@${rspackVersion}`, + '--no-save', + '--no-package-lock', + ], + { cwd: RSPACK_PACKAGE_DIR } + ); + } + + console.log('Installing ignore-loader in the app...'); + await execa('npm', ['install', 'ignore-loader', '--save'], { cwd: appDir }); + + console.log(`Linking local meteor-rspack from ${RSPACK_PACKAGE_DIR}...`); + await execa('npm', ['link', RSPACK_PACKAGE_DIR], { cwd: appDir }); + + console.log('Local meteor-rspack linked successfully.'); +} + +module.exports = { linkLocalRspack, REPO_ROOT, METEOR_EXECUTABLE, RSPACK_PACKAGE_DIR }; + +// CLI mode +if (require.main === module) { + const appDir = process.argv[2]; + if (!appDir) { + console.error('Usage: node link-rspack.js '); + process.exit(1); + } + linkLocalRspack(path.resolve(appDir)).catch(err => { + console.error(err.message); + process.exit(1); + }); +} diff --git a/tools/e2e-tests/server-only.test.js b/tools/e2e-tests/server-only.test.js new file mode 100644 index 0000000000..53eca14d53 --- /dev/null +++ b/tools/e2e-tests/server-only.test.js @@ -0,0 +1,27 @@ +import { + waitForMeteorOutput, +} from './helpers'; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Other / Server-only App Bundling /', () => { + describe( + 'Meteor+Rspack Bundler /', + testMeteorRspackBundler({ + appName: 'server-only', + port: 3123, + skipClient: true, + skipTestClient: true, + filePaths: { + server: 'server/main.js', + }, + customAssertions: { + afterRun: async ({ result }) => { + await waitForMeteorOutput( + result.outputLines, + 'server/main.js loaded' + ); + }, + }, + }) + ); +}); diff --git a/tools/e2e-tests/skeleton.test.js b/tools/e2e-tests/skeleton.test.js new file mode 100644 index 0000000000..1556a00c51 --- /dev/null +++ b/tools/e2e-tests/skeleton.test.js @@ -0,0 +1,231 @@ +/** + * This file contains tests for different Meteor skeletons. + * It uses the testMeteorSkeleton function to test the creation, running, testing, and building + * of different Meteor skeletons (apollo, react, etc.). + */ + +import path from 'path'; +import fs from 'fs'; + +import { assertStyles } from './assertions'; +import { testMeteorSkeleton } from './test-helpers'; + +const isCI = process.env.GITHUB_ACTIONS === 'true'; + +describe('Meteor Skeletons /', () => { + describe( + 'Angular Skeleton /', + testMeteorSkeleton({ + skeletonName: 'angular', + port: 3213, + filePaths: { + client: 'client/main.ts', + server: 'server/main.ts', + test: 'tests/main.ts', + }, + }), + ); + + describe( + 'Apollo Skeleton /', + testMeteorSkeleton({ + skeletonName: 'apollo', + port: 3201, + filePaths: { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.js', + }, + }), + ); + + describe( + 'Babel Skeleton /', + testMeteorSkeleton({ + skeletonName: 'babel', + port: 3212, + filePaths: { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.js', + }, + }), + ); + + describe( + "Other / Bare Skeleton /", + testMeteorSkeleton({ + skeletonName: "bare", + port: 3219, + checkAppTitle: false, + checkBodyStyles: false, + skipTestClient: true, + skipBuildCacheCheck: true, + }) + ); + + describe( + 'Blaze Skeleton /', + testMeteorSkeleton({ + skeletonName: 'blaze', + port: 3202, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js', + }, + }), + ); + + describe( + 'ChakraUI Skeleton /', + testMeteorSkeleton({ + skeletonName: 'chakra-ui', + port: 3203, + filePaths: { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.js', + }, + checkBodyStyles: false, + }), + ); + + describe( + 'Coffeescript Skeleton /', + testMeteorSkeleton({ + skeletonName: 'coffeescript', + port: 3211, + filePaths: { + client: 'client/main.coffee', + server: 'server/main.coffee', + test: 'tests/main.coffee', + }, + }), + ); + + describe( + 'Other / Full Skeleton /', + testMeteorSkeleton({ + skeletonName: 'full', + port: 3204, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'imports/api/links/methods.tests.js', + }, + }) + ); + + describe( + 'React Skeleton /', + testMeteorSkeleton({ + skeletonName: 'react', + port: 3205, + filePaths: { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.js', + }, + bodyStyles: { + 'font-family': process.platform === 'darwin' + ? 'Inter, -apple-system, "system-ui", "Segoe UI", Roboto, sans-serif' + : 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + padding: '10px', + }, + }), + ); + + describe( + 'Solid Skeleton /', + testMeteorSkeleton({ + skeletonName: 'solid', + port: 3206, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js', + }, + }), + ); + + describe( + 'Svelte Skeleton /', + testMeteorSkeleton({ + skeletonName: 'svelte', + port: 3207, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js', + }, + }), + ); + + describe( + 'Other / Tailwind Skeleton /', + testMeteorSkeleton({ + skeletonName: 'tailwind', + port: 3208, + filePaths: { + client: 'client/main.tsx', + server: 'server/main.ts', + test: 'tests/main.ts', + }, + customAssertions: { + afterRun: async () => { + // Verify Tailwind styles for '.bg-gray-100' element + await assertStyles('.bg-gray-100', { + ['background-color']: 'oklch(0.967 0.003 264.542)', + }); + }, + afterRunProduction: async () => { + // Verify Tailwind styles for '.bg-gray-100' element + await assertStyles('.bg-gray-100', { + ['background-color']: 'lab(96.1596 -0.0823438 -1.13575)', + }); + }, + }, + }) + ); + + describe( + 'Typescript Skeleton /', + testMeteorSkeleton({ + skeletonName: 'typescript', + port: 3209, + filePaths: { + client: 'client/main.tsx', + server: 'server/main.ts', + test: 'tests/main.ts', + }, + customAssertions: { + afterCreate({ tempDir }) { + if (isCI) { + const rspackConfigPath = path.join(tempDir, 'rspack.config.ts'); + // Remove the TsCheckerRspackPlugin plugin as is resource-intense, CI gets exhausted and fails + let configContent = fs.readFileSync(rspackConfigPath, 'utf8'); + configContent = configContent.replace( + /\s*new\s+TsCheckerRspackPlugin\(\)/, + '', + ); + fs.writeFileSync(rspackConfigPath, configContent); + } + }, + }, + }), + ); + + describe( + 'Vue Skeleton /', + testMeteorSkeleton({ + skeletonName: 'vue', + port: 3210, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js', + }, + }), + ); +}); diff --git a/tools/e2e-tests/solid.test.js b/tools/e2e-tests/solid.test.js new file mode 100644 index 0000000000..5ada134998 --- /dev/null +++ b/tools/e2e-tests/solid.test.js @@ -0,0 +1,26 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Solid App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'solid', + port: 3122, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js' + }, + customAssertions: { + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + } + })); +}); diff --git a/tools/e2e-tests/svelte.test.js b/tools/e2e-tests/svelte.test.js new file mode 100644 index 0000000000..f61dd55ff7 --- /dev/null +++ b/tools/e2e-tests/svelte.test.js @@ -0,0 +1,26 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Svelte App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'svelte', + port: 3122, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js' + }, + customAssertions: { + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + } + })); +}); diff --git a/tools/e2e-tests/test-helpers.js b/tools/e2e-tests/test-helpers.js new file mode 100644 index 0000000000..52c11201f3 --- /dev/null +++ b/tools/e2e-tests/test-helpers.js @@ -0,0 +1,1163 @@ +/** + * This file contains helper functions for testing Meteor applications. + * It provides reusable test patterns for both the test apps. + */ + +import { + appendFileContent, + buildMeteorApp, + cleanupTempDir, + createMeteorApp, + killMeteorProcess, + killProcessByPort, + runMeteorApp, + runMeteorCommand, + runMeteorTests, + setupMeteorApp, + wait, + waitForMeteorOutput, + waitForPlaywrightConsole +} from "./helpers"; +import { + assertBodyStyles, + assertFileExist, + assertMeteorApp, + assertMeteorReactApp, + assertPathNotExist, + assertRspackScriptTag +} from "./assertions"; +import fs from "fs-extra"; +import path from "path"; +import execa from "execa"; +import waitOn from "wait-on"; + +const isCI = process.env.GITHUB_ACTIONS === "true"; +// Link local npm-packages/meteor-rspack so tests run against the latest dev version. +// Set NPM_LINK_RSPACK=false to disable. +const npmLinkLocalRspack = process.env.NPM_LINK_RSPACK !== 'false'; +if (!npmLinkLocalRspack) { + console.warn( + '\x1b[33m⚠ NPM_LINK_RSPACK=false — tests will install @meteorjs/rspack from npm.\n' + + ' If CI fails, ensure the latest @meteorjs/rspack version has been published.\x1b[0m' + ); +} + +const WAIT_ON = isCI ? 2000 : 500; + +const { linkLocalRspack: _linkLocalRspack } = require('./scripts/link-rspack'); + +async function linkLocalRspack(appDir) { + if (!npmLinkLocalRspack) return; + await _linkLocalRspack(appDir); +} + +/** + * Helper function to set up and run tests for the Meteor Bundler + * @param {Object} options - Options for the test + * @param {string} options.appName - Name of the app ('react' or 'typescript') + * @param {number} options.port - Port to run the app on + * @param {Function} options.customAssertions - Custom assertions to run after the app is started + * @param {Function} options.beforeAllBehavior - Additional behavior to run in beforeAll + * @param {Function} options.afterAllBehavior - Additional behavior to run in afterAll + * @returns {Function} - Jest test function + */ +export function testMeteorBundler(options) { + const { appName, port, customAssertions, beforeAllBehavior, afterAllBehavior, env = {} } = options; + + return () => { + let meteorProcess; + let tempDir; + + beforeAll(async () => { + // Run additional beforeAll behavior + if (beforeAllBehavior) { + await beforeAllBehavior({ tempDir, port }); + } + + // Ensure any process on the port is killed + await killProcessByPort(port); + + // Setup the Meteor app + tempDir = (await setupMeteorApp(appName))?.tempDir; + + // Link local meteor-rspack so the app picks up the latest dev version + await linkLocalRspack(tempDir); + }); + + afterAll(async () => { + // Clean up the temporary directory + await cleanupTempDir(tempDir); + + // Run additional afterAll behavior + if (afterAllBehavior) { + await afterAllBehavior({ tempDir, port }); + } + }); + + beforeEach(async () => { + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + + test(`"meteor run" / should start the app`, async () => { + // Run the Meteor app + meteorProcess = (await runMeteorApp(tempDir, port, { env: env.meteorRun }))?.meteorProcess; + + // Assert that the Meteor app is running correctly + await assertMeteorReactApp(port, { title: appName }); + + // Run custom assertions if provided + if (customAssertions) { + await customAssertions({ tempDir, port, meteorProcess }); + } + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + }; +} + +/** + * Helper function to set up and run tests for the Meteor+Rspack Bundler + * @param {Object} options - Options for the test + * @param {string} options.appName - Name of the app ('react', 'typescript', etc) + * @param {number} options.port - Port to run the app on + * @param {Object} options.filePaths - File paths for the app + * @param {string} options.filePaths.client - Client file path (e.g., 'client/main.jsx') + * @param {string} options.filePaths.server - Server file path (e.g., 'server/main.js') + * @param {string} options.filePaths.test - Test file path (e.g., 'tests/main.js') + * @param {Object} options.customMessages - Custom messages for console logs + * @param {string} options.customMessages.devClient - Message for development client + * @param {string} options.customMessages.devServer - Message for development server + * @param {string} options.customMessages.prodClient - Message for production client + * @param {string} options.customMessages.prodServer - Message for production server + * @param {string} options.customMessages.test - Message for test + * @param {Function} options.customAssertions - Custom assertions to run after each test + * @param {boolean} options.verbose - Whether to enable verbose output (default: true) + * @param {boolean} options.testFullApp - Whether to run tests with the --full-app flag (default: false) + * @param {boolean} options.testBundleVisualizer - Whether to run tests with bundle-visualizer in production mode (default: false) + * @param {boolean} options.skipClient - Whether to skip client-specific assertions (default: false) + * @param {boolean} options.skipTestClient - Whether to skip client-side tests (default: false) + * @param {string[]} options.checkBundleFilePaths - Array of file paths to check for existence in the bundle + * @param {Function} options.beforeAllBehavior - Additional behavior to run in beforeAll + * @param {Function} options.afterAllBehavior - Additional behavior to run in afterAll + * @returns {Function} - Jest test function + */ +export function testMeteorRspackBundler(options) { + const { + appName, + port, + isMonorepo = false, + filePaths = { + client: 'client/main.jsx', + server: 'server/main.js', + test: 'tests/main.js', + testClient: undefined, + testServer: undefined, + }, + customMessages = { + devClient: "Hello from dev client", + devServer: "Hello from dev server", + prodClient: "Hello from prod client", + prodServer: "Hello from prod server", + test: "Hello from test", + testClient: "Hello from test client", + testServer: "Hello from test server", + }, + customUpdates = { + devClient: (message) => `if (Meteor.isDevelopment) console.log("${message}");`, + devServer: (message) => `if (Meteor.isDevelopment) console.log("${message}");`, + prodClient: (message) => `if (Meteor.isProduction) console.log("${message}");`, + prodServer: (message) => `if (Meteor.isProduction) console.log("${message}");`, + test: (message) => `console.log("${message}");` + }, + customAssertions, + // Some existing tests may fail if this is not set + verbose = true, + // Option to run tests with --full-app flag + testFullApp = false, + // Option to test with bundle-visualizer in production mode + testBundleVisualizer = false, + // Array of file paths to check for existence in the bundle + checkBundleFilePaths = [], + // Additional behavior for beforeAll and afterAll + beforeAllBehavior, + afterAllBehavior, + // Build directory (default: '_build') + buildDir = '_build', + // Assets context directory (default: 'build-assets') + assetsContext = 'build-assets', + // Chunks context directory (default: 'build-chunks') + chunksContext = 'build-chunks', + // Rspack config file (default: 'rspack.config.js') + configFile = 'rspack.config.js', + // Custom environment variables + // Per-phase env vars: { meteorRun, meteorRunProduction, meteorTest, meteorTestOnce, meteorBuild } + env = {}, + // Whether to skip client-specific assertions + skipClient = false, + // Whether to skip client-side tests + skipTestClient = false, + // Skip isDevelopment/isProduction/isRun/isTest/isBuild verbose output checks + skipEnvCheck = false, + } = options; + + return () => { + let meteorProcess; + let tempDir; + let appDir; + + beforeAll(async () => { + // Run additional beforeAll behavior + if (beforeAllBehavior) { + await beforeAllBehavior({ tempDir, port }); + } + + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + + // Setup the Meteor app + tempDir = (await setupMeteorApp(appName, { isMonorepo }))?.tempDir; + + // Wait for a margin + await wait(WAIT_ON); + + // Run custom assertions if provided + if (customAssertions.afterCreate) { + await customAssertions.afterCreate({ tempDir }); + } + + // Add Rspack package + appDir = isMonorepo ? path.join(tempDir, 'app') : tempDir; + + await runMeteorCommand("add", ["rspack"], appDir, { + checkExitCode: true, + }); + + // Link local meteor-rspack so the app picks up the latest dev version + await linkLocalRspack(appDir); + + // Set meteor.modern.verbose to true + if (verbose) { + await execa('npm', ['pkg', 'delete', 'meteor.modern'], { cwd: appDir }); + await execa('npm', ['pkg', 'set', 'meteor.modern.verbose=true'], { cwd: appDir }); + } + + // Run the Meteor app to install Rspack + const result = await runMeteorApp(tempDir, port, { + waitForOutput: "=> App running at", + isMonorepo, + env: { ...env, ...(env.meteorRun || {}) }, + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the config files exists + await assertFileExist(appDir, '.gitignore', { content: buildDir }); + await assertFileExist(appDir, configFile, { content: '@meteorjs/rspack' }); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterInit) { + await customAssertions.afterInit({ tempDir, port, meteorProcess, result }); + } + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + + afterAll(async () => { + // Clean up the temporary directory + await cleanupTempDir(tempDir); + + // Run additional afterAll behavior + if (afterAllBehavior) { + await afterAllBehavior({ tempDir, port }); + } + }); + + beforeEach(async () => { + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + + test(`"meteor run" / should run and rebuild the app with Rspack`, async () => { + // Run the Meteor app and wait for "restarted at" output + const result = await runMeteorApp(tempDir, port, { + waitForOutput: "=> App running at", + isMonorepo, + skipWaitOn: skipClient, + env: { ...env, ...(env.meteorRun || {}) }, + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the app files exists + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/main-dev/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-dev/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-dev/client-meteor.js`); + } + await assertFileExist(appDir, `${buildDir}/main-dev/server-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-dev/server-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-dev/server-meteor.js`); + + if (!skipClient) { + // Assert that the Meteor app is running correctly + await assertMeteorReactApp(port, { title: appName }); + + // Assert that the app is using Rspack + await assertRspackScriptTag(port, true); + + // Assert that the body has the expected CSS styles + await assertBodyStyles({ + 'padding': '10px', + 'font-family': 'sans-serif' + }); + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRun) { + await customAssertions.afterRun({ tempDir, port, meteorProcess, result }); + } + + // Update the client code + if (!skipClient) { + await appendFileContent(tempDir, filePaths.client, { + content: customUpdates.devClient(customMessages.devClient), + }); + const consoleLogs = await waitForPlaywrightConsole(customMessages.devClient, { returnAllLogs: true }); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunRebuildClient) { + await customAssertions.afterRunRebuildClient({ + tempDir, + port, + meteorProcess, + result, + allConsoleLogs: consoleLogs.allLogs + }); + } + } + + // Update the server code + await appendFileContent(tempDir, filePaths.server, { + content: customUpdates.devServer(customMessages.devServer), + }); + await waitForMeteorOutput( + result.outputLines, + customMessages.devServer + ); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunRebuildServer) { + await customAssertions.afterRunRebuildServer({ tempDir, port, meteorProcess, result }); + } + + if (verbose && !skipEnvCheck) { + await waitForMeteorOutput( + result.outputLines, + /.*isDevelopment:.*true.*/ + ); + await waitForMeteorOutput( + result.outputLines, + /.*isRun:.*true.*/ + ); + } + + // Wait for a margin + await wait(WAIT_ON); + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + + test(`"meteor run --production" / should run and rebuild the app with Rspack in production`, async () => { + // Run the Meteor app and wait for "restarted at" output + const result = await runMeteorApp(tempDir, port, { + waitForOutput: "=> App running at", + commandOptions: ['--production'], + isMonorepo, + skipWaitOn: skipClient, + env: { ...env, ...(env.meteorRunProduction || {}) }, + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the app files exists + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/main-prod/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/client-meteor.js`); + } + await assertFileExist(appDir, `${buildDir}/main-prod/server-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/server-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/server-meteor.js`); + + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/main-prod/index.html`); + } + + await assertFileExist(tempDir, filePaths.server); + + if (!skipClient) { + // Assert that the Meteor app is running correctly + await assertMeteorReactApp(port, { title: appName }); + + // Assert that the app is using Rspack + await assertRspackScriptTag(port, false); + + // Assert that the body has the expected CSS styles + await assertBodyStyles({ + 'padding': '10px', + 'font-family': 'sans-serif' + }); + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunProduction) { + await customAssertions.afterRunProduction({ tempDir, port, meteorProcess, result }); + } + + // Update the client code + if (!skipClient) { + await appendFileContent(tempDir, filePaths.client, { + content: customUpdates.prodClient(customMessages.prodClient), + }); + const consoleLogs = await waitForPlaywrightConsole(customMessages.prodClient, { returnAllLogs: true }); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunProductionRebuildClient) { + await customAssertions.afterRunProductionRebuildClient({ + tempDir, + port, + meteorProcess, + result, + allConsoleLogs: consoleLogs.allLogs + }); + } + } + + // Update the server code + await appendFileContent(tempDir, filePaths.server, { + content: customUpdates.prodServer(customMessages.prodServer), + }); + await waitForMeteorOutput( + result.outputLines, + customMessages.prodServer + ); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunProductionRebuildServer) { + await customAssertions.afterRunProductionRebuildServer({ tempDir, port, meteorProcess, result }); + } + + if (verbose && !skipEnvCheck) { + await waitForMeteorOutput( + result.outputLines, + /.*isProduction:.*true.*/ + ); + await waitForMeteorOutput( + result.outputLines, + /.*isRun:.*true.*/ + ); + } + + // Wait for a margin + await wait(WAIT_ON); + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + + // Conditional test for bundle-visualizer in production mode + if (testBundleVisualizer) { + test(`"meteor run --extra-packages bundle-visualizer --production" / should run with bundle-visualizer in production mode`, async () => { + // Run the Meteor app with bundle-visualizer in production mode + const result = await runMeteorApp(tempDir, port, { + waitForOutput: "=> App running at", + commandOptions: ['--extra-packages', 'bundle-visualizer', '--production'], + isMonorepo, + env: env.meteorRunProduction + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the app files exists + await assertFileExist(appDir, `${buildDir}/main-prod/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/client-meteor.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/server-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/server-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/server-meteor.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/index.html`); + + // Assert that the Meteor app is running correctly + await assertMeteorReactApp(port, { title: appName }); + + // Wait for bundle-visualizer ports to be available + console.log('Waiting for bundle-visualizer ports 8081 and 8082 to be available...'); + try { + await waitOn({ + resources: [ + `http-get://localhost:8081`, + `http-get://localhost:8082` + ], + timeout: 30000 + }); + console.log('Bundle-visualizer ports 8081 and 8082 are available'); + } catch (error) { + console.error('Error waiting for bundle-visualizer ports:', error); + throw error; + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunBundleVisualizer) { + await customAssertions.afterRunBundleVisualizer({ tempDir, port, meteorProcess, result }); + } + + // Wait for a margin + await wait(WAIT_ON); + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + } + + test(`"meteor test${testFullApp ? ' --full-app' : ''}" / should run tests with Rspack`, async () => { + const result = await runMeteorTests(tempDir, port, { + waitForOutput: skipTestClient + ? 'TEST_CLIENT=0' + : '=> App running at', + commandOptions: testFullApp ? ['--full-app'] : [], + checkTestResults: false, + isMonorepo, + testClient: !skipTestClient, + env: { ...env, ...(env.meteorTest || {}) }, + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + const isTestModule = filePaths.test && !filePaths.testClient && !filePaths.testServer; + + // Assert that the app files exists + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/test/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/test/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/test/client-meteor.js`); + } + await assertFileExist(appDir, `${buildDir}/test/server-entry.js`); + await assertFileExist(appDir, `${buildDir}/test/server-rspack.js`); + await assertFileExist(appDir, `${buildDir}/test/server-meteor.js`); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterTest) { + await customAssertions.afterTest({ tempDir, port, meteorProcess, result }); + } + + // Update the test code + if (isTestModule) { + if (filePaths.test) { + await appendFileContent(tempDir, filePaths.test, { + content: customUpdates.test(customMessages.test), + }); + await waitForMeteorOutput(result.outputLines, customMessages.test); + } + } else { + if (!skipClient && filePaths.testClient) { + await appendFileContent(tempDir, filePaths.testClient, { + content: customUpdates.test(customMessages.testClient), + }); + await waitForMeteorOutput( + result.outputLines, + customMessages.testClient + ); + } + + if (filePaths.testServer) { + await appendFileContent(tempDir, filePaths.testServer, { + content: customUpdates.test(customMessages.testServer), + }); + await waitForMeteorOutput( + result.outputLines, + customMessages.testServer + ); + } + } + + if (verbose && !skipEnvCheck) { + await waitForMeteorOutput( + result.outputLines, + /.*isDevelopment:.*true.*/ + ); + await waitForMeteorOutput( + result.outputLines, + /.*isTest:.*true.*/ + ); + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterTestRebuild) { + await customAssertions.afterTestRebuild({ tempDir, port, meteorProcess, result }); + } + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + + test(`"meteor test${testFullApp ? ' --full-app' : ''} --once" / should run tests once with Rspack`, async () => { + // Test the app with Rspack once + const result = await runMeteorTests(tempDir, port, { + waitForOutput: skipTestClient + ? 'TEST_CLIENT=0' + : '=> App running at', + commandOptions: testFullApp ? ['--full-app', '--once'] : ['--once'], + checkTestResults: true, + isMonorepo, + testClient: !skipTestClient, + env: { ...env, ...(env.meteorTestOnce || {}) }, + }); + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the app files exists + await assertFileExist(appDir, `${buildDir}/test/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/test/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/test/client-meteor.js`); + await assertFileExist(appDir, `${buildDir}/test/server-entry.js`); + await assertFileExist(appDir, `${buildDir}/test/server-rspack.js`); + await assertFileExist(appDir, `${buildDir}/test/server-meteor.js`); + + if (verbose && !skipEnvCheck) { + await waitForMeteorOutput( + result.outputLines, + /.*isDevelopment:.*true.*/ + ); + await waitForMeteorOutput( + result.outputLines, + /.*isTest:.*true.*/ + ); + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterTestOnce) { + await customAssertions.afterTestOnce({ tempDir, port, result }); + } + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + + test(`"meteor build" / should build the app with Rspack`, async () => { + // Build the app with Rspack + const { buildOutputDir, processResult: result } = await buildMeteorApp(tempDir, { + commandOptions: ['--directory'], + captureOutput: true, + isMonorepo, + env: env.meteorBuild + }); + + // Wait for a margin + await wait(WAIT_ON); + + if (verbose && !skipEnvCheck) { + await waitForMeteorOutput( + result.outputLines, + /.*isProduction:.*true.*/ + ); + await waitForMeteorOutput( + result.outputLines, + /.*isBuild:.*true.*/ + ); + } + + try { + // Assert that the build output directory exists + const buildDirExists = await fs.pathExists(buildOutputDir); + expect(buildDirExists).toBe(true); + + // Assert that the main.js file exists + expect(await fs.pathExists(`${buildOutputDir}/bundle/main.js`)).toBe(true); + + // Assert that the server/package.json file exists + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/package.json`)).toBe(true); + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/program.json`)).toBe(true); + + // Assert that the [web.browser|web.browser.legacy]/program.json file exists + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser/program.json`)).toBe(true); + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser.legacy/program.json`)).toBe(true); + + // Run npm install in the server directory + console.log('Running npm install in the server directory...'); + const serverDir = path.join(buildOutputDir, 'bundle', 'programs', 'server'); + const npmInstallResult = await execa('npm', ['install'], { + cwd: serverDir, + stdio: 'inherit', + shell: true, + }); + + // Check if the npm install command was successful + expect(npmInstallResult.exitCode).toBe(0); + + // Check for the existence of specified file paths in the bundle + const fileCheckResults = {}; + if (checkBundleFilePaths.length > 0) { + console.log(`Checking for existence of ${checkBundleFilePaths.length} file paths in the bundle...`); + + // Check each file path + for (const filePath of checkBundleFilePaths) { + const fullPath = path.join(buildOutputDir, 'bundle', filePath); + try { + const exists = await fs.pathExists(fullPath); + fileCheckResults[filePath] = exists; + console.log(`Checking file ${filePath}: ${exists ? 'exists' : 'does not exist'}`); + expect(exists).toBe(true); + } catch (error) { + console.error(`Error checking file ${filePath}:`, error); + fileCheckResults[filePath] = false; + expect(false).toBe(true); // This will fail the test + } + } + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterBuild) { + await customAssertions.afterBuild({ tempDir, buildOutputDir, result, fileCheckResults }); + } + } finally { + // Clean up the build output directory + await cleanupTempDir(buildOutputDir); + } + }); + + test(`"meteor reset" / should clear all caches and build artifacts`, async () => { + // Derive METEOR_LOCAL_DIR-aware paths for assertions + const resetEnv = { ...env, ...(env.meteorReset || {}) }; + const meteorLocalDirEnv = resetEnv.METEOR_LOCAL_DIR; + const meteorLocalDirName = meteorLocalDirEnv + ? path.basename(meteorLocalDirEnv.replace(/\\/g, '/')) + : ''; + const localDirSuffix = meteorLocalDirName ? `-${meteorLocalDirName}` : ''; + + // Verify build artifacts exist from previous tests + await assertFileExist(appDir, buildDir); + await assertFileExist(appDir, 'node_modules/.cache/rspack'); + + // Run meteor reset + await runMeteorCommand("reset", [], appDir, { + checkExitCode: true, + env: resetEnv, + }); + + // Verify Rspack build artifacts removed + await assertPathNotExist(appDir, buildDir); + await assertPathNotExist(appDir, 'node_modules/.cache/rspack'); + await assertPathNotExist(appDir, assetsContext); + await assertPathNotExist(appDir, chunksContext); + await assertPathNotExist(appDir, `public/${assetsContext}`); + await assertPathNotExist(appDir, `public/${chunksContext}`); + + // Also verify defaults are cleaned to prevent regressions + await assertPathNotExist(appDir, '_build'); + await assertPathNotExist(appDir, 'public/build-assets'); + await assertPathNotExist(appDir, 'public/build-chunks'); + + // When METEOR_LOCAL_DIR is set, also verify suffixed paths are cleaned + if (localDirSuffix) { + await assertPathNotExist(appDir, `${buildDir}${localDirSuffix}`); + await assertPathNotExist(appDir, `${assetsContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `${chunksContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `public/${assetsContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `public/${chunksContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `_build${localDirSuffix}`); + await assertPathNotExist(appDir, `public/build-assets${localDirSuffix}`); + await assertPathNotExist(appDir, `public/build-chunks${localDirSuffix}`); + } + + // Verify default .meteor/local caches are always cleaned + await assertPathNotExist(appDir, '.meteor/local/build'); + await assertPathNotExist(appDir, '.meteor/local/bundler-cache'); + await assertPathNotExist(appDir, '.meteor/local/plugin-cache'); + + // When METEOR_LOCAL_DIR is set, also verify custom local dir is cleaned + if (meteorLocalDirEnv && meteorLocalDirEnv !== '.meteor/local') { + await assertPathNotExist(appDir, `${meteorLocalDirEnv}/build`); + await assertPathNotExist(appDir, `${meteorLocalDirEnv}/bundler-cache`); + await assertPathNotExist(appDir, `${meteorLocalDirEnv}/plugin-cache`); + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterReset) { + await customAssertions.afterReset({ tempDir, appDir }); + } + }); + }; +} + +/** + * Helper function to test a Meteor skeleton + * @param {Object} options - Options for the test + * @param {string} options.skeletonName - Name of the skeleton to test (e.g., 'react', 'apollo', 'vue') + * @param {string} options.title - Title to use for assertions (defaults to skeletonName if not provided) + * @param {number} options.port - Port to run the app on + * @param {Object} options.filePaths - File paths for the app + * @param {string} options.filePaths.client - Client file path (e.g., 'client/main.jsx') + * @param {string} options.filePaths.server - Server file path (e.g., 'server/main.js') + * @param {string} options.filePaths.test - Test file path (e.g., 'tests/main.js') + * @param {Object} options.customAssertions - Custom assertions to run after each step + * @param {Function} options.customAssertions.afterCreate - Custom assertions to run after creating the app + * @param {Function} options.customAssertions.afterRun - Custom assertions to run after running the app + * @param {Function} options.customAssertions.afterRunProduction - Custom assertions to run after running the app in production mode + * @param {Function} options.customAssertions.afterTestOnce - Custom assertions to run after running tests once + * @param {Function} options.customAssertions.afterBuild - Custom assertions to run after building the app + * @param {boolean} options.checkBodyStyles - Whether to check the body styles (default: true) + * @param {boolean} options.checkAppTitle - Whether to check the Meteor app title (default: true) + * @param {Object} options.bodyStyles - Expected CSS styles for the body + * @param {boolean} options.skipTestClient - Whether to skip client-side tests (default: false) + * @param {string[]} options.checkBundleFilePaths - Array of file paths to check for existence in the bundle + * @param {Function} options.beforeAllBehavior - Additional behavior to run in beforeAll + * @param {Function} options.afterAllBehavior - Additional behavior to run in afterAll + * @returns {Function} - Jest test function + */ +export function testMeteorSkeleton(options) { + const { + skeletonName, + title = skeletonName, // Default to skeletonName if title is not provided + port, + filePaths = { + client: "client/main.jsx", + server: "server/main.js", + test: "tests/main.js" + }, + customAssertions = {}, + checkBodyStyles = true, + checkAppTitle = true, + bodyStyles, + skipTestClient = false, + checkBundleFilePaths = [], + beforeAllBehavior, + afterAllBehavior, + // Per-phase env vars: { meteorRun, meteorRunProduction, meteorTest, meteorBuild } + env = {}, + // Bare skeleton may not create build artifacts (e.g. _build, node_modules/.cache/rspack) + skipBuildCacheCheck = false, + // Assets context directory (default: 'build-assets') + assetsContext = 'build-assets', + // Chunks context directory (default: 'build-chunks') + chunksContext = 'build-chunks', + } = options; + + return () => { + let meteorProcess; + let tempDir; + + beforeAll(async () => { + // Run additional beforeAll behavior + if (beforeAllBehavior) { + await beforeAllBehavior({ tempDir, port }); + } + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + + afterAll(async () => { + // Clean up the temporary directory + if (tempDir) { + await cleanupTempDir(tempDir); + } + + // Run additional afterAll behavior + if (afterAllBehavior) { + await afterAllBehavior({ tempDir, port }); + } + }); + + beforeEach(async () => { + // Ensure any process on the port is killed + await killProcessByPort([port, '8080']); + }); + + test(`"meteor create --${skeletonName}" / should create a new Meteor ${skeletonName} app`, async () => { + // Create a new Meteor app with the specified skeleton + const result = await createMeteorApp(skeletonName, skeletonName); + tempDir = result.tempDir; + const newAppMeteorProcess = result.meteorProcess; + + // Wait for the process to complete + await newAppMeteorProcess; + + // Check if the app directory exists + const appDirExists = await fs.pathExists(tempDir); + expect(appDirExists).toBe(true); + + // Check if package.json exists + const packageJsonPath = path.join(tempDir, "package.json"); + const packageJsonExists = await fs.pathExists(packageJsonPath); + expect(packageJsonExists).toBe(true); + + // Link local meteor-rspack so the app picks up the latest dev version + await linkLocalRspack(tempDir); + + // Run custom assertions if provided + if (customAssertions.afterCreate) { + await customAssertions.afterCreate({ tempDir, packageJsonPath }); + } + }); + + test(`"meteor run" / should run the ${skeletonName} app`, async () => { + // Run the newly created app + const result = await runMeteorApp(tempDir, port, { + waitForOutput: "=> App running at", + env: env.meteorRun + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the Meteor app is running correctly + if (checkAppTitle) { + await assertMeteorApp(port, { title }); + } + + if (checkBodyStyles) { + // Assert that the body has the expected CSS styles + await assertBodyStyles(bodyStyles || { + "padding": "10px", + "font-family": "sans-serif" + }); + } + + // Run custom assertions if provided + if (customAssertions.afterRun) { + await customAssertions.afterRun({ tempDir, port, meteorProcess, result }); + } + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + + test(`"meteor run --production" / should run the ${skeletonName} app in production mode`, async () => { + // Run the app in production mode + const result = await runMeteorApp(tempDir, port, { + waitForOutput: "=> App running at", + commandOptions: ["--production"], + env: env.meteorRunProduction + }); + meteorProcess = result.meteorProcess; + + // Wait for a margin + await wait(WAIT_ON); + + // Assert that the Meteor app is running correctly + if (checkAppTitle) { + await assertMeteorApp(port, { title }); + } + + if (checkBodyStyles) { + // Assert that the body has the expected CSS styles + await assertBodyStyles(bodyStyles || { + "padding": "10px", + "font-family": "sans-serif" + }); + } + + // Run custom assertions if provided + if (customAssertions.afterRunProduction) { + await customAssertions.afterRunProduction({ tempDir, port, meteorProcess, result }); + } + + // Kill the meteor process + await killMeteorProcess(meteorProcess); + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + + test(`"meteor test --once" / should run tests once for the ${skeletonName} app`, async () => { + // Install playwright as a dev dependency + console.log("Installing playwright as a dev dependency..."); + const repoRoot = path.resolve(process.cwd(), "..", ".."); + const meteorBin = path.join(repoRoot, "meteor"); + await execa.command(`${meteorBin} npm i --save-dev playwright`, { + cwd: tempDir, + stdio: "inherit", + shell: true + }); + await linkLocalRspack(tempDir); + + // Run tests once for the app + const result = await runMeteorTests(tempDir, port, { + waitForOutput: skipTestClient ? "TEST_CLIENT=0" : "=> App running at", + commandOptions: ["--once"], + checkTestResults: true, + testClient: !skipTestClient, + env: { ...env, ...(env.meteorTest || {}) }, + }); + + // Wait for a margin + await wait(WAIT_ON); + + // Run custom assertions if provided + if (customAssertions.afterTestOnce) { + await customAssertions.afterTestOnce({ tempDir, port, result }); + } + + // Ensure any process on the port is killed + await killProcessByPort(port); + }); + + test(`"meteor build" / should build the ${skeletonName} app`, async () => { + // Build the app + const { buildOutputDir, processResult: result } = await buildMeteorApp(tempDir, { + commandOptions: ["--directory"], + captureOutput: true, + env: { ...env, ...(env.meteorBuild || {}) }, + }); + + // Wait for a margin + await wait(WAIT_ON); + + try { + // Assert that the build output directory exists + const buildDirExists = await fs.pathExists(buildOutputDir); + expect(buildDirExists).toBe(true); + + // Assert that the main.js file exists + expect(await fs.pathExists(`${buildOutputDir}/bundle/main.js`)).toBe(true); + + // Assert that the server/package.json file exists + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/package.json`)).toBe(true); + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/server/program.json`)).toBe(true); + + // Assert that the [web.browser|web.browser.legacy]/program.json file exists + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser/program.json`)).toBe(true); + expect(await fs.pathExists(`${buildOutputDir}/bundle/programs/web.browser.legacy/program.json`)).toBe(true); + + // Check for the existence of specified file paths in the bundle + const fileCheckResults = {}; + if (checkBundleFilePaths.length > 0) { + console.log(`Checking for existence of ${checkBundleFilePaths.length} file paths in the bundle...`); + + // Check each file path + for (const filePath of checkBundleFilePaths) { + const fullPath = path.join(buildOutputDir, 'bundle', filePath); + try { + const exists = await fs.pathExists(fullPath); + fileCheckResults[filePath] = exists; + console.log(`Checking file ${filePath}: ${exists ? 'exists' : 'does not exist'}`); + expect(exists).toBe(true); + } catch (error) { + console.error(`Error checking file ${filePath}:`, error); + fileCheckResults[filePath] = false; + expect(false).toBe(true); // This will fail the test + } + } + } + + // Run custom assertions if provided + if (customAssertions.afterBuild) { + await customAssertions.afterBuild({ tempDir, buildOutputDir, result, fileCheckResults }); + } + } finally { + // Clean up the build output directory + await cleanupTempDir(buildOutputDir); + } + }); + + test(`"meteor reset" / should clear all caches and build artifacts`, async () => { + // Derive METEOR_LOCAL_DIR-aware paths for assertions + const resetEnv = { ...env, ...(env.meteorReset || {}) }; + const meteorLocalDirEnv = resetEnv.METEOR_LOCAL_DIR; + const meteorLocalDirName = meteorLocalDirEnv + ? path.basename(meteorLocalDirEnv.replace(/\\/g, '/')) + : ''; + const localDirSuffix = meteorLocalDirName ? `-${meteorLocalDirName}` : ''; + + // Verify build artifacts exist from previous tests + if (!skipBuildCacheCheck) { + await assertFileExist(tempDir, "_build"); + await assertFileExist(tempDir, "node_modules/.cache/rspack"); + } + + // Run meteor reset + await runMeteorCommand('reset', [], tempDir, { + checkExitCode: true, + env: resetEnv, + }); + + // Verify Rspack build artifacts removed + await assertPathNotExist(tempDir, '_build'); + await assertPathNotExist(tempDir, 'node_modules/.cache/rspack'); + await assertPathNotExist(tempDir, 'node_modules/.cache/meteor'); + await assertPathNotExist(tempDir, assetsContext); + await assertPathNotExist(tempDir, chunksContext); + await assertPathNotExist(tempDir, `public/${assetsContext}`); + await assertPathNotExist(tempDir, `public/${chunksContext}`); + + // Also verify defaults are cleaned to prevent regressions + await assertPathNotExist(tempDir, 'public/build-assets'); + await assertPathNotExist(tempDir, 'public/build-chunks'); + + // When METEOR_LOCAL_DIR is set, also verify suffixed paths are cleaned + if (localDirSuffix) { + await assertPathNotExist(tempDir, `_build${localDirSuffix}`); + await assertPathNotExist(tempDir, `${assetsContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `${chunksContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/${assetsContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/${chunksContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/build-assets${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/build-chunks${localDirSuffix}`); + } + + // Verify default .meteor/local caches are always cleaned + await assertPathNotExist(tempDir, '.meteor/local/build'); + await assertPathNotExist(tempDir, '.meteor/local/bundler-cache'); + await assertPathNotExist(tempDir, '.meteor/local/plugin-cache'); + + // When METEOR_LOCAL_DIR is set, also verify custom local dir is cleaned + if (meteorLocalDirEnv && meteorLocalDirEnv !== '.meteor/local') { + await assertPathNotExist(tempDir, `${meteorLocalDirEnv}/build`); + await assertPathNotExist(tempDir, `${meteorLocalDirEnv}/bundler-cache`); + await assertPathNotExist(tempDir, `${meteorLocalDirEnv}/plugin-cache`); + } + + // Run custom assertions if provided + if (customAssertions.afterReset) { + await customAssertions.afterReset({ tempDir }); + } + }); + }; +} diff --git a/tools/e2e-tests/typescript.test.js b/tools/e2e-tests/typescript.test.js new file mode 100644 index 0000000000..7be3de5bed --- /dev/null +++ b/tools/e2e-tests/typescript.test.js @@ -0,0 +1,154 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; +import { assertBodyStyles, assertFileExist } from "./assertions"; +import path from "path"; +import fs from "fs"; + +const isCI = process.env.GITHUB_ACTIONS === 'true'; + +describe('TypeScript App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'typescript', + port: 3112, + filePaths: { + client: 'client/main.tsx', + server: 'server/main.ts', + testClient: 'tests/client.ts', + testServer: 'tests/server.ts', + }, + buildDir: 'build', + assetsContext: 'assets', + chunksContext: 'chunks', + configFile: 'rspack.config.ts', + customAssertions: { + afterCreate({ tempDir }) { + if (isCI) { + const rspackConfigPath = path.join(tempDir, 'rspack.config.ts'); + // Remove the TsCheckerRspackPlugin plugin as is resource-intense, CI gets exhausted and fails + let configContent = fs.readFileSync(rspackConfigPath, 'utf8'); + configContent = configContent.replace( + /\s*new\s+TsCheckerRspackPlugin\(\)/, + '', + ); + fs.writeFileSync(rspackConfigPath, configContent); + } + }, + afterRun: async ({ result, tempDir }) => { + // SCSS styles support + await assertBodyStyles({ + 'white-space': 'break-spaces', + }); + await waitForTypeScriptEnvs(result.outputLines, { isTsxEnabled: true }); + await waitForTypeScriptErrorFree(result.outputLines); + await assertFileExist(tempDir, ".meteor/local/types"); + // Portable build: Meteor.isDevelopment and Meteor.isProduction must not be defined + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/, + { negate: true } + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/, + { negate: true } + ); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProduction: async ({ result }) => { + // SCSS styles support + await assertBodyStyles({ + 'white-space': 'break-spaces', + }); + await waitForTypeScriptEnvs(result.outputLines, { isTsxEnabled: true }); + // Portable build: Meteor.isDevelopment and Meteor.isProduction must not be defined + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/, + { negate: true } + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/, + { negate: true } + ); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + afterTest: async ({ result }) => { + await waitForTypeScriptEnvs(result.outputLines); + }, + afterTestOnce: async ({ result }) => { + await waitForTypeScriptEnvs(result.outputLines); + }, + afterBuild: async ({ result }) => { + await waitForTypeScriptEnvs(result.outputLines, { isTsxEnabled: true }); + // Portable build: Meteor.isDevelopment and Meteor.isProduction must not be defined + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/, + { negate: true } + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/, + { negate: true } + ); + }, + } + })); +}); + +/** + * Helper function to wait for TypeScript environment output from both Rspack Client and Server + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @returns {Promise} - A promise that resolves when both client and server are error-free + */ +export async function waitForTypeScriptEnvs(outputLines, options = {}) { + await waitForMeteorOutput( + outputLines, + /.*isTypescriptEnabled:.*true.*/, + options + ); + if (options.isTsxEnabled) { + await waitForMeteorOutput( + outputLines, + /.*isTsxEnabled:.*true.*/, + options + ); + } +} + +/** + * Helper function to wait for TypeScript error-free output from both Rspack Client and Server + * @param {string[]} outputLines - Array that will be populated with output lines + * @param {Object} options - Options for waiting + * @param {number} options.timeout - Maximum time to wait in milliseconds + * @param {number} options.checkInterval - Interval between checks in milliseconds + * @returns {Promise} - A promise that resolves when both client and server are error-free + */ +export async function waitForTypeScriptErrorFree(outputLines, options = {}) { + // Disable this check in CI as tsPlugin is resource-intensive and CI gets exhausted and fails + if (isCI) return; + + await waitForMeteorOutput( + outputLines, + /.*\[Rspack.*Client].*no.*errors.*found.*/, + options + ); + await waitForMeteorOutput( + outputLines, + /.*\[Rspack.*Client].*no.*errors.*found.*/, + options + ); + console.log(`Custom Plugin usage: ts-checker-rspack-plugin`); +} diff --git a/tools/e2e-tests/vue.test.js b/tools/e2e-tests/vue.test.js new file mode 100644 index 0000000000..db298b2b14 --- /dev/null +++ b/tools/e2e-tests/vue.test.js @@ -0,0 +1,39 @@ +import { + waitForMeteorOutput, +} from "./helpers"; +import { testMeteorRspackBundler } from './test-helpers'; +import { assertStyles } from "./assertions"; + +describe('Vue App Bundling /', () => { + describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ + appName: 'vue', + port: 3132, + filePaths: { + client: 'client/main.js', + server: 'server/main.js', + test: 'tests/main.js' + }, + customAssertions: { + afterRun: async () => { + // Verify Tailwind styles for ".p-8" element + await assertStyles('.p-8', { + ['padding']: '32px', + }); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); + }, + afterRunProduction: async () => { + // Verify Tailwind styles for ".p-8" element + await assertStyles('.p-8', { + ['padding']: '32px', + }); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); + }, + } + })); +}); diff --git a/tools/fs/optimistic.ts b/tools/fs/optimistic.ts index 8f59486eb3..0e90a1972c 100644 --- a/tools/fs/optimistic.ts +++ b/tools/fs/optimistic.ts @@ -353,7 +353,8 @@ export const optimisticReadMeteorIgnore = wrap((dir: string) => { const customMeteorIgnore = process.env.METEOR_IGNORE; if (customMeteorIgnore != null) { ignoreConfig = ignoreConfig || ignore(); - ignoreConfig = ignoreConfig.add(customMeteorIgnore); + const allCustomMeteorIgnores = customMeteorIgnore.trim().split(/\s+/); + ignoreConfig = ignoreConfig.add(allCustomMeteorIgnores); } return ignoreConfig; diff --git a/tools/fs/safe-watcher-legacy.ts b/tools/fs/safe-watcher-legacy.ts index f9f108afc8..dd7ffa9470 100644 --- a/tools/fs/safe-watcher-legacy.ts +++ b/tools/fs/safe-watcher-legacy.ts @@ -11,10 +11,24 @@ import { import { join as nativeJoin } from 'path'; -import nsfw from 'vscode-nsfw'; const pathwatcher = require('pathwatcher'); +let nsfw: any = null; +function getNsfw() { + if (nsfw === null) { + try { + nsfw = require('vscode-nsfw'); + } catch (error: any) { + console.error('Failed to load vscode-nsfw:', error.message); + console.error('Falling back to polling-based file watching.'); + nsfw = false; + watcherEnabled = false; + } + } + return nsfw; +} + // Default to prioritizing changed files, but disable that behavior (and // thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED // is explicitly set to a string that parses to a falsy value. @@ -416,10 +430,16 @@ export const watch = Profile( } ); -const fireNames = { - [nsfw.actions.CREATED]: 'change', - [nsfw.actions.MODIFIED]: 'change', - [nsfw.actions.DELETED]: 'delete' +function getFireNames() { + const nsfwLib = getNsfw(); + if (!nsfwLib) { + return {}; + } + return { + [nsfwLib.actions.CREATED]: 'change', + [nsfwLib.actions.MODIFIED]: 'change', + [nsfwLib.actions.DELETED]: 'delete' + }; } export function addWatchRoot(absPath: string) { @@ -427,6 +447,11 @@ export function addWatchRoot(absPath: string) { return; } + const nsfwLib = getNsfw(); + if (!nsfwLib) { + return; + } + watchRoots.add(absPath); // If there already is a watcher for a parent directory, there is no need @@ -445,11 +470,13 @@ export function addWatchRoot(absPath: string) { // TODO: check if there are any existing watchers that are children of this // watcher and stop them - nsfw( + const fireNames = getFireNames(); + + nsfwLib( convertToOSPath(absPath), (events) => { events.forEach(event => { - if(event.action === nsfw.actions.RENAMED) { + if(event.action === nsfwLib.actions.RENAMED) { let oldPath = nativeJoin(event.directory, event.oldFile); let oldEntry = entries[toPosixPath(oldPath)]; if (oldEntry) { @@ -472,5 +499,7 @@ export function addWatchRoot(absPath: string) { } ).then(watcher => { watcher.start() + }).catch((error: any) => { + console.error('Failed to start nsfw watcher for', absPath, ':', error.message); }); } diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 07fe605fb7..38e57d0bc6 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -138,6 +138,13 @@ function shouldIgnorePath(absPath: string): boolean { const cwd = toPosixPath(process.cwd()); const isWithinCwd = absPath.startsWith(cwd); + // TODO(modern): Review support for .meteor/local/modern + // to hold intermediate bundler results. dot contexts are + // hidden and commonly ignored by tools that scan directories + if (isWithinCwd && absPath.includes(`.meteor/local/modern`)) { + return false; + } + if (isWithinCwd && absPath.includes(`${cwd}/.meteor/local`)) { return true; } @@ -295,7 +302,7 @@ async function ensureWatchRoot(dirPath: string): Promise { const cwd = toPosixPath(process.cwd()); const isWithinCwd = dirPath.startsWith(cwd); const ignPrefix = isWithinCwd ? "" : "**/"; - const ignorePatterns = [`${ignPrefix}node_modules/**`, `${ignPrefix}.meteor/local/**`]; + const ignorePatterns = [`${ignPrefix}node_modules/**`, `${ignPrefix}.meteor/local/!(modern)`]; try { watchRoots.add(dirPath); const subscription = await ParcelWatcher.subscribe( @@ -305,9 +312,13 @@ async function ensureWatchRoot(dirPath: string): Promise { if (/Events were dropped/.test(err.message)) { return; } + if (/RootResolveError/.test(err.message) || /failed to resolve root/.test(err.message)) { + console.warn(`Parcel watcher root resolve error on ${osDirPath}, ignoring: ${err.message}`); + ignoredWatchRoots.add(dirPath); + watchRoots.delete(dirPath); + return; + } console.error(`Parcel watcher error on ${osDirPath}:`, err); - // Only disable native watching for critical errors (like ENOSPC). - // @ts-ignore if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { fallbackToPolling(); } @@ -333,9 +344,11 @@ async function ensureWatchRoot(dirPath: string): Promise { (e.code === "ENOTDIR" || /Not a directory/.test(e.message) || e.code === "EBADF" || - /Bad file descriptor/.test(e.message)) + /Bad file descriptor/.test(e.message) || + /RootResolveError/.test(e.message) || + /failed to resolve root/.test(e.message)) ) { - console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + console.warn(`Skipping watcher for ${osDirPath}: ${e.message || 'not watchable'}`); ignoredWatchRoots.add(dirPath); } else { console.error(`Failed to start watcher for ${osDirPath}:`, e); diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 5d42c1b61e..855d7ce2b0 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -293,7 +293,9 @@ class NodeModulesDirectory { // Normalize .npm/package/node_modules/... paths so that they get // copied into the bundle as if they were in the top-level local // node_modules directory of the package. - if (relParts[1] === "package") { + if (relParts[1] === "devPackage") { + relParts.splice(0, 2, 'dev'); + } else if (relParts[1] === "package") { relParts.splice(0, 2); } else if (relParts[1] === "plugin") { relParts.splice(0, 3); @@ -1989,6 +1991,29 @@ function hashOfFiles(files) { ClientTarget.prototype[method] = Profile(`ClientTarget#${method}`, ClientTarget.prototype[method]); }); +/** + * Collects all dev-only package names. + * + * @returns {string[]} Array of dev-only package names + */ +function getDevOnlyPackages() { + const targets = global.meteorBundlerTargets || {}; + return [ + ...new Set( + ['web.browser', 'server'].flatMap(target => { + const pkgMap = targets[target]?.unibuilds; + + if (!pkgMap) { + return []; + } + + return pkgMap + .filter(unibuild => unibuild?.pkg?.devOnly) + .map(unibuild => unibuild?.pkg.name); + }) + ) + ]; +} //////////////////// JsImageTarget and JsImage //////////////////// @@ -2418,6 +2443,10 @@ class JsImage { addNodeModulesDirToObject(nmd, nodeModulesDirectories); }); + var devOnlySkipPackages = []; + const isProductionLike = ['build', 'deploy'].includes(global.currentCommand?.name) && buildMode === 'production'; + if (isProductionLike) devOnlySkipPackages = getDevOnlyPackages(); + // If multiple load files share the same asset, only write one copy of // each. (eg, for app assets). var assetFilesBySha = {}; @@ -2429,6 +2458,11 @@ class JsImage { throw new Error("No targetPath?"); } + // Skip dev-only packages on build for production + if (devOnlySkipPackages.some(_package => item?.targetPath?.includes(`${_package}.js`))) { + continue; + } + var loadItem = { node_modules: {} }; @@ -2543,6 +2577,11 @@ class JsImage { for (const nmd of Object.values(nodeModulesDirectories)) { assert.strictEqual(typeof nmd.preferredBundlePath, "string"); + // Skip dev-only packages on build for production + if (devOnlySkipPackages.includes(nmd?.packageName)) { + continue; + } + // Skip calculating isPortable in 'meteor run' since the // modules are never rebuilt if (includeNodeModules !== 'symlink' && !nmd.isPortable()) { @@ -2551,9 +2590,25 @@ class JsImage { } if (nmd.sourcePath !== nmd.preferredBundlePath) { + const hasDevNodeModules = nmd.sourcePath.includes('npm/dev/node_modules') + || nmd.sourcePath.includes('devPackage/node_modules'); + // Skip copy dev node_modules in production-like mode + if (isProductionLike && hasDevNodeModules) { + continue; + } + // Skip copy prod node_modules in dev mode when dev node_modules exist + if (!isProductionLike + && files.exists(`${nmd.sourceRoot}/npm/dev/node_modules`) + && nmd.sourcePath.includes('npm/node_modules')) { + continue; + } + // Ensure to copy dev node_modules to proper context + const to = hasDevNodeModules + ? nmd.preferredBundlePath.replace('/dev/node_modules', '/node_modules') + : nmd.preferredBundlePath; var copyOptions = { from: nmd.sourcePath, - to: nmd.preferredBundlePath, + to: to, npmDiscards: nmd.npmDiscards, symlink: includeNodeModules === 'symlink' }; @@ -3078,6 +3133,7 @@ Node.js ${process.version}. To run the application: $ export MONGO_URL='mongodb://user:password@host:port/databasename' $ export ROOT_URL='http://example.com' $ export MAIL_URL='smtp://user:password@mailhost:port/' + $ export METEOR_SETTINGS='{"public":{"key":"value"}}' $ node main.js Use the PORT environment variable to set the port where the @@ -3467,6 +3523,11 @@ async function bundle({ targets.server = await makeServerTarget(app, webArchs); } + if (buildOptions.buildMode === 'production') { + // Store targets in global variable for access in JsImage.write + global.meteorBundlerTargets = targets; + } + if (outputPath !== null) { if (hasCachedBundle) { // If we already have a cached bundle, just recreate the new targets. diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js index 6eaa2f332c..0e9d689607 100644 --- a/tools/isobuild/compiler-plugin.js +++ b/tools/isobuild/compiler-plugin.js @@ -1602,6 +1602,11 @@ export class PackageSourceBatch { return; } + if (id.startsWith("#")) { + // CSS fragment identifiers are not JS modules. + return; + } + if (id in serverLibPackages && archinfo.matches(info.bundleArch, "os")) { // Packages in dev_bundle/server-lib/node_modules can always be diff --git a/tools/isobuild/compiler.js b/tools/isobuild/compiler.js index 8e03f3d8ca..af5b4df41c 100644 --- a/tools/isobuild/compiler.js +++ b/tools/isobuild/compiler.js @@ -124,6 +124,7 @@ compiler.compile = Profile(function (packageSource, options) { // We run this even if we have no dependencies, because we might // need to delete dependencies we used to have. var nodeModulesPath = null; + var devNodeModulesPath = null; if (packageSource.npmCacheDirectory) { if (await meteorNpm.updateDependencies(packageSource.name, packageSource.npmCacheDirectory, @@ -135,6 +136,17 @@ compiler.compile = Profile(function (packageSource, options) { } } + if (packageSource.npmDevCacheDirectory) { + if (await meteorNpm.updateDependencies(packageSource.name, + packageSource.npmDevCacheDirectory, + packageSource.npmDevDependencies)) { + devNodeModulesPath = files.pathJoin( + packageSource.npmDevCacheDirectory, + 'node_modules' + ); + } + } + // Find all the isobuild:* pseudo-packages that this package depends on. Why // do we need to do this? Well, we actually load the plugins in this package // before we've fully compiled the package --- plugins are loaded before the @@ -174,6 +186,7 @@ compiler.compile = Profile(function (packageSource, options) { debugOnly: packageSource.debugOnly, prodOnly: packageSource.prodOnly, testOnly: packageSource.testOnly, + devOnly: packageSource.devOnly, pluginCacheDir: options.pluginCacheDir, isobuildFeatures }); @@ -191,6 +204,7 @@ compiler.compile = Profile(function (packageSource, options) { sourceArch: architecture, isopackCache: isopackCache, nodeModulesPath: nodeModulesPath, + devNodeModulesPath: devNodeModulesPath, }); Object.assign(pluginProviderPackageNames, @@ -354,6 +368,7 @@ var compileUnibuild = Profile(function (options) { const inputSourceArch = options.sourceArch; const isopackCache = options.isopackCache; const nodeModulesPath = options.nodeModulesPath; + const devNodeModulesPath = options.devNodeModulesPath; const isApp = ! inputSourceArch.pkg.name; const resources = []; const pluginProviderPackageNames = {}; @@ -464,6 +479,31 @@ var compileUnibuild = Profile(function (options) { watch.readAndWatchFile(watchSet, shrinkwrapPath); } + if (devNodeModulesPath) { + addNodeModulesDirectory({ + packageName: inputSourceArch.pkg.name, + sourceRoot: inputSourceArch.sourceRoot, + sourcePath: devNodeModulesPath, + npmDiscards: isopk.npmDiscards, + local: false, + }); + + // If this slice has node modules, we should consider the shrinkwrap file + // to be part of its inputs. (This is a little racy because there's no + // guarantee that what we read here is precisely the version that's used, + // but it's better than nothing at all.) + // + // Note that this also means that npm modules used by plugins will get + // this npm-shrinkwrap.json in their pluginDependencies (including for all + // packages that depend on us)! This is good: this means that a tweak to + // an indirect dependency of the coffee-script npm module used by the + // coffeescript package will correctly cause packages with *.coffee files + // to be rebuilt. + const shrinkwrapPath = devNodeModulesPath.replace( + /node_modules$/, 'npm-shrinkwrap.json'); + watch.readAndWatchFile(watchSet, shrinkwrapPath); + } + // This function needs to be factored out to support legacy handlers later on // in the compilation process function addAsset(contents, relPath, hash) { diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index 7aff1ee0e6..d00d93104d 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -27,6 +27,7 @@ import { writeFileAtomically, readFile, } from "../fs/files"; +import rspackHelpers from "../tool-env/rspack"; const { SourceNode, SourceMapConsumer } = require("source-map"); @@ -469,6 +470,14 @@ export default class ImportScanner { const old = this.outputFiles[ this.absPathToOutputIndex[absLowerPath]]; + // Check if this is a case-sensitivity conflict (same path when lowercased, but different actual paths) + if (old.absPath && old.absPath !== absPath) { + throw new Error( + `Filename collision detected: "${old.sourcePath}" and "${file.sourcePath}" resolve to the same path when case is ignored. ` + + `Please ensure file names have consistent casing to avoid conflicts.` + ); + } + // If the old file is just an empty stub, let the new file take // precedence over it. if (old.implicit === true) { @@ -997,6 +1006,12 @@ export default class ImportScanner { file: File, ): Promise> { const fileHash = file.hash instanceof Promise ? await file.hash : file.hash; + + // Ignore rspack output files + if (rspackHelpers.isRspackOutputFile(file.sourcePath)) { + return {}; + } + if (IMPORT_SCANNER_CACHE.has(fileHash)) { return IMPORT_SCANNER_CACHE.get(fileHash) as Record; } diff --git a/tools/isobuild/isopack.js b/tools/isobuild/isopack.js index aa397e0a80..91f62b9faf 100644 --- a/tools/isobuild/isopack.js +++ b/tools/isobuild/isopack.js @@ -21,6 +21,9 @@ var Console = require('../console/console.js').Console; var Profile = require('../tool-env/profile').Profile; import { requestGarbageCollection } from "../utils/gc.js"; import { Unibuild } from "./unibuild.js"; +import rspackHelpers from "../tool-env/rspack"; +import { getCurrentNodeBinDir, getDevBundle } from "../fs/files"; +import { runLogInstance } from "../runners/run-log"; var rejectBadPath = function (p) { if (p.match(/\.\./)) { @@ -56,6 +59,7 @@ var Isopack = function () { self.debugOnly = false; self.prodOnly = false; self.testOnly = false; + self.devOnly = false; // Unibuilds, an array of class Unibuild. self.unibuilds = []; @@ -267,6 +271,7 @@ Object.assign(Isopack.prototype, { self.debugOnly = options.debugOnly; self.prodOnly = options.prodOnly; self.testOnly = options.testOnly; + self.devOnly = options.devOnly; self.pluginCacheDir = options.pluginCacheDir || null; self.isobuildFeatures = options.isobuildFeatures; }, @@ -519,6 +524,16 @@ Object.assign(Isopack.prototype, { // Share the meteorConfig object as part of plugin API getMeteorConfig: getMeteorConfig, + // Share functions to get the dev bundle context + getDevBundle, + getCurrentNodeBinDir, + + // Share the rspackHelpers as part of plugin API + rspackHelpers, + + // Share the runLogInstance as part of plugin API + runLogInstance, + // 'extension' is a file extension without the separation dot // (eg 'js', 'coffee', 'coffee.md') // @@ -909,6 +924,7 @@ Object.assign(Isopack.prototype, { self.debugOnly = !!mainJson.debugOnly; self.prodOnly = !!mainJson.prodOnly; self.testOnly = !!mainJson.testOnly; + self.devOnly = !!mainJson.devOnly; } for (const pluginMeta of mainJson.plugins) { rejectBadPath(pluginMeta.path); @@ -1060,6 +1076,9 @@ Object.assign(Isopack.prototype, { if (self.testOnly) { mainJson.testOnly = true; } + if (self.devOnly) { + mainJson.devOnly = true; + } if (! _.isEmpty(self.cordovaDependencies)) { mainJson.cordovaDependencies = self.cordovaDependencies; } diff --git a/tools/isobuild/package-namespace.js b/tools/isobuild/package-namespace.js index dc5bf7be9a..ae43270e25 100644 --- a/tools/isobuild/package-namespace.js +++ b/tools/isobuild/package-namespace.js @@ -46,6 +46,9 @@ export class PackageNamespace { * @param {String} options.documentation Optional Filepath to * documentation. Set to 'README.md' by default. Set this to null to submit * no documentation. + * @param {Boolean} options.devOnly A package with this flag set to true + * will ONLY be bundled as part of `meteor run`, and completely excluded + * from production builds with `meteor build`. * @param {Boolean} options.debugOnly A package with this flag set to true * will not be bundled into production builds. This is useful for packages * meant to be used in development only. @@ -141,6 +144,8 @@ export class PackageNamespace { source.prodOnly = !!value; } else if (key === "testOnly") { source.testOnly = !!value; + } else if (key === "devOnly") { + source.devOnly = !!value; } else if (key === "deprecated") { if (typeof(value) === "string") { source.deprecatedMessage = value; @@ -150,9 +155,9 @@ export class PackageNamespace { // Do nothing. We might want to add some keys later, and we should err // on the side of backwards compatibility. } - if (size(compact([source.debugOnly, source.prodOnly, source.testOnly])) > 1) { + if (size(compact([source.debugOnly, source.prodOnly, source.testOnly, source.devOnly])) > 1) { buildmessage.error( - "Package can't have more than one of: debugOnly, prodOnly, testOnly."); + "Package can't have more than one of: debugOnly, prodOnly, testOnly or devOnly."); } }); } diff --git a/tools/isobuild/package-npm.js b/tools/isobuild/package-npm.js index 0f4ca8e8e6..c613e313d5 100644 --- a/tools/isobuild/package-npm.js +++ b/tools/isobuild/package-npm.js @@ -16,6 +16,9 @@ export class PackageNpm { // the Npm.strip comment below for further usage information. this._discards = new NpmDiscards; this._dependencies = null; + this._devDependencies = null; + this._dependenciesCalled = false; + this._devDependenciesCalled = false; } /** @@ -50,17 +53,20 @@ export class PackageNpm { // XXX make dependencies be separate between use and test, so that // production doesn't have to ship all of the npm modules used by test // code - if (this._dependencies) { + if (this._dependenciesCalled && this._dependencies) { buildmessage.error("Npm.depends may only be called once per package", { useMyCaller: true }); // recover by ignoring the Npm.depends line return; } + this._dependenciesCalled = true; + if (typeof dependencies !== 'object') { buildmessage.error("the argument to Npm.depends should be an " + - "object, like this: {gcd: '0.0.0'}", - { useMyCaller: true }); + "object, like this: {gcd: '0.0.0'}", + { useMyCaller: true } + ); // recover by ignoring the Npm.depends line return; } @@ -72,7 +78,7 @@ export class PackageNpm { // confidence we're running the same code? try { ensureOnlyValidVersions(dependencies, { - forCordova: false + forCordova: false, }); } catch (e) { @@ -85,7 +91,70 @@ export class PackageNpm { return; } - this._dependencies = dependencies; + this._dependencies = { + ...(this._dependencies || {}), + ...dependencies, + }; + } + + /** + * @summary Specify which [NPM](https://www.npmjs.org/) packages + * your Meteor package depends on for development only. + * @param {Object} dependencies An object where the keys are package + * names and the values are version numbers in string form. + * + * These dependencies will only be included during development and testing, + * but will be excluded when using "meteor build" or "meteor deploy" commands, + * reducing the size of the production bundle. + * + * ```js + * Npm.devDepends({ + * "jest": "29.5.0", + * "eslint": "8.36.0" + * }); + * ``` + * @locus package.js + */ + devDepends(devDependencies) { + if (this._devDependenciesCalled && this._devDependencies) { + buildmessage.error("Npm.devDepends may only be called once per package", + { useMyCaller: true }); + // recover by ignoring the Npm.devDepends line + return; + } + + this._devDependenciesCalled = true; + + if (typeof devDependencies !== 'object') { + buildmessage.error("the argument to Npm.devDepends should be an " + + "object, like this: {jest: '29.5.0'}", + { useMyCaller: true } + ); + // recover by ignoring the Npm.devDepends line + return; + } + + // don't allow npm fuzzy versions so that there is complete + // consistency when deploying a meteor app + try { + ensureOnlyValidVersions(devDependencies, { + forCordova: false + }); + } catch (e) { + buildmessage.error(e.message, { + useMyCaller: true, + downcase: true + }); + + // recover by ignoring the Npm.devDepends line + return; + } + + // Add dev dependencies mode including dependencies + this._devDependencies = { + ...(this._dependencies || {}), + ...devDependencies, + }; } // The `Npm.strip` method makes up for packages that have missing diff --git a/tools/isobuild/package-source.js b/tools/isobuild/package-source.js index 560d5341b7..8ed7303e68 100644 --- a/tools/isobuild/package-source.js +++ b/tools/isobuild/package-source.js @@ -328,6 +328,9 @@ var PackageSource = function () { // as a string. self.npmDependencies = {}; + // npm packages used for development of this package. + self.npmDevDependencies = {}; + // Files to be stripped from the installed NPM dependency tree. See the // Npm.strip comment below for further usage information. self.npmDiscards = null; @@ -438,6 +441,9 @@ Object.assign(PackageSource.prototype, { utils.ensureOnlyValidVersions(options.npmDependencies, {forCordova: false}); self.npmDependencies = options.npmDependencies; + utils.ensureOnlyValidVersions(options.npmDevDependencies, {forCordova: false}); + self.npmDevDependencies = options.npmDevDependencies; + // If options.npmDir is a string, make sure it contains no colons. self.npmCacheDirectory = _.isString(options.npmDir) ? convertColonsInPath(options.npmDir) @@ -772,7 +778,10 @@ Object.assign(PackageSource.prototype, { // dirs for use vs test? self.npmCacheDirectory = files.pathResolve(files.pathJoin(self.sourceRoot, '.npm', 'package')); + self.npmDevCacheDirectory = + files.pathResolve(files.pathJoin(self.sourceRoot, '.npm', 'devPackage')); self.npmDependencies = Npm._dependencies; + self.npmDevDependencies = Npm._devDependencies; self.npmDiscards = Npm._discards; self.cordovaDependencies = Cordova._dependencies; @@ -907,13 +916,13 @@ Object.assign(PackageSource.prototype, { const projectWatchSet = projectContext.getProjectWatchSet(); - const mainModulesByArch = + let mainModulesByArch = projectContext.meteorConfig.getMainModulesByArch(); - const testModulesByArch = + let testModulesByArch = projectContext.meteorConfig.getTestModulesByArch(); - const nodeModulesToRecompileByArch = + let nodeModulesToRecompileByArch = projectContext.meteorConfig.getNodeModulesToRecompileByArch(); projectWatchSet.merge(projectContext.meteorConfig.watchSet); @@ -926,15 +935,35 @@ Object.assign(PackageSource.prototype, { return; } - const mainModule = projectContext.meteorConfig + let mainModule = projectContext.meteorConfig .getMainModule(arch, mainModulesByArch); - const testModule = projectContext.meteorConfig + let testModule = projectContext.meteorConfig .getTestModule(arch, testModulesByArch); - const nodeModulesToRecompile = projectContext.meteorConfig + let nodeModulesToRecompile = projectContext.meteorConfig .getNodeModulesToRecompile(arch, nodeModulesToRecompileByArch); + // If the config is reinitialized dynamically, reload needs to happen + // in order to get the new mainModule, testModule, and nodeModulesToRecompile + // for the build process. We ensure to compute the new values once. + function tryReloadMeteorConfig() { + if (projectContext.meteorConfig?._needReload?.[arch]) { + mainModulesByArch = + projectContext.meteorConfig.getMainModulesByArch(); + testModulesByArch = + projectContext.meteorConfig.getTestModulesByArch(); + mainModule = projectContext.meteorConfig + .getMainModule(arch, mainModulesByArch); + testModule = projectContext.meteorConfig + .getTestModule(arch, testModulesByArch); + nodeModulesToRecompile = projectContext.meteorConfig + .getNodeModulesToRecompile(arch, nodeModulesToRecompileByArch); + + projectContext.meteorConfig._needReload[arch] = false; + } + } + // XXX what about /web.browser/* etc, these directories could also // be for specific client targets. @@ -945,6 +974,8 @@ Object.assign(PackageSource.prototype, { sourceRoot: self.sourceRoot, uses: uses, getFiles(sourceProcessorSet, watchSet) { + tryReloadMeteorConfig(); + sourceProcessorSet.watchSet = watchSet; const findOptions = { diff --git a/tools/packaging/catalog/catalog-local.js b/tools/packaging/catalog/catalog-local.js index d56335652a..be03a37e30 100644 --- a/tools/packaging/catalog/catalog-local.js +++ b/tools/packaging/catalog/catalog-local.js @@ -451,6 +451,7 @@ Object.assign(LocalCatalog.prototype, { debugOnly: packageSource.debugOnly, prodOnly: packageSource.prodOnly, testOnly: packageSource.testOnly, + devOnly: packageSource.devOnly, deprecated: packageSource.deprecated, deprecatedMessage: packageSource.deprecatedMessage, diff --git a/tools/packaging/package-client.js b/tools/packaging/package-client.js index c79a2463df..bf7bc6dcaf 100644 --- a/tools/packaging/package-client.js +++ b/tools/packaging/package-client.js @@ -798,6 +798,7 @@ exports.publishPackage = async function (options) { debugOnly: packageSource.debugOnly, prodOnly: packageSource.prodOnly, testOnly: packageSource.testOnly, + devOnly: packageSource.devOnly, deprecated: packageSource.deprecated, deprecatedMessage: packageSource.deprecatedMessage, diff --git a/tools/project-context.js b/tools/project-context.js index f114f3a38a..d8e85fe45b 100644 --- a/tools/project-context.js +++ b/tools/project-context.js @@ -95,6 +95,7 @@ import { import Resolver from "./isobuild/resolver"; import { addWatchRoot } from './fs/safe-watcher'; +import compiler from "./isobuild/compiler"; const CAN_DELAY_LEGACY_BUILD = ! JSON.parse( process.env.METEOR_DISALLOW_DELAYED_LEGACY_BUILD || "false" @@ -474,6 +475,9 @@ Object.assign(ProjectContext.prototype, { }); await self.platformList._init(); + if (buildmessage.jobHasMessages()) + return; + if (buildmessage.jobHasMessages()) return; @@ -495,6 +499,17 @@ Object.assign(ProjectContext.prototype, { }); self.meteorConfig._ensureInitialized(); + // Reinitialize the config + // The new config object is marked to be reloaded, + // so it will be reloaded on the next build. + global.reinitializeMeteorConfig = () => { + self.meteorConfig._ensureInitialized(); + self.meteorConfig._needReload = {}; + _.each(compiler.ALL_ARCHES, function (arch) { + self.meteorConfig._needReload[arch] = true; + }); + }; + if (buildmessage.jobHasMessages()) { return; } @@ -541,7 +556,7 @@ Object.assign(ProjectContext.prototype, { self.platformList, self.cordovaPluginsFile].forEach( function (metadataFile) { metadataFile && watchSet.merge(metadataFile.watchSet); - }); + }); if (self.localCatalog) { watchSet.merge(self.localCatalog.packageLocationWatchSet); @@ -693,6 +708,9 @@ Object.assign(ProjectContext.prototype, { localCatalog: self.localCatalog }); + // Provide the packageVersionMap to plugins via global scope + global.packageVersionMap = self.packageMap.toVersionMap(); + self.packageMapDelta = new packageMapModule.PackageMapDelta({ cachedVersions: cachedVersions, packageMap: self.packageMap, @@ -1452,8 +1470,7 @@ Object.assign(exports.PlatformList.prototype, { getCordovaPlatforms: function () { var self = this; - return _.difference(self._platforms, - exports.PlatformList.DEFAULT_PLATFORMS); + return _.intersection(self._platforms, ['ios', 'android']); }, usesCordova: function () { @@ -1718,7 +1735,6 @@ Object.assign(exports.ReleaseFile.prototype, { } }); - // Represents .meteor/.finished-upgraders. // This is only used in a few places, so we don't cache its value in memory; // we just read it when we need it. There's also no need to add it to a @@ -1828,7 +1844,10 @@ export class MeteorConfig { // Updates config when package.json changes trigger rebuilds setMeteorConfig({ ...(this._config || {}), - modern: normalizeModernConfig(modernForced || this._config?.modern || false), + modern: { + ...normalizeModernConfig(modernForced || this._config?.modern || false), + ...(this._config?.verbose || this._config?.modern?.verbose) && { verbose: true }, + }, }); return this._config; diff --git a/tools/runners/run-all.js b/tools/runners/run-all.js index 594e20589c..b2b2567b48 100644 --- a/tools/runners/run-all.js +++ b/tools/runners/run-all.js @@ -218,7 +218,7 @@ class Runner { { arrow: true } ); } else { - runLog.log("App running at: " + self.rootUrl, { arrow: true }); + runLog.log("App running at " + self.rootUrl, { arrow: true }); } if (process.platform === "win32") { diff --git a/tools/runners/run-app.js b/tools/runners/run-app.js index 9d92b57780..bf77ae678c 100644 --- a/tools/runners/run-app.js +++ b/tools/runners/run-app.js @@ -13,16 +13,87 @@ import { closeAllWatchers } from "../fs/safe-watcher"; import { loadIsopackage } from '../tool-env/isopackets.js'; import { eachline } from "../utils/eachline"; -// Parse out s as if it were a bash command line. -var bashParse = function (s) { - if (s.search("\"") !== -1 || s.search("'") !== -1) { - throw new Error("Meteor cannot currently handle quoted SERVER_NODE_OPTIONS"); +// Minimal shell-like splitter for SERVER_NODE_OPTIONS. +// Handles single/double quotes and backslash escapes so that values like +// --flag="hello world" or --require='/path/with spaces/f.js' are kept as +// single tokens. This is NOT a full bash parser — it only covers the +// quoting subset needed for Node CLI flags. +var splitQuotedArgs = exports.splitQuotedArgs = function (s) { + const args = []; + let current = ''; + let inDouble = false; + let inSingle = false; + let escaped = false; + let hasQuotes = false; + + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + + if (escaped) { + current += ch; + escaped = false; + continue; + } + + // Backslash: escape next char only when it precedes a meaningful + // character (space, quote, or another backslash). This preserves + // Windows-style paths like C:\temp\my-file.js. + if (ch === '\\' && !inSingle && i + 1 < s.length) { + const next = s[i + 1]; + if (inDouble) { + // Inside double quotes, only \" and \\ are escape sequences + if (next === '"' || next === '\\') { + escaped = true; + continue; + } + } else { + // Outside quotes, escape spaces, quotes, and backslashes + if (next === ' ' || next === '"' || next === "'" || next === '\\') { + escaped = true; + continue; + } + } + } + + if (ch === '"' && !inSingle) { + inDouble = !inDouble; + hasQuotes = true; + continue; + } + + if (ch === "'" && !inDouble) { + inSingle = !inSingle; + hasQuotes = true; + continue; + } + + if (/\s/.test(ch) && !inDouble && !inSingle) { + if (current || hasQuotes) { + args.push(current); + current = ''; + hasQuotes = false; + } + continue; + } + + current += ch; } - return s.split(/\s+/).filter(Boolean); + + if (current || hasQuotes) { + args.push(current); + } + + if (inDouble || inSingle) { + throw new Error( + "Unterminated quote in SERVER_NODE_OPTIONS: " + s + ); + } + + return args; }; var getNodeOptionsFromEnvironment = function () { - return bashParse(process.env.SERVER_NODE_OPTIONS || ""); + return splitQuotedArgs(process.env.SERVER_NODE_OPTIONS || ""); }; /////////////////////////////////////////////////////////////////////////////// diff --git a/tools/runners/run-app.test.js b/tools/runners/run-app.test.js new file mode 100644 index 0000000000..58190d4892 --- /dev/null +++ b/tools/runners/run-app.test.js @@ -0,0 +1,153 @@ +// Unit tests for splitQuotedArgs — the SERVER_NODE_OPTIONS parser in run-app.js + +// run-app.js pulls in most of the Meteor tool-chain (isobuild, catalog, etc.). +// None of those are needed for splitQuotedArgs, so we stub them out to keep +// the test fast and dependency-free. +jest.mock('../fs/files', () => ({})); +jest.mock('../fs/watch', () => ({})); +jest.mock('../isobuild/bundler.js', () => ({})); +jest.mock('../utils/buildmessage.js', () => ({})); +jest.mock('./run-log.js', () => ({})); +jest.mock('../meteor-services/stats.js', () => ({})); +jest.mock('../console/console.js', () => ({ Console: {} })); +jest.mock('../packaging/catalog/catalog.js', () => ({})); +jest.mock('../tool-env/profile', () => ({ Profile: {} })); +jest.mock('../packaging/release.js', () => ({})); +jest.mock('../cordova/index.js', () => ({ pluginVersionsFromStarManifest: () => {} })); +jest.mock('../fs/safe-watcher', () => ({ closeAllWatchers: () => {} })); +jest.mock('../tool-env/isopackets.js', () => ({ loadIsopackage: () => {} })); +jest.mock('../utils/eachline', () => ({ eachline: () => {} })); + +const { splitQuotedArgs } = require('./run-app.js'); + +// --- Tests --- + +describe('splitQuotedArgs', () => { + describe('unquoted values', () => { + test('empty string returns empty array', () => { + expect(splitQuotedArgs('')).toEqual([]); + }); + + test('simple flags split on whitespace', () => { + expect(splitQuotedArgs('--inspect --max-old-space-size=4096')).toEqual([ + '--inspect', + '--max-old-space-size=4096', + ]); + }); + + test('multiple spaces between args', () => { + expect(splitQuotedArgs(' --a --b ')).toEqual(['--a', '--b']); + }); + }); + + describe('quoted values', () => { + test('double-quoted value', () => { + expect(splitQuotedArgs('--test-name-pattern="validation"')).toEqual([ + '--test-name-pattern=validation', + ]); + }); + + test('single-quoted value', () => { + expect(splitQuotedArgs("--test-name-pattern='validation'")).toEqual([ + '--test-name-pattern=validation', + ]); + }); + + test('double-quoted value with spaces', () => { + expect(splitQuotedArgs('--test-name-pattern="my pattern"')).toEqual([ + '--test-name-pattern=my pattern', + ]); + }); + + test('mixed quoted and unquoted args', () => { + expect(splitQuotedArgs('--inspect --test-reporter="my reporter" --once')).toEqual([ + '--inspect', + '--test-reporter=my reporter', + '--once', + ]); + }); + + test('escaped quote inside double quotes', () => { + expect(splitQuotedArgs('--pattern="say \\"hello\\""')).toEqual([ + '--pattern=say "hello"', + ]); + }); + + test('single quotes preserve double quotes literally', () => { + expect(splitQuotedArgs("--pattern='say \"hello\"'")).toEqual([ + '--pattern=say "hello"', + ]); + }); + + test('unterminated double quote throws', () => { + expect(() => splitQuotedArgs('--flag="unterminated')).toThrow( + /Unterminated quote/ + ); + }); + + test('unterminated single quote throws', () => { + expect(() => splitQuotedArgs("--flag='unterminated")).toThrow( + /Unterminated quote/ + ); + }); + }); + + describe('edge cases', () => { + test('empty double-quoted arg is preserved', () => { + expect(splitQuotedArgs('--foo ""')).toEqual(['--foo', '']); + }); + + test('empty single-quoted arg is preserved', () => { + expect(splitQuotedArgs("--foo ''")).toEqual(['--foo', '']); + }); + + test('backslash-escaped space outside quotes', () => { + expect(splitQuotedArgs('--foo hello\\ world')).toEqual([ + '--foo', + 'hello world', + ]); + }); + + test('Windows backslash paths are preserved', () => { + expect(splitQuotedArgs('--require=C:\\temp\\my-file.js')).toEqual([ + '--require=C:\\temp\\my-file.js', + ]); + }); + + test('Windows path inside double quotes', () => { + expect(splitQuotedArgs('"C:\\Program Files\\node\\node.exe"')).toEqual([ + 'C:\\Program Files\\node\\node.exe', + ]); + }); + + test('concatenated double-quoted segments', () => { + expect(splitQuotedArgs('"abc""def"')).toEqual(['abcdef']); + }); + + test('concatenated single-quoted segments', () => { + expect(splitQuotedArgs("'abc''def'")).toEqual(['abcdef']); + }); + }); + + describe('real-world examples', () => { + test('node:test coverage + reporter', () => { + expect(splitQuotedArgs( + '--experimental-test-coverage --test-reporter=spec' + )).toEqual([ + '--experimental-test-coverage', + '--test-reporter=spec', + ]); + }); + + test('multi-reporter setup', () => { + expect(splitQuotedArgs( + '--test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=./results.xml' + )).toEqual([ + '--test-reporter=spec', + '--test-reporter-destination=stdout', + '--test-reporter=junit', + '--test-reporter-destination=./results.xml', + ]); + }); + }); +}); diff --git a/tools/runners/run-log.js b/tools/runners/run-log.js index 51efd8a4b3..f533b1787a 100644 --- a/tools/runners/run-log.js +++ b/tools/runners/run-log.js @@ -158,7 +158,7 @@ Object.assign(RunLog.prototype, { self.consecutiveRestartMessages = 1; } - var message = "=> Meteor server restarted at: " + options.rootUrl; + var message = "=> Meteor server restarted at " + options.rootUrl; if (self.consecutiveRestartMessages > 1) { message += " (x" + self.consecutiveRestartMessages + ")"; } @@ -225,3 +225,6 @@ var runLogInstance = new RunLog; function (method) { exports[method] = runLogInstance[method].bind(runLogInstance); }); + +// Export the singleton instance for use in plugins +exports.runLogInstance = runLogInstance; diff --git a/tools/runners/run-mongo.js b/tools/runners/run-mongo.js index c9a7de7334..c15feffbed 100644 --- a/tools/runners/run-mongo.js +++ b/tools/runners/run-mongo.js @@ -1024,6 +1024,12 @@ Object.assign(MRp, { 'https://github.com/meteor/meteor/issues/4019 for more details.'; } + if (signal === "SIGILL"){ + message += + '\n\n' + + "MongoDB crashed with SIGILL, you may be running a build of Meteor not compatible with your architecture. If this persists, try re-installing Meteor."; + } + runLog.log(message); self._fail(); }, diff --git a/tools/runners/run-proxy.js b/tools/runners/run-proxy.js index f237f68fde..231fc6042a 100644 --- a/tools/runners/run-proxy.js +++ b/tools/runners/run-proxy.js @@ -244,38 +244,172 @@ Object.assign(Proxy.prototype, { }); function showErrorPage(res) { - // XXX serve an app that shows the logs nicely and that also - // knows how to reload when the server comes back up + // TODO: reload when the server comes back up res.writeHead(200, {'Content-Type': 'text/html'}); res.write(` - App crashing + Meteor App - Error + -

    Your app is crashing. Here's the latest log:

    +
    +
    +

    App Error

    +
    Your application server has encountered an error
    +
    +
    + +
    +
    +
    Server Log
    + `); -
    `);
    +  for (const item of runLog.getLog()) {
    +    res.write(Anser.ansiToHtml(Anser.escapeForHtml(item.message)) + "\n");
    +  }
     
    -  runLog.getLog().forEach(function (item) {
    -        res.write(Anser.ansiToHtml(Anser.escapeForHtml(item.message)) + "\n");
    -      });
    -
    -      res.write(`
    + res.write(`
    +
    + +
    + Fix the error in your code and save your files. Once your server is running without errors, then reload this page. +
    + +
    -`) +`); res.end(); } diff --git a/tools/static-assets/skel-angular/.gitignore b/tools/static-assets/skel-angular/.gitignore new file mode 100644 index 0000000000..4e24749299 --- /dev/null +++ b/tools/static-assets/skel-angular/.gitignore @@ -0,0 +1,12 @@ +node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor + +# Meteor Angular specific ignores +.nx +public/main.html +*.tsbuildinfo diff --git a/tools/static-assets/skel-angular/.meteor/.gitignore b/tools/static-assets/skel-angular/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/static-assets/skel-angular/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/static-assets/skel-angular/.meteor/packages b/tools/static-assets/skel-angular/.meteor/packages new file mode 100644 index 0000000000..b60cd50cfe --- /dev/null +++ b/tools/static-assets/skel-angular/.meteor/packages @@ -0,0 +1,25 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +~prototype~ +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data +zodern:types # Pull in type declarations from other Meteor packages + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-angular/.meteor/platforms b/tools/static-assets/skel-angular/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/static-assets/skel-angular/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/static-assets/skel-angular/client/main.css b/tools/static-assets/skel-angular/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/static-assets/skel-angular/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-angular/client/main.html b/tools/static-assets/skel-angular/client/main.html new file mode 100644 index 0000000000..9cb4a7318b --- /dev/null +++ b/tools/static-assets/skel-angular/client/main.html @@ -0,0 +1,11 @@ + + + ~name~ + + + + + + + + diff --git a/tools/static-assets/skel-angular/client/main.ts b/tools/static-assets/skel-angular/client/main.ts new file mode 100644 index 0000000000..1a275b9a76 --- /dev/null +++ b/tools/static-assets/skel-angular/client/main.ts @@ -0,0 +1,11 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { Meteor } from 'meteor/meteor'; +import { App } from '/imports/ui/app.component'; +import { appConfig } from '/imports/ui/app.config'; +import 'zone.js'; +import './main.css'; + +Meteor.startup(() => { + bootstrapApplication(App, appConfig) + .catch(err => console.error(err)); +}); diff --git a/tools/static-assets/skel-angular/imports/api/links.ts b/tools/static-assets/skel-angular/imports/api/links.ts new file mode 100644 index 0000000000..96cb6fc731 --- /dev/null +++ b/tools/static-assets/skel-angular/imports/api/links.ts @@ -0,0 +1,10 @@ +import { Mongo } from 'meteor/mongo'; + +export interface Link { + _id?: string; + title: string; + url: string; + createdAt?: Date; +} + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/static-assets/skel-angular/imports/ui/app.component.ts b/tools/static-assets/skel-angular/imports/ui/app.component.ts new file mode 100644 index 0000000000..f845730bb4 --- /dev/null +++ b/tools/static-assets/skel-angular/imports/ui/app.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { HelloComponent } from './hello.component'; +import { InfoComponent } from './info.component'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet, HelloComponent, InfoComponent], + templateUrl: './app.html', +}) +export class App implements OnInit { + title = 'Angular Meteor App'; + + ngOnInit(): void { + console.log('App component initialized!'); + } +} diff --git a/tools/static-assets/skel-angular/imports/ui/app.config.ts b/tools/static-assets/skel-angular/imports/ui/app.config.ts new file mode 100644 index 0000000000..aa340b12fa --- /dev/null +++ b/tools/static-assets/skel-angular/imports/ui/app.config.ts @@ -0,0 +1,11 @@ +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes) + ] +}; diff --git a/tools/static-assets/skel-angular/imports/ui/app.html b/tools/static-assets/skel-angular/imports/ui/app.html new file mode 100644 index 0000000000..bc2b859d5b --- /dev/null +++ b/tools/static-assets/skel-angular/imports/ui/app.html @@ -0,0 +1,58 @@ + + +
    +
    +

    Welcome to Meteor!

    + + + + +
    +
    + + diff --git a/tools/static-assets/skel-angular/imports/ui/app.routes.ts b/tools/static-assets/skel-angular/imports/ui/app.routes.ts new file mode 100644 index 0000000000..dc39edb5f2 --- /dev/null +++ b/tools/static-assets/skel-angular/imports/ui/app.routes.ts @@ -0,0 +1,3 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = []; diff --git a/tools/static-assets/skel-angular/imports/ui/hello.component.ts b/tools/static-assets/skel-angular/imports/ui/hello.component.ts new file mode 100644 index 0000000000..dbbbf669d0 --- /dev/null +++ b/tools/static-assets/skel-angular/imports/ui/hello.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-hello', + standalone: true, + template: ` +
    + +

    You've pressed the button {{ counter }} times.

    +
    + ` +}) +export class HelloComponent { + counter = 0; + + increment() { + this.counter += 1; + } +} diff --git a/tools/static-assets/skel-angular/imports/ui/info.component.ts b/tools/static-assets/skel-angular/imports/ui/info.component.ts new file mode 100644 index 0000000000..fe61b067e8 --- /dev/null +++ b/tools/static-assets/skel-angular/imports/ui/info.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit, OnDestroy, NgZone } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; +import { LinksCollection } from '../api/links'; + +interface Link { + _id?: string; + title: string; + url: string; +} + +@Component({ + selector: 'app-info', + standalone: true, + imports: [CommonModule], + template: ` +
    +

    Learn Meteor!

    + +
    Loading...
    +
    {{ error }}
    + + +
    + ` +}) +export class InfoComponent implements OnInit, OnDestroy { + loading = true; + error: string | null = null; + links: Link[] = []; + + private linksSub: Meteor.SubscriptionHandle | null = null; + private tracker: Tracker.Computation | null = null; + + constructor(private ngZone: NgZone) {} + + ngOnInit() { + // Start the subscription with simple lifecycle callbacks + this.linksSub = Meteor.subscribe('links', { + onReady: () => { + this.ngZone.run(() => { + this.loading = false; + }); + }, + onStop: (err?: Error) => { + this.ngZone.run(() => { + if (err) this.error = err.message || 'Subscription stopped'; + this.loading = false; + }); + } + }); + + // Reactively fetch data from the collection + this.tracker = Tracker.autorun(() => { + const links = LinksCollection.find({}).fetch() as Link[]; + this.ngZone.run(() => { + this.links = links; + }); + }); + } + + trackById = (index: number, link: Link) => link._id ?? index; + + ngOnDestroy() { + this.linksSub?.stop(); + this.tracker?.stop(); + } +} diff --git a/tools/static-assets/skel-angular/package.json b/tools/static-assets/skel-angular/package.json new file mode 100644 index 0000000000..fa3105b024 --- /dev/null +++ b/tools/static-assets/skel-angular/package.json @@ -0,0 +1,41 @@ +{ + "name": "~name~", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@angular/common": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/forms": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/router": "^20.0.0", + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "rxjs": "^7.8.1", + "zone.js": "^0.15.1" + }, + "devDependencies": { + "@angular/compiler-cli": "^20.0.0", + "@meteorjs/rspack": "^1.1.0-beta.31", + "@nx/angular-rspack": "^21.1.0", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@types/meteor": "^2.9.9", + "typescript": "~5.8.2" + }, + "meteor": { + "mainModule": { + "client": "client/main.ts", + "server": "server/main.ts" + }, + "testModule": "tests/main.ts", + "modern": true + } +} diff --git a/tools/static-assets/skel-angular/rspack.config.ts b/tools/static-assets/skel-angular/rspack.config.ts new file mode 100644 index 0000000000..320618af69 --- /dev/null +++ b/tools/static-assets/skel-angular/rspack.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "@meteorjs/rspack"; +import { createConfig } from '@nx/angular-rspack'; + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +export default defineConfig(async Meteor => { + const angularConfig = await createConfig({ + options: { + browser: Meteor.mainClientEntry, + index: Meteor.mainClientHtmlEntry, + }, + }); + + return { + ...(Meteor.isClient && !Meteor.isTest && angularConfig), + }; +}); diff --git a/tools/static-assets/skel-angular/server/main.ts b/tools/static-assets/skel-angular/server/main.ts new file mode 100644 index 0000000000..5b21e08ff6 --- /dev/null +++ b/tools/static-assets/skel-angular/server/main.ts @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { LinksCollection } from '/imports/api/links'; + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://angular.io/tutorial', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://angular.io/guide/architecture', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://angular.io/docs', + }); + + await insertLink({ + title: 'Join the Community', + url: 'https://angular.io/community', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/static-assets/skel-angular/tests/main.ts b/tools/static-assets/skel-angular/tests/main.ts new file mode 100644 index 0000000000..ea7a8da1e1 --- /dev/null +++ b/tools/static-assets/skel-angular/tests/main.ts @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("~name~", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "~name~"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/static-assets/skel-angular/tsconfig.app.json b/tools/static-assets/skel-angular/tsconfig.app.json new file mode 100644 index 0000000000..f96a960d6b --- /dev/null +++ b/tools/static-assets/skel-angular/tsconfig.app.json @@ -0,0 +1,18 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": ["meteor", "node"], + "skipLibCheck": true + }, + "include": [ + "client/**/*.ts", + "imports/**/*.ts", + "imports/ui/app.spec.ts" + ], + "exclude": [ + "imports/**/*.spec.ts" + ] +} diff --git a/tools/static-assets/skel-angular/tsconfig.json b/tools/static-assets/skel-angular/tsconfig.json new file mode 100644 index 0000000000..3e1f7efdb6 --- /dev/null +++ b/tools/static-assets/skel-angular/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es2022", + "lib": ["es2020", "dom"], + "experimentalDecorators": true, + "emitDecoratorMetadata": false, + + /* Module Resolution Options */ + "module": "preserve", + "types": ["node", "mocha"], + "baseUrl": ".", + "paths": { + /* Support absolute /imports/* with a leading '/' */ + "/*": ["*"], + /* Pull in type declarations for Meteor packages */ + "meteor/*": [ + "node_modules/@types/meteor/*", + ".meteor/local/types/packages.d.ts" + ] + }, + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "exclude": [ + "./.meteor/**", + "./packages/**", + "./_build/**", + "./public/build-chunks/**", + "./public/build-assets/**" + ] +} diff --git a/tools/static-assets/skel-apollo/.gitignore b/tools/static-assets/skel-apollo/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-apollo/.gitignore +++ b/tools/static-assets/skel-apollo/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-apollo/.meteor/packages b/tools/static-assets/skel-apollo/.meteor/packages index 718d356e3d..875c378835 100644 --- a/tools/static-assets/skel-apollo/.meteor/packages +++ b/tools/static-assets/skel-apollo/.meteor/packages @@ -20,3 +20,5 @@ hot-module-replacement # Update client in development without reloading the pag static-html # Define static page content in .html files apollo # Basic Apollo integration for Meteor apps compat:graphql # Import .graphql files + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-apollo/client/main.html b/tools/static-assets/skel-apollo/client/main.html index b0e2cf6b26..b8c5df2d31 100644 --- a/tools/static-assets/skel-apollo/client/main.html +++ b/tools/static-assets/skel-apollo/client/main.html @@ -1,5 +1,5 @@ - Apollo Skeleton + apollo diff --git a/tools/static-assets/skel-apollo/package.json b/tools/static-assets/skel-apollo/package.json index 8af983a7cb..5fd18f2131 100644 --- a/tools/static-assets/skel-apollo/package.json +++ b/tools/static-assets/skel-apollo/package.json @@ -17,6 +17,15 @@ "react": "^18.2.0", "react-dom": "^18.2.0" }, + "devDependencies": { + "@graphql-tools/webpack-loader": "^7.0.0", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "react-refresh": "^0.17.0" + }, "meteor": { "mainModule": { "client": "client/main.jsx", diff --git a/tools/static-assets/skel-apollo/rspack.config.js b/tools/static-assets/skel-apollo/rspack.config.js new file mode 100644 index 0000000000..48cbbabc56 --- /dev/null +++ b/tools/static-assets/skel-apollo/rspack.config.js @@ -0,0 +1,25 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + module: { + rules: [ + { + test: /\.(graphql|gql)$/i, + exclude: /node_modules/, + loader: '@graphql-tools/webpack-loader', + }, + ], + }, + }; +}); diff --git a/tools/static-assets/skel-babel/.babelrc b/tools/static-assets/skel-babel/.babelrc new file mode 100644 index 0000000000..283dab1090 --- /dev/null +++ b/tools/static-assets/skel-babel/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + ["module-resolver", { + "alias": { + "@ui": "./imports/ui", + "@api": "./imports/api" + } + }] + ] +} diff --git a/tools/static-assets/skel-babel/.meteor/.gitignore b/tools/static-assets/skel-babel/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/static-assets/skel-babel/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/static-assets/skel-babel/.meteor/packages b/tools/static-assets/skel-babel/.meteor/packages new file mode 100644 index 0000000000..522f0d0ce9 --- /dev/null +++ b/tools/static-assets/skel-babel/.meteor/packages @@ -0,0 +1,24 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +~prototype~ +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-babel/.meteor/platforms b/tools/static-assets/skel-babel/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/static-assets/skel-babel/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/static-assets/skel-babel/client/main.css b/tools/static-assets/skel-babel/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/static-assets/skel-babel/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-babel/client/main.html b/tools/static-assets/skel-babel/client/main.html new file mode 100644 index 0000000000..7c3d674342 --- /dev/null +++ b/tools/static-assets/skel-babel/client/main.html @@ -0,0 +1,8 @@ + + ~name~ + + + + +
    + diff --git a/tools/static-assets/skel-babel/client/main.jsx b/tools/static-assets/skel-babel/client/main.jsx new file mode 100644 index 0000000000..d2e380f93c --- /dev/null +++ b/tools/static-assets/skel-babel/client/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Meteor } from 'meteor/meteor'; +import { App } from '/imports/ui/App'; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); + const root = createRoot(container); + root.render(); +}); diff --git a/tools/static-assets/skel-babel/imports/api/links.js b/tools/static-assets/skel-babel/imports/api/links.js new file mode 100644 index 0000000000..050c508eae --- /dev/null +++ b/tools/static-assets/skel-babel/imports/api/links.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/static-assets/skel-babel/imports/ui/App.jsx b/tools/static-assets/skel-babel/imports/ui/App.jsx new file mode 100644 index 0000000000..6f7340caf9 --- /dev/null +++ b/tools/static-assets/skel-babel/imports/ui/App.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Hello } from './Hello.jsx'; +import { Info } from './Info.jsx'; + +export const App = () => ( +
    +

    Welcome to Meteor!

    + + +
    +); diff --git a/tools/static-assets/skel-babel/imports/ui/Hello.jsx b/tools/static-assets/skel-babel/imports/ui/Hello.jsx new file mode 100644 index 0000000000..15e0f185ac --- /dev/null +++ b/tools/static-assets/skel-babel/imports/ui/Hello.jsx @@ -0,0 +1,16 @@ +import React, { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
    + +

    You've pressed the button {counter} times.

    +
    + ); +}; diff --git a/tools/static-assets/skel-babel/imports/ui/Info.jsx b/tools/static-assets/skel-babel/imports/ui/Info.jsx new file mode 100644 index 0000000000..a9a7a45cfe --- /dev/null +++ b/tools/static-assets/skel-babel/imports/ui/Info.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useFind, useSubscribe } from 'meteor/react-meteor-data'; +import { LinksCollection } from '../api/links'; + +export const Info = () => { + const isLoading = useSubscribe('links'); + const links = useFind(() => LinksCollection.find()); + + if(isLoading()) { + return
    Loading...
    ; + } + + return ( +
    +

    Learn Meteor!

    + +
    + ); +}; diff --git a/tools/static-assets/skel-babel/package.json b/tools/static-assets/skel-babel/package.json new file mode 100644 index 0000000000..8c9c45753f --- /dev/null +++ b/tools/static-assets/skel-babel/package.json @@ -0,0 +1,37 @@ +{ + "name": "~name~", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@babel/preset-env": "^7.28.3", + "@babel/preset-react": "^7.23.3", + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "babel-loader": "^9.1.3", + "babel-plugin-module-resolver": "^5.0.0", + "react-refresh": "^0.17.0" + }, + "meteor": { + "mainModule": { + "client": "client/main.jsx", + "server": "server/main.js" + }, + "testModule": "tests/main.js", + "modern": true + } +} diff --git a/tools/static-assets/skel-babel/rspack.config.js b/tools/static-assets/skel-babel/rspack.config.js new file mode 100644 index 0000000000..9316881419 --- /dev/null +++ b/tools/static-assets/skel-babel/rspack.config.js @@ -0,0 +1,27 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + module: { + rules: [ + { + test: /\.(js|jsx)$/i, + exclude: /node_modules|\.meteor\/local/, + use: { + loader: 'babel-loader', + }, + }, + ], + }, + }; +}); diff --git a/tools/static-assets/skel-babel/server/main.js b/tools/static-assets/skel-babel/server/main.js new file mode 100644 index 0000000000..49452ad352 --- /dev/null +++ b/tools/static-assets/skel-babel/server/main.js @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { LinksCollection } from '/imports/api/links'; + +async function insertLink({ title, url }) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'https://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/static-assets/skel-babel/tests/main.js b/tools/static-assets/skel-babel/tests/main.js new file mode 100644 index 0000000000..ea7a8da1e1 --- /dev/null +++ b/tools/static-assets/skel-babel/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("~name~", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "~name~"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/static-assets/skel-blaze/.gitignore b/tools/static-assets/skel-blaze/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-blaze/.gitignore +++ b/tools/static-assets/skel-blaze/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-blaze/.meteor/packages b/tools/static-assets/skel-blaze/.meteor/packages index 5e929125ff..5e90e72654 100644 --- a/tools/static-assets/skel-blaze/.meteor/packages +++ b/tools/static-assets/skel-blaze/.meteor/packages @@ -23,3 +23,5 @@ shell-server # Server-side component of the `meteor shell` command hot-module-replacement # Update code in development without reloading the page blaze-hot # Update files using Blaze's API with HMR + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-blaze/package.json b/tools/static-assets/skel-blaze/package.json index e197964537..6dd838aaf6 100644 --- a/tools/static-assets/skel-blaze/package.json +++ b/tools/static-assets/skel-blaze/package.json @@ -13,6 +13,13 @@ "jquery": "^3.7.1", "meteor-node-stubs": "^1.2.12" }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3" + }, "meteor": { "mainModule": { "client": "client/main.js", diff --git a/tools/static-assets/skel-blaze/rspack.config.js b/tools/static-assets/skel-blaze/rspack.config.js new file mode 100644 index 0000000000..33f02e3bbe --- /dev/null +++ b/tools/static-assets/skel-blaze/rspack.config.js @@ -0,0 +1,15 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return {}; +}); diff --git a/tools/static-assets/skel-chakra-ui/.gitignore b/tools/static-assets/skel-chakra-ui/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-chakra-ui/.gitignore +++ b/tools/static-assets/skel-chakra-ui/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-chakra-ui/.meteor/packages b/tools/static-assets/skel-chakra-ui/.meteor/packages index 90ce4b06dd..522f0d0ce9 100644 --- a/tools/static-assets/skel-chakra-ui/.meteor/packages +++ b/tools/static-assets/skel-chakra-ui/.meteor/packages @@ -20,3 +20,5 @@ hot-module-replacement # Update client in development without reloading the pag ~prototype~ static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-chakra-ui/package.json b/tools/static-assets/skel-chakra-ui/package.json index 99b3e13d76..a30ef9f180 100644 --- a/tools/static-assets/skel-chakra-ui/package.json +++ b/tools/static-assets/skel-chakra-ui/package.json @@ -20,6 +20,14 @@ "react": "^18.2.0", "react-dom": "^18.2.0" }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "react-refresh": "^0.17.0" + }, "meteor": { "mainModule": { "client": "client/main.jsx", diff --git a/tools/static-assets/skel-chakra-ui/rspack.config.js b/tools/static-assets/skel-chakra-ui/rspack.config.js new file mode 100644 index 0000000000..33f02e3bbe --- /dev/null +++ b/tools/static-assets/skel-chakra-ui/rspack.config.js @@ -0,0 +1,15 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return {}; +}); diff --git a/tools/static-assets/skel-chakra-ui/tests/main.js b/tools/static-assets/skel-chakra-ui/tests/main.js index c48096a6fb..4d9b5bdf13 100644 --- a/tools/static-assets/skel-chakra-ui/tests/main.js +++ b/tools/static-assets/skel-chakra-ui/tests/main.js @@ -3,7 +3,7 @@ import assert from "assert"; describe("chakra-template", function () { it("package.json has correct name", async function () { const { name } = await import("../package.json"); - assert.strictEqual(name, "chakra-template"); + assert.strictEqual(name, "~name~"); }); if (Meteor.isClient) { diff --git a/tools/static-assets/skel-coffeescript/.gitignore b/tools/static-assets/skel-coffeescript/.gitignore new file mode 100644 index 0000000000..3e954aa73f --- /dev/null +++ b/tools/static-assets/skel-coffeescript/.gitignore @@ -0,0 +1,7 @@ +node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-coffeescript/.meteor/.gitignore b/tools/static-assets/skel-coffeescript/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/static-assets/skel-coffeescript/.meteor/packages b/tools/static-assets/skel-coffeescript/.meteor/packages new file mode 100644 index 0000000000..ea68995653 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/.meteor/packages @@ -0,0 +1,25 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +~prototype~ +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data +coffeescript # CoffeeScript support + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-coffeescript/.meteor/platforms b/tools/static-assets/skel-coffeescript/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/static-assets/skel-coffeescript/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/static-assets/skel-coffeescript/client/main.coffee b/tools/static-assets/skel-coffeescript/client/main.coffee new file mode 100644 index 0000000000..406b81b26c --- /dev/null +++ b/tools/static-assets/skel-coffeescript/client/main.coffee @@ -0,0 +1,9 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { Meteor } from 'meteor/meteor' +import { App } from '/imports/ui/App.coffee' + +Meteor.startup -> + container = document.getElementById('react-target') + root = createRoot(container) + root.render() diff --git a/tools/static-assets/skel-coffeescript/client/main.css b/tools/static-assets/skel-coffeescript/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-coffeescript/client/main.html b/tools/static-assets/skel-coffeescript/client/main.html new file mode 100644 index 0000000000..7c3d674342 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/client/main.html @@ -0,0 +1,8 @@ + + ~name~ + + + + +
    + diff --git a/tools/static-assets/skel-coffeescript/imports/api/links.coffee b/tools/static-assets/skel-coffeescript/imports/api/links.coffee new file mode 100644 index 0000000000..95c5ed8cfc --- /dev/null +++ b/tools/static-assets/skel-coffeescript/imports/api/links.coffee @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo' + +export LinksCollection = new Mongo.Collection('links') diff --git a/tools/static-assets/skel-coffeescript/imports/ui/App.coffee b/tools/static-assets/skel-coffeescript/imports/ui/App.coffee new file mode 100644 index 0000000000..f6cf0f2b01 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/imports/ui/App.coffee @@ -0,0 +1,10 @@ +import React from 'react' +import { Hello } from './Hello.coffee' +import { Info } from './Info.coffee' + +export App = -> +
    +

    Welcome to Meteor!

    + + +
    diff --git a/tools/static-assets/skel-coffeescript/imports/ui/Hello.coffee b/tools/static-assets/skel-coffeescript/imports/ui/Hello.coffee new file mode 100644 index 0000000000..ab48a1e4f2 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/imports/ui/Hello.coffee @@ -0,0 +1,12 @@ +import React, { useState } from 'react' + +export Hello = -> + [counter, setCounter] = useState(0) + + increment = -> + setCounter(counter + 1) + +
    + +

    You've pressed the button {counter} times.

    +
    diff --git a/tools/static-assets/skel-coffeescript/imports/ui/Info.coffee b/tools/static-assets/skel-coffeescript/imports/ui/Info.coffee new file mode 100644 index 0000000000..062d35eee2 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/imports/ui/Info.coffee @@ -0,0 +1,19 @@ +import React from 'react' +import { useFind, useSubscribe } from 'meteor/react-meteor-data' +import { LinksCollection } from '../api/links.coffee' + +export Info = -> + isLoading = useSubscribe('links') + links = useFind(-> LinksCollection.find()) + + if isLoading() + return
    Loading...
    + +
    +

    Learn Meteor!

    + +
    diff --git a/tools/static-assets/skel-coffeescript/package.json b/tools/static-assets/skel-coffeescript/package.json new file mode 100644 index 0000000000..6f2ebbce93 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/package.json @@ -0,0 +1,36 @@ +{ + "name": "~name~", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "meteor-node-stubs": "^1.2.12", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "coffee-loader": "^5.0.0", + "coffeescript": "^2.7.0", + "react-refresh": "^0.17.0", + "swc-loader": "^0.2.6" + }, + "meteor": { + "mainModule": { + "client": "client/main.coffee", + "server": "server/main.coffee" + }, + "testModule": "tests/main.coffee", + "modern": true + } +} diff --git a/tools/static-assets/skel-coffeescript/rspack.config.js b/tools/static-assets/skel-coffeescript/rspack.config.js new file mode 100644 index 0000000000..00811e5840 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/rspack.config.js @@ -0,0 +1,36 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + module: { + rules: [ + { + test: /\.coffee$/i, + use: [ + { + loader: 'swc-loader', + // preserve SWC config in the Meteor project level + options: Meteor.swcConfigOptions, + }, + { + loader: 'coffee-loader', + }, + ], + }, + ], + }, + resolve: { + extensions: ['.coffee'], + }, + }; +}); diff --git a/tools/static-assets/skel-coffeescript/server/main.coffee b/tools/static-assets/skel-coffeescript/server/main.coffee new file mode 100644 index 0000000000..35d653b1a7 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/server/main.coffee @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor' +import { LinksCollection } from '/imports/api/links.coffee' + +insertLink = ({ title, url }) -> + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }) + +Meteor.startup -> + # If the Links collection is empty, add some data. + if await LinksCollection.find().countAsync() is 0 + await insertLink + title: 'Do the Tutorial' + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html' + + await insertLink + title: 'Follow the Guide' + url: 'https://guide.meteor.com' + + await insertLink + title: 'Read the Docs' + url: 'https://docs.meteor.com' + + await insertLink + title: 'Discussions' + url: 'https://forums.meteor.com' + + # We publish the entire Links collection to all clients. + # In order to be fetched in real-time to the clients + Meteor.publish "links", -> + LinksCollection.find() diff --git a/tools/static-assets/skel-coffeescript/tests/main.coffee b/tools/static-assets/skel-coffeescript/tests/main.coffee new file mode 100644 index 0000000000..5adec09774 --- /dev/null +++ b/tools/static-assets/skel-coffeescript/tests/main.coffee @@ -0,0 +1,15 @@ +import { Meteor } from 'meteor/meteor' +import assert from 'assert' + +describe '~name~', -> + it 'package.json has correct name', -> + packageJson = require('../package.json') + assert.strictEqual(packageJson.name, '~name~') + + if Meteor.isClient + it 'client is not server', -> + assert.strictEqual(Meteor.isServer, false) + + if Meteor.isServer + it 'server is not client', -> + assert.strictEqual(Meteor.isClient, false) diff --git a/tools/static-assets/skel-full/.gitignore b/tools/static-assets/skel-full/.gitignore index 40b878db5b..3e954aa73f 100644 --- a/tools/static-assets/skel-full/.gitignore +++ b/tools/static-assets/skel-full/.gitignore @@ -1 +1,7 @@ -node_modules/ \ No newline at end of file +node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-full/.meteor/packages b/tools/static-assets/skel-full/.meteor/packages index 42dd3fa370..45b99ff8b6 100644 --- a/tools/static-assets/skel-full/.meteor/packages +++ b/tools/static-assets/skel-full/.meteor/packages @@ -24,4 +24,6 @@ ostrio:flow-router-extra # FlowRouter is a very simple router for Meteor less # Leaner CSS language meteortesting:mocha # A package for writing and running your meteor app and package tests with mocha -johanbrook:publication-collector # Test a Meteor publication by collecting its output +communitypackages:publication-collector@2.0.0-rc.1 # Test a Meteor publication by collecting its output + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-full/client/main.less b/tools/static-assets/skel-full/client/main.less index bca7b340a3..fee4b2d78c 100644 --- a/tools/static-assets/skel-full/client/main.less +++ b/tools/static-assets/skel-full/client/main.less @@ -1 +1,6 @@ @import "{}/imports/ui/stylesheets/not-found.less"; + +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-full/imports/api/links/links.tests.js b/tools/static-assets/skel-full/imports/api/links/links.tests.js index 3f700304fe..183b07b3ae 100644 --- a/tools/static-assets/skel-full/imports/api/links/links.tests.js +++ b/tools/static-assets/skel-full/imports/api/links/links.tests.js @@ -8,14 +8,14 @@ import { Links } from './links.js'; if (Meteor.isServer) { describe('links collection', function () { - it('insert correctly', function () { - const linkId = Links.insert({ + it('insert correctly', async function () { + const linkId = await Links.insertAsync({ title: 'meteor homepage', url: 'https://www.meteor.com', }); const added = Links.find({ _id: linkId }); const collectionName = added._getCollectionName(); - const count = added.count(); + const count = await added.countAsync(); assert.equal(collectionName, 'links'); assert.equal(count, 1); diff --git a/tools/static-assets/skel-full/imports/api/links/methods.js b/tools/static-assets/skel-full/imports/api/links/methods.js index 07ec236180..4b76d99977 100644 --- a/tools/static-assets/skel-full/imports/api/links/methods.js +++ b/tools/static-assets/skel-full/imports/api/links/methods.js @@ -5,11 +5,11 @@ import { check } from 'meteor/check'; import { Links } from './links.js'; Meteor.methods({ - 'links.insert'(title, url) { + async 'links.insert'(title, url) { check(url, String); check(title, String); - return Links.insert({ + return Links.insertAsync({ url, title, createdAt: new Date(), diff --git a/tools/static-assets/skel-full/imports/api/links/methods.tests.js b/tools/static-assets/skel-full/imports/api/links/methods.tests.js index 8011df3759..69908d1582 100644 --- a/tools/static-assets/skel-full/imports/api/links/methods.tests.js +++ b/tools/static-assets/skel-full/imports/api/links/methods.tests.js @@ -9,16 +9,16 @@ import './methods.js'; if (Meteor.isServer) { describe('links methods', function () { - beforeEach(function () { - Links.remove({}); + beforeEach(async function () { + await Links.removeAsync({}); }); - it('can add a new link', function () { + it('can add a new link', async function () { const addLink = Meteor.server.method_handlers['links.insert']; - addLink.apply({}, ['meteor.com', 'https://www.meteor.com']); + await addLink.apply({}, ['meteor.com', 'https://www.meteor.com']); - assert.equal(Links.find().count(), 1); + assert.equal(await Links.find().countAsync(), 1); }); }); } diff --git a/tools/static-assets/skel-full/imports/api/links/publications.js b/tools/static-assets/skel-full/imports/api/links/publications.js new file mode 100644 index 0000000000..249b44ca59 --- /dev/null +++ b/tools/static-assets/skel-full/imports/api/links/publications.js @@ -0,0 +1,10 @@ +// All links-related publications + +import { Meteor } from 'meteor/meteor'; +import { Links } from './links.js'; + +if (Meteor.isServer) { + Meteor.publish('links.all', function() { + return Links.find(); + }); +} diff --git a/tools/static-assets/skel-full/imports/api/links/publications.tests.js b/tools/static-assets/skel-full/imports/api/links/publications.tests.js new file mode 100644 index 0000000000..c96cdb201b --- /dev/null +++ b/tools/static-assets/skel-full/imports/api/links/publications.tests.js @@ -0,0 +1,30 @@ +// Tests for the links publications +// +// https://guide.meteor.com/testing.html + +import { assert } from 'chai'; +import { Links } from './links.js'; +import { PublicationCollector } from 'meteor/communitypackages:publication-collector'; +import './publications.js'; + +if (Meteor.isServer) { + describe('links publications', function() { + beforeEach(async function() { + await Links.removeAsync({}); + await Links.insertAsync({ + title: 'meteor homepage', + url: 'https://www.meteor.com', + }); + }); + + describe('links.all', function() { + it('sends all links', function(done) { + const collector = new PublicationCollector(); + collector.collect('links.all', (collections) => { + assert.equal(collections.links.length, 1); + done(); + }); + }); + }); + }); +} diff --git a/tools/static-assets/skel-full/imports/api/links/server/publications.tests.js b/tools/static-assets/skel-full/imports/api/links/server/publications.tests.js deleted file mode 100644 index 8725954ae8..0000000000 --- a/tools/static-assets/skel-full/imports/api/links/server/publications.tests.js +++ /dev/null @@ -1,28 +0,0 @@ -// Tests for the links publications -// -// https://guide.meteor.com/testing.html - -import { assert } from 'chai'; -import { Links } from '../links.js'; -import { PublicationCollector } from 'meteor/johanbrook:publication-collector'; -import './publications.js'; - -describe('links publications', function () { - beforeEach(function () { - Links.remove({}); - Links.insert({ - title: 'meteor homepage', - url: 'https://www.meteor.com', - }); - }); - - describe('links.all', function () { - it('sends all links', function (done) { - const collector = new PublicationCollector(); - collector.collect('links.all', (collections) => { - assert.equal(collections.links.length, 1); - done(); - }); - }); - }); -}); diff --git a/tools/static-assets/skel-full/imports/startup/server/register-api.js b/tools/static-assets/skel-full/imports/startup/server/register-api.js index 79e834f4ac..c4646d9789 100644 --- a/tools/static-assets/skel-full/imports/startup/server/register-api.js +++ b/tools/static-assets/skel-full/imports/startup/server/register-api.js @@ -1,4 +1,4 @@ // Register your apis here +import '../../api/links/publications.js'; import '../../api/links/methods.js'; -import '../../api/links/server/publications.js'; diff --git a/tools/static-assets/skel-full/imports/ui/components/hello/hello.html b/tools/static-assets/skel-full/imports/ui/components/hello/hello.html index 58bd0b35d6..3cc0557024 100644 --- a/tools/static-assets/skel-full/imports/ui/components/hello/hello.html +++ b/tools/static-assets/skel-full/imports/ui/components/hello/hello.html @@ -1,4 +1,6 @@ diff --git a/tools/static-assets/skel-full/package.json b/tools/static-assets/skel-full/package.json index 1abdbdd683..e7f8bb952e 100644 --- a/tools/static-assets/skel-full/package.json +++ b/tools/static-assets/skel-full/package.json @@ -12,6 +12,10 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", "chai": "^4.2.0" }, "meteor": { diff --git a/tools/static-assets/skel-full/rspack.config.js b/tools/static-assets/skel-full/rspack.config.js new file mode 100644 index 0000000000..33f02e3bbe --- /dev/null +++ b/tools/static-assets/skel-full/rspack.config.js @@ -0,0 +1,15 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return {}; +}); diff --git a/tools/static-assets/skel-legacy/.gitignore b/tools/static-assets/skel-legacy/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/static-assets/skel-legacy/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/static-assets/skel-legacy/.meteor/.gitignore b/tools/static-assets/skel-legacy/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/static-assets/skel-legacy/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/static-assets/skel-legacy/.meteor/packages b/tools/static-assets/skel-legacy/.meteor/packages new file mode 100644 index 0000000000..5e929125ff --- /dev/null +++ b/tools/static-assets/skel-legacy/.meteor/packages @@ -0,0 +1,25 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +blaze-html-templates # Compile .html files into Meteor Blaze views +jquery # Wrapper package for npm-installed jquery +reactive-var # Reactive variable for tracker +tracker # Meteor's client-side reactive programming library + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command + +~prototype~ + +hot-module-replacement # Update code in development without reloading the page +blaze-hot # Update files using Blaze's API with HMR diff --git a/tools/static-assets/skel-legacy/.meteor/platforms b/tools/static-assets/skel-legacy/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/static-assets/skel-legacy/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/static-assets/skel-legacy/client/main.css b/tools/static-assets/skel-legacy/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/static-assets/skel-legacy/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-legacy/client/main.html b/tools/static-assets/skel-legacy/client/main.html new file mode 100644 index 0000000000..e2f29f3248 --- /dev/null +++ b/tools/static-assets/skel-legacy/client/main.html @@ -0,0 +1,26 @@ + + ~name~ + + + + +

    Welcome to Meteor!

    + + {{> hello}} + {{> info}} + + + + + diff --git a/tools/static-assets/skel-legacy/client/main.js b/tools/static-assets/skel-legacy/client/main.js new file mode 100644 index 0000000000..ecb3282a2f --- /dev/null +++ b/tools/static-assets/skel-legacy/client/main.js @@ -0,0 +1,22 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; + +import './main.html'; + +Template.hello.onCreated(function helloOnCreated() { + // counter starts at 0 + this.counter = new ReactiveVar(0); +}); + +Template.hello.helpers({ + counter() { + return Template.instance().counter.get(); + }, +}); + +Template.hello.events({ + 'click button'(event, instance) { + // increment the counter when button is clicked + instance.counter.set(instance.counter.get() + 1); + }, +}); diff --git a/tools/static-assets/skel-legacy/package.json b/tools/static-assets/skel-legacy/package.json new file mode 100644 index 0000000000..e197964537 --- /dev/null +++ b/tools/static-assets/skel-legacy/package.json @@ -0,0 +1,24 @@ +{ + "name": "~name~", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "jquery": "^3.7.1", + "meteor-node-stubs": "^1.2.12" + }, + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + }, + "testModule": "tests/main.js", + "modern": true + } +} diff --git a/tools/static-assets/skel-legacy/server/main.js b/tools/static-assets/skel-legacy/server/main.js new file mode 100644 index 0000000000..31a9e0e2d6 --- /dev/null +++ b/tools/static-assets/skel-legacy/server/main.js @@ -0,0 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + +Meteor.startup(() => { + // code to run on server at startup +}); diff --git a/tools/static-assets/skel-legacy/tests/main.js b/tools/static-assets/skel-legacy/tests/main.js new file mode 100644 index 0000000000..ea7a8da1e1 --- /dev/null +++ b/tools/static-assets/skel-legacy/tests/main.js @@ -0,0 +1,20 @@ +import assert from "assert"; + +describe("~name~", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "~name~"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/static-assets/skel-react/.gitignore b/tools/static-assets/skel-react/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-react/.gitignore +++ b/tools/static-assets/skel-react/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-react/.meteor/packages b/tools/static-assets/skel-react/.meteor/packages index 90ce4b06dd..522f0d0ce9 100644 --- a/tools/static-assets/skel-react/.meteor/packages +++ b/tools/static-assets/skel-react/.meteor/packages @@ -20,3 +20,5 @@ hot-module-replacement # Update client in development without reloading the pag ~prototype~ static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-react/.swcrc b/tools/static-assets/skel-react/.swcrc new file mode 100644 index 0000000000..e0a05f1803 --- /dev/null +++ b/tools/static-assets/skel-react/.swcrc @@ -0,0 +1,9 @@ +{ + "jsc": { + "transform": { + "react": { + "runtime": "automatic" + } + } + } +} diff --git a/tools/static-assets/skel-react/client/main.html b/tools/static-assets/skel-react/client/main.html index 7c3d674342..c64994b75c 100644 --- a/tools/static-assets/skel-react/client/main.html +++ b/tools/static-assets/skel-react/client/main.html @@ -1,6 +1,10 @@ ~name~ + diff --git a/tools/static-assets/skel-react/client/main.jsx b/tools/static-assets/skel-react/client/main.jsx index d2e380f93c..879c8859a8 100644 --- a/tools/static-assets/skel-react/client/main.jsx +++ b/tools/static-assets/skel-react/client/main.jsx @@ -1,10 +1,10 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { Meteor } from 'meteor/meteor'; -import { App } from '/imports/ui/App'; +import { createRoot } from "react-dom/client"; +import { Meteor } from "meteor/meteor"; +import { App } from "/imports/ui/App"; +import "/imports/ui/styles.css"; Meteor.startup(() => { - const container = document.getElementById('react-target'); + const container = document.getElementById("react-target"); const root = createRoot(container); root.render(); }); diff --git a/tools/static-assets/skel-react/imports/ui/App.jsx b/tools/static-assets/skel-react/imports/ui/App.jsx index 6f7340caf9..6b12ade912 100644 --- a/tools/static-assets/skel-react/imports/ui/App.jsx +++ b/tools/static-assets/skel-react/imports/ui/App.jsx @@ -1,11 +1,13 @@ -import React from 'react'; -import { Hello } from './Hello.jsx'; -import { Info } from './Info.jsx'; +import { Counter } from "./Counter.jsx"; +import { Header } from "./Header.jsx"; +import { Info } from "./Info.jsx"; export const App = () => ( -
    -

    Welcome to Meteor!

    - - +
    +
    +
    + + +
    ); diff --git a/tools/static-assets/skel-react/imports/ui/Counter.jsx b/tools/static-assets/skel-react/imports/ui/Counter.jsx new file mode 100644 index 0000000000..7fcb4d8404 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/Counter.jsx @@ -0,0 +1,24 @@ +import { useState } from "react"; + +export const Counter = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
    +
    + +

    + You've pressed the button{" "} + {counter}{" "} + {counter === 1 ? "time" : "times"}. +

    +
    +
    + ); +}; diff --git a/tools/static-assets/skel-react/imports/ui/Header.jsx b/tools/static-assets/skel-react/imports/ui/Header.jsx new file mode 100644 index 0000000000..9024b823c5 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/Header.jsx @@ -0,0 +1,14 @@ +import MeteorLogo from "./meteor-logo.svg"; + +export const Header = () => { + return ( +
    + +
    + ); +}; diff --git a/tools/static-assets/skel-react/imports/ui/Info.jsx b/tools/static-assets/skel-react/imports/ui/Info.jsx index a9a7a45cfe..823309d4d1 100644 --- a/tools/static-assets/skel-react/imports/ui/Info.jsx +++ b/tools/static-assets/skel-react/imports/ui/Info.jsx @@ -1,23 +1,30 @@ -import React from 'react'; -import { useFind, useSubscribe } from 'meteor/react-meteor-data'; -import { LinksCollection } from '../api/links'; +import { useFind, useSubscribe } from "meteor/react-meteor-data"; +import { LinksCollection } from "../api/links"; export const Info = () => { - const isLoading = useSubscribe('links'); + const isLoading = useSubscribe("links"); const links = useFind(() => LinksCollection.find()); - if(isLoading()) { + if (isLoading()) { return
    Loading...
    ; } return ( -
    -

    Learn Meteor!

    - -
    +
    +

    Learn Meteor!

    + +
    ); }; diff --git a/tools/static-assets/skel-react/imports/ui/meteor-logo.svg b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg new file mode 100644 index 0000000000..3610e0e026 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/static-assets/skel-react/imports/ui/styles.css b/tools/static-assets/skel-react/imports/ui/styles.css new file mode 100644 index 0000000000..79b55b3465 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/styles.css @@ -0,0 +1,304 @@ +/* this file is imported in client/main.jsx */ + +:root { + /* Colors */ + --color-background: hsl(210, 20%, 98%); + --color-foreground: hsl(220, 20%, 15%); + --color-card: hsl(0, 0%, 100%); + --color-primary: hsl(4, 70%, 55%); + --color-primary-hover: hsl(4, 70%, 45%); + --color-muted: hsl(220, 10%, 50%); + --color-border: hsl(220, 14%, 90%); + + /* Shadows */ + --shadow-card: 0 1px 3px 0 hsl(220 20% 15% / 0.04), + 0 1px 2px -1px hsl(220 20% 15% / 0.04); + --shadow-card-hover: 0 10px 15px -3px hsl(220 20% 15% / 0.08), + 0 4px 6px -4px hsl(220 20% 15% / 0.04); + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + /* Border radius */ + --radius: 0.75rem; + --radius-sm: 0.5rem; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 200ms ease; + --transition-slow: 250ms ease; +} + +/* ============ Reset & Base Styles ============ */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; + background-color: var(--color-background); + color: var(--color-foreground); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + text-decoration: none; + color: inherit; +} + +/* ============ Layout ============ */ +.page { + min-height: 100vh; + background-color: var(--color-background); +} + +.container { + width: 100%; + max-width: 1280px; + margin: 0 auto; + padding-left: var(--spacing-md); + padding-right: var(--spacing-md); +} + +@media (min-width: 768px) { + .container { + padding-left: var(--spacing-lg); + padding-right: var(--spacing-lg); + } +} + +/* ============ Header / Navigation ============ */ +.header { + border-bottom: 1px solid var(--color-border); + background-color: var(--color-card); + border-radius: var(--radius); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + height: 4rem; +} + +.logo-container { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.logo { + width: 4rem; + height: 4rem; +} + +.logo-text { + font-weight: 600; + color: var(--color-foreground); + display: none; +} + +@media (min-width: 640px) { + .logo-text { + display: inline; + } +} + +.page-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-foreground); + letter-spacing: -0.025em; +} + +@media (min-width: 768px) { + .page-title { + font-size: 1.5rem; + } +} + +.nav-link { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-primary); + transition: opacity var(--transition-fast); +} + +.nav-link:hover { + opacity: 0.8; +} + +/* ============ Main Content ============ */ +.main { + padding-top: var(--spacing-xl); + padding-bottom: var(--spacing-xl); +} + +@media (min-width: 768px) { + .main { + padding-top: var(--spacing-2xl); + padding-bottom: var(--spacing-2xl); + } +} + +/* ============ Card Component ============ */ +.card { + background-color: var(--color-card); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + border: 1px solid var(--color-border); +} + +/* ============ Counter Section ============ */ +.counter-card { + padding: var(--spacing-lg); + margin-bottom: 2.5rem; +} + +@media (min-width: 768px) { + .counter-card { + padding: var(--spacing-xl); + margin-bottom: var(--spacing-2xl); + } +} + +.counter-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-md); +} + +@media (min-width: 640px) { + .counter-content { + flex-direction: row; + } +} + +.counter-text { + color: var(--color-muted); + text-align: center; +} + +@media (min-width: 640px) { + .counter-text { + text-align: left; + } +} + +.counter-value { + font-weight: 600; + color: var(--color-foreground); +} + +/* ============ Button Component ============ */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; + padding: 0.625rem 1.5rem; + font-size: 0.875rem; + font-weight: 500; + font-family: inherit; + color: white; + background-color: var(--color-primary); + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background-color var(--transition-normal), + transform var(--transition-fast); +} + +.button:hover { + background-color: var(--color-primary-hover); +} + +.button:active { + transform: scale(0.98); +} + +.button:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* ============ Resources Section ============ */ +.section-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-foreground); + margin-bottom: var(--spacing-lg); + letter-spacing: -0.025em; +} + +.resources-grid { + list-style-type: none; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-md); +} + +@media (min-width: 640px) { + .resources-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +/* ============ Resource Card ============ */ +.resource-link { + display: block; +} + +.resource-card { + padding: 1.25rem; + transition: box-shadow var(--transition-slow), + transform var(--transition-slow); +} + +.resource-card:hover { + box-shadow: var(--shadow-card-hover); + transform: translateY(-2px); +} + +.resource-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.resource-title { + font-weight: 500; + color: var(--color-foreground); + transition: color var(--transition-fast); +} + +.resource-link:hover .resource-title { + color: var(--color-primary); +} + +.resource-icon { + width: 1rem; + height: 1rem; + color: var(--color-muted); + opacity: 0; + transition: opacity var(--transition-fast), color var(--transition-fast); +} + +.resource-link:hover .resource-icon { + opacity: 1; + color: var(--color-primary); +} diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index ee33ff775c..1091b1727a 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -14,6 +14,15 @@ "react": "^18.2.0", "react-dom": "^18.2.0" }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "@svgr/webpack": "^8.1.0", + "react-refresh": "^0.17.0" + }, "meteor": { "mainModule": { "client": "client/main.jsx", diff --git a/tools/static-assets/skel-react/rspack.config.js b/tools/static-assets/skel-react/rspack.config.js new file mode 100644 index 0000000000..dc159ce4d2 --- /dev/null +++ b/tools/static-assets/skel-react/rspack.config.js @@ -0,0 +1,26 @@ +const { defineConfig } = require("@meteorjs/rspack"); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig((Meteor) => { + return { + module: { + rules: [ + // Add support for importing SVGs as React components + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + use: ["@svgr/webpack"], + }, + ], + }, + }; +}); diff --git a/tools/static-assets/skel-react/server/main.js b/tools/static-assets/skel-react/server/main.js index 49452ad352..b6adcc3a56 100644 --- a/tools/static-assets/skel-react/server/main.js +++ b/tools/static-assets/skel-react/server/main.js @@ -1,5 +1,6 @@ -import { Meteor } from 'meteor/meteor'; -import { LinksCollection } from '/imports/api/links'; +import { Meteor } from "meteor/meteor"; +import { LinksCollection } from "/imports/api/links"; +import { Random } from "meteor/random"; async function insertLink({ title, url }) { await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); @@ -7,25 +8,35 @@ async function insertLink({ title, url }) { Meteor.startup(async () => { // If the Links collection is empty, add some data. - if (await LinksCollection.find().countAsync() === 0) { + if ((await LinksCollection.find().countAsync()) === 0) { await insertLink({ - title: 'Do the Tutorial', - url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + title: "Do the Tutorial", + url: "https://docs.meteor.com/tutorials/react/", }); await insertLink({ - title: 'Follow the Guide', - url: 'https://guide.meteor.com', + title: "Follow the Guide", + url: "https://docs.meteor.com/tutorials/application-structure/", }); await insertLink({ - title: 'Read the Docs', - url: 'https://docs.meteor.com', + title: "Read the Docs", + url: "https://docs.meteor.com", }); await insertLink({ - title: 'Discussions', - url: 'https://forums.meteor.com', + title: "Discussions", + url: "https://forums.meteor.com", + }); + + await insertLink({ + title: "Join us on Discord", + url: "https://discord.gg/6mS3wHNg", + }); + + await insertLink({ + title: "Deploying in Galaxy", + url: "https://www.meteor.com/hosting", }); } @@ -35,3 +46,9 @@ Meteor.startup(async () => { return LinksCollection.find(); }); }); + +Meteor.methods({ + about() { + return `This is a Meteor application running React with React Router. this is a generated id: ${Random.id()}`; + }, +}); diff --git a/tools/static-assets/skel-solid/.gitignore b/tools/static-assets/skel-solid/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-solid/.gitignore +++ b/tools/static-assets/skel-solid/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-solid/.meteor/packages b/tools/static-assets/skel-solid/.meteor/packages index 665679a90c..7255cc3d21 100644 --- a/tools/static-assets/skel-solid/.meteor/packages +++ b/tools/static-assets/skel-solid/.meteor/packages @@ -19,4 +19,5 @@ hot-module-replacement # Update client in development without reloading the pag ~prototype~ static-html # Define static page content in .html files -jorgenvatle:vite + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-solid/client/entry-meteor.js b/tools/static-assets/skel-solid/client/entry-meteor.js deleted file mode 100644 index 0ec3ddc121..0000000000 --- a/tools/static-assets/skel-solid/client/entry-meteor.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Entrypoint for the Meteor client - * - * Generally, this file can be left empty. Vite will add imports for - * lazy-loaded Meteor packages to this file to ensure they aren't omitted from - * the final production bundle. - * - * Use ./main.js as the primary entrypoint for your client code to take full - * advantage of Vite's plugin and build system. - * - * This can also be a good place to put code that you don't want Vite to - * process, for example, if you run into a compatibility issue or need to use - * nested imports which Vite doesn't support. - */ \ No newline at end of file diff --git a/tools/static-assets/skel-solid/package.json b/tools/static-assets/skel-solid/package.json index 17401d9c1b..d14262c517 100644 --- a/tools/static-assets/skel-solid/package.json +++ b/tools/static-assets/skel-solid/package.json @@ -11,22 +11,24 @@ "@babel/runtime": "^7.23.9", "@swc/helpers": "^0.5.17", "meteor-node-stubs": "^1.2.12", - "picocolors": "^1.1.1", - "solid-js": "^1.9.4" + "picocolors": "^1.1.1" + }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "babel-loader": "10.0.0", + "babel-preset-solid": "^1.8.15", + "solid-js": "^1.9.4", + "solid-refresh": "0.7.5" }, "meteor": { "mainModule": { - "client": "client/entry-meteor.js", - "server": "server/entry-meteor.js" + "client": "client/main.js", + "server": "server/main.js" }, "testModule": "tests/main.js", "modern": true - }, - "devDependencies": { - "babel-preset-solid": "^1.8.15", - "meteor-vite": "^3.2.1", - "vite": "^6.0.11", - "vite-plugin-solid": "^2.11.0", - "vite-plugin-solid-svg": "^0.8.1" } } diff --git a/tools/static-assets/skel-solid/rspack.config.js b/tools/static-assets/skel-solid/rspack.config.js new file mode 100644 index 0000000000..2a399ac99c --- /dev/null +++ b/tools/static-assets/skel-solid/rspack.config.js @@ -0,0 +1,38 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.isClient && { + module: { + rules: [ + { + test: /\.jsx$/, + use: [ + { + loader: 'babel-loader', + options: { + presets: [['solid']], + plugins: ['solid-refresh/babel'], + }, + }, + ], + }, + { + test: /\.svg$/, + type: 'asset/resource', + }, + ], + }, + } + }; +}); diff --git a/tools/static-assets/skel-solid/tests/main.js b/tools/static-assets/skel-solid/tests/main.js index 6486533db3..ea7a8da1e1 100644 --- a/tools/static-assets/skel-solid/tests/main.js +++ b/tools/static-assets/skel-solid/tests/main.js @@ -1,9 +1,9 @@ import assert from "assert"; -describe("solid-template", function () { +describe("~name~", function () { it("package.json has correct name", async function () { const { name } = await import("../package.json"); - assert.strictEqual(name, "solid-template"); + assert.strictEqual(name, "~name~"); }); if (Meteor.isClient) { diff --git a/tools/static-assets/skel-solid/vite.config.mjs b/tools/static-assets/skel-solid/vite.config.mjs deleted file mode 100644 index e0215dbb10..0000000000 --- a/tools/static-assets/skel-solid/vite.config.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'vite'; -import solidPlugin from 'vite-plugin-solid'; -import solidSvg from "vite-plugin-solid-svg"; -import { meteor } from 'meteor-vite/plugin'; - -export default defineConfig({ - plugins: [ - solidPlugin(), - solidSvg({ defaultExport: 'component' }), - meteor({ - clientEntry: 'client/main.js', - serverEntry: 'server/main.js', - enableExperimentalFeatures: true, - stubValidation: { - ignorePackages: ['meteor/mongo'], - }, - }), - ], -}); diff --git a/tools/static-assets/skel-svelte/.gitignore b/tools/static-assets/skel-svelte/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-svelte/.gitignore +++ b/tools/static-assets/skel-svelte/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-svelte/.meteor/packages b/tools/static-assets/skel-svelte/.meteor/packages index b6c7d8a95d..8b4220ddda 100644 --- a/tools/static-assets/skel-svelte/.meteor/packages +++ b/tools/static-assets/skel-svelte/.meteor/packages @@ -17,6 +17,7 @@ shell-server # Server-side component of the `meteor shell` command ~prototype~ static-html # Define static page content in .html files -zodern:melte # Meteor package to allow us to create files with the .svelte extension hot-module-replacement # Update client in development without reloading the page zodern:types # Enable types from meteor/atmosphere packages + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-svelte/client/main.js b/tools/static-assets/skel-svelte/client/main.js index 376cf55474..11a9eeb2f2 100644 --- a/tools/static-assets/skel-svelte/client/main.js +++ b/tools/static-assets/skel-svelte/client/main.js @@ -1,9 +1,26 @@ import { Meteor } from 'meteor/meteor'; import App from '../imports/ui/App.svelte'; +import { mount, unmount } from 'svelte'; +let app; // will hold the mounted instance Meteor.startup(() => { - new App({ - target: document.getElementById('app') - }); -}); \ No newline at end of file + const target = document.getElementById('app'); + + // (Re)mount + app = mount(App, { target }); + + // Clean up on HMR so we don't double-mount + if (import.meta.webpackHot) { + import.meta.webpackHot.accept(); + import.meta.webpackHot.dispose(() => { + if (app) { + // pass the instance you got from mount() + unmount(app, { outro: false }); // set outro:true if you want transitions + app = null; + } + // optional: clear target to be extra safe + target.innerHTML = ''; + }); + } +}); diff --git a/tools/static-assets/skel-svelte/imports/ui/App.svelte b/tools/static-assets/skel-svelte/imports/ui/App.svelte index 9f18322c0b..f011b8b2c9 100644 --- a/tools/static-assets/skel-svelte/imports/ui/App.svelte +++ b/tools/static-assets/skel-svelte/imports/ui/App.svelte @@ -1,23 +1,42 @@ -

    Welcome to Meteor!

    -

    You've pressed the button {counter} times.

    @@ -31,6 +50,4 @@ {:else}
    Loading ...
    {/if} -

    Typescript ready

    -

    Just add lang="ts" to .svelte components.

    diff --git a/tools/static-assets/skel-svelte/package.json b/tools/static-assets/skel-svelte/package.json index 0929124049..d864cf7c23 100644 --- a/tools/static-assets/skel-svelte/package.json +++ b/tools/static-assets/skel-svelte/package.json @@ -10,24 +10,24 @@ "dependencies": { "@babel/runtime": "^7.23.5", "@swc/helpers": "^0.5.17", - "meteor-node-stubs": "^1.2.12", - "svelte": "^3.59.2" + "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "svelte-preprocess": "^5.0.0" + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "postcss-load-config": "^5.1.0", + "svelte": "^5.38.2", + "svelte-check": "^4.3.1", + "svelte-loader": "^3.2.4", + "svelte-preprocess": "^6.0.3" }, "meteor": { "mainModule": { "client": "client/main.js", "server": "server/main.js" }, - "nodeModules": { - "recompile": { - "svelte": [ - "legacy" - ] - } - }, "testModule": "tests/main.js", "modern": true } diff --git a/tools/static-assets/skel-svelte/rspack.config.js b/tools/static-assets/skel-svelte/rspack.config.js new file mode 100644 index 0000000000..05275dfece --- /dev/null +++ b/tools/static-assets/skel-svelte/rspack.config.js @@ -0,0 +1,45 @@ +const { defineConfig } = require("@meteorjs/rspack"); +const sveltePreprocess = require("svelte-preprocess"); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig((Meteor) => { + return { + ...(Meteor.isClient && { + resolve: { + extensions: [".mjs", ".js", ".ts", ".svelte", ".json"], + mainFields: ["svelte", "browser", "module", "main"], + conditionNames: ["svelte", "browser", "import", "module", "default"], + }, + module: { + rules: [ + { + test: /\.svelte$/, + use: [ + { + loader: "svelte-loader", + options: { + compilerOptions: { dev: !Meteor.isProduction }, + emitCss: Meteor.isProduction, + hotReload: !Meteor.isProduction, + preprocess: sveltePreprocess({ + sourceMap: !Meteor.isProduction, + postcss: true, + }), + }, + }, + ], + }, + ], + }, + }), + }; +}); diff --git a/tools/static-assets/skel-tailwind/.gitignore b/tools/static-assets/skel-tailwind/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-tailwind/.gitignore +++ b/tools/static-assets/skel-tailwind/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-tailwind/.meteor/packages b/tools/static-assets/skel-tailwind/.meteor/packages index 90ce4b06dd..522f0d0ce9 100644 --- a/tools/static-assets/skel-tailwind/.meteor/packages +++ b/tools/static-assets/skel-tailwind/.meteor/packages @@ -20,3 +20,5 @@ hot-module-replacement # Update client in development without reloading the pag ~prototype~ static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-tailwind/.meteorignore b/tools/static-assets/skel-tailwind/.meteorignore new file mode 100644 index 0000000000..4568c54a7d --- /dev/null +++ b/tools/static-assets/skel-tailwind/.meteorignore @@ -0,0 +1,2 @@ +# Ignore Meteor CSS handling; let Rspack resolve Tailwind styles +client/main.css diff --git a/tools/static-assets/skel-tailwind/client/main.css b/tools/static-assets/skel-tailwind/client/main.css index b5c61c9567..6ea2603dca 100644 --- a/tools/static-assets/skel-tailwind/client/main.css +++ b/tools/static-assets/skel-tailwind/client/main.css @@ -1,3 +1,6 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; + +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-tailwind/client/main.jsx b/tools/static-assets/skel-tailwind/client/main.jsx index a42cee8ff3..ea3a779300 100644 --- a/tools/static-assets/skel-tailwind/client/main.jsx +++ b/tools/static-assets/skel-tailwind/client/main.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Meteor } from 'meteor/meteor'; import { render } from 'react-dom'; import { App } from '/imports/ui/App'; +import './main.css'; Meteor.startup(() => { render(, document.getElementById('react-target')); diff --git a/tools/static-assets/skel-tailwind/imports/ui/Hello.jsx b/tools/static-assets/skel-tailwind/imports/ui/Hello.jsx index ca51ed5233..ad3ae3c053 100644 --- a/tools/static-assets/skel-tailwind/imports/ui/Hello.jsx +++ b/tools/static-assets/skel-tailwind/imports/ui/Hello.jsx @@ -13,9 +13,9 @@ export const Hello = () => {
    -

    +

    Welcome to Meteor! -

    +
    diff --git a/tools/static-assets/skel-tailwind/package.json b/tools/static-assets/skel-tailwind/package.json index 73971202ba..79935caba4 100644 --- a/tools/static-assets/skel-tailwind/package.json +++ b/tools/static-assets/skel-tailwind/package.json @@ -12,11 +12,21 @@ "@swc/helpers": "^0.5.17", "autoprefixer": "^10.4.4", "meteor-node-stubs": "^1.2.12", - "postcss": "^8.4.12", - "postcss-load-config": "^3.1.4", "react": "^17.0.2", - "react-dom": "^17.0.2", - "tailwindcss": "^3.0.23" + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "@tailwindcss/postcss": "^4.1.12", + "@types/meteor": "^2.9.7", + "postcss": "^8.5.6", + "postcss-loader": "^8.1.1", + "react-refresh": "^0.17.0", + "tailwindcss": "^4.1.12" }, "meteor": { "mainModule": { diff --git a/tools/static-assets/skel-tailwind/postcss.config.js b/tools/static-assets/skel-tailwind/postcss.config.js index 33ad091d26..c2ddf74822 100644 --- a/tools/static-assets/skel-tailwind/postcss.config.js +++ b/tools/static-assets/skel-tailwind/postcss.config.js @@ -1,6 +1,5 @@ -module.exports = { +export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + "@tailwindcss/postcss": {}, }, -} +}; diff --git a/tools/static-assets/skel-tailwind/rspack.config.js b/tools/static-assets/skel-tailwind/rspack.config.js new file mode 100644 index 0000000000..b5de81b007 --- /dev/null +++ b/tools/static-assets/skel-tailwind/rspack.config.js @@ -0,0 +1,27 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.isClient && { + module: { + rules: [ + { + test: /\.css$/, + use: ["postcss-loader"], + type: "css", + }, + ], + }, + }, + }; +}); diff --git a/tools/static-assets/skel-tailwind/tailwind.config.js b/tools/static-assets/skel-tailwind/tailwind.config.js index b8bf0cafc2..42926720ae 100644 --- a/tools/static-assets/skel-tailwind/tailwind.config.js +++ b/tools/static-assets/skel-tailwind/tailwind.config.js @@ -1,7 +1,12 @@ +// tailwind.config.js module.exports = { - content: ["./imports/ui/**/*.{js,jsx,ts,tsx}", './client/*.html'], + purge: [], + darkMode: false, // or 'media' or 'class' theme: { extend: {}, }, + variants: { + extend: {}, + }, plugins: [], } diff --git a/tools/static-assets/skel-typescript/.gitignore b/tools/static-assets/skel-typescript/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-typescript/.gitignore +++ b/tools/static-assets/skel-typescript/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-typescript/.meteor/packages b/tools/static-assets/skel-typescript/.meteor/packages index 033932891e..b60cd50cfe 100644 --- a/tools/static-assets/skel-typescript/.meteor/packages +++ b/tools/static-assets/skel-typescript/.meteor/packages @@ -21,3 +21,5 @@ hot-module-replacement # Update client in development without reloading the pag static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data zodern:types # Pull in type declarations from other Meteor packages + +rspack # Integrate Rspack into Meteor for client and server app bundling diff --git a/tools/static-assets/skel-typescript/.swcrc b/tools/static-assets/skel-typescript/.swcrc new file mode 100644 index 0000000000..bbc8887edb --- /dev/null +++ b/tools/static-assets/skel-typescript/.swcrc @@ -0,0 +1,9 @@ +{ + "jsc": { + "transform": { + "react": { + "runtime": "automatic" + } + } + } +} \ No newline at end of file diff --git a/tools/static-assets/skel-typescript/client/main.tsx b/tools/static-assets/skel-typescript/client/main.tsx index 523141b528..57653a16f7 100644 --- a/tools/static-assets/skel-typescript/client/main.tsx +++ b/tools/static-assets/skel-typescript/client/main.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createRoot } from 'react-dom/client'; import { Meteor } from 'meteor/meteor'; import { App } from '/imports/ui/App'; diff --git a/tools/static-assets/skel-typescript/imports/ui/App.tsx b/tools/static-assets/skel-typescript/imports/ui/App.tsx index d354e1b352..5956933766 100644 --- a/tools/static-assets/skel-typescript/imports/ui/App.tsx +++ b/tools/static-assets/skel-typescript/imports/ui/App.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Hello } from './Hello'; import { Info } from './Info'; diff --git a/tools/static-assets/skel-typescript/imports/ui/Hello.tsx b/tools/static-assets/skel-typescript/imports/ui/Hello.tsx index 15e0f185ac..527d5af607 100644 --- a/tools/static-assets/skel-typescript/imports/ui/Hello.tsx +++ b/tools/static-assets/skel-typescript/imports/ui/Hello.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; export const Hello = () => { const [counter, setCounter] = useState(0); diff --git a/tools/static-assets/skel-typescript/imports/ui/Info.tsx b/tools/static-assets/skel-typescript/imports/ui/Info.tsx index 23cb8f07a3..809fbc6716 100644 --- a/tools/static-assets/skel-typescript/imports/ui/Info.tsx +++ b/tools/static-assets/skel-typescript/imports/ui/Info.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useFind, useSubscribe } from "meteor/react-meteor-data"; import { LinksCollection, Link } from "../api/links"; diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json index 77ff7934bf..59017498e0 100644 --- a/tools/static-assets/skel-typescript/package.json +++ b/tools/static-assets/skel-typescript/package.json @@ -15,11 +15,19 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "@types/meteor": "^2.9.9", "@types/mocha": "^8.2.3", "@types/node": "^22.10.6", "@types/react": "^18.2.5", "@types/react-dom": "^18.2.4", - "typescript": "^5.4.5" + "react-refresh": "^0.17.0", + "ts-checker-rspack-plugin": "^1.1.5", + "typescript": "^5.9.3" }, "meteor": { "mainModule": { diff --git a/tools/static-assets/skel-typescript/rspack.config.ts b/tools/static-assets/skel-typescript/rspack.config.ts new file mode 100644 index 0000000000..04d51e67bd --- /dev/null +++ b/tools/static-assets/skel-typescript/rspack.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "@meteorjs/rspack"; +import { TsCheckerRspackPlugin } from "ts-checker-rspack-plugin"; + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +export default defineConfig((/* Meteor */) => { + return { + plugins: [new TsCheckerRspackPlugin()], + }; +}); diff --git a/tools/static-assets/skel-typescript/tsconfig.json b/tools/static-assets/skel-typescript/tsconfig.json index 5806f496d5..aff98434d9 100644 --- a/tools/static-assets/skel-typescript/tsconfig.json +++ b/tools/static-assets/skel-typescript/tsconfig.json @@ -36,10 +36,14 @@ "resolveJsonModule": true, "types": ["node", "mocha"], "esModuleInterop": true, - "preserveSymlinks": true + "preserveSymlinks": true, + "skipLibCheck": true }, "exclude": [ "./.meteor/**", - "./packages/**" + "./packages/**", + "./_build/**", + "./public/build-chunks/**", + "./public/build-assets/**" ] } diff --git a/tools/static-assets/skel-vue/.gitignore b/tools/static-assets/skel-vue/.gitignore index c2658d7d1b..3e954aa73f 100644 --- a/tools/static-assets/skel-vue/.gitignore +++ b/tools/static-assets/skel-vue/.gitignore @@ -1 +1,7 @@ node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-vue/.meteor/packages b/tools/static-assets/skel-vue/.meteor/packages index b5e90db564..3c087c03a6 100644 --- a/tools/static-assets/skel-vue/.meteor/packages +++ b/tools/static-assets/skel-vue/.meteor/packages @@ -18,5 +18,7 @@ shell-server # Server-side component of the `meteor shell` com hot-module-replacement # Update client in development without reloading the page static-html # Define static page content in .html files -jorgenvatle:vite + +rspack # Integrate Rspack into Meteor for client and server app bundling + ~prototype~ diff --git a/tools/static-assets/skel-vue/README.md b/tools/static-assets/skel-vue/README.md index 7ba6226cb0..716f664502 100644 --- a/tools/static-assets/skel-vue/README.md +++ b/tools/static-assets/skel-vue/README.md @@ -1,4 +1,4 @@ -# Meteor + Vue3 + Vite +# Meteor.js 3 + Vue3 This is a simple example of how to use Vue3 with Meteor. @@ -12,7 +12,7 @@ This is a simple example of how to use Vue3 with Meteor. ## Libraries used - [Vue3](https://v3.vuejs.org/) -- [Vite](https://vitejs.dev/) +- [Rspack](https://rspack.dev/) - [Vue Router](https://next.router.vuejs.org/) - [Meteor](https://www.meteor.com/) - [Vue Meteor Tracker](https://github.com/meteor-vue/vue-meteor-tracker) diff --git a/tools/static-assets/skel-vue/client/entry-meteor.js b/tools/static-assets/skel-vue/client/entry-meteor.js deleted file mode 100644 index 0ec3ddc121..0000000000 --- a/tools/static-assets/skel-vue/client/entry-meteor.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Entrypoint for the Meteor client - * - * Generally, this file can be left empty. Vite will add imports for - * lazy-loaded Meteor packages to this file to ensure they aren't omitted from - * the final production bundle. - * - * Use ./main.js as the primary entrypoint for your client code to take full - * advantage of Vite's plugin and build system. - * - * This can also be a good place to put code that you don't want Vite to - * process, for example, if you run into a compatibility issue or need to use - * nested imports which Vite doesn't support. - */ \ No newline at end of file diff --git a/tools/static-assets/skel-vue/imports/ui/main.css b/tools/static-assets/skel-vue/imports/ui/main.css index a461c505f1..6ea2603dca 100644 --- a/tools/static-assets/skel-vue/imports/ui/main.css +++ b/tools/static-assets/skel-vue/imports/ui/main.css @@ -1 +1,6 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-vue/package.json b/tools/static-assets/skel-vue/package.json index e77cf9ea19..a156645d98 100644 --- a/tools/static-assets/skel-vue/package.json +++ b/tools/static-assets/skel-vue/package.json @@ -16,20 +16,24 @@ "vue-meteor-tracker": "^3.0.0-beta.7", "vue-router": "^4.2.5" }, + "devDependencies": { + "@meteorjs/rspack": "^1.1.0-beta.31", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@tailwindcss/postcss": "^4.1.12", + "@types/meteor": "^2.9.7", + "postcss": "^8.5.6", + "postcss-loader": "^8.1.1", + "tailwindcss": "^4.1.12", + "vue-loader": "^17.4.2" + }, "meteor": { "modern": true, "mainModule": { - "client": "client/entry-meteor.js", - "server": "server/entry-meteor.js" + "client": "client/main.js", + "server": "server/main.js" }, "testModule": "tests/main.js" - }, - "devDependencies": { - "@types/meteor": "^2.9.7", - "@tailwindcss/vite": "^4.1.11", - "@vitejs/plugin-vue": "^5.2.1", - "meteor-vite": "^3.2.1", - "tailwindcss": "^4.1.11", - "vite": "^6.0.11" } } diff --git a/tools/static-assets/skel-vue/postcss.config.js b/tools/static-assets/skel-vue/postcss.config.js new file mode 100644 index 0000000000..c2ddf74822 --- /dev/null +++ b/tools/static-assets/skel-vue/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/tools/static-assets/skel-vue/rspack.config.js b/tools/static-assets/skel-vue/rspack.config.js new file mode 100644 index 0000000000..9f7fb1cd8f --- /dev/null +++ b/tools/static-assets/skel-vue/rspack.config.js @@ -0,0 +1,37 @@ +const { defineConfig } = require('@meteorjs/rspack'); +const { VueLoaderPlugin } = require('vue-loader'); + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +module.exports = defineConfig(Meteor => { + return { + ...Meteor.isClient && { + plugins: [new VueLoaderPlugin()], + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + // Note, for the majority of features to be available, make sure this option is `true` + experimentalInlineMatchResource: true, + }, + }, + { + test: /\.css$/, + use: ["postcss-loader"], + type: "css", + }, + ], + }, + }, + }; +}); diff --git a/tools/static-assets/skel-vue/server/entry-meteor.js b/tools/static-assets/skel-vue/server/entry-meteor.js deleted file mode 100644 index 8a066f8e94..0000000000 --- a/tools/static-assets/skel-vue/server/entry-meteor.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Entrypoint for the Meteor server - * Generally, this file can be left empty. Vite will add imports for your app's - * server bundle here during both development and production build. - * - * Use ./main.js as the primary entrypoint for your app to take full advantage - * of Vite's plugin and build system. - * - * This can also be a good place to put code that you don't want Vite to - * process, for example, if you run into a compatibility issue or need to use - * nested imports. - */ \ No newline at end of file diff --git a/tools/static-assets/skel-vue/tests/main.js b/tools/static-assets/skel-vue/tests/main.js index 086819d896..02301cdca8 100644 --- a/tools/static-assets/skel-vue/tests/main.js +++ b/tools/static-assets/skel-vue/tests/main.js @@ -1,20 +1,20 @@ -import assert from 'assert' +import assert from 'assert'; -describe('vue-skeleton', function () { +describe('~name~', function () { it('package.json has correct name', async function () { - const { name } = await import('../package.json') - assert.strictEqual(name, 'vue-skeleton') + const { name } = await import('../package.json'); + assert.strictEqual(name, '~name~') }) if (Meteor.isClient) { it('client is not server', function () { - assert.strictEqual(Meteor.isServer, false) + assert.strictEqual(Meteor.isServer, false); }) } if (Meteor.isServer) { it('server is not client', function () { - assert.strictEqual(Meteor.isClient, false) + assert.strictEqual(Meteor.isClient, false); }) } }) diff --git a/tools/static-assets/skel-vue/vite.config.mjs b/tools/static-assets/skel-vue/vite.config.mjs deleted file mode 100644 index f955234c6a..0000000000 --- a/tools/static-assets/skel-vue/vite.config.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import vue from '@vitejs/plugin-vue'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; -import { meteor } from 'meteor-vite/plugin'; - -export default defineConfig({ - plugins: [ - vue(), - tailwindcss(), - meteor({ - clientEntry: 'client/main.js', - serverEntry: 'server/main.js', - enableExperimentalFeatures: true, - stubValidation: { - ignorePackages: ['meteor/mongo'], - }, - }), - ], - optimizeDeps: { - exclude: ['vue-meteor-tracker'], - }, -}); diff --git a/tools/tests/create.js b/tools/tests/create.js index a6a51eefe9..784c22e03b 100644 --- a/tools/tests/create.js +++ b/tools/tests/create.js @@ -9,7 +9,7 @@ selftest.define("create main", async function () { await s.init(); // Can we create an app? Yes! - var run = s.run("create", "foobar", "--blaze"); + var run = s.run("create", "foobar", "--legacy"); await run.match("Created a new Meteor app in 'foobar'."); await run.match("To run your new app"); await run.expectExit(0); @@ -50,7 +50,11 @@ selftest.define("create main", async function () { await run.expectExit(0); }); -AVAILABLE_SKELETONS.forEach(template => { +// TODO: Enable once rspack is published for the first time +// Also, the new modern test suite covers more than this test. +// This test may not work, as rspack relies on project npm dependencies +// being installed, and this suite apparently does not install them. +/* AVAILABLE_SKELETONS.forEach(template => { selftest.define("create --" + template, async function () { const s = new Sandbox; await s.init(); @@ -74,4 +78,4 @@ AVAILABLE_SKELETONS.forEach(template => { await run.stop(); }); -}); +}); */ diff --git a/tools/tests/modern.js b/tools/tests/modern.js index 2b1fdff0e8..374c219a15 100644 --- a/tools/tests/modern.js +++ b/tools/tests/modern.js @@ -235,7 +235,6 @@ selftest.define("modern build stack - transpiler boolean-like options", async fu /* check verbose logs */ await run.match(/SWC Custom Config/, false, true); - await run.match(/SWC Legacy Config/, false, true); await run.match(/Meteor Config/, false, true); /* check transpiler options */ @@ -331,7 +330,6 @@ console.log('Loaded NPM package "config"', require('config').id);`); /* check verbose logs */ await run.match(/SWC Custom Config/, false, true); - await run.match(/SWC Legacy Config/, false, true); await run.match(/Meteor Config/, false, true); /* check transpiler options */ diff --git a/tools/tests/old.js b/tools/tests/old.js index 1dd9750759..d6590acb69 100644 --- a/tools/tests/old.js +++ b/tools/tests/old.js @@ -81,6 +81,14 @@ selftest.define("bundler-npm", ["slow", "net", "checkout"], function () { return runOldTest('test-bundler-npm.js'); }); +selftest.define("bundler-devonly", ["checkout"], function () { + return runOldTest('test-bundler-devonly.js'); +}); + +selftest.define("bundler-devdepends", ["checkout"], function () { + return runOldTest('test-bundler-devdepends.js'); +}); + // This last one's is a shell script! // XXX pardon the hacky glue to make it work with a sandbox diff --git a/tools/tests/old/empty-app/.meteor/packages b/tools/tests/old/empty-app/.meteor/packages index 52277ef16b..255549a91e 100644 --- a/tools/tests/old/empty-app/.meteor/packages +++ b/tools/tests/old/empty-app/.meteor/packages @@ -1,4 +1,5 @@ # no packages meteor-base +babel-compiler standard-minifiers diff --git a/tools/tests/old/test-bundler-devdepends.js b/tools/tests/old/test-bundler-devdepends.js new file mode 100644 index 0000000000..6cd2ed6956 --- /dev/null +++ b/tools/tests/old/test-bundler-devdepends.js @@ -0,0 +1,453 @@ +require("../../tool-env/install-babel.js"); + +var _ = require("underscore"); +var assert = require("assert"); +var bundler = require("../../isobuild/bundler.js"); +var release = require("../../packaging/release.js"); +var files = require("../../fs/files"); +var catalog = require("../../packaging/catalog/catalog.js"); +var buildmessage = require("../../utils/buildmessage.js"); +var meteorNpm = require("../../isobuild/meteor-npm.js"); +var isopackets = require("../../tool-env/isopackets.js"); +var projectContextModule = require("../../project-context.js"); +var safeWatcher = require("../../fs/safe-watcher"); +const { makeGlobalAsyncLocalStorage } = require("../../utils/fiber-helpers"); + +var lastTmpDir = null; +var tmpDir = function () { + return (lastTmpDir = files.mkdtemp()); +}; + +var makeProjectContext = async function (appName) { + var projectDir = files.mkdtemp("test-bundler-devdepends"); + await files.cp_r( + files.pathJoin(files.convertToStandardPath(__dirname), appName), + projectDir, + { preserveSymlinks: true } + ); + var projectContext = new projectContextModule.ProjectContext({ + projectDir: projectDir, + }); + await doOrThrow(async function () { + await projectContext.prepareProjectForBuild(); + }); + + return projectContext; +}; + +var doOrThrow = async function (f) { + var ret; + var messages = await buildmessage.capture(async function () { + ret = await f(); + }); + if (messages.hasMessages()) { + throw Error(messages.formatMessages()); + } + return ret; +}; + +var getTestPackageDir = function (projectContext) { + return files.pathJoin(projectContext.projectDir, "packages", "test-package"); +}; + +var reloadPackages = async function (projectContext) { + projectContext.reset(); + await doOrThrow(async function () { + await projectContext.prepareProjectForBuild(); + }); +}; + +var updateTestPackageWithDevDepends = async function ( + projectContext, + npmDependencies, + npmDevDependencies, + options +) { + options = options || {}; + files.writeFile( + files.pathJoin(getTestPackageDir(projectContext), "package.js"), + "Package.describe({version: '1.0.0'});\n" + + "\n" + + "Npm.depends(" + + JSON.stringify(npmDependencies) + + ");" + + "\n" + + "Npm.devDepends(" + + JSON.stringify(npmDevDependencies) + + ");" + + "\n" + + "Package.onUse(function (api) { api.addFiles('dummy.js', 'server'); });" + ); + if (!options.noReload) await reloadPackages(projectContext); +}; + +/// +/// HELPERS +/// + +var _assertCorrectPackageNpmDir = function (projectContext, deps, context = 'package') { + // test-package/.npm was generated + + // Get the actual npm-shrinkwrap.json content + var actualShrinkwrapContent = JSON.parse( + files.readFile( + files.pathJoin( + getTestPackageDir(projectContext), + ".npm", + context, + "npm-shrinkwrap.json" + ), + "utf8" + ) + ); + var actualMeteorNpmShrinkwrapDependencies = + actualShrinkwrapContent.dependencies; + + // Instead of comparing the entire JSON string, we'll verify that each expected dependency + // exists in the actual dependencies with the correct version + _.each(deps, function (version, name) { + // Check that the dependency exists + assert( + actualMeteorNpmShrinkwrapDependencies[name], + `Dependency ${name} not found in npm-shrinkwrap.json` + ); + + // Check that the version matches + if (!/tarball/.test(version)) { + assert.equal( + actualMeteorNpmShrinkwrapDependencies[name].version, + version, + `Expected version ${version} for ${name}, but got ${actualMeteorNpmShrinkwrapDependencies[name].version}` + ); + } + }); + + // Check that there are no extra dependencies + var actualDependencyNames = Object.keys( + actualMeteorNpmShrinkwrapDependencies + ); + var expectedDependencyNames = Object.keys(deps); + + assert.equal( + actualDependencyNames.length, + expectedDependencyNames.length, + `Expected ${expectedDependencyNames.length} dependencies, but found ${actualDependencyNames.length}` + ); + + _.each(actualDependencyNames, function (name) { + assert( + name in deps, + `Unexpected dependency ${name} found in npm-shrinkwrap.json` + ); + }); + + var testPackageDir = getTestPackageDir(projectContext); + assert.equal( + files.readFile( + files.pathJoin(testPackageDir, ".npm", context, ".gitignore"), + "utf8" + ), + "node_modules\n" + ); + assert( + files.exists(files.pathJoin(testPackageDir, ".npm", context, "README")) + ); + + // verify the contents of the `node_modules` dir + var nodeModulesDir = files.pathJoin( + testPackageDir, + ".npm", + context, + "node_modules" + ); + + // all expected dependencies are installed correctly, with the correct version + _.each(deps, function (version, name) { + assert(looksInstalled(nodeModulesDir, name)); + + if (!/tarball/.test(version)) { + // 'version' in package.json from a tarball won't be correct + assert.equal( + JSON.parse( + files.readFile( + files.pathJoin(nodeModulesDir, name, "package.json"), + "utf8" + ) + ).version, + version + ); + } + }); + + // all installed dependencies were expected to be found there, + // meaning we correctly removed unused node_modules directories + _.each(files.readdir(nodeModulesDir), function (installedNodeModule) { + // Skip files that are not directories + if ( + !files + .stat(files.pathJoin(nodeModulesDir, installedNodeModule)) + .isDirectory() + ) { + return; + } + + if ( + files.exists( + files.pathJoin(nodeModulesDir, installedNodeModule, "package.json") + ) + ) + assert(installedNodeModule in deps); + }); +}; + +var _assertCorrectBundleNpmContents = function (bundleDir, deps) { + // sanity check -- main.js has expected contents. + assert.strictEqual( + files.readFile(files.pathJoin(bundleDir, "main.js"), "utf8"), + bundler._mainJsContents + ); + + // Use the default path for the node_modules directory + var bundledPackageNodeModulesDir = files.pathJoin( + bundleDir, + "programs", + "server", + "npm", + "node_modules", + "meteor", + "test-package", + "node_modules" + ); + + // Check that all expected dependencies are in the bundle + _.each(deps, function (version, name) { + // Verify the dependency is installed + assert( + looksInstalled(bundledPackageNodeModulesDir, name), + `Dependency ${name} not found in the bundle` + ); + + // Check version if not a tarball + if (!/tarball/.test(version)) { + var packageJson = JSON.parse( + files.readFile( + files.pathJoin(bundledPackageNodeModulesDir, name, "package.json"), + "utf8" + ) + ); + + assert.equal( + packageJson.version, + version, + `Expected version ${version} for ${name}, but got ${packageJson.version}` + ); + } + }); + + // Check that there are no extra dependencies in the bundle + var installedModules = files + .readdir(bundledPackageNodeModulesDir) + .filter(function (module) { + // Skip files that are not directories + if ( + !files + .stat(files.pathJoin(bundledPackageNodeModulesDir, module)) + .isDirectory() + ) { + return false; + } + return files.exists( + files.pathJoin(bundledPackageNodeModulesDir, module, "package.json") + ); + }); + + _.each(installedModules, function (module) { + assert( + module in deps, + `Unexpected dependency ${module} found in the bundle` + ); + }); +}; + +var looksInstalled = function (nodeModulesDir, name) { + // First check if the directory exists and is actually a directory + const packageDir = files.pathJoin(nodeModulesDir, name); + if (!files.exists(packageDir) || !files.stat(packageDir).isDirectory()) { + return false; + } + + // All of the packages in this test have one of these two files, so presumably + // if one of these files is here we have correctly installed the package. + return ( + files.exists(files.pathJoin(packageDir, "README.md")) || + files.exists(files.pathJoin(packageDir, "LICENSE")) + ); +}; + +/// +/// TESTS +/// + +var runTest = async function () { + // Initialize the official catalog + await catalog.official.initialize(); + + var projectContext = await makeProjectContext("app-with-package"); + var testPackageDir = getTestPackageDir(projectContext); + + // Test development mode + try { + var regularDeps = { gcd: "0.0.0" }; + var devDeps = { mime: "1.2.7" }; + var allDeps = { ...regularDeps, ...devDeps }; // In dev mode, both should be installed + + // Save the original global.currentCommand + var originalCommand = global.currentCommand; + + // Set global.currentCommand to null to simulate development mode + global.currentCommand = null; + + await updateTestPackageWithDevDepends(projectContext, regularDeps, devDeps); + var tmpOutputDir = tmpDir(); + var result = await bundler.bundle({ + projectContext: projectContext, + outputPath: tmpOutputDir, + }); + assert.strictEqual(result.errors, false, result.errors && result.errors[0]); + + // In development mode, both regular and dev dependencies should be installed + _assertCorrectPackageNpmDir(projectContext, allDeps, 'devPackage'); + _assertCorrectBundleNpmContents(tmpOutputDir, allDeps); + + // Restore the original global.currentCommand + global.currentCommand = originalCommand; + } catch (e) { + assert.fail("Development mode test failed: " + e); + } + + // Test production mode + projectContext = await makeProjectContext("app-with-package"); + try { + var regularDeps = { gcd: "0.0.0" }; + var devDeps = { mime: "1.2.7" }; + + // Save the original global.currentCommand + var originalCommand = global.currentCommand; + + // Set global.currentCommand to simulate production mode (build command) + global.currentCommand = { name: "build" }; + + await updateTestPackageWithDevDepends(projectContext, regularDeps, devDeps); + var tmpOutputDir = tmpDir(); + var result = await bundler.bundle({ + projectContext: projectContext, + outputPath: tmpOutputDir, + buildOptions: { minifyMode: "production", buildMode: "production" }, + }); + assert.strictEqual(result.errors, false, result.errors && result.errors[0]); + + // In production mode, only regular dependencies should be installed + _assertCorrectPackageNpmDir(projectContext, regularDeps, 'package'); + _assertCorrectBundleNpmContents(tmpOutputDir, regularDeps); + + // Restore the original global.currentCommand + global.currentCommand = originalCommand; + } catch (e) { + assert.fail("Production mode test failed: " + e); + } + + // Test that depends can only be called once + try { + // Create a package.js file with depends called twice + var testPackageDir = getTestPackageDir(projectContext); + files.writeFile( + files.pathJoin(testPackageDir, "package.js"), + "Package.describe({version: '1.0.0'});\n" + + "\n" + + "Npm.depends({gcd: '0.0.0'});" + + "\n" + + "Npm.depends({mime: '1.2.7'});" + + "\n" + + "Package.onUse(function (api) { api.addFiles('dummy.js', 'server'); });" + ); + + // Reload packages and check for error message + projectContext.reset(); + var messages = await buildmessage.capture(async function () { + await projectContext.prepareProjectForBuild(); + }); + + assert(messages.hasMessages()); + var job = _.find(messages.jobs, function (job) { + return job.title.includes("test-package"); + }); + assert(job); + assert( + /Npm\.depends may only be called once per package/.test( + job.messages[0].message + ) + ); + } catch (e) { + assert.fail("Test for 'depends can only be called once' failed: " + e); + } + + // Test that devDepends can only be called once + try { + // Create a package.js file with devDepends called twice + var testPackageDir = getTestPackageDir(projectContext); + files.writeFile( + files.pathJoin(testPackageDir, "package.js"), + "Package.describe({version: '1.0.0'});\n" + + "\n" + + "Npm.devDepends({gcd: '0.0.0'});" + + "\n" + + "Npm.devDepends({mime: '1.2.7'});" + + "\n" + + "Package.onUse(function (api) { api.addFiles('dummy.js', 'server'); });" + ); + + // Reload packages and check for error message + projectContext.reset(); + var messages = await buildmessage.capture(async function () { + await projectContext.prepareProjectForBuild(); + }); + + assert(messages.hasMessages()); + var job = _.find(messages.jobs, function (job) { + return job.title.includes("test-package"); + }); + assert(job); + assert( + /Npm\.devDepends may only be called once per package/.test( + job.messages[0].message + ) + ); + } catch (e) { + assert.fail("Test for 'devDepends can only be called once' failed: " + e); + } +}; + +makeGlobalAsyncLocalStorage().run( + { name: "test-bundler-devdepends.js" }, + async function () { + if (!files.inCheckout()) { + throw Error("This test doesn't support non-checkout"); + } + + release.setCurrent(await release.load(null)); + await isopackets.ensureIsopacketsLoadable(); + + try { + await runTest(); + } catch (err) { + console.log(err.stack); + console.log("\nBundle can be found at " + lastTmpDir); + process.exit(1); + } + + // Allow the process to exit normally, since optimistic file watchers + // may be keeping the event loop busy. + safeWatcher.closeAllWatchers(); + process.exit(0); + } +); diff --git a/tools/tests/old/test-bundler-devonly.js b/tools/tests/old/test-bundler-devonly.js new file mode 100644 index 0000000000..2aa5e0bfe9 --- /dev/null +++ b/tools/tests/old/test-bundler-devonly.js @@ -0,0 +1,137 @@ +require('../../tool-env/install-babel.js'); + +var _ = require('underscore'); +var assert = require('assert'); +var bundler = require('../../isobuild/bundler.js'); +var release = require('../../packaging/release.js'); +var files = require('../../fs/files'); +var catalog = require('../../packaging/catalog/catalog.js'); +var buildmessage = require('../../utils/buildmessage.js'); +var isopackets = require('../../tool-env/isopackets.js'); +var projectContextModule = require('../../project-context.js'); +var safeWatcher = require("../../fs/safe-watcher"); +const { makeGlobalAsyncLocalStorage } = require("../../utils/fiber-helpers"); + +var lastTmpDir = null; +var tmpDir = function () { + return (lastTmpDir = files.mkdtemp()); +}; + +var makeProjectContext = async function (appName) { + var projectDir = files.mkdtemp("test-bundler-devonly"); + await files.cp_r( + files.pathJoin(files.convertToStandardPath(__dirname), appName), + projectDir, + { preserveSymlinks: true }, + ); + var projectContext = new projectContextModule.ProjectContext({ + projectDir: projectDir + }); + await doOrThrow(async function () { + await projectContext.prepareProjectForBuild(); + }); + + return projectContext; +}; + +var doOrThrow = async function (f) { + var ret; + var messages = await buildmessage.capture(async function () { + ret = await f(); + }); + if (messages.hasMessages()) { + throw Error(messages.formatMessages()); + } + return ret; +}; + +var runTest = async function () { + // As preparation, let's initialize the official catalog. It servers as our + // data store, so we will probably need it. + await catalog.official.initialize(); + + // an empty app. notably this app has no .meteor/release file. + var projectContext = await makeProjectContext('empty-app'); + + // Define an array of devOnly dependencies to check + const devOnlyDeps = ['babel-compiler', 'standard-minifiers']; + + console.log("testing devOnly dependencies are skipped in production build"); + try { + // Set the current command to 'build' + global.currentCommand = { + name: 'build', + }; + + var tmpOutputDir = tmpDir(); + var result = await bundler.bundle({ + projectContext: projectContext, + outputPath: tmpOutputDir, + buildOptions: { minifyMode: 'production', buildMode: 'production' }, + }); + assert.strictEqual(result.errors, false, result.errors && result.errors[0]); + + // sanity check -- main.js has expected contents. + assert.strictEqual( + files.readFile(files.pathJoin(tmpOutputDir, "main.js"), "utf8"), + bundler._mainJsContents); + + // Check that devOnly packages are not present in the bundle + for (const dep of devOnlyDeps) { + console.log(`Checking that ${dep} is not present in the bundle`); + + // Check in node_modules + assert(!files.exists(files.pathJoin( + tmpOutputDir, "programs", "server", "npm", "node_modules", "meteor", dep)), + `${dep} should not be present in node_modules`); + + // Check in web.browser/program.json + const programJson = JSON.parse(files.readFile( + files.pathJoin(tmpOutputDir, "programs", "web.browser", "program.json"), "utf8")); + + // Verify the dependency is not in the manifest + const manifest = programJson.manifest || []; + const hasDepInManifest = manifest.some(item => + item.path && item.path.includes(`${dep}.js`)); + assert(!hasDepInManifest, `${dep} should not be present in web.browser/program.json`); + + // Check in npm-rebuilds.json + const npmRebuildsJson = JSON.parse(files.readFile( + files.pathJoin(tmpOutputDir, "programs", "server", "npm-rebuilds.json"), "utf8")); + + // Verify the dependency is not in npm-rebuilds.json + const hasDepInRebuilds = npmRebuildsJson.some(path => + path.includes(dep)); + assert(!hasDepInRebuilds, `${dep} should not be present in npm-rebuilds.json`); + } + + assert.ok(true); + } catch (e) { + assert.fail("devOnly dependencies test fails", e); + } +}; + +makeGlobalAsyncLocalStorage().run( + { name: "test-bundler-devonly.js" }, + async function () { + if (!files.inCheckout()) { + throw Error("This test doesn't support non-checkout"); + } + + release.setCurrent(await release.load(null)); + await isopackets.ensureIsopacketsLoadable(); + + try { + await runTest(); + } catch (err) { + console.log(err.stack); + console.log("\nBundle can be found at " + lastTmpDir); + process.exit(1); + } + + // Allow the process to exit normally, since optimistic file watchers + // may be keeping the event loop busy. + safeWatcher.closeAllWatchers(); + process.exit(0); + } +); diff --git a/tools/tests/releases.js b/tools/tests/releases.js index 54bbdd09e3..96d28c75ae 100644 --- a/tools/tests/releases.js +++ b/tools/tests/releases.js @@ -38,7 +38,7 @@ selftest.define( await run.expectExit(0); // Apps are created with the latest release ... - run = s.run("create", "myapp", "--blaze"); + run = s.run("create", "myapp", "--legacy"); run.waitSecs(5); await run.expectExit(0); @@ -53,7 +53,7 @@ selftest.define( .run( "create", "myapp2", - "--blaze", + "--legacy", "--release", DEFAULT_RELEASE_TRACK + "@v1" ) @@ -155,7 +155,7 @@ selftest.define( var run; // Create an app with the latest release. - run = s.run("create", "myapp", "--blaze"); + run = s.run("create", "myapp", "--legacy"); run.waitSecs(15); await run.expectExit(0); s.cd("myapp"); diff --git a/tools/tests/run.js b/tools/tests/run.js index 0093676d50..e5c7ffeab6 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -463,12 +463,12 @@ selftest.define("'meteor run --port' accepts/rejects proper values", async funct run = s.run("run", "--port", "3500"); run.waitSecs(30); - await run.match('App running at: http://localhost:3500/'); + await run.match('App running at http://localhost:3500/'); await run.stop(); run = s.run("run", "--port", "127.0.0.1:3500"); run.waitSecs(30); - await run.match('App running at: http://127.0.0.1:3500/'); + await run.match('App running at http://127.0.0.1:3500/'); await run.stop(); }); @@ -491,7 +491,7 @@ selftest.define("update package during run", async function () { var runRun = s.run(); runRun.waitSecs(3); - await runRun.match("App running at:"); + await runRun.match("App running at"); var updateRun = s.run("update", "glasser:package-for-selftest"); await updateRun.match( diff --git a/tools/tests/server-restart-port.js b/tools/tests/server-restart-port.js index 47a7e3a49c..7a02aea685 100644 --- a/tools/tests/server-restart-port.js +++ b/tools/tests/server-restart-port.js @@ -23,5 +23,5 @@ async function testHelper(server) { (match, n) => `module.id, ${ ++n }`, )); - await run.match("Meteor server restarted at: http://localhost:21000/"); + await run.match("Meteor server restarted at http://localhost:21000/"); } diff --git a/tools/tests/test-modes.js b/tools/tests/test-modes.js index f06eccebeb..eca226e461 100644 --- a/tools/tests/test-modes.js +++ b/tools/tests/test-modes.js @@ -20,17 +20,17 @@ selftest.define("'meteor test --port' accepts/rejects proper values", async func run = s.run("test", "--port", "3700", "--driver-package", "tmeasday:acceptance-test-driver"); run.waitSecs(30); - await run.match('App running at: http://localhost:3700/'); + await run.match('App running at http://localhost:3700/'); await run.stop(); run = s.run("test", "--port", "127.0.0.1:3700", "--driver-package", "tmeasday:acceptance-test-driver"); run.waitSecs(30); - await run.match('App running at: http://127.0.0.1:3700/'); + await run.match('App running at http://127.0.0.1:3700/'); await run.stop(); run = s.run("test", "--port", "[::]:3700", "--driver-package", "tmeasday:acceptance-test-driver"); run.waitSecs(30); - await run.match('App running at: http://[::]:3700/'); + await run.match('App running at http://[::]:3700/'); await run.stop(); }); diff --git a/tools/tests/typescript.js b/tools/tests/typescript.js index 2828cc6a2b..7933ee5265 100644 --- a/tools/tests/typescript.js +++ b/tools/tests/typescript.js @@ -1,27 +1,28 @@ -// var selftest = require('../tool-testing/selftest.js'); -// var Sandbox = selftest.Sandbox; -// -// selftest.define("typescript template works", function () { -// const s = new Sandbox; -// -// let run = s.run("create", "--typescript", "typescript"); -// -// run.waitSecs(60); -// run.match("Created a new Meteor app in 'typescript'."); -// run.match("To run your new app"); -// -// s.cd("typescript"); -// -// run = s.run("npm", "install"); -// run.expectExit(0); -// -// run = s.run("lint"); -// run.waitSecs(60); -// run.match("[zodern:types] Exiting \"meteor lint\" early"); -// run.expectExit(0); -// -// run = s.run("npx", "tsc"); -// run.waitSecs(60); -// run.expectEnd(); -// run.expectExit(0); -// }); +var selftest = require('../tool-testing/selftest.js'); +var Sandbox = selftest.Sandbox; + +selftest.define("typescript template works", async function () { + const s = new Sandbox(); + await s.init(); + + let run = s.run("create", "--typescript", "typescript"); + + run.waitSecs(60); + await run.match("Created a new Meteor app in 'typescript'."); + await run.match("To run your new app"); + + s.cd("typescript"); + + run = s.run("npm", "install"); + await run.expectExit(0); + + run = s.run("lint"); + run.waitSecs(60); + await run.match('[zodern:types] Exiting "meteor lint" early'); + await run.expectExit(0); + + run = s.run("npx", "tsc"); + run.waitSecs(60); + await run.expectEnd(); + await run.expectExit(0); +}); diff --git a/tools/tool-env/meteor-config.js b/tools/tool-env/meteor-config.js index 4083a0a371..f92c707acb 100644 --- a/tools/tool-env/meteor-config.js +++ b/tools/tool-env/meteor-config.js @@ -13,12 +13,14 @@ export let meteorConfig; * @property {boolean} minifier - Whether to use the modern minifier. * @property {boolean} webArchOnly - Whether to use modern features only for web architecture. * @property {boolean} watcher - Whether to use the modern watcher. + * @property {boolean} cordova - Whether to use modern bundle for Cordova. */ const DEFAULT_MODERN = { transpiler: true, minifier: true, webArchOnly: true, watcher: true, + cordova: true, }; /** @@ -47,7 +49,7 @@ export const normalizeModernConfig = (r = false) => Object.fromEntries( * @param {string|null} appDir - The application directory path. If null, only environment variables are used. * @returns {Object} - The initialized Meteor configuration object. */ -export function initMeteorConfig(appDir) { +export function initMeteorConfig(appDir = process.cwd()) { const modernForced = JSON.parse(process.env.METEOR_MODERN || "false"); let packageJson; if (appDir) { @@ -63,7 +65,10 @@ export function initMeteorConfig(appDir) { } setMeteorConfig({ ...(packageJson?.meteor || {}), - modern: normalizeModernConfig(modernForced || packageJson?.meteor?.modern || false), + modern: { + ...normalizeModernConfig(modernForced || packageJson?.meteor?.modern || false), + ...(packageJson?.meteor?.verbose || packageJson?.meteor?.modern?.verbose) && { verbose: true }, + }, }); return meteorConfig; } diff --git a/tools/tool-env/rspack.js b/tools/tool-env/rspack.js new file mode 100644 index 0000000000..be7794dfba --- /dev/null +++ b/tools/tool-env/rspack.js @@ -0,0 +1,93 @@ +// Helper functions for Rspack integration +const files = require('../fs/files'); +const path = require('path'); +const { getMeteorConfig } = require("./meteor-config"); + +const config = getMeteorConfig(); + +// Derive the METEOR_LOCAL_DIR suffix the same way packages/rspack/lib/constants.js does, +// so reset cleans the correct directories when running multiple instances. +const meteorLocalDirName = process.env.METEOR_LOCAL_DIR + ? path.basename(process.env.METEOR_LOCAL_DIR.replace(/\\/g, '/')) + : ''; +const localDirSuffix = meteorLocalDirName ? `-${meteorLocalDirName}` : ''; + +// Get the build context from environment variable or use default "_build" +const rspackBuildContext = config?.buildContext || process.env.RSPACK_BUILD_CONTEXT || `_build${localDirSuffix}`; + +// Get the assets context from environment variable or use default "build-assets" +const rspackAssetsContext = config?.assetsContext || process.env.RSPACK_ASSETS_CONTEXT || `build-assets${localDirSuffix}`; + +// Get the bundles context from environment variable or use default "build-chunks" +const rspackChunksContext = config?.chunksContext || process.env.RSPACK_CHUNKS_CONTEXT || `build-chunks${localDirSuffix}`; + +// Cache the regex pattern for performance +const rspackFilePattern = new RegExp(`^${rspackBuildContext}\\/.*\\/[^\\/]*-rspack\\.js$`); + +// Export the variables for use in other files +exports.rspackBuildContext = rspackBuildContext; +exports.rspackAssetsContext = rspackAssetsContext; +exports.rspackChunksContext = rspackChunksContext; +exports.rspackFilePattern = rspackFilePattern; + +// Function to check if a file is a Rspack output file +exports.isRspackOutputFile = function(filePath) { + return rspackFilePattern.test(filePath); +}; + +// Function to get the rspack resources contexts +exports.getRspackResourcesContexts = function() { + return [ + rspackAssetsContext, + rspackChunksContext + ]; +}; + +// Function to get the rspack app contexts for cleanup. +// Reads the app's package.json meteor config to resolve custom context names, +// and always includes the default paths to prevent regressions. +exports.getRspackAppContexts = function(appDir) { + let appConfig = null; + try { + const pkgPath = files.pathJoin(appDir, 'package.json'); + if (files.exists(pkgPath)) { + const pkg = JSON.parse(files.readFile(pkgPath, 'utf8')); + appConfig = pkg?.meteor || null; + } + } catch (e) { + // Fall back to defaults if package.json can't be read + } + + const appBuildContext = appConfig?.buildContext || process.env.RSPACK_BUILD_CONTEXT || `_build${localDirSuffix}`; + const appAssetsContext = appConfig?.assetsContext || process.env.RSPACK_ASSETS_CONTEXT || `build-assets${localDirSuffix}`; + const appChunksContext = appConfig?.chunksContext || process.env.RSPACK_CHUNKS_CONTEXT || `build-chunks${localDirSuffix}`; + + const contexts = [ + files.pathJoin(appDir, "node_modules", ".cache", "rspack"), + ]; + + // Collect unique context names (configured + defaults to prevent regressions) + const allNames = new Set([ + appBuildContext, '_build', + appAssetsContext, 'build-assets', + appChunksContext, 'build-chunks', + ]); + + for (const name of allNames) { + contexts.push(files.pathJoin(appDir, name)); + contexts.push(files.pathJoin(appDir, `public/${name}`)); + contexts.push(files.pathJoin(appDir, `private/${name}`)); + } + + // When METEOR_LOCAL_DIR is set, also include suffixed paths + if (localDirSuffix) { + for (const name of allNames) { + const suffixed = `${name}${localDirSuffix}`; + contexts.push(files.pathJoin(appDir, suffixed)); + contexts.push(files.pathJoin(appDir, `public/${suffixed}`)); + contexts.push(files.pathJoin(appDir, `private/${suffixed}`)); + } + } + + return contexts; +}; diff --git a/tools/unit-tests/README.md b/tools/unit-tests/README.md new file mode 100644 index 0000000000..9b7f49a3d4 --- /dev/null +++ b/tools/unit-tests/README.md @@ -0,0 +1,23 @@ +# Unit Tests + +Isolated Jest environment for unit-testing Meteor `tools/` and `scripts/`. + +The repo root `node_modules/` is used to build the dev bundle, which becomes the Meteor tool itself. Installing test deps (jest, swc, semver, underscore) there could pull in incompatible transitive versions (e.g. lru-cache v10 vs v5) and silently break the dev bundle build or a published Meteor release. This subfolder keeps test dependencies fully isolated so they never affect how Meteor is built or shipped. + +Test files use `*.test.js` next to their source. + +All commands below should be run from the repo root: + +```sh +# Install dependencies (first time) +npm run install:unit + +# Run all unit tests +npm run test:unit + +# Run a specific test file +npm run test:unit -- tools/path/to/file.test.js + +# Run tests matching a name pattern +npm run test:unit -- -t "my test name" +``` diff --git a/tools/unit-tests/jest.config.js b/tools/unit-tests/jest.config.js new file mode 100644 index 0000000000..f78071ebc9 --- /dev/null +++ b/tools/unit-tests/jest.config.js @@ -0,0 +1,42 @@ +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '../..'); + +module.exports = { + rootDir: repoRoot, + testMatch: [ + "/tools/**/*.test.js", + "/scripts/**/*.test.js", + ], + testPathIgnorePatterns: [ + "/node_modules/", + "/tools/e2e-tests/", + "/tools/tests/", + "/packages/", + "/.github/", + ], + modulePathIgnorePatterns: [ + "/tools/e2e-tests/", + "/tools/tests/", + "/tools/static-assets/", + "/npm-packages/", + "/scripts/admin/", + "/docs/", + "/packages/non-core/", + ], + modulePaths: [ + path.resolve(__dirname, 'node_modules'), + ], + transform: { + "^.+\\.js$": [require.resolve("@swc/jest"), { + jsc: { + parser: { syntax: "ecmascript" }, + target: "es2022", + }, + module: { type: "commonjs" }, + }], + }, + transformIgnorePatterns: ["/node_modules/"], + testTimeout: 10_000, + verbose: true, +}; diff --git a/tools/unit-tests/package-lock.json b/tools/unit-tests/package-lock.json new file mode 100644 index 0000000000..c261671527 --- /dev/null +++ b/tools/unit-tests/package-lock.json @@ -0,0 +1,4672 @@ +{ + "name": "meteor-unit-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meteor-unit-tests", + "version": "1.0.0", + "devDependencies": { + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", + "jest": "^30.2.0", + "semver": "^7.7.2", + "underscore": "^1.13.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz", + "integrity": "sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "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/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@swc/core": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/jest": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", + "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^30.0.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "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/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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/cliui/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/cliui/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/cliui/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/cliui/node_modules/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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "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/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/path-scurry/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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "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/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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/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-length/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/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/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.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/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/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/wrap-ansi/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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/yargs/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/yargs/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/yargs/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tools/unit-tests/package.json b/tools/unit-tests/package.json new file mode 100644 index 0000000000..38644a7619 --- /dev/null +++ b/tools/unit-tests/package.json @@ -0,0 +1,16 @@ +{ + "name": "meteor-unit-tests", + "version": "1.0.0", + "private": true, + "description": "Isolated Jest environment for Meteor unit tests", + "scripts": { + "test": "jest --config jest.config.js --passWithNoTests" + }, + "devDependencies": { + "@swc/core": "^1.15.18", + "@swc/jest": "^0.2.39", + "jest": "^30.2.0", + "semver": "^7.7.2", + "underscore": "^1.13.7" + } +} diff --git a/tools/utils/archinfo.ts b/tools/utils/archinfo.ts index 18c7c3177e..2ebfa58ca7 100644 --- a/tools/utils/archinfo.ts +++ b/tools/utils/archinfo.ts @@ -1,6 +1,7 @@ import { max } from 'underscore'; import os from 'os'; const utils = require('./utils'); +import { getMeteorConfig } from '../tool-env/meteor-config.js'; /* Meteor's current architecture scheme defines the following virtual * machine types, which are defined by specifying what is promised by @@ -235,33 +236,49 @@ export function matches(host: string, program: string): boolean { host.substr(program.length, 1) === "."); } -const legacyArches = [ - "web.browser.legacy", - // It's important to include web.browser.legacy resources in the Cordova - // bundle, since Cordova bundles are built into the mobile application, - // rather than being downloaded from a web server at runtime. This means - // we can't distinguish between clients at runtime, so we have to use - // code that works for all clients. - "web.cordova", -]; + +function getLegacyArches(): string[] { + const arches = ["web.browser.legacy"]; + + // Check if cordova should use legacy mode + // This needs to access the meteor config at runtime + try { + const meteorConfig = getMeteorConfig(); + + if (meteorConfig?.modern?.cordova === false) { + arches.push("web.cordova"); + } + } catch (e) { + // If config is not available, default to modern (don't add web.cordova) + } + + return arches; +} export function isLegacyArch(arch: string): boolean { + const legacyArches = getLegacyArches(); return legacyArches.some(la => matches(arch, la)); } export function mapWhereToArches(where: string) { const arches: string[] = []; + const legacyArches = getLegacyArches(); // Shorthands for common arch prefixes: // "server" => os.* // "client" => web.* - // "legacy" => web.browser.legacy, web.cordova + // "modern" => web.browser, web.cordova (unless modern.cordova is set to false) + // "legacy" => web.browser.legacy, web.cordova (if modern.cordova is false) if (where === "server") { arches.push("os"); } else if (where === "client") { arches.push("web"); } else if (where === "modern") { arches.push("web.browser"); + // Only add web.cordova to modern if it's not in legacy mode + if (!legacyArches.includes("web.cordova")) { + arches.push("web.cordova"); + } } else if (where === "legacy") { arches.push(...legacyArches); } else { diff --git a/tools/utils/utils.js b/tools/utils/utils.js index 54bbfe0b31..92b5baed20 100644 --- a/tools/utils/utils.js +++ b/tools/utils/utils.js @@ -10,6 +10,17 @@ var packageVersionParser = require('../packaging/package-version-parser.js'); var utils = exports; +// Cache regex patterns to avoid recompiling them on every function call. +// This improves performance in hot paths like URL parsing. +var REGEX_PORT_ONLY = /^[0-9]+$/; +var REGEX_HAS_SCHEME = /^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//; +var REGEX_IPV4_ADDRESS = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; +var REGEX_VALID_EMAIL = /^[^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*@([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}$/; +var REGEX_FILE_SCHEME = /^file:\/\/.+/; +var REGEX_URL_WITH_SHA = /^https?:\/\/.*[0-9a-f]{40}/; +var REGEX_NPM_URL_PROTOCOL = /^(git|git\+ssh|git\+http|git\+https|https|http)?:\/\//; +var REGEX_RELEASE_VERSION = /^(\d{1,4}(?:\.\d{1,4})*)(?:-([-A-Za-z.]{1,15})(\d{0,4}))?$/; + // Parses ://: into an object { protocol: *, host: // *, port: * }. The input can also be of the form : or just // . We're not simply using 'url.parse' because we want '3000' to @@ -26,7 +37,7 @@ exports.parseUrl = function (str, defaults) { var defaultPort = defaults.port || undefined; var defaultProtocol = defaults.protocol || undefined; - if (str.match(/^[0-9]+$/)) { // just a port + if (REGEX_PORT_ONLY.test(str)) { // just a port return { port: str, hostname: defaultHostname, @@ -34,11 +45,7 @@ exports.parseUrl = function (str, defaults) { } var hasScheme = exports.hasScheme(str); - if (! hasScheme) { - str = "http://" + str; - } - - var parsed = url.parse(str); + const parsed = url.parse(hasScheme ? str : `http://${str}`); // for consistency remove colon at the end of protocol parsed.protocol = parsed.protocol.replace(/\:$/, ''); @@ -59,10 +66,7 @@ exports.parseUrl = function (str, defaults) { exports.formatUrl = function (options) { // For consistency with `Meteor.absoluteUrl`, add a trailing slash to make // this a valid URL - if (!options.pathname) - options.pathname = "/"; - - return url.format(options); + return url.format({ ...options, pathname: options.pathname || "/" }); }; exports.ipAddress = function () { @@ -89,36 +93,19 @@ ${addressEntries.map(entry => entry.address).join(', ')}`); }; exports.hasScheme = function (str) { - return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//); -}; - - -exports.hasScheme = function (str) { - return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//); + return REGEX_HAS_SCHEME.test(str); }; exports.isIPv4Address = function (str) { - return str.match(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/); -} + return REGEX_IPV4_ADDRESS.test(str); +}; // XXX: Move to e.g. formatters.js? // Prints a package list in a nice format. // Input is an array of objects with keys 'name' and 'description'. exports.printPackageList = function (items, options) { - options = options || {}; - - var rows = _.map(items, function (item) { - var name = item.name; - var description = item.description || 'No description'; - return [name, description]; - }); - - var alphaSort = function (row) { - return row[0]; - }; - rows = _.sortBy(rows, alphaSort); - - var Console = require('../console/console.js').Console; + const rows = _.sortBy(items.map(item => [item.name, item.description || 'No description']), row => row[0]); + const Console = require('../console/console.js').Console; return Console.printTwoColumns(rows, options); }; @@ -331,7 +318,7 @@ exports.validatePackageNameOrExit = function (packageName, options) { // - IP addresses in domains (eg, foo@1.2.3.4 or the IPv6 equivalent) // because they're weird and we don't want them in our database. exports.validEmail = function (address) { - return /^[^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*@([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}$/.test(address); + return REGEX_VALID_EMAIL.test(address); }; // Like Perl's quotemeta: quotes all regexp metacharacters. See @@ -357,7 +344,7 @@ exports.timeoutScaleFactor = timeoutScaleFactor; // the prerelease for a given release will sort before it. Because $ sorts // before '.', this means that 1.2 will sort before 1.2.3.) exports.defaultOrderKeyForReleaseVersion = function (v) { - var m = v.match(/^(\d{1,4}(?:\.\d{1,4})*)(?:-([-A-Za-z.]{1,15})(\d{0,4}))?$/); + var m = v.match(REGEX_RELEASE_VERSION); if (!m) { return null; } @@ -468,21 +455,21 @@ exports.generateSubsetsOfIncreasingSize = function (total, cb) { }; exports.isUrlWithFileScheme = function (x) { - return /^file:\/\/.+/.test(x); + return REGEX_FILE_SCHEME.test(x); }; exports.isUrlWithSha = function (x) { // Is a URL with a fixed SHA? We use this for Cordova -- although theoretically we could use // a URL like isNpmUrl(), there are a variety of problems with this, // see https://github.com/meteor/meteor/pull/5562 - return /^https?:\/\/.*[0-9a-f]{40}/.test(x); -} + return REGEX_URL_WITH_SHA.test(x); +}; exports.isNpmUrl = function (x) { // These are the various protocols that NPM supports, which we use to download NPM dependencies // See https://docs.npmjs.com/files/package.json#git-urls-as-dependencies return exports.isUrlWithSha(x) || - /^(git|git\+ssh|git\+http|git\+https|https|http)?:\/\//.test(x); + REGEX_NPM_URL_PROTOCOL.test(x); }; exports.isPathRelative = function (x) { @@ -773,3 +760,4 @@ export function isEmacs() { emacsDetected = !! (process.env.EMACS === "t" || process.env.INSIDE_EMACS); return emacsDetected; } + diff --git a/tools/utils/utils.test.js b/tools/utils/utils.test.js new file mode 100644 index 0000000000..faf5a522fa --- /dev/null +++ b/tools/utils/utils.test.js @@ -0,0 +1,210 @@ +jest.mock('./archinfo', () => ({ + host: jest.fn(() => 'os.osx.x86_64'), + matches: jest.fn((host, pattern) => host.startsWith(pattern)), +})); + +jest.mock('./buildmessage.js', () => ({ + error: jest.fn(), +})); + +jest.mock('../fs/files', () => ({ + stat: jest.fn(), + inCheckout: jest.fn(() => true), + getToolsVersion: jest.fn(() => '3.0.0'), + getCurrentToolsDir: jest.fn(() => '/mock/tools'), + convertToOSPath: jest.fn(p => p), + pathJoin: jest.fn((...args) => args.join('/')), +})); + +jest.mock('../packaging/package-version-parser.js', () => ({ + parsePackageConstraint: jest.fn(), + validatePackageName: jest.fn((name) => { + if (name === 'INVALID') { + const err = new Error('bad package name'); + err.versionParserError = true; + throw err; + } + }), + parse: jest.fn((version) => { + if (version === 'bad') { + const err = new Error('bad version'); + err.versionParserError = true; + throw err; + } + return version; + }), +})); + +const utils = require('./utils'); +const buildmessage = require('./buildmessage.js'); + +describe('parseUrl', () => { + test.each([ + ['3000', {}, { port: '3000', hostname: undefined, protocol: undefined }], + ['4000', { hostname: 'h', protocol: 'https' }, { port: '4000', hostname: 'h', protocol: 'https' }], + ['localhost', {}, { hostname: 'localhost' }], + ['localhost:3000', {}, { hostname: 'localhost', port: '3000', protocol: undefined }], + ['https://ex.com:8080/path', {}, { protocol: 'https', hostname: 'ex.com', port: '8080', pathname: '/path' }], + ['ex.com:3000', { protocol: 'https' }, { protocol: 'https', hostname: 'ex.com', port: '3000' }], + ['http://ex.com', { protocol: 'https' }, { protocol: 'http', hostname: 'ex.com' }], + ['http://ex.com', { port: '9999' }, { protocol: 'http', hostname: 'ex.com', port: '9999' }], + ])('parseUrl(%s) with defaults %j', (input, defaults, expected) => { + const result = utils.parseUrl(input, defaults); + expect(result).toMatchObject(expected); + }); + + test('excludes pathname for root path', () => { + expect(utils.parseUrl('http://ex.com/').pathname).toBeUndefined(); + }); +}); + +describe('hasScheme', () => { + test.each([ + ['http://x', true], ['https://x', true], ['git+ssh://x', true], + ['my2proto://x', true], ['example.com', false], ['3000', false], + ['http:x', false], ['2http://x', false], + ])('(%s) = %s', (input, expected) => { + expect(!!utils.hasScheme(input)).toBe(expected); + }); +}); + +describe('isIPv4Address', () => { + test.each([ + ['192.168.1.1', true], ['0.0.0.0', true], ['255.255.255.255', true], + ['localhost', false], ['192.168.1', false], ['::1', false], ['1.2.3.4.5', false], + ])('(%s) = %s', (input, expected) => { + expect(!!utils.isIPv4Address(input)).toBe(expected); + }); +}); + +describe('validEmail', () => { + test.each([ + ['user@example.com', true], ['a.b@mail.co.uk', true], + ['user+tag@example.com', true], ['a@my-host.com', true], + ['userexample.com', false], ['user@', false], ['@example.com', false], + ['us er@x.com', false], ['', false], ['u@x.c', false], + ])('(%s) = %s', (input, expected) => { + expect(utils.validEmail(input)).toBe(expected); + }); +}); + +describe('quotemeta', () => { + test.each([ + ['a.b*c+d?e', 'a\\.b\\*c\\+d\\?e'], + ['[a](b)\\c', '\\[a\\]\\(b\\)\\\\c'], + ['abc123', 'abc123'], + ])('(%s) = %s', (input, expected) => { + expect(utils.quotemeta(input)).toBe(expected); + }); + + test('escaped string works as literal RegExp', () => { + const s = 'price: $100 (USD)'; + expect(new RegExp(utils.quotemeta(s)).test(s)).toBe(true); + }); +}); + +describe('defaultOrderKeyForReleaseVersion', () => { + test.each([ + ['1.2.3', '0001.0002.0003$'], + ['5', '0005$'], + ['1.2.3.4', '0001.0002.0003.0004$'], + ['1.0-beta', '0001.0000!beta!!!!!!!!!!!$'], + ['1.0-beta.rc3', '0001.0000!beta.rc!!!!!!!!0003$'], + ])('(%s) = %s', (input, expected) => { + expect(utils.defaultOrderKeyForReleaseVersion(input)).toBe(expected); + }); + + test('prerelease key contains ! and tag, ends with $', () => { + const key = utils.defaultOrderKeyForReleaseVersion('1.0-rc1'); + expect(key).toMatch(/!.*rc.*\$$/); + }); + + test('sort order: prerelease < release, 1.2 < 1.2.3, 2 < 10', () => { + const k = (v) => utils.defaultOrderKeyForReleaseVersion(v); + expect(k('1.0-rc1') < k('1.0')).toBe(true); + expect(k('1.2') < k('1.2.3')).toBe(true); + expect(k('2') < k('10')).toBe(true); + }); + + test.each([ + 'abc', '01.2.3', '1.02.3', '1.0-rc01', '12345', '', + ])('returns null for invalid input: %s', (input) => { + expect(utils.defaultOrderKeyForReleaseVersion(input)).toBeNull(); + }); +}); + +describe('generateSubsetsOfIncreasingSize', () => { + test('enumerates all subsets in order and supports early stop', () => { + const all = []; + utils.generateSubsetsOfIncreasingSize([1, 2, 3], (s) => { all.push([...s]); }); + expect(all).toEqual([[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]); + + const stopped = []; + utils.generateSubsetsOfIncreasingSize([1, 2, 3], (s) => { + stopped.push([...s]); + return s.length === 2; + }); + expect(stopped).toEqual([[], [1], [2], [3], [1, 2]]); + }); + + test('empty array yields only the empty subset', () => { + const r = []; + utils.generateSubsetsOfIncreasingSize([], (s) => { r.push([...s]); }); + expect(r).toEqual([[]]); + }); +}); + +describe('URL scheme matchers', () => { + test.each([ + ['isUrlWithFileScheme', 'file:///path', true], + ['isUrlWithFileScheme', 'file://host/path', true], + ['isUrlWithFileScheme', 'file://', false], + ['isUrlWithFileScheme', 'http://x', false], + ['isUrlWithSha', `https://x/${'a'.repeat(40)}`, true], + ['isUrlWithSha', `http://x/${'b'.repeat(40)}`, true], + ['isUrlWithSha', 'https://x/abc123', false], + ['isUrlWithSha', 'not-a-url', false], + ['isNpmUrl', 'git://github.com/r', true], + ['isNpmUrl', 'git+ssh://git@github.com/r', true], + ['isNpmUrl', 'git+http://github.com/r', true], + ['isNpmUrl', 'git+https://github.com/r', true], + ['isNpmUrl', 'https://x/pkg', true], + ['isNpmUrl', 'http://x/pkg', true], + ['isNpmUrl', 'lodash', false], + ])('%s(%s) = %s', (fn, input, expected) => { + expect(!!utils[fn](input)).toBe(expected); + }); +}); + +describe('sourceMapLength', () => { + test.each([ + [null, 0], + [undefined, 0], + [{ mappings: 'AAAA' }, 4], + [{ mappings: 'ABC', sourcesContent: ['hello', 'world'] }, 13], + [{ mappings: 'AB', sourcesContent: [null, 'code', null] }, 6], + ])('sourceMapLength(%j) = %s', (input, expected) => { + expect(utils.sourceMapLength(input)).toBe(expected); + }); +}); + +describe('parsePackageAndVersion', () => { + test.each([ + ['my-pkg 1.0.0', { package: 'my-pkg', version: '1.0.0' }], + ['my-pkg@2.0.0', { package: 'my-pkg', version: '2.0.0' }], + ['user:pkg 1.0.0', { package: 'user:pkg', version: '1.0.0' }], + ])('parses %s', (input, expected) => { + expect(utils.parsePackageAndVersion(input)).toEqual(expected); + }); + + test('throws for missing separator or invalid version', () => { + expect(() => utils.parsePackageAndVersion('noseparator')).toThrow('Malformed package version'); + expect(() => utils.parsePackageAndVersion('pkg bad')).toThrow(); + }); + + test('returns null with useBuildmessage on malformed input', () => { + buildmessage.error.mockClear(); + expect(utils.parsePackageAndVersion('noseparator', { useBuildmessage: true })).toBeNull(); + expect(buildmessage.error).toHaveBeenCalled(); + }); +}); diff --git a/v3-docs/docs/.gitignore b/v3-docs/docs/.gitignore index bcd2cabb66..60c63498eb 100644 --- a/v3-docs/docs/.gitignore +++ b/v3-docs/docs/.gitignore @@ -3,4 +3,7 @@ /.vitepress/dist /data/data.js -/data/names.json \ No newline at end of file +/data/names.json + +# Generated API reference for LLMs +/public/api-reference.json \ No newline at end of file diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index dfdb6f956c..25eb149a16 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -37,8 +37,8 @@ export default defineConfig({ link: "/tutorials/react/index", }, { - text: "Meteor + Vue + vue-meteor-tracker", - link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", + text: "Meteor.js 3 + Vue", + link: "/tutorials/vue/meteorjs3-vue3", }, { text: "Meteor.js 3 + Solid", @@ -163,10 +163,6 @@ export default defineConfig({ text: "Roadmap", link: "/about/roadmap", }, - { - text: "Contributing", - link: "/about/contributing", - } ], collapsed: true, }, @@ -181,28 +177,24 @@ export default defineConfig({ text: "Web Apps", link: "/about/web-apps", }, - { - text: "Cordova", - link: "/about/cordova", - }, { text: "Modern Build Stack", link: "/about/modern-build-stack.md", items: [ { - text: "Transpiler: SWC", - link: "/about/modern-build-stack/transpiler-swc.md", + text: "Meteor Bundler", + link: "/about/modern-build-stack/meteor-bundler-optimizations.md", }, { - text: "Bundler", - link: "/about/modern-build-stack/bundler.md", - }, - { - text: "Dev Server", - link: "/about/modern-build-stack/dev-server.md", + text: "Rspack Bundler", + link: "/about/modern-build-stack/rspack-bundler-integration.md", }, ] }, + { + text: "Cordova", + link: "/about/cordova", + }, ], collapsed: true, }, @@ -320,6 +312,10 @@ export default defineConfig({ text: "roles", link: "/packages/roles", }, + { + text: "service-configuration", + link: "/packages/service-configuration", + }, { text: "oauth-encryption", link: "/packages/oauth-encryption", @@ -413,10 +409,26 @@ export default defineConfig({ }, ] }, + { + text: "Using Atmosphere packages", + link: "/packages/6.using-atmosphere-packages", + }, + { + text: "Writing Atmosphere packages", + link: "/packages/7.writing-atmosphere-packages", + }, { link: "/packages/packages-listing", text: "Maintained Packages", }, + { + text: "Using npm packages", + link: "/packages/4.using-npm-packages", + }, + { + text: "Writing npm packages", + link: "/packages/5.writing-npm-packages", + }, { link: "/community-packages/index", text: "Community Packages", @@ -449,6 +461,14 @@ export default defineConfig({ text: "jam:offline", link: "/community-packages/offline", }, + { + text: "dupontbertrand:cluster", + link: "/community-packages/cluster", + }, + { + text: "dupontbertrand:mail-preview", + link: "/community-packages/mail-preview", + }, ], collapsed: true, }, @@ -467,6 +487,10 @@ export default defineConfig({ text: "MongoDB Connection", link: "/troubleshooting/mongodb-connection", }, + { + text: "Hot Code Push", + link: "/troubleshooting/hot-code-push", + }, ], collapsed: true, }, @@ -487,8 +511,8 @@ export default defineConfig({ link: "/tutorials/react/index", }, { - link: "/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker", - text: "Meteor + Vue + vue-meteor-tracker", + text: "Meteor.js 3 + Vue", + link: "/tutorials/vue/meteorjs3-vue3", }, { text: "Meteor.js 3 + Solid", @@ -506,6 +530,78 @@ export default defineConfig({ link: "/tutorials/application-structure/index", text: "Application structure", }, + { + text: "Build System", + link: "/about/build-tool", + }, + { + text: "Core Concepts", + items: [ + { + text: "Methods", + link: "/tutorials/methods/methods", + }, + { + text: "Data Loading", + link: "/tutorials/data-loading/data-loading", + }, + { + text: "Collections & Schemas", + link: "/tutorials/collections/collections", + }, + { + text: "Accounts", + link: "/tutorials/accounts/accounts", + }, + { + text: "Routing", + link: "/tutorials/routing/routing", + }, + ] + }, + { + text: "Production", + items:[ + { + text: "Security", + link: "/tutorials/security/security", + }, + { + text: "Testing", + link: "/tutorials/testing/testing", + }, + { + text: "Deployment", + link: "/tutorials/deployment/deployment", + }, + ] + }, + { + text: "Advanced Topics", + items: [ + { + text: "Apollo & GraphQL", + link: "/tutorials/apollo/apollo", + }, + { + text: "Code Style", + link: "/tutorials/code-style/code-style", + }, + ] + }, + { + text: "Integrations", + items: [ + { + text: "React Native", + link: "/tutorials/integrations/react-native", + }, + { + text: "Flowbite UI", + link: "/tutorials/integrations/flowbite", + }, + ] + }, ], collapsed: true, }, @@ -528,11 +624,30 @@ export default defineConfig({ { text: "Performance", items: [ + { + text: "Performance Improvements", + link: "/performance/performance-improvement", + }, { text: "WebSocket Compression", link: "/performance/websocket-compression", }, ], + collapsed: true, + }, + { + text: "Community", + items: [ + { + text: "Contributing", + link: "/community/contributing", + }, + { + text: "Contributors", + link: "/community/contributors", + }, + ], + collapsed: true, }, ], @@ -570,7 +685,27 @@ export default defineConfig({ vite: { plugins: [ llmstxt({ - title: "Meteor.js 3 Docs", + title: "Meteor.js 3 Documentation", + domain: "https://docs.meteor.com", + description: "Full-stack JavaScript platform for modern web and mobile applications.", + details: ` +Meteor is a full-stack JavaScript platform for developing web and mobile applications. + +Key capabilities: +- Real-time data synchronization with publications and subscriptions +- Built-in accounts and authentication system +- Frontend agnostic (React, Vue, Solid, Blaze, Svelte) +- Zero-config build system with modern tooling (SWC, Rspack) +- One-command deployment to Galaxy Cloud +- TypeScript support with full type inference + +Current version: Meteor ${metadata.currentVersion}. + +## Structured API Data + +For complete API documentation in machine-readable format, see: +- [api-reference.json](/api-reference.json) - Full API reference with all functions, parameters, and types + `.trim(), }), ], }, diff --git a/v3-docs/docs/.vitepress/theme/Contributors.vue b/v3-docs/docs/.vitepress/theme/Contributors.vue new file mode 100644 index 0000000000..2b817192b7 --- /dev/null +++ b/v3-docs/docs/.vitepress/theme/Contributors.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/v3-docs/docs/.vitepress/theme/index.ts b/v3-docs/docs/.vitepress/theme/index.ts index afddc3f960..e546ad156d 100644 --- a/v3-docs/docs/.vitepress/theme/index.ts +++ b/v3-docs/docs/.vitepress/theme/index.ts @@ -3,6 +3,7 @@ import type { Theme } from "vitepress"; import DefaultTheme from "vitepress/theme"; import ApiBox from "../../components/ApiBox.vue"; import ApiMap from "../../components/ApiMap.vue"; +import Contributors from "./Contributors.vue"; import Layout from "./Layout.vue"; import "./theme.css"; @@ -12,6 +13,7 @@ export default { // register your custom global components app.component("ApiBox", ApiBox); app.component("ApiMap", ApiMap); + app.component("Contributors", Contributors); }, Layout, } satisfies Theme; diff --git a/v3-docs/docs/about/build-tool.md b/v3-docs/docs/about/build-tool.md new file mode 100644 index 0000000000..752a3a1164 --- /dev/null +++ b/v3-docs/docs/about/build-tool.md @@ -0,0 +1,388 @@ +# Build System + +This guide covers how Meteor's build system compiles your app. + +After reading this guide, you'll know: + +1. What the Meteor build tool does +2. How to choose between Meteor Bundler and Rspack integration +3. How JavaScript transpilation works +4. How to configure CSS processing +5. How to use Hot Module Replacement +6. How to write custom build plugins + +## What does it do? + +The Meteor build system is the actual command line tool that you get when you install Meteor. You run it by typing the `meteor` command in your terminal, possibly followed by a set of arguments. Read the [CLI documentation](/cli/) or type `meteor help` in your terminal to learn about all of the commands. + +The Meteor build tool is what compiles, runs, deploys, and publishes all of your Meteor apps and packages. It's Meteor's built-in solution to the problems also solved by tools like Grunt, Gulp, Webpack, Browserify, Nodemon, and many others. + +Starting with Meteor 3.3, you can use the **Modern Build Stack** which includes optimizations with SWC transpilation. With Meteor 3.4, you can integrate **Rspack** as your application bundler for even faster builds, smaller bundle sizes, and modern bundling features. See the [Modern Build Stack overview](/about/modern-build-stack) for details. + +### Reloads app on file change + +After executing the `meteor` command to start the build tool you should leave it running while further developing your app. The build tool automatically detects any relevant file changes using a file watching system and recompiles the necessary changes, restarting your client or server environment as needed. [Hot module replacement](#hot-module-replacement) can optionally be used so you can view and test your changes even quicker. + +### Compiles files with build plugins + +The main function of the Meteor build tool is to run "build plugins". These plugins define different parts of your app build process. Meteor puts heavy emphasis on reducing or removing build configuration files, so you won't see any large build process config files like you would in Gulp or Webpack. The Meteor build process is configured almost entirely through adding and removing packages to your app and putting files in specially named directories. + +For example, to get all of the newest stable ES2015 JavaScript features in your app, you add the [`ecmascript` package](/packages/ecmascript). This package provides support for ES2015 modules, which gives you even more fine-grained control over file load order using ES2015 `import` and `export`. As new Meteor releases add new features to this package you get them for free. + +### Controlling which files to build + +By default Meteor will build certain files as controlled by your application [file structure](/tutorials/application-structure/) and Meteor's default file load order rules. However, you may override the default behavior using `.meteorignore` files, which cause the build system to ignore certain files and directories using the same pattern syntax as `.gitignore` files. These files may appear in any directory of your app or package, specifying rules for the directory tree below them. These `.meteorignore` files are also fully integrated with Meteor's file watching system, so they can be added, removed, or modified during development. + +### Combines and minifies code + +Another important feature of the Meteor build tool is that it automatically concatenates your application asset files, and in production minifies these bundles. This lets you add all of the comments and whitespace you want to your source code and split your code into as many files as necessary, all without worrying about app performance and load times. + +By default, this is enabled by the [`standard-minifier-js`](https://atmospherejs.com/meteor/standard-minifiers-js) and [`standard-minifier-css`](/packages/standard-minifier-css) packages, which use Terser for JavaScript minification. + +**Using SWC Minifier (Meteor 3.3+)**: When you enable the Modern Build Stack with `"modern": true`, Meteor automatically uses the SWC minifier, which is significantly faster than Terser while producing similar or smaller bundle sizes. + +**Using Rspack (Meteor 3.4+)**: When using the Rspack integration, minification is handled by Rspack's built-in SWC minifier, offering the fastest minification with advanced optimizations. + +If you need different minification behavior, you can replace these packages (see [zodern:standard-minifier-js](https://atmospherejs.com/zodern/standard-minifier-js) as an example). + +### Development vs. production + +Running an app in development is all about fast iteration time. All kinds of different parts of your app are handled differently and instrumented to enable better reloads and debugging. In production, the app is reduced to the necessary code and functions just like any standard Node.js app. + +Therefore, you shouldn't run your app in production by executing the `meteor run` command. Instead, follow the directions in [Deploying Meteor Applications](/tutorials/deployment/deployment). If you find an error in production that you suspect is related to minification, you can run the minified version of your app locally for testing with `meteor --production`. + +## Modern Build Stack (Meteor 3.3+) + +Meteor 3.3 introduced the Modern Build Stack, a series of optimizations to make your builds faster and more efficient. Meteor 3.4 expanded this with Rspack integration for even greater performance. + +### Meteor Bundler Optimizations (3.3+) + +The optimized Meteor bundler includes: + +- **SWC Transpilation**: Replace Babel with the faster SWC transpiler for JavaScript/TypeScript compilation +- **SWC Minification**: Use SWC minifier instead of Terser for faster production builds +- **Modern-only Development**: Skip legacy browser builds during development +- **Fast File Watching**: Uses @parcel/watcher for native recursive file watching +- **Enhanced .meteorignore**: Better control over which files are built + +To enable these optimizations, add to your `package.json`: + +```json +{ + "meteor": { + "modern": true + } +} +``` + +Learn more in the [Meteor Bundler Optimizations guide](/about/modern-build-stack/meteor-bundler-optimizations). + +### Rspack Bundler Integration (3.4+) + +Meteor 3.4 introduces optional Rspack integration as a modern, high-performance alternative to the traditional Meteor bundler. Rspack offers significantly faster builds (~5-10x), smaller bundle sizes (20-40% reduction), and access to the modern bundler ecosystem. + +**Quick Start:** + +```bash +# Add the rspack package (included by default in new apps) +meteor add rspack +``` + +**For complete details on Rspack features, configuration, migration guides, and framework integrations, see the [Rspack Bundler Integration guide](/about/modern-build-stack/rspack-bundler-integration).** + +### Comparing Build Options + +| Feature | Meteor Bundler | Meteor + Optimizations | Meteor + Rspack | +|---------|---------------|----------------------|-----------------| +| **Setup** | Default | Add `"modern": true` | Add `rspack` package | +| **Transpiler** | Babel | SWC (with Babel fallback) | SWC via Rspack | +| **Minifier** | Terser | SWC | SWC via Rspack | +| **Build Speed** | Baseline | ~2-3x faster | ~5-10x faster | +| **Bundle Size** | Baseline | Similar | 20-40% smaller | +| **HMR Speed** | Standard | Faster | Fastest | +| **Code Splitting** | Limited | Limited | Full HTTP/2 support | +| **Tree Shaking** | Basic | Basic | Advanced | +| **Ecosystem** | Atmosphere | Atmosphere | Rspack + Atmosphere | +| **Entry Points** | Optional | Optional | Required | +| **Migration** | N/A | Minimal | Moderate | + +## JavaScript transpilation + +These days, the landscape of JavaScript tools and frameworks is constantly shifting, and the language itself is evolving just as rapidly. It's no longer reasonable to wait for web browsers to implement the language features you want to use. Most JavaScript development workflows rely on compiling code to work on the lowest common denominator of environments, while letting you use the newest features in development. Meteor has support for some of the most popular tools out of the box. + +### ES2015+ (recommended) + +The `ecmascript` package (which is installed into all new apps and packages by default, but can be removed), allows support for many ES2015+ features. We recommend using it. You can read more about it in the [Code Style](/tutorials/code-style/code-style) article. + +### SWC (Meteor 3.3+, recommended for performance) + +Starting with Meteor 3.3, you can use SWC as your transpiler for significantly faster builds. SWC is a Rust-based JavaScript/TypeScript compiler that's 20-70x faster than Babel while supporting the same features. + +To enable SWC, add to your `package.json`: + +```json +{ + "meteor": { + "modern": true + } +} +``` + +SWC will handle all transpilation with automatic fallback to Babel for any incompatible code. Learn more in the [Meteor Bundler Optimizations guide](/about/modern-build-stack/meteor-bundler-optimizations). + +### Babel + +Babel is a mature, configurable transpiler which allows you to write code in the latest version of JavaScript even when your supported environments don't support certain features natively. Babel will compile those features down to a supported version. + +Meteor provides a set of appropriate core plugins for each environment (Node.js, modern browsers, and legacy browsers) and React to support most modern JavaScript code practices. In addition, Meteor supports custom `.babelrc` files which allows developers to further customize their Babel configuration to suit their needs (e.g., Stage 0 proposals). + +Developers are encouraged to avoid adding large presets (such as babel-preset-env and babel-preset-react) and instead add specific plugins as needed. You will avoid unnecessary Babel compilation and you'll be less likely to experience plugin ordering issues. + +> **Note**: When using the Modern Build Stack with `"modern": true`, Babel is used as a fallback for any code that SWC cannot handle, ensuring compatibility with all existing Meteor code. + +### CoffeeScript + +While we recommend using ES2015 with the `ecmascript` package as the best development experience for Meteor, everything in the platform is 100% compatible with [CoffeeScript](http://coffeescript.org/) and many people in the Meteor community prefer it. + +All you need to do to use CoffeeScript is add the right Meteor package: + +```bash +meteor add coffeescript +``` + +All code written in CoffeeScript compiles to JavaScript under the hood, and is completely compatible with any code in other packages that is written in JS or ES2015. + +> **Recommended (3.4+)**: If you're using CoffeeScript, consider using it with the [Rspack bundler](/about/modern-build-stack/rspack-bundler-integration#coffeescript) instead of the Atmosphere build plugin. Rspack handles CoffeeScript via `coffee-loader` and `swc-loader`, providing faster builds and better integration with the modern build stack. You can quickly scaffold a new project with `meteor create --coffeescript`. + +### TypeScript + +[TypeScript](https://www.typescriptlang.org/) is modern JavaScript with optional types and more. Adding types will make your code more readable and less prone to runtime errors. + +TypeScript can be installed with: + +```bash +meteor add typescript +``` + +It is necessary to configure the TypeScript compiler with a `tsconfig.json` file. Here's the one generated by `meteor create --typescript`: + +```json +{ + "compilerOptions": { + "target": "es2018", + "module": "esNext", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "jsx": "preserve", + "incremental": true, + "noEmit": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + "baseUrl": ".", + "paths": { + "/*": ["*"] + }, + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node", "mocha"], + "esModuleInterop": true, + "preserveSymlinks": true + }, + "exclude": [ + "./.meteor/**", + "./packages/**" + ] +} +``` + +If you want to add TypeScript from the point of project creation, you can run the create command with the --typescript flag: + +```bash +meteor create --typescript name-of-my-new-typescript-app +``` + +#### Conditional imports + +TypeScript does not support nested `import` statements, therefore conditionally importing modules requires you to use the `require` statement. + +To maintain type safety, you can take advantage of TypeScript's import elision and reference the types using the `typeof` keyword. See the [TypeScript handbook article](https://www.typescriptlang.org/docs/handbook/modules.html#optional-module-loading-and-other-advanced-loading-scenarios) for details. + +## Templates and HTML + +Since Meteor uses client-side rendering for your app's UI, all of your HTML code, UI components, and templates need to be compiled to JavaScript. There are a few options at your disposal to write your UI code. + +### Blaze HTML templates + +The aptly named `blaze-html-templates` package that comes with every new Meteor app by default compiles your `.html` files written using [Spacebars](http://blazejs.org/api/spacebars.html) into Blaze-compatible JavaScript code. You can also add `blaze-html-templates` to any of your packages to compile template files located in the package. + +[Read about how to use Blaze and Spacebars in the Blaze documentation.](http://blazejs.org/guide/spacebars.html) + +### Blaze Jade templates + +If you don't like the Spacebars syntax Meteor uses by default and want something more concise, you can give Jade a try by using [`pacreach:jade`](https://atmospherejs.com/pacreach/jade). This package will compile all files in your app with the `.jade` extension into Blaze-compatible code, and can be used side-by-side with `blaze-html-templates` if you want to have some of your code in Spacebars and some in Jade. + +### JSX for React + +If you're building your app's UI with React, currently the most popular way to write your UI components involves JSX, an extension to JavaScript that allows you to type HTML tags that are converted to React DOM elements. JSX code is handled automatically by the `ecmascript` package. + +## CSS processing + +> **Using Rspack (3.4+)?** When using the Rspack bundler, CSS is handled by Rspack's built-in loaders instead of Meteor's Atmosphere build plugins. This gives you standard bundler conventions, proper CSS HMR, and access to tools like Tailwind and PostCSS without extra Meteor packages. See the [Rspack CSS guide](/about/modern-build-stack/rspack-bundler-integration#css) for details. + +All your CSS style files will be processed using Meteor's default file load order rules along with any import statements and concatenated into a single stylesheet, `merged-stylesheets.css`. In a production build this file is also minified. By default this single stylesheet is injected at the beginning of the HTML `` section of your application. + +However, this can potentially be an issue for some applications that use a third party UI framework, such as Bootstrap, which is loaded from a CDN. This could cause Bootstrap's CSS to come after your CSS and override your user-defined styles. + +To get around this problem Meteor supports the use of a pseudo tag `` that if placed anywhere in the `` section your app will be replaced by a link to this concatenated CSS file. If this pseudo tag isn't used, the CSS file will be placed at the beginning of the `` section as before. + +### CSS pre-processors + +It's no secret that writing plain CSS can often be a hassle as there's no way to share common CSS code between different selectors or have a consistent color scheme between different elements. CSS compilers, or pre-processors, solve these issues by adding extra features on top of the CSS language like variables, mixins, math, and more, and in some cases also significantly change the syntax of CSS to be easier to read and write. + +Here are three example CSS pre-processors supported by Meteor: + +1. [Sass](http://sass-lang.com/) +2. [Less.js](http://lesscss.org/) +3. [Stylus](https://learnboost.github.io/stylus/) + +They all have their pros and cons, and different people have different preferences. Sass with the SCSS syntax is quite popular as CSS frameworks like Bootstrap have switched to Sass, and the C++ LibSass implementation appears to be faster than some of the other compilers available. + +CSS framework compatibility should be a primary concern when picking a pre-processor, because a framework written with Less won't be compatible with one written in Sass. + +### Source vs. import files + +An important feature shared by all of the available CSS pre-processors is the ability to import files. This lets you split your CSS into smaller pieces, and provides a lot of the same benefits that you get from JavaScript modules: + +1. You can control the load order of files by encoding dependencies through imports, since the load order of CSS matters. +2. You can create reusable CSS "modules" that only have variables and mixins and don't actually generate any CSS. + +In Meteor, each of your `.scss`, `.less`, or `.styl` source files will be one of two types: "source" or "import". + +A "source" file is evaluated eagerly and adds its compiled form to the CSS of the app immediately. + +An "import" file is evaluated only if imported from some other file and can be used to share common mixins and variables between different CSS files in your app. + +Read the documentation for each package listed below to see how to indicate which files are source files vs. imports. + +### Importing styles + +In all three Meteor supported CSS pre-processors you can import other style files from both relative and absolute paths in your app and from both npm and Meteor Atmosphere packages. + +```less +@import '../stylesheets/colors.less'; // a relative path +@import '{}/imports/ui/stylesheets/button.less'; // absolute path with `{}` syntax +``` + +You can also import CSS from a JavaScript file if you have the `ecmascript` package installed: + +```js +import '../stylesheets/styles.css'; +``` + +> When importing CSS from a JavaScript file, that CSS is not bundled with the rest of the CSS processed with the Meteor build tool, but instead is put in your app's `` tag inside `` after the main concatenated CSS file. + +Importing styles from an Atmosphere package using the `{}` package name syntax: + +```less +@import '{my-package:pretty-buttons}/buttons/styles.import.less'; +``` + +> CSS files in an Atmosphere package are declared with `api.addFiles`, and therefore will be eagerly evaluated, and automatically bundled with all the other CSS in your app. + +Importing styles from an npm package using the `{}` syntax: + +```less +@import '{}/node_modules/npm-package-name/button.less'; +``` + +```js +import 'npm-package-name/stylesheets/styles.css'; +``` + +### Sass + +The best Sass build plugin for Meteor is [`leonardoventurini:scss`](https://atmospherejs.com/leonardoventurini/scss). An alternative to the previous recommended [`fourseven:scss`](https://atmospherejs.com/fourseven/scss) package. + +With Rspack (3.4+), you can replace the Atmosphere package with `sass-embedded` and `sass-loader` configured in your `rspack.config.js`. See the [Rspack CSS guide](/about/modern-build-stack/rspack-bundler-integration#css) for setup instructions. + +### Less + +Less is maintained as a [Meteor core package called `less`](/packages/less). + +With Rspack (3.4+), you can replace the Atmosphere package with `less` and `less-loader` configured in your `rspack.config.js`. See the [Rspack CSS guide](/about/modern-build-stack/rspack-bundler-integration#css) for setup instructions. + +### Stylus + +The best Stylus build plugin for Meteor is [coagmano:stylus](https://atmospherejs.com/coagmano/stylus). + +## PostCSS and Autoprefixer + +In addition to CSS pre-processors like Sass, Less, and Stylus, there is now an ecosystem of CSS post-processors. Regardless of which CSS pre-processor you use, a post-processor can give you additional benefits like cross-browser compatibility. + +The most popular CSS post-processor right now is [PostCSS](https://github.com/postcss/postcss), which supports a variety of plugins. [Autoprefixer](https://github.com/postcss/autoprefixer) is perhaps the most useful plugin, since it enables you to stop worrying about browser prefixes and compatibility and write standards-compliant CSS. No more copying 5 different statements every time you want a CSS gradient - you can write a standard gradient without any prefixes and Autoprefixer handles it for you. + +Meteor automatically runs PostCSS for you once you've configured it. Learn more about enabling it in the docs for [standard-minifier-css](/packages/standard-minifier-css). + +## Hot Module Replacement + +In Meteor apps, JavaScript, TypeScript, CSS files that are dynamically imported, and many other types of files are converted into JavaScript modules during the build process. Instead of reloading the client after a rebuild, Meteor is able to update the JavaScript modules within the running application that were modified. This reduces the feedback cycle while developing by allowing you to view and test your changes quicker. + +### Meteor HMR + +Hot module replacement (HMR) can be enabled by adding the [hot-module-replacement](/packages/hot-module-replacement) package to your app: + +```bash +meteor add hot-module-replacement +``` + +Many types of JavaScript modules cannot be updated with HMR, so HMR has to be configured to know which modules can be replaced and how to replace them. Most apps never need to do this manually. Instead, you can use integrations that configure HMR for you: + +- React components are automatically updated using [React Fast Refresh](https://atmospherejs.com/meteor/react-fast-refresh). This integration is enabled for all Meteor apps that use HMR and a supported React version. +- Svelte files can be automatically updated with HMR by using the [zodern:melte](https://atmospherejs.com/zodern/melte) compiler package. +- Vue components can be updated with HMR using [akryum:vue-component](https://atmospherejs.com/akryum/vue-component). +- Some packages are able to help automatically dispose old versions of modules. For example, [zodern:pure-admin](https://atmospherejs.com/zodern/pure-admin) removes menu items and pages added in the old version of the module. + +To further control how HMR applies updates in your app, you can use the [hot API](/packages/hot-module-replacement). This can be used to accept updates for additional types of files, help dispose a module so the old version no longer affects the app (such as stopping Tracker.autorun computations), or creating your own integrations with other view layers or libraries. + +If a change was made to the app that cannot be applied with HMR, it reloads the page with hot code push, as is done when HMR is not enabled. It currently only supports app code in the modern client architecture. + +### Rspack HMR (3.4+) + +When using the [Rspack integration](/about/modern-build-stack/rspack-bundler-integration), you get Rspack's native HMR, which is significantly faster than Meteor's traditional HMR: + +- **Faster Updates**: Changes are reflected almost instantly +- **Persistent State**: Better preservation of application state during updates +- **Framework Integration**: Automatic support for React Fast Refresh, Vue HMR, Svelte HMR, and more +- **CSS HMR**: Style changes apply without full page reload (unlike traditional Cordova apps) + +**Note**: Blaze HMR is not currently supported with Rspack. Blaze apps will still reload quickly due to Rspack's fast rebuild times (97% reduction), but will perform a full page reload instead of hot module replacement. + +To use Rspack HMR, simply add the `rspack` package - HMR is enabled automatically for supported frameworks. + +## Build plugins + +The most powerful feature of Meteor's build system is the ability to define custom build plugins. If you find yourself writing scripts that mangle one type of file into another, merge multiple files, or something else, it's likely that these scripts would be better implemented as a build plugin. The `ecmascript`, `templating`, and `coffeescript` packages are all implemented as build plugins, so you can replace them with your own versions if you want to! + +[Read the documentation about build plugins.](/api/package#build-plugin-api) + +### Types of build plugins + +There are three types of build plugins supported by Meteor today: + +1. **Compiler plugin** - compiles source files (LESS, CoffeeScript) into built output (JS, CSS, asset files, and HTML). Only one compiler plugin can handle a single file extension. +2. **Minifier plugin** - compiles lots of built CSS or JS files into one or more minified files, for example `standard-minifiers`. Only one minifier can handle each of `js` and `css`. +3. **Linter plugin** - processes any number of files, and can print lint errors. Multiple linters can process the same files. + +### Writing your own build plugin + +Writing a build plugin is a very advanced task that only the most advanced Meteor users should get into. The best place to start is to copy a different plugin that is the most similar to what you are trying to do. For example, if you wanted to make a new CSS compiler plugin, you could fork the `less` package; if you wanted to make your own JS transpiler, you could fork `ecmascript`. A good example of a linter is the `jshint` package, and for a minifier you can look at `standard-minifiers-js` and `standard-minifiers-css`. + +### Caching + +The best way to make your build plugin fast is to use caching anywhere you can - the best way to save time is to do less work! Check out the [documentation about CachingCompiler](/api/package#caching-compiler) to learn more. It's used in all of the above examples, so you can see how to use it by looking at them. diff --git a/v3-docs/docs/about/cordova.md b/v3-docs/docs/about/cordova.md index 362ba86f26..621898dce7 100644 --- a/v3-docs/docs/about/cordova.md +++ b/v3-docs/docs/about/cordova.md @@ -1,3 +1,7 @@ +--- +outline: + level: [2, 3] +--- # Cordova @@ -5,7 +9,7 @@ Meteor allows developers to build mobile applications using web technologies lik Cordova apps run in a web view, which is like a browser without the UI. Different browser engines have varying implementations and support for web standards. This means the web view your app uses can greatly affect its performance and available features. (For details on supported features across browsers and versions, check caniuse.com.) -There is a [Meteor Cordova guide](https://guide.meteor.com/cordova) available that offers advanced configuration details for Meteor Cordova projects. Feel free to refer to it while we update the information in the new documentation. +There is a [Meteor Cordova guide](/about/cordova) available that offers advanced configuration details for Meteor Cordova projects. Feel free to refer to it while we update the information in the new documentation. This section will summarize the steps needed to set up your environment for Meteor Cordova development, manage development, and generate native artifacts for store uploads. @@ -202,6 +206,20 @@ meteor run ios-device You can manage connected devices in Android Studio and Xcode. +### Run HCP + +Hot Code Push (HCP) lets the client automatically get the latest version when code changes are detected. This improves development with live reloads and ensures production apps receive updates without republishing to the stores. + +For development, enable HCP by starting the application server with the `--mobile-server` option. + +- On an emulator, + - Run: `meteor run android --mobile-server 10.0.2.2:3000` + +- On a real device, both the device and server must be on the same network + - Run: `meteor run android --mobile-server XXX.XXX.XXX.XXX`, replacing the IP with your local development address (e.g. 192.168.1.4). + +For production, HCP is enabled automatically when you provide the `--server` option to the [`meteor build` command](../cli/index.md#meteor-build-meteorbuild). For more details on how HCP works with apps already published to production, see [Hot Code Push on mobile](/troubleshooting/hot-code-push). + ### Open IDE Once you have set up your Meteor project with Cordova, you may want to run or debug your mobile app using **Android Studio** or **XCode** directly. This can be useful for advanced debugging, custom configurations, or accessing specific platform tools @@ -262,17 +280,17 @@ After building your Cordova project with Meteor, you can use **Android Studio** 6. In the **Organizer** window, click **Distribute App** and follow the prompts to configure signing and export the IPA file. 7. Upload the IPA file to the App Store or distribute via TestFlight. -# Legacy device support +## Legacy device support Meteor distinguishes between legacy and modern browsers - see the [modern browsers package](../packages/modern-browsers). Web apps include different code bundles for each, but Cordova apps only have a single code bundle. From Meteor 3.3.2 onwards, the default code bundle changed from legacy to modern. -You can force Meteor to use the legacy browser code bundle by setting the variable `cordova.disableModern` to `true` in `package.json` when running or building your app. For example: +You can force Meteor to use the legacy browser code bundle by setting the variable `modern.cordova` to `false` in `package.json` when running or building your app. For example: ``` "meteor": { "mainModule": { ... }, "testModule": { ... }, - "cordova": { "disableModern": true} + "modern": { "cordova": false } } ``` diff --git a/v3-docs/docs/about/install.md b/v3-docs/docs/about/install.md index e19d252106..c0170b5d2c 100644 --- a/v3-docs/docs/about/install.md +++ b/v3-docs/docs/about/install.md @@ -31,7 +31,7 @@ And it will prompt you to choose a project name and frontend framework. ## Installation -Install the latest official version of Meteor.js from your terminal by running one of the commands below. You can check our [changelog](https://v3-docs.meteor.com/history.html) for the release notes. +Install the latest official version of Meteor.js from your terminal by running one of the commands below. You can check our [changelog](/history) for the release notes. For Windows, Linux and OS X, you can run the following command: diff --git a/v3-docs/docs/about/modern-build-stack.md b/v3-docs/docs/about/modern-build-stack.md index 9a3c57d311..519c64805d 100644 --- a/v3-docs/docs/about/modern-build-stack.md +++ b/v3-docs/docs/about/modern-build-stack.md @@ -1,31 +1,74 @@ +--- +outline: + level: [2, 3] +--- + # Modern Build Stack -The Meteor bundler is made up of several key components that enhance your experience both during development and when deploying to production. These include: +**Meteor’s modern build stack** delivers better speed and productivity, plus new features and plugins that follow current bundler standards. -- **Transpiler**: Responsible for converting each file into a syntax compatible across different browsers and runtime environments. -- **Bundler**: Handles discovering your app’s files and dependencies, including Meteor packages and core modules, then links them into production-ready bundles. It also applies optimizations to produce lighter builds and faster processes. -- **Dev Server**: During development, it watches for file changes, and supports fast feedback via HMR, bundle visualizers, debug tools, and more. At runtime, it provides a full-featured server environment with support for SSR and modern APIs powered by Express. +**Two major overhauls** make this possible: -To improve the development and deployment experience for all Meteor projects, we’re revamping each of these components with a focus on better performance, smarter tooling, and leaner bundle sizes: +1. **Meteor Bundler Optimizations.** Meteor builds the final bundle with Atmosphere packages built in and Meteor specifics. We reviewed key components to get the most performance gains and speed up builds and the dev experience, mainly with SWC integration. -- **Modern Transpiler**: Meteor is adopting **SWC** as a faster alternative to Babel. -- **Modern Bundler**: A new bundler will handle only your app’s code, supporting tree-shaking, popular plugins, and better features for both development and production. Meanwhile, Meteor’s core bundler will continue handling Meteor-specific tasks, such as compiling Atmosphere packages, with optimized workflows. -- **Modern Dev Server**: The dev server remains a core part of Meteor, now with ongoing improvements in performance and developer features. A new bundler will complement the dev server, providing additional enhancements. +2. **Rspack Bundler Integration.** Rspack bundles your app code only. Meteor Bundler then builds the final bundle, preserving compatibility with Meteor specifics like Atmosphere packages. We reviewed Meteor Bundler to ensure it delegates app code to Rspack, unlocking more speed, tree shaking, full ESM support, and compatibility with standard plugins and modern project configurations. ## Quick start -Start using the new build stack by creating a Meteor app, or add this to your `package.json` in an existing one: +**New Meteor apps enable the modern build stack by default**, with both **Meteor Bundler optimizations** and **Rspack Bundler integration**. -```json +For existing apps, you can enable one or both. If your app relies heavily on Meteor specifics like nested imports, use Meteor Bundler optimizations. If it follows a standard syntax without nested imports, you can also enable Rspack Bundler integration for greater benefits. + +### Meteor Bundler Optimizations + +:::info +Starting with Meteor 3.3 +::: + +Add this to your app’s `package.json`: + +``` json "meteor": { "modern": true } ``` -With this configuration, you enable all improvements from the modern build stack in your Meteor app. +This enables all Meteor bundler optimizations, with SWC adoption as the main highlight. -See the following sections to learn about each component and its settings: +> See the [**"Meteor Bundler Optimizations"** section](./modern-build-stack/meteor-bundler-optimizations.md) for migration requirements and config customization. -- [Transpiler](modern-build-stack/transpiler-swc.md) -- [Bundler](modern-build-stack/bundler.md) -- [Dev Server](modern-build-stack/dev-server.md) +### Rspack Bundler Integration + +:::info +Starting with Meteor 3.4 +::: + +Add this Atmosphere package to your app: + +``` bash +meteor add rspack +``` + +On first run, the package installs the required Rspack setup at the project level. It compiles your app code with Rspack to get the full benefit of this integration. + +> See the [**"Rspack Bundler Integration"** section](./modern-build-stack/rspack-bundler-integration.md) for migration requirements and config customization. + +## Learn more + +📄 [Build System guide](/about/build-tool) — In-depth guide covering Meteor's build tool, JavaScript transpilation, CSS processing, HMR, and build plugins. + +📹 [Modern Build Stack in Meteor 3: Empower Your Meteor Apps with Faster, Feature-Rich Bundling](https://www.youtube.com/watch?v=LqU1eDbnG4I) + +Presented by [@nachocodoner](https://github.com/nachocodoner) at [Meteor Impact](https://impact.meteorjs.community/), this video covers the motivation behind this new era of Meteor bundler optimizations and modernization with Rspack integration, what initially drove us there, what we have achieved, and how you can adopt it today. It was recorded while Meteor 3.4 was in beta, but the content is still accurate. The only difference is that you can now upgrade directly to the official Meteor 3.4 release. + +📄 [Unlocking Meteor 3.2: New Profiling Tool to Track Bundler Performance and Size](https://dev.to/meteor/unlocking-meteor-32-new-profiling-tool-to-track-bundler-performance-and-size-1jc8) + +Release article introducing the profiling tool that laid the groundwork for measuring and improving bundler performance. + +📄 [Faster Builds in Meteor 3.3: Modern Build Stack with SWC and Bundler Optimizations](https://dev.to/meteor/faster-builds-in-meteor-33-modern-build-stack-with-swc-and-bundler-optimizations-fm2) + +Release article covering the SWC transpiler adoption and the bundler optimizations shipped in Meteor 3.3. + +📄 [Meteor 3.4 is out: Rspack integration, 4x faster builds, 8x smaller bundles, and extended bundler features](https://blog.galaxycloud.app/meteor-3-4-is-out-rspack-integration-4x-faster-builds-8x-smaller-bundles-and-extended-bundler-features) + +Release article covering the Rspack integration and the new features in Meteor 3.4. diff --git a/v3-docs/docs/about/modern-build-stack/bundler.md b/v3-docs/docs/about/modern-build-stack/bundler.md deleted file mode 100644 index 7eb0d6d78f..0000000000 --- a/v3-docs/docs/about/modern-build-stack/bundler.md +++ /dev/null @@ -1,73 +0,0 @@ -# Bundler - -Meteor handles linking all project files into the final bundle. While we'd like to offload more of this to a modern bundler, we're still focused on keeping what's left in the Meteor context as fast as possible. - -Integration with a modern bundler is in progress for Meteor 3.4. Meanwhile, we've optimized existing processes for better performance. - -## Web Arch - -:::info -Starting with Meteor 3.3 -::: - -> Web archs are the builds Meteor generates for modern browsers, legacy browsers, and Cordova. - -New apps skip `web.browser.legacy` and `web.cordova` by default in development mode (unless developing for native). This results on getting a faster build process on development mode. - -For existing apps, enable this by adding to `package.json`: - -```json -"meteor": { - "modern": true -} -``` - -This works like using `--exclude-archs web.browser.legacy,web.cordova` with `meteor run`. - -By default, `"modern": true` enables all build stack upgrades. To opt out of web arch-only compilation, set `"webArchOnly": false` in your `package.json`. - -```json -"meteor": { - "modern": { - "webArchOnly": false - } -} -``` - -This setting doesn’t affect production; legacy builds are still included by default on Meteor apps. - -To exclude legacy builds in production, add "modern" to the `.meteor/platforms` file. - -```sh -server -browser -modern -``` - -## Minifier - -:::info -Starting with Meteor 3.3 -::: - -> The minifier reduces and obfuscates your app’s production bundle for security and efficiency. - -New apps use an SWC-based minifier, replacing the legacy [Terser](https://github.com/terser/terser) minifier. This speeds up production builds and deployments. - -For existing apps, enable this by adding to `package.json`: - -```json -"meteor": { - "modern": true -} -``` - -By default, `"modern": true` enables all build stack upgrades. To opt out of the new minifier, set `"minifier": false` in your `package.json`. - -```json -"meteor": { - "modern": { - "minifier": false - } -} -``` diff --git a/v3-docs/docs/about/modern-build-stack/dev-server.md b/v3-docs/docs/about/modern-build-stack/dev-server.md deleted file mode 100644 index 0bf9553330..0000000000 --- a/v3-docs/docs/about/modern-build-stack/dev-server.md +++ /dev/null @@ -1,47 +0,0 @@ -# Dev Server - -Meteor Dev Server provides real-time file watching during development, with fast feedback through HMR, bundle visualizers, debug tools, Mongo built-in instance, CLI tools and more. At runtime, it offers a complete server environment supporting SSR and modern Express-based APIs. - -As part of the modern build stack, we update each component for improved performance, smarter tooling, and better bundle observability and debugging. While Meteor Dev Server remains at the core, we’ll integrate modern tools and a new bundler to enhance your app development. - -## Watcher - -:::info -Starting with Meteor 3.3 -::: - -> The watcher listens for changes in your app’s code files and triggers quick recompilations. - -New apps use a modern, cross-platform watcher: [`@parcel/watcher`](https://github.com/parcel-bundler/watcher). It responds quickly to file changes using native file watching. Symbolic link changes and all traversed files are supported via polling. - -For existing apps, enable this by adding to `package.json`: - -```json -"meteor": { - "modern": true -} -``` - -If you run into issues with the new watcher, you can revert to the previous implementation for better file change detection. To disable the new watcher, set `"watcher": false` in your package.json. - -```json -"meteor": { - "modern": { - "watcher": false - } -} -``` - -The modern watcher uses the OS's native file watching with a performance-first approach. Both modern and legacy watchers support environment variables for polling, useful in edge cases like WSL with host, volumes, or remote setups. - -To enable polling, run your Meteor app with: - -```shell -# enable polling -METEOR_WATCH_FORCE_POLLING=true meteor run - -# set polling interval (in ms) -METEOR_WATCH_POLLING_INTERVAL_MS=1000 METEOR_WATCH_FORCE_POLLING=true meteor run -``` - -> Polling uses more CPU and RAM, but it's the most reliable option in some environments. diff --git a/v3-docs/docs/about/modern-build-stack/transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md similarity index 57% rename from v3-docs/docs/about/modern-build-stack/transpiler-swc.md rename to v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md index c0a9153411..6c3d84cb53 100644 --- a/v3-docs/docs/about/modern-build-stack/transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md @@ -1,16 +1,61 @@ -# Transpiler: SWC +--- +outline: + level: [2, 4] +--- + +# Meteor Bundler Optimizations + +The Meteor bundler is made up of key components that enhance your experience in development and production. We worked on each to bring all possible optimizations. As a result: + +- [**Transpiler**](#transpiler-swc): Meteor is adopting **SWC** as a faster alternative to Babel. +- [**Minifier**](#minifier): Meteor uses the **SWC** minifier as a faster alternative to Terser. +- [**Web archs**](#web-arch): Meteor now skips legacy architectures in development mode. +- [**Watcher**](#watcher): Meteor now uses [`@parcel/watcher`](https://github.com/parcel-bundler/watcher) for a faster, more stable watch experience via native recursive file watching across all OS. +- [**.meteorignore**](#meteorignore): Exclude files and directories from the bundler to reduce build and watch overhead. + +## Quick start :::info Starting with Meteor 3.3 ::: +Add this to your app’s `package.json`: + +``` json +"meteor": { + "modern": true +} +``` + +This enables all Meteor bundler optimizations, with SWC adoption as the main highlight. + +## Requirements + +**Meteor Bundler optimizations are backward compatible**, meaning enabling modern will not introduce breaking changes to your app’s development experience. + +These optimizations apply modern behavior by default and **fall back** to the previous approach if any error is encountered. For example, **SWC** compiles each file in your project, but if a syntax error appears due to an SWC incompatibility like nested imports or a missing plugin, it falls back to the **legacy Babel** transpiler configuration for Meteor and your project. + +To encourage a standard structure and syntax, the next section guide you on getting the most performance from the SWC transpiler and on migrating specific plugins from legacy Babel to make your project SWC compliant. + +> [**Transpiler (SWC): Optimize SWC and Handle Fallbacks**](#optimize-swc-and-handle-fallbacks) + +You can also learn more about each part of the optimized components: + +- [**Transpiler**](#transpiler-swc) +- [**Minifier**](#minifier) +- [**Web archs**](#web-arch) +- [**Watcher**](#watcher) +- [**.meteorignore**](#meteorignore) + +--- + +## Transpiler (SWC) + > The transpiler converts modern JS syntax in all app code to a cross-browser compatible version. Meteor has long used Babel, a mature and still widely adopted transpiler. However, it lags behind newer tools like SWC in terms of speed. SWC and others are not only faster but are growing in use and features, reaching parity with Babel. -Since transpilation is one of the slowest steps in development, Meteor now gives you the option to use SWC for your apps. - -## Enable SWC +Since transpilation is one of the slowest steps in development, **Meteor now gives you the option to use SWC** for your apps. Add this to your app's `package.json`: @@ -32,7 +77,7 @@ By default, `"modern": true` enables all build stack upgrades. To opt out of SWC } ``` -## Verbose transpilation process +### Optimize SWC and Handle Fallbacks To analyze and improve transpilation, you can enable verbose output. Add this to `package.json`: @@ -48,8 +93,6 @@ To analyze and improve transpilation, you can enable verbose output. Add this to This shows each file being processed, its context, cache usage, and whether it fell back to Babel due to incompatibilities. -## Adapt your code to benefit from SWC - If all your code uses SWC, you're good and can turn off verbosity. But if you [see logs like](https://forums.meteor.com/uploads/default/original/3X/e/1/e1a2c285284f82ab736bcada647d88bd4fa8d3ec.png): ``` shell @@ -59,11 +102,13 @@ If all your code uses SWC, you're good and can turn off verbosity. But if you [s This means SWC encountered syntax incompatibilities on the files. There are a few things you can do. -First, check the fallback details to **fix the syntax**. They might explain why SWC failed. +First, check the fallback details to **fix the syntax** so it’s compatible with SWC. This will maximize performance benefits and help you understand why SWC failed. - A common cause is [**nested import statements** inside functions](#nested-imports). Move them to the top level. These work in Babel due to a Meteor-specific plugin, which SWC doesn’t support. - Other issues may come from features tied to Babel plugins. You’ll need to find SWC equivalents. See the [SWC plugin list](https://plugins.swc.rs/versions/range/271). +> Check [**Migration Topics**](#migration-topics) to learn common scenarios for migrating your code and setting up your SWC config. + Second, **ignore the fallback** if those files run fine with Babel. SWC will still speed up other files. Meteor will keep using Babel for incompatible files on future builds. Third, **exclude files or contexts from SWC**. Even though it falls back automatically, you can skip the overhead of trying SWC on known-incompatible files. @@ -100,7 +145,7 @@ Most apps will benefit just by enabling `modern: true`. Most Meteor packages sho > Remember to turn off verbosity when you're done with optimizations. -## Externalize SWC Helpers +### Externalize SWC Helpers By default, SWC inlines transformation helpers (e.g. _extends, _objectSpread) into every file that uses them. While this ensures compatibility out of the box, it can lead to duplicated code across your bundles increasing bundle size. @@ -114,13 +159,15 @@ meteor npm install --save @swc/helpers Meteor’s build tool comes pre-configured to externalize SWC helpers for you, no extra setup or .swcrc tweaks are needed. As soon as you install @swc/helpers, Meteor’s SWC pipeline will automatically emit imports for shared helper functions rather than inlining them, ensuring your app ships each helper just once. -## Custom .swcrc +### Custom .swcrc You can use `.swcrc` config in the root of your project to describe specific [SWC plugins](https://github.com/swc-project/plugins) there, that will be applied to compile the entire files of your project. You can also configure other options using the `.swcrc` format. For custom SWC configs, see the [SWC configuration API](https://swc.rs/docs/configuration/compilation). -Use `swc.config.js` in your project root for dynamic configuration. Meteor will import and apply the SWC config automatically. This lets you choose a config based on environment variables or other runtime factors. +Use `swc.config.js` in your project root for dynamic configuration. Meteor will import and apply the SWC config automatically. This lets you choose a config based on environment variables or other runtime factors. If you prefer TypeScript, `swc.config.ts` is also supported, Meteor will transpile and load it automatically. + +Meteor checks for config files in this order: `.swcrc` > `swc.config.js` > `swc.config.ts`. Only the first one found is used. You can also review these migration topics that use custom `.swcrc` configs: @@ -136,7 +183,7 @@ The standard name for the SWC configuration file is [`.swcrc`](https://swc.rs/do Using as an extension, such as `config.swcrc`, won’t work. ::: -## Config API +### Config API - `modern.transpiler: [true|false]` - Default: `true` Enables or disables the use of the modern transpiler (SWC). If disabled, Babel will be used directly instead. @@ -159,11 +206,15 @@ Using as an extension, such as `config.swcrc`, won’t work. - `modern.transpiler.verbose: [true|false]` If true, the transpilation process for files is shown when running the app. This helps understand which transpiler is used for each file, what fallbacks are applied, and gives a chance to either exclude files to always use Babel or migrate fully to SWC. -## Migration Topics +### Migration Topics -### Nested Imports +#### Nested Imports -Nested imports are a Meteor-specific feature in its bundler, unlike standard bundlers. Meteor introduced them during a time when bundling standards were still evolving and experimented with its own approach. This feature comes from the [`reify` module](https://github.com/benjamn/reify/tree/main) and works with Babel transpilation. SWC doesn't support them since they were never standardized. +Nested imports are a feature of Meteor’s bundler, not supported in standard bundlers. Meteor introduced them during a time when bundling standards were still evolving and experimented with its own approach. This feature comes from the [`reify` module](https://github.com/benjamn/reify/tree/main) and works with Babel transpilation. SWC doesn't support them since they were never standardized. + +:::warning +Don't confuse nested imports with standardized dynamic imports using `import()` in module blocks, these are supported. +::: Example with a nested import: @@ -186,9 +237,9 @@ if (condition) { For background, see: [Why nested import](https://github.com/benjamn/reify/blob/main/WHY_NEST_IMPORTS.md). -With `"modern.transpiler": true`, if SWC finds one, it silently falls back to Babel (only shows in `"verbose": true`). Nested imports isn’t standard, most modern projects use other deferred loading methods. You might want to move imports to the top or use require instead, letting SWC handle the file and speeding up builds. Still, this decision is up to the devs, some Meteor devs use them for valid reasons. +With `"modern.transpiler": true`, if SWC finds one, it silently falls back to Babel (only shows in `"verbose": true`). Nested imports isn’t standard, most modern projects use other deferred loading methods. Move imports to the top, or use require or dynamic imports. Let SWC handle the file and speeding up builds. Still, this decision is up to the devs, some Meteor devs use them for valid reasons. -### Import Aliases +#### Import Aliases Meteor Babel lets you define aliases for import paths with [babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver). @@ -263,7 +314,7 @@ SWC resolve aliases for imports correctly, but require calls won’t. For requir SWC has no [module-resolver plugin like Babel’s](https://www.npmjs.com/package/babel-plugin-module-resolver) yet, which could affect require calls in the future. -### JSX Syntax in JS files +#### JSX Syntax in JS files When migrating your app to use SWC, Meteor SWC falls back to Babel if you include JSX in `.js` files, since JSX is only recognized in `.jsx` files. @@ -284,7 +335,7 @@ To enable JSX in `.js` files, create a [`.swcrc`](#custom-swcrc) file with this This overrides Meteor’s internal SWC config so SWC handles `.js` and `.ts` files with React components instead of falling back to Babel. -### React Runtime +#### React Runtime Meteor Babel lets you skip importing React in your files by using the [`@babel/plugin-transform-react-jsx`](https://www.npmjs.com/package/@babel/plugin-transform-react-jsx) runtime config. @@ -302,13 +353,13 @@ To use the same config in SWC, add it to your [`.swcrc`](#custom-swcrc): } ``` -### Transform Imports +#### Transform Imports You might have used Meteor Babel with the [`babel-plugin-transform-imports`](https://www.npmjs.com/package/babel-plugin-transform-imports) plugin to rewrite imports in your app. SWC offers a similar plugin: [`@swc/plugin-transform-imports`](https://www.npmjs.com/package/@swc/plugin-transform-imports). -To switch to SWC, install the plugin: +To switch to SWC, install the plugin: ```bash meteor npm install -D @swc/plugin-transform-imports @@ -353,7 +404,7 @@ avoiding full-package imports and reducing bundle size. You can use advanced import transformations. [See the test suite for examples.](https://github.com/swc-project/plugins/blob/main/packages/transform-imports/__tests__/wasm.test.ts#L12-L63) -### Private Properties +#### Private Properties SWC supports many of the most modern JS systax features, including private class properties, which Meteor Babel doesn’t. @@ -375,7 +426,156 @@ class ClassWithPrivate { You can opt-out of [private properties in SWC options with "privateMethod" setting](https://swc.rs/docs/configuration/compilation#ecmascript) with the [`.swcrc`](#custom-swcrc) file. -## Troubleshotting +## Minifier + +> The minifier reduces and obfuscates your app’s production bundle for security and efficiency. + +New apps use an **SWC-based minifier**, replacing the legacy [Terser](https://github.com/terser/terser) minifier. This speeds up production builds and deployments. + +For existing apps, enable this by adding to `package.json`: + +```json +"meteor": { + "modern": true +} +``` + +By default, `"modern": true` enables all build stack upgrades. To opt out of the new minifier, set `"minifier": false` in your `package.json`. + +```json +"meteor": { + "modern": { + "minifier": false + } +} +``` + +## Web Arch + +> Web archs are the builds Meteor generates for modern browsers, legacy browsers, and Cordova. + +New apps **skip `web.browser.legacy` and `web.cordova` by default in development mode** (unless developing for native). This results on getting a faster build process on development mode. + +For existing apps, enable this by adding to `package.json`: + +```json +"meteor": { + "modern": true +} +``` + +This works like using `--exclude-archs web.browser.legacy,web.cordova` with `meteor run`. + +By default, `"modern": true` enables all build stack upgrades. To opt out of web arch-only compilation, set `"webArchOnly": false` in your `package.json`. + +```json +"meteor": { + "modern": { + "webArchOnly": false + } +} +``` + +This setting doesn’t affect production; legacy builds are still included by default on Meteor apps. + +To exclude legacy builds in production, add "modern" to the `.meteor/platforms` file. + +```sh +server +browser +modern +``` + +## Watcher + +> The watcher listens for changes in your app’s code files and triggers quick recompilations. + +New apps use a modern, **cross-platform watcher: [`@parcel/watcher`](https://github.com/parcel-bundler/watcher)**. It responds quickly to file changes using native file watching. Symbolic link changes and all traversed files are supported via polling. + +For existing apps, enable this by adding to `package.json`: + +```json +"meteor": { + "modern": true +} +``` + +If you run into issues with the new watcher, you can revert to the previous implementation for better file change detection. To disable the new watcher, set `"watcher": false` in your package.json. + +```json +"meteor": { + "modern": { + "watcher": false + } +} +``` + +The modern watcher uses the OS's native file watching with a performance-first approach. Both modern and legacy watchers support environment variables for polling, useful in edge cases like WSL with host, volumes, or remote setups. + +To enable polling, run your Meteor app with: + +```shell +# enable polling +METEOR_WATCH_FORCE_POLLING=true meteor run + +# set polling interval (in ms) +METEOR_WATCH_POLLING_INTERVAL_MS=1000 METEOR_WATCH_FORCE_POLLING=true meteor run +``` + +> Polling uses more CPU and RAM, but it's the most reliable option in some environments. + +--- + +## .meteorignore + +> The `.meteorignore` file tells Meteor's bundler to skip specific files and directories, reducing unnecessary work during builds. + +Meteor's bundler processes every file in your project tree by default. In projects with folders that are not part of the app, such as documentation, design assets, or standalone tooling, the bundler still traverses and watches those directories. This adds overhead to both initial builds and file-change recompilations. + +You can place a `.meteorignore` file in any directory of your app or package. It uses the same pattern syntax as `.gitignore` and applies rules to the directory tree below it. Meteor's file watching system is fully integrated with `.meteorignore`, so you can add, remove, or modify these files during development and the changes take effect immediately. + +### Example .meteorignore + +Place this file at the root of your project as `.meteorignore`: + +```gitignore +# Documentation +docs/ + +# Design and static assets not used by the app +design/ +mockups/ +screenshots/ + +# CI/CD and infrastructure +docker/ + +# Testing tools outside Meteor +cypress/ +playwright/ + +# Scripts and tooling unrelated to the app +scripts/ +tools/ +benchmarks/ + +# Misc +LICENSE +CHANGELOG.md +CONTRIBUTING.md +*.md +!README.md +``` + +This keeps the bundler focused on your actual app code and Meteor packages, skipping folders and files that have no role in the build. The result is faster rebuilds during development, especially in larger projects with significant non-app content. + +You can also place `.meteorignore` files in subdirectories for more granular control. For example, a `.meteorignore` inside `packages/my-package/` applies only to that package's tree. + +For a more dynamic approach, you can use the [`METEOR_IGNORE`](../../cli/environment-variables.md#meteor-ignore) environment variable to define ignore patterns per command without modifying project files. This is especially useful when you need different ignore rules for `meteor run` and `meteor test`. + +--- + +## Troubleshooting If you run into issues, try `meteor reset` or delete the `.meteor/local` folder in the project root. @@ -383,4 +583,4 @@ For help or to report issues, post on [GitHub](https://github.com/meteor/meteor/ You can compare performance before and after enabling `modern` by running [`meteor profile`](../../cli/index.md#meteorprofile). Share your results to show progress to others. -> **[Check out modern bundler options](bundler.md) to improve performance and access newer build features.** +> **[Check out Rspack Bundler integration](rspack-bundler-integration.md) to improve performance and access newer build features.** diff --git a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md new file mode 100644 index 0000000000..b376088bb5 --- /dev/null +++ b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md @@ -0,0 +1,1004 @@ +--- +outline: + level: [2, 3] +--- + +# Rspack Bundler Integration + +Rspack integration updates Meteor apps to modern bundling standards, offering faster builds, quicker reloads, smaller bundles, and a smoother development experience with built-in features and configurations. + +In this setup, Rspack bundles your app code, while Meteor Bundler produces the final output, maintaining support for Meteor features like Atmosphere packages. + +## Quick start + +:::info +Starting with Meteor 3.4 +::: + +Add this Atmosphere package to your app: + +``` bash +meteor add rspack +``` + +On first run, the package installs the required Rspack setup at the project level. It compiles your app code with Rspack to get the full benefit of this integration. + +## Requirements + +### Define the app’s entry points + +Your app must define entry points for the Rspack integration to work. Entry points tell Rspack which files start execution on the client and server. + +In Meteor, set this in `package.json`: + +```json +{ + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + } + } +} +``` + +[Check out the Meteor migration guide](#entry-points) on describing entry points in your app. + +### Remove nested imports + +Your app code cannot use Meteor's specific nested imports (not to be confused with dynamic imports, which are supported). These are ES import statements placed inside conditions or functions. + +``` javascript +if (condition) { + import { a as b } from "./c"; // This is a nested import + console.log(b); +} +``` + +[Refer to the Meteor migration guide](#nested-imports) to ensure your app code has no nested imports. + +### Reserve a new build context + +A Meteor-Rspack project reserves the folders `_build`, `{public,private}/build-assets`, and `{public,private}/build-chunks` to store intermediate and production bundles. + +- `_build/*`. Contains the Rspack entry point, the intermediate Rspack app, and the overridden Meteor entry point that imports the Rspack app. It creates contexts for different environments, such as main or test modules, development and production, client and server. You can explore the contents to understand more. +- `build-assets/*`. Placed inside `public` (client) and `private` (server) folders. Stores assets built by Rspack that are packed into the final Meteor app. +- `build-chunks/*`. Placed inside `public` (client) folder. Stores chunks built by Rspack from code splitting (dynamic imports), CSS generation, and other chunk processing, to be packed into the final Meteor app. + +The folders are prepared and cleared automatically and should not be modified directly, as they are autogenerated. They are added to `.gitignore` when using Git, and it’s recommended to exclude them from IDE analysis. + +You don’t need to migrate your project for this, just ensure these folders are reserved for Meteor-Rspack. If you already use them for something else, move that content elsewhere. + +If you want to customize the folder names, set these options in `package.json`: + +``` json +{ + "meteor": { + "buildContext": "build", + "assetsContext": "assets", + "chunksContext": "chunks" + } +} +``` + +:::warning +Attempts were made to reuse the existing `.meteor/local` cache context instead of creating new build contexts, but this was not possible. + +Use `.meteor/local` or folders that suggest internals or hidden content (e.g., starting with a dot). These affect debug visibility, file watching, final compilation, and inclusion in the Cordova bundle. +::: + +### Replace build plugins + +Meteor build plugins extend the Meteor bundler by letting you handle new file types and process them for the final app bundle. They’ve commonly handled HTML templating, style files for Less or SCSS, CoffeeScript, and more, since the system allows third-party customization. + +However, Meteor’s build system solves the same problems as other bundlers, including Rspack. Build plugins are largely deprecated in favor of Rspack alternatives. Some plugins may still be useful if they don’t act directly on app files and do something Meteor-specific that can be preserved. + +Among the compatible plugins: +- [`zodern:types`](https://packosphere.com/zodern/types). Still compatible, automatically providing Meteor types for core and community packages. + +For others, please refer to the migration topics. +- [CSS, Less, and SCSS](#css-less-and-scss) (when using [`less`](https://packosphere.com/meteor/less), [`fourseven:scss`](https://packosphere.com/fourseven/scss)) +- [Coffeescript](#coffeescript) (when using [`coffeescript`](https://packosphere.com/meteor/coffeescript)) +- [Svelte](#svelte) (when using [`zodern:melte`](https://packosphere.com/zodern/melte)) + +You can still use these plugins to handle files inside Meteor atmosphere packages. You only need consider Rspack alternative when it’s required for your app code, which will usually be the case. An exception applies to HTML and CSS files: you can still use Meteor plugins touching these files if they are in the entry folder (e.g. `client/*.[html|css]` in most apps), or when using `modules` config as explained in the [Entry Points migration guide](#entry-points). + +Please report your plugin usage as [GitHub issues](https://github.com/meteor/meteor/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) or [forum posts](https://forums.meteor.com/), so we can suggest an Rspack alternative or assess compatibility. + +### Further migration + +Refer to the [Migration Topics](#migration-topics) section for more details on other specific requirements your app might have. + +## Custom `rspack.config.js` + +Meteor-Rspack projects can be customized using the `rspack.config.js` file, which is automatically available when installing the `rspack` package. You can also use `rspack.config.mjs` or `rspack.config.cjs` if you prefer strict ESM or CommonJS formats. + +This file defines dynamic configurations, so you return the config from a resolved function. + +```javascript +const { defineConfig } = require('@meteorjs/rspack'); +const { rspack } = require('@rspack/core'); +const HtmlRspackPlugin = require('html-rspack-plugin'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); + +/** + * Example: Using different plugins for client and server builds + * + * - For client: Load Lodash automatically with ProvidePlugin + * - For server: Add Node.js polyfills with NodePolyfillPlugin + * - For both: Add progress plugin + */ +module.exports = defineConfig(Meteor => { + return { + plugins: [ + Meteor.isClient && new rspack.ProvidePlugin({ _: 'lodash' }), + Meteor.isServer && new NodePolyfillPlugin(), + new rspack.ProgressPlugin() + ].filter(Boolean), + }; +}); +``` + +You can use flags to control the final configuration based on the environment. The available flags are passed in the `Meteor` parameter. + +| Flag | Type | Description | +|---------------------| -------- |-----------------------------------------------------------------------------------------------------------------------------------| +| `isDevelopment` | boolean | True when running in development mode | +| `isProduction` | boolean | True when running in production mode | +| `isClient` | boolean | True when building or running client code | +| `isServer` | boolean | True when building or running server code | +| `isTest` | boolean | True when running in test mode | +| `isDebug` | boolean | True when debug mode is enabled | +| `isRun` | boolean | True when running the project with `meteor run` | +| `isBuild` | boolean | True when building the project with `meteor build` | +| `swcConfigOptions` | object | Project-level SWC config available for reusing | +| `HtmlRspackPlugin` | function | Custom HtmlRspackPlugin function for extending the config | +| `compileWithMeteor` | function | Forces given npm deps ([Condition](https://rspack.rs/config/module#condition)[]) to be compiled by Meteor | +| `compileWithRspack` | function | Forces given npm deps ([Condition](https://rspack.rs/config/module#condition)[]) to be compiled by Rspack | +| `setCache` | function | Enables or disables cache. Accepts true (persistent, default), false, or 'memory' | +| `splitVendorChunk` | function | Splits vendor libraries so they are automatically served from a separate chunk | +| `extendSwcConfig` | function | Smart-merges custom options into Meteor's default [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options), applying only to app code | +| `replaceSwcConfig` | function | Replaces Meteor's default [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options) entirely with the provided options, applying only to app code | +| `extendConfig` | function | Extends the config by applying merged object configs | +| `enablePortableBuild` | function | Omits `Meteor.isDevelopment` and `Meteor.isProduction` from the bundle, making it portable across environments | + +Some configurations in the Rspack config are reserved for the Meteor-Rspack setup to work, such as Rspack options inside the `entry` and `output` objects. These will trigger warnings if modified. All other settings can be overridden, giving you the flexibility to make any setup compatible with the modern bundler. + +If you want to see the final Rspack config applying your overrides, you can enable [verbose mode](#enable-verbose-mode) in the modern build stack. + +## Logging + +Starting with Meteor 3.4.1, the log output for the default Meteor-Rspack app is simplified to stay as close as possible to Meteor's native experience. By default, logs are less verbose and only show essential information like server restarts and client modifications. + +If there are any compilation warnings or errors, Rspack logs will be shown with their own style and colors. + +### Enable Verbose Mode + +If you need more details about Meteor and Rspack processes, you can enable verbose mode in your `package.json`: + +```json +{ + "meteor": { + "modern": { + "verbose": true + } + } +} +``` + +### Advanced Rspack Logging + +For even deeper insights into the Rspack compilation process, you can configure [`stats`](https://rspack.rs/config/stats#stats) and [`infrastructureLogging`](https://rspack.rs/config/infrastructure-logging#infrastructurelogging) directly in your `rspack.config.js`. + +- **`stats`**: Controls what bundle information is displayed on each compilation. +- **`infrastructureLogging`**: Controls Rspack infrastructure logs, including HMR verbosity in both the terminal and the browser. To enable detailed logs for updates and serving client code changes, set `infrastructureLogging.level` to `'info'` or higher (it is not enabled by default). + +```javascript +module.exports = defineConfig(Meteor => { + return { + stats: 'detailed', // or other Rspack stats options + infrastructureLogging: { + level: 'info', + }, + // ... rest of your config + }; +}); +``` + +## Migration Topics + +### Entry Points + +Meteor entry points allow a modular, modern, bundler-compliant structure for your Meteor app. Modern bundlers define entry points where the evaluation and bootstrap of your app begin. In Meteor, you can set these for both the client and server, and optionally for tests. + +``` json +{ + "meteor": { + "mainModule": { + "client": "client/main.js", + "server": "server/main.js" + }, + "testModule": "tests.js" + } +} +``` + +Learn more in [“Modular application structure” in Meteor](/packages/modules#modular-application-structure). + +Ensure your app defines these entry files with the correct paths where each module is expected to load. Organize your app so the loading order of modules is clear. + +Defining entry points improves performance even with the Meteor bundler, as Meteor stops scanning and eagerly loading unnecessary files. For Meteor-Rspack integration, this is required, since it does not support automatic code discovery for efficiency. + +In Meteor-Rspack integration, all app code is ignored by Meteor and handled by Rspack. By default, Meteor still processes eagerly CSS and HTML files in the entry folder (e.g. `client/*.[html|css]` in most apps). + +If you need Meteor to handle CSS or HTML files outside the main entry folder, add them to the `modules` field. This field accepts an array of strings, each pointing to a file or folder. + +``` json +{ + "meteor": { + "modules": ["styles/main.css"] + } +} +``` + +With this, Meteor will process these files, merge stylesheets, generate the final HTML, and support files a Meteor plugin may use, except for JS or script code now handled by Rspack. You can also process CSS and HTML files directly with Rspack using loaders from imports in your app code, as mentioned in ["CSS, Less and SCSS"](#css-less-and-scss) or ["HtmlRspackPlugin"](#htmlrspackplugin). If you prefer Meteor's loading approach, you can still rely on it. + +Keep in mind: compiling styles with the Meteor compilers triggers Meteor HMR, which is slower than Rspack HMR. Migrating to compile styles with Rspack as part of the app code ensures the fastest HMR for style changes in development. + +### Nested Imports + +Nested imports are a feature of Meteor’s bundler, not supported in standard bundlers. Meteor introduced them during a time when bundling standards were still evolving and experimented with its own approach. This feature comes from the [`reify` module](https://github.com/benjamn/reify/tree/main) and works with Babel transpilation. SWC doesn't support them since they were never standardized. + +:::warning +Don't confuse nested imports with standardized dynamic imports using `import()` in module blocks, these are supported. +::: + +Example with a nested import: + +```javascript +// import { a as b } from "./c"; // root import +if (condition) { + import { a as b } from "./c"; // nested import + console.log(b); +} +``` + +For background, see: [Why nested import](https://github.com/benjamn/reify/blob/main/WHY_NEST_IMPORTS.md). + +To use Rspack, migrate your nested imports to a standard form. To identify and fix nested imports in your project, use [verbose mode](#enable-verbose-mode) to see which files are failing. + +When you run your app, `[Transpiler]` logs will show each file. Focus on `(app)` files that fail with messages like: + +`Error: 'import' and 'export' cannot be used outside of module code` + +![](https://forums.meteor.com/uploads/default/original/3X/e/1/e1a2c285284f82ab736bcada647d88bd4fa8d3ec.png) + +**Fix nested imports by moving them to the top of the file, or by replacing them with require or dynamic import.** + +You can skip migrating `(package)` code with nested imports. Meteor packages are still handled by the Meteor bundler in Rspack integration, but your app code is fully delegated to Rspack and must use standard syntax. + +Nested imports isn’t standard, most modern projects use other deferred loading methods. Let Rspack handle files to speed builds and enable modern features. The choice is up to the devs. Some Meteor devs use nested imports for valid reasons. You can opt out of Rspack and still get build speed gains from Meteor bundler optimizations. + +:::info +With Meteor–Rspack integration, you can still use nested imports if they are defined in Meteor Atmosphere packages. These will be accepted without any breaking changes. +::: + +### Import Aliasses + +An import alias is a shortcut that maps a custom name to a specific file path or directory, making imports shorter and easier to manage. + +With Meteor-Rspack integration you can define aliases using the `resolve.alias` configuration in your `rspack.config.js`. For example: + +``` javascript +export default defineConfig(Meteor => { + return { + resolve: { + alias: { + '@ui': '/imports/ui', + '@api': '/imports/api', + }, + }; +} +``` + +Learn more in the [Rspack alias docs](https://rspack.rs/config/resolve#resolvealias). + +If you use TypeScript, also update your `tsconfig.json` to support IDE autocompletion and ESLint resolution: + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@ui/*": ["imports/ui/*"], + "@api/*": ["imports/api/*"] + } + } +} +``` + +You can also [configure aliases at the transpiler level](meteor-bundler-optimizations.md#import-aliases). For SWC, enable it through the `.swcrc` file (note that SWC aliases have some limitations when resolving files or `node_modules`). If you use Babel, you can rely on the [module-resolver plugin](https://www.npmjs.com/package/babel-plugin-module-resolver). + +### React + +Meteor-Rspack supports React projects out of the box. Just install the `rspack` package and run your app. Meteor will detect it and automatically add the needed Rspack dependencies, including `react-refresh` for a full development experience. + +Learn more in the [official Rspack and React integration guide](https://rspack.rs/guide/tech/react). + +> Use `meteor create --react` to start with a preconfigured Rspack React app. + +### React Compiler + +Meteor-Rspack supports React Compiler. To enable it, install the required dependencies and add the new configuration to Meteor’s `rspack.config.js` file. + +Learn more in the [official Rspack and React Compiler integration guide](https://rspack.rs/guide/tech/react#react-compiler). + +### Vue + +Meteor-Rspack supports Vue projects out of the box. To enable it, install the required dependencies and add the new configuration to Meteor’s `rspack.config.js` file. + +Learn more in the [official Rspack and Vue integration guide](https://rspack.rs/guide/tech/vue). + +> Use `meteor create --vue` to start with a preconfigured Rspack Vue app. + +:::warning +Previous official support in the Meteor bundler was through [jorgenvatle:vite](https://github.com/JorgenVatle/meteor-vite). + +With Meteor-Rspack integration, you no longer need vite-related packages, so you should remove them from your project. +::: + +### Solid + +Meteor-Rspack supports Solid projects out of the box. To enable it, install the required dependencies and add the new configuration to Meteor’s `rspack.config.js` file. + +Learn more in the [official Rspack and Solid integration guide](https://rspack.rs/guide/tech/solid). + +> Use `meteor create --solid` to start with a preconfigured Rspack Solid app. + +:::warning +Previous official support in the Meteor bundler was through [jorgenvatle:vite](https://github.com/JorgenVatle/meteor-vite). + +With Meteor-Rspack integration, you no longer need vite-related packages, so you should remove them from your project. +::: + +### Coffeescript + +Meteor-Rspack supports CoffeeScript projects out of the box. To enable it, install the needed dependencies and add the configuration to Meteor’s rspack.config.js. + +[See the official Webpack and CoffeeScript integration guide](https://webpack.js.org/loaders/coffee-loader/#getting-started). Since Rspack is based on Webpack, the same setup applies. + +If you want to use SWC with CoffeeScript, combine `swc-loader` with `coffee-loader`. + +```bash +npm install --save-dev coffeescript swc-loader coffee-loader +``` + +In your `rspack.config.js` you would add something like: + +``` javascript +export default defineConfig(Meteor => { + return { + module: { + rules: [ + { + test: /\.coffee$/i, + use: [ + { + loader: 'swc-loader', + // perserve SWC config in the Meteor project level + options: Meteor.swcConfigOptions, + }, + { + loader: 'coffee-loader', + }, + ], + }, + ], + }, + resolve: { + extensions: ['.coffee'], + }, + }; +}); +``` + +> Use `meteor create --coffeescript` to start with a preconfigured Rspack Coffeescript app. + +### Svelte + +Meteor-Rspack supports Svelte projects out of the box. To enable it, install the required dependencies and add the new configuration to Meteor’s `rspack.config.js` file. + +Learn more in the [official Rspack and Svelte integration guide](https://rspack.rs/guide/tech/svelte). + +> Use `meteor create --svelte` to start with a preconfigured Rspack Svelte app. + +:::warning +Official Svelte support in the Meteor bundler was via [zodern:melte](https://github.com/zodern/melte). + +With the Meteor–Rspack integration, `zodern:melte` no longer works. Use the official Rspack Svelte integration instead. If you relied on melte-specific features like `$` or `$m`, you may need to update parts of your code. Create your own abstractions or migrate them to standard npm package. +::: + +### CSS + +Meteor-Rspack comes with built-in CSS support. You can import any CSS file into your code, and it will be processed and included in your HTML skeleton automatically. In addition, any CSS file placed in the same folder as your Meteor entry point will be processed and added as global styles without the need for explicit imports. + +### CSS Modules + +[CSS Modules](https://rspack.rs/guide/tech/css#css-modules) are supported out of the box — any file named `*.module.css` is automatically scoped locally. + +By default, rspack uses **named exports**, so imports look like: + +``` js +import { app } from './App.module.css'; +``` + +If you prefer **default imports** (`import styles from './App.module.css'`), disable `namedExports` on both the `css/auto` and `css/module` parsers: + +``` js +module.exports = defineConfig(Meteor => ({ + module: { + parser: { + 'css/auto': { + namedExports: false, + }, + 'css/module': { + namedExports: false, + }, + }, + }, +})); +``` + +#### TypeScript + +When using CSS Modules with TypeScript, add a declaration file (e.g. `imports/css-modules.d.ts`) so the compiler recognizes `.module.css` imports: + +``` typescript +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} +``` + +For more details, check [the official Rspack CSS Modules guide](https://rspack.rs/guide/tech/css#css-modules). + +### Less + +Less support is available in Meteor-Rspack. You need to replace the existing [Meteor `less` package](https://github.com/meteor/meteor/tree/master/packages/non-core/less) or similar with the Rspack configuration. + +#### Install + +``` bash +npm i -D less less-loader +``` + +#### Config + +``` js +module.exports = defineConfig(Meteor => ({ + module: { + rules: [ + { + test: /\.less$/, + use: [ + { + loader: 'less-loader', + }, + ], + type: 'css/auto', + }, + ], + }, +})); +``` + +For details, check [the official Rspack and Less guide](https://rspack.rs/guide/tech/css#less). + +### SCSS + +SCSS support is available in Meteor-Rspack. You need to replace the existing Meteor [`fourseven:scss`package](https://github.com/Meteor-Community-Packages/meteor-scss) or similar with the Rspack configuration. + +#### Install + +``` bash +npm i -D sass-embedded sass-loader +``` + +#### Config + +``` js +module.exports = defineConfig(Meteor => ({ + module: { + rules: [ + { + test: /\.scss$/i, + use: [ + { + loader: 'sass-loader', + options: { + api: 'modern-compiler', + implementation: require.resolve('sass-embedded'), + }, + }, + ], + type: 'css/auto', + }, + ], + }, +})); +``` + +For more details, check [the official Rspack and SCSS guide](https://rspack.rs/guide/tech/css#sass). + +### Tailwind & PostCSS + +Meteor-Rspack supports Tailwind projects out of the box. For details, check [the official Rspack and Tailwind guide](https://tailwindcss.com/docs/installation/framework-guides/rspack/react). + +> Use `meteor create --tailwind` to start with a preconfigured Rspack Tailwind app. + +### Babel + +Meteor-Rspack supports Babel projects as an alternative to default SWC. + +> Use `meteor create --babel` to start with a preconfigured Rspack Babel app. + +Using Babel will increase build times. Prefer SWC. If you need Babel for specific files, limit Babel to those files, or use a hybrid with SWC and Babel. For example, [enabling React Compiler is available only via Babel using module rules](https://rspack.rs/guide/tech/react#react-compiler). + +### Angular + +Meteor-Rspack supports Angular projects. It’s experimental, but it already works in development, production, and with Meteor testing. We still need to refine it and add the Angular test suite and other details. + +> Use `meteor create --angular` to start with a preconfigured Rspack Angular app. + +### HtmlRspackPlugin + +Meteor-Rspack includes its own HtmlRspackPlugin, enabled by default to attach chunks and assets to the HTML skeleton. Meteor then uses this HTML to generate the final index file. + +If you want to customize HtmlRspackPlugin, add it to your `rspack.config.js` file: + +```javascript +export default defineConfig(Meteor => { + return { + plugins: [ + Meteor.HtmlRspackPlugin({ + meta: { + // Will generate: + viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no', + // Will generate: + 'theme-color': '#4285f4', + // Will generate: + 'Content-Security-Policy': { + 'http-equiv': 'Content-Security-Policy', + content: 'default-src https:', + }, + }, + }), + ], + }; +}); +``` + +This example adds meta tags to the HTML. For more options, see the [official Rspack and HTML integration guide](https://rspack.rs/plugins/rspack/html-rspack-plugin). + +:::warning +You can still use HTML files near your Meteor client entry point to define customizations (for example, `./client/main.html` will generate correctly and apply the contents you add). +::: + +### Delegating Dependencies to Rspack + +**Meteor.compileWithRspack(deps: [Condition](https://rspack.rs/config/module#condition)[], options?: [SwcLoaderOptions](https://v0.rspack.dev/guide/features/builtin-swc-loader#options))** + +This helper forces **Rspack (via SWC and custom loaders)** to parse and transpile specific npm dependencies during the build. + +Use this when a dependency: + +* Uses modern syntax (ESM, TypeScript, etc.) +* Lives inside a monorepo and isn’t precompiled, +* Needs to be reprocessed according to your SWC config + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Force-compile modern or local packages via SWC + ...Meteor.compileWithRspack(['grubba-rpc']), + // Force-compile zod with ES5 target + ...Meteor.compileWithRspack(['zod'], { jsc: { target: 'es5' } }), +})); +``` + +### Delegating Dependencies to Meteor + +**Meteor.compileWithMeteor(deps: [Condition](https://rspack.rs/config/module#condition)[])** + +This helper marks specific npm dependencies as externals, meaning they are skipped by Rspack and instead handled by Meteor/Node at runtime. + +Use this when a dependency: + +* Contains native or binary code (e.g. sharp) +* Belongs to a Meteor Atmosphere package that maintains internal state +* Comes precompiled or is large enough to run better outside the bundle + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Exclude native modules from the bundle (use Meteor runtime) + ...(Meteor.isServer ? Meteor.compileWithMeteor(['sharp']) : {}), +})); +``` + +--- + +A reported use case for this is with the `thread-stream` dependency, a transitive dependency of Mongo packages. If you get this error: + +``` shell +Error: Cannot find module '/_build/main-dev/lib/worker.js' +``` + +It means the worker can’t be found. Let the Node/Meteor ecosystem handle this dependency so it can automatically pick the right worker. + +``` js +module.exports = defineConfig((Meteor) => { + return { + // .. + ...Meteor.compileWithMeteor([ + // .. + "thread-stream" + ]), + }; +}); +``` + +More info in [this forum post](https://forums.meteor.com/t/new-3-4-beta-12-release-faster-builds-smaller-bundles-and-modern-setups-with-the-rspack-integration/64124/94). + +### Split Vendor Chunk + +When using dynamic imports (`import()`), you might unintentionally include libraries like React, Mantine, or date utilities in multiple async chunks. To avoid this, it's best to define a stable `vendor` chunk for shared dependencies. + +We provide a helper to easily configure this in your `rspack.config.js`: + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Split vendor chunk + ...Meteor.splitVendorChunk(), +})); +``` + +This is a quick configuration for split chunks all within `node_modules` as a `vendor` chunk, if you need more control you can use the [official Rspack split chunks integration guide](https://rspack.rs/guide/optimization/code-splitting#splitchunksplugin). + +### Customizing SWC config + +Rspack uses the SWC configuration to transpile your app code. By default, it inherits any settings from the `.swcrc` file, which also [impacts how Meteor transpiles core and package code](meteor-bundler-optimizations.md#custom-swcrc). + +If you want a configuration to apply only to your app code (not Meteor packages), two helpers are available: + +#### `Meteor.extendSwcConfig` - smart merge (recommended) + +Merges your custom options on top of Meteor's defaults using a deep merge strategy (the same used by `Meteor.extendConfig`). Only the properties you specify are overridden; everything else (parser settings, React refresh, external helpers, etc) is preserved. + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Add decorator support while keeping all Meteor defaults + ...Meteor.extendSwcConfig({ + jsc: { + parser: { + decorators: true, + }, + }, + }), +})); +``` + +#### `Meteor.replaceSwcConfig` - full replacement + +Discards Meteor's defaults entirely and uses the provided config as-is. Use this when you need complete control over SWC and the smart merge doesn't fit your use case. + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Full SWC config — no Meteor defaults applied + ...Meteor.replaceSwcConfig({ + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: true, + }, + target: 'es2020', + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }), +})); +``` + +:::warning +When using `replaceSwcConfig`, you are responsible for providing all necessary SWC options. Features like React refresh, external helpers and parser defaults that Meteor configures (`Meteor.swcConfigOptions`) will not be applied unless you include them yourself. +::: + +### Interop for Default Imports + +Meteor originally handled default imports from CommonJS modules automatically. This allowed you to write: +```js +import all from 'some-commonjs-lib'; +``` +even when the library used module.exports = ... under the hood. + +With Rspack and SWC, that behavior no longer happens by default, you now need to use: + +```js +import * as all from 'some-commonjs-lib'; +``` +unless you re-enable interop support. + +If you prefer to restore Meteor’s earlier behavior, you can configure SWC like this in your `.swcrc`: + +``` json +{ + "jsx": { ... }, + "module": { + "type": "commonjs", + "noInterop": false, + "importInterop": "node" + } +} +``` + +* `"type": "commonjs"` tells SWC to emit CommonJS output (require, module.exports, etc.). +* `"noInterop": false` injects interop helpers so default imports from CommonJS modules work properly. +* `"importInterop": "node"` aligns behavior with how Node handles ESM and CJS interop. + +This configuration ensures compatibility for mixed module imports, allowing default imports from CommonJS packages to behave as the old Meteor-style import behavior. + +However, enabling this globally means SWC will convert all `import`/`export` statements into `require` calls. Rspack then loses access to ES module boundaries, preventing optimizations like tree-shaking and static analysis. + +In short, this option trades runtime compatibility for build-time optimization. + +We recommend migrating away from this pattern and using standard named or namespace imports (`import { ... }` or `import * as`...) for long-term compatibility and better build performance. + +### Cache + +Meteor cache remains active and continues to handle Atmosphere packages and intermediate builds. There’s an additional cache layer managed by Rspack to speed up rebuilds for your app code. + +This Rspack cache is enabled by default in persistent mode. If you [encounter issues](https://github.com/web-infra-dev/rspack/issues/11804) or prefer to disable it, you can do so in your `rspack.config.js` using the helper: + +```javascript +const { defineConfig } = require('@meteorjs/rspack'); +const { rspack } = require('@rspack/core'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); + +module.exports = defineConfig(Meteor => ({ + // Disable cache, or use 'memory' to switch to in-memory cache + ...Meteor.setCache(false), +})); + + +``` + +This helper provide a shortcut to apply the needed Rspack configuration and safely override defaults, so you don’t have to handle it manually. + +### Service Worker + +Rspack lets you use standard plugins to manage Service Workers, such as Workbox, so you don’t need to maintain your own setup. You can follow the Webpack guide for integrating Workbox with [`workbox-webpack-plugin`](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin) (it should be compatible), or try the Rspack-specific version [`@aaroon/workbox-rspack-plugin`](https://github.com/Clarkkkk/workbox-rspack-plugin). + +Whether you use a managed tool or a custom setup, ensure that only the Rspack dev server endpoints are treated as network-only for proper development. Otherwise, you may end up with infinite reload loops that only clear after removing the Service Worker. Skip caching of `__rspack__` endpoints. + +If you use a custom implementation in your `sw.js`, intercept fetch requests to ignore Rspack contexts like this: + +```js +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + const sameOrigin = url.origin === self.location.origin; + // Skip Rspack devServer + if (sameOrigin && url.pathname.includes('/__rspack__/')) { + // Never cache ignores and hot updates; hit the network every time + event.respondWith(fetch(event.request, { cache: 'no-store' })); + return; + } + + // ... +}); +``` + +When using Workbox, the equivalent with `GenerateSW` might look like this: + +```js +new GenerateSW({ + // ... + runtimeCaching: [ + { + urlPattern: ({ url }) => url.pathname.includes('/__rspack__/'), + handler: 'NetworkOnly', + }, + // ... + ], + // ... +}) +``` + +During development, the HMR dev server writes `sw.js` to disk by default, so build-generated service workers are served by Meteor's web server without extra configuration. If your service worker uses a different filename, see the [Dev Server](#dev-server) section for how to extend `writeToDisk`. + +### Dev Server + +You can customize the Rspack dev server much like you would when using meteor run. Any [devServer option listed in the official Rspack guide](https://rspack.rs/config/dev-server) can be applied in your app’s [`rspack.config.js`](./rspack-bundler-integration.md#custom-rspackconfigjs). + +The only exception is the port configuration. To set a specific port for the Rspack dev server, use the `RSPACK_DEVSERVER_PORT` environment variable: + +```bash +# Assign a specific port for the Rspack dev server +RSPACK_DEVSERVER_PORT=3232 meteor run +``` + +The reason is that the Rspack dev server is handled by the Meteor so it can make both dev server works together, and the info of the port needs to be properly shared via the env. + +During development, the HMR dev server keeps most build assets in memory and only writes HTML files and `sw.js` to disk by default. This means if your build pipeline generates files that need to be served from the root path, like `service-worker.js`, `manifest.json`, or any other output that Meteor's web server should serve directly, you can extend `writeToDisk` in your `rspack.config.js`: + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + devServer: { + devMiddleware: { + writeToDisk: (filePath) => + /\.(html)$/.test(filePath) || filePath.endsWith('service-worker.js'), + }, + }, +})); +``` + +In production, all build outputs are written to disk normally, so this only affects local development. + +### Disable Plugins + +Meteor allows disabling Rspack plugins that are added by default or through presets. This is useful when troubleshooting build issues or replacing a plugin with a custom implementation. + +Plugins are matched by name (constructor name) and can be specified as a string, RegExp, a predicate function, or an array of all. + +``` js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Disable one or more Rspack plugins + ...Meteor.disablePlugins([ + 'DefinePlugin', + /Html/i, + p => p?.constructor?.name === 'CustomConsoleLogPlugin', + ]), +})); +``` + +### Portable Build + +By default, Meteor-Rspack replaces `Meteor.isDevelopment` and `Meteor.isProduction` with static values at build time. This follows modern bundler conventions where `mode: "production"` enables aggressive dead-code elimination, any code inside `if (Meteor.isDevelopment) { ... }` blocks is stripped entirely from production builds. + +This is the recommended default. It produces smaller, more secure bundles by ensuring development-only code never ships to production. `meteor build` benefits directly from this, as the final output is as lean as possible. + +If you need a single build that works across environments (for example, building once and deploying to both staging and production without rebuilding), you can opt in to portable builds. This omits `Meteor.isDevelopment` and `Meteor.isProduction` from compile-time replacement, keeping them as runtime checks instead. + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + ...Meteor.enablePortableBuild(), +})); +``` + +Note that this trades build optimization for portability — dead-code elimination for development/production branches will no longer apply, resulting in larger bundles. Other flags like `Meteor.isClient`, `Meteor.isServer`, and `Meteor.isTest` are always replaced at build time, since they depend on the build target. + +### Running Multiple Instances + +By default, Meteor and Rspack use fixed directories for their build caches (`.meteor/local` and `_build`). If you try to run multiple instances of the same app simultaneously, they may conflict by attempting to write to the same folders. + +To run multiple instances, you can use the `METEOR_LOCAL_DIR` environment variable to specify a unique local directory for each instance. When this variable is set, the Meteor-Rspack integration automatically extracts the directory name and uses it as a suffix for Rspack's build contexts (`_build`, `build-chunks`, and `build-assets`), ensuring complete isolation between instances. + +```bash +# Instance 1 +PORT=3000 METEOR_LOCAL_DIR=.meteor/local-1 meteor run + +# Instance 2 +PORT=3001 METEOR_LOCAL_DIR=.meteor/local-2 meteor run +``` + +For more details on how this variable affects Rspack, see the [`METEOR_LOCAL_DIR`](../../cli/environment-variables.md#meteor_local_dir) documentation. + +## Benefits + +Meteor–Rspack integration sends your app code to Rspack to use modern bundler features. Meteor then uses Rspack’s output to handle Meteor-specific tasks (like Atmosphere package compilation) and create the final bundle. + +This implementation preserves Rspack HMR for faster reloads. + +With `Rspack` compiling your app you get: + +- Standard code splitting via HTTP +- Tree shaking to significantly reduce bundle size +- Support for ESM packages, including npm packages with export fields +- Bundler plugins for build hooks, alternative compilers, file resolvers, image optimizations, PWAs with service workers, and more +- Advanced analysis tools to track code impact and guide later optimizations +- More features coming soon + +With `Meteor` you get: + +- Core API with reactive and non-reactive endpoints (DDP, Express) +- Built-in Mongo/Minimongo API for isomorphic business logic +- Third-party packages for faster development +- Multiplatform development support +- All other Meteor strengths + +## Limitations + +### No Blaze HMR support + +Blaze templates build correctly with Rspack, but Meteor’s Hot Module Replacement (HMR) for Blaze is not available. Normally, Blaze HMR updates the UI instantly without reloading the whole page, keeping the current state (like form inputs or scroll position). + +With Rspack, Blaze changes will instead trigger a full live reload. This reload is still very fast thanks to Rspack’s reduced rebuild time (about 97% reduction), but the page state will reset after each change. The limitation exists because Blaze’s HMR relies on Meteor’s internal mechanism, which is not yet compatible with Rspack. + +This limitation only applies to Blaze. Any other modern project will work with HMR as soon as Rspack natively supports it, which is likely if it’s a modern library. + +## Troubleshotting + +If you run into issues, try `meteor reset` or delete the `.meteor/local` and `_build` folders in the project root. + +For help or to report issues, post on [GitHub](https://github.com/meteor/meteor/issues) or the [Meteor forums](https://forums.meteor.com). We're focused on making Meteor faster and your feedback helps. + +You can compare performance before and after enabling `modern` by running [`meteor profile`](../../cli/index.md#meteorprofile). Share your results to show progress to others. + +### Memory Crashes + +Large apps are more likely to hit memory limits during Meteor-Rspack builds, but this can also happen on smaller projects depending on the number of dependencies, cache size, and available system memory. If you experience crashes or out-of-memory errors, it's likely that the Rspack child process is running out of heap memory. + +Starting from Meteor 3.4.1, you can use [`TOOL_NODE_FLAGS`](../../cli/environment-variables.md#tool-node-flags) to set memory limits that will be automatically inherited by Rspack and other tool processes: + +```bash +TOOL_NODE_FLAGS="--max-old-space-size=16384" meteor run +``` + +For Meteor 3.4, you should use the standard `NODE_OPTIONS` environment variable, which Node.js propagates to child processes: + +```bash +NODE_OPTIONS="--max-old-space-size=16384" meteor run +``` + +This raises the heap limit for the Rspack process and should reduce how often memory-related crashes occur. Adjust the value according to your machine's available memory. + +Another approach is to disable Rspack's persistent cache, which is enabled by default and can be memory-intensive. See the [Cache](#cache) migration topic to disable it: + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + ...Meteor.setCache(false), +})); +``` + +You can combine both solutions: raise the heap limit with `TOOL_NODE_FLAGS` (3.4.1+) or `NODE_OPTIONS` (3.4) and disable persistent cache to reduce overall memory pressure. + +Rspack itself has reported plans to optimize persistent cache and overall RAM consumption in [Rspack 2.0](https://rspack.rs/misc/planning/roadmap), which should improve memory behavior in future Meteor-Rspack releases. + +### Docker + +When building or deploying a Meteor-Rspack app inside Docker, you may encounter errors like `Rspack plugin error: Could not find rspack.config.js`. This typically means the NPM dependencies expected by Meteor are not aligned with the Meteor version in use. + +Each Meteor release requires specific minimum versions of NPM packages like Rspack. If these were not committed after upgrading Meteor locally, the Docker environment won't have them. To fix this, run `meteor update --npm` before `meteor npm install` in your Dockerfile: + +```dockerfile +RUN (meteor update --npm 2>/dev/null || true) && meteor npm install && meteor build [...] +``` + +The `(meteor update --npm 2>/dev/null || true)` wrapper is for compatibility. The `--npm` option was introduced in Meteor 3.4. Older versions don't support it and would fail, so redirecting the error and allowing the command to continue ensures the same Docker step works across Meteor versions. + +> Keep `meteor update --npm` in the same Docker step as `meteor build` or `meteor deploy`. If you forget to commit and push the NPM bumps locally, this lets the Docker environment apply them on the fly. When using multiple Docker steps, each step is isolated, so NPM bumps won't carry over between steps. + +::: info +To avoid this issue entirely, run `meteor update --npm` locally after upgrading Meteor, or run the app once so the bumps are applied, then commit and push both the Meteor update and the updated NPM dependencies. +::: diff --git a/v3-docs/docs/about/roadmap.md b/v3-docs/docs/about/roadmap.md index 282b3c003f..e401b90ed9 100644 --- a/v3-docs/docs/about/roadmap.md +++ b/v3-docs/docs/about/roadmap.md @@ -4,7 +4,7 @@ Describes the high-level features and actions for the Meteor project in the near ## Introduction -**Last updated: June 16, 2025.** +**Last updated: January 30, 2026.** The description of many items includes sentences and ideas from Meteor community members. @@ -14,16 +14,13 @@ Contributors are encouraged to focus their efforts on work that aligns with the > If you have new feature requests or ideas, you should open a new [discussion](https://github.com/meteor/meteor/discussions/new). -## Current project: Bundle optimization +## Current project: Modern Build Stack > We need to improve the bundle size and performance of Meteor apps. We should consider tree-shaking, code-splitting, > and other optimizations to make our apps leaner and faster. -> To achieve that we plan to integrate or have an easy way to integrate with modern bundlers like RSPack, ESBuild, or Rollup. +> To achieve that we plan to integrate or have an easy way to integrate with modern bundlers like Rspack. -**Discussion links:** - -- [GitHub discussion](https://github.com/meteor/meteor/discussions/11587) -- [forums discussion](https://forums.meteor.com/t/join-the-effort-to-speed-up-meteor-bundler/63406/17) +[📄 Modern Build Stack Documentation](./modern-build-stack) ### Implementation plan: @@ -33,8 +30,9 @@ Contributors are encouraged to focus their efforts on work that aligns with the **Goal:** Add a command([meteor profile](/cli/#meteorprofile)) to measure if our changes are actually making our builds faster and smaller. +🔗 [Unlocking Meteor 3.2: New Profiling Tool to Track Bundler Performance and Size](https://dev.to/meteor/unlocking-meteor-32-new-profiling-tool-to-track-bundler-performance-and-size-1jc8) -#### Phase 2: External Transpiler Integration +#### Phase 2: External Transpiler Integration (SWC) **Target Release:** 3.3 ✅ @@ -43,93 +41,81 @@ Contributors are encouraged to focus their efforts on work that aligns with the the same benefits. - To have an external transpiler working with Meteor and producing a bundle that is smaller or faster than the current Meteor bundle. +🔗 [Faster Builds in Meteor 3.3: Modern Build Stack with SWC and Bundler Optimizations](https://dev.to/meteor/faster-builds-in-meteor-33-modern-build-stack-with-swc-and-bundler-optimizations-fm2) -#### Phase 3: HMR Improvements +#### Phase 3: Bundler Improvements & Feedback -**Target Release:** 3.3 ✅ - -**Goal:** Improve the HMR performance, so that it is faster and more reliable on what needs to be changed. - -#### Phase 4: Bundler Improvements & feedback - -**Target Release:** 3.3.x ⏳ +**Target Release:** 3.3.2 ✅ **Goal:** Improve the build size and make meteor use less resources for building, decreasing even more build and rebuild time. - Expanding compatibility and updates based on the feedback from the community, so that we can have a better experience with our new build tools, in this case SWC -#### Phase 5: External Bundler integration +#### Phase 4: External Bundler integration (Rspack) -**Target Release:** 3.4 ⏳ +**Target Release:** 3.4 ✅ -**Goal:** And an external bundler (like RSPack, ESBuild, or Rollup) working with Meteor and producing a bundle that is smaller or faster than the current Meteor bundle. -- This will also allow Meteor to have features like tree-shaking, code-splitting, and other optimizations that will make our apps leaner and faster. +**Goal:** Integrate an external bundler like Rspack with Meteor, producing a bundle that is smaller or faster than the current Meteor bundle. +- This also enables features like tree-shaking, code-splitting, full ESM support, community plugins, and other optimizations that make Meteor apps leaner, faster, and more standardized to configure. -#### Phase 6: Build Process Optimization +🔗 [Meteor 3.4 is out: Rspack integration, 4x faster builds, 8x smaller bundles, and extended bundler features](https://blog.galaxycloud.app/meteor-3-4-is-out-rspack-integration-4x-faster-builds-8x-smaller-bundles-and-extended-bundler-features) + +#### Phase 5: Resource Optimization & Feedback **Target Release:** 3.4.x ⏳ -**Goal:** Improve the build size and make meteor use less resources for building, decreasing even more build and rebuild time. -- Expanding compatibility and updates based on the feedback from the community, so that we can have a better experience with our new build tools - +**Goal:** Improve memory consumption on large apps when using Meteor and Rspack. This comes mainly from [identified optimizations on the Meteor side](https://forums.meteor.com/t/3-4-rc-3-release-candidate-faster-builds-smaller-bundles-and-modern-setups-with-the-rspack-integration/64124/225), and also from [new improvements in Rspack 2.0](https://rspack.rs/misc/planning/roadmap) as they become available. +- Expanding compatibility and updates based on community feedback, to improve the experience when working with the integrated Rspack bundler, like a clearer debugging process, more stable testing flows, and better support for different kinds of projects. #### Documentation Strategy We plan to document the changes in the Meteor documentation, including: -- How to use the new features -- How to integrate with the new bundler -- How the meteor bundler pipeline works for future contributors -- Examples and guides on how to integrate with the new bundler +* How to use the new features +* How to integrate with the new bundler +* How the Meteor bundler pipeline works for future contributors +* Examples and guides for integrating with the new bundler -## Next releases +## Next project: Change streams -- Support package.json exports fields ([Discussion](https://github.com/meteor/meteor/discussions/11727)) -- Tree-shaking +> Change Streams is the official way to listen to changes in MongoDB, btw Meteor reactivity works based on polling the database for changes or via oplog mongo system that can be inefficient and lead to performance issues compared with the newst techlogies we have in 2026 (especially with large datasets or high-frequency updates), so we want to leverage MongoDB Change Streams to provide real-time updates to Meteor applications in a more efficient way. - > Tree-shaking and exports fields may be implemented by integrating with more modern build tools. +**Feedback and discussion** -- Capacitor support +🔗 [MongoDB Change Streams support in Meteor](https://forums.meteor.com/t/mongodb-change-streams-support-in-meteor/63681) - > Capacitor is a modern alternative to Cordova; we should provide an easy way to build mobile apps using Capacitor. +### Phase 1: Opined implementation -- MongoDB Change Streams support ([Discussion](https://github.com/meteor/meteor/discussions/11842)) +**Target Release:** 3.5 ⏳ +**Goal:** Implement a first version for MongoDB Change Streams in Meteor, allowing developers to opt-in to using change streams for real-time updates with a simple configuration option. This version should be transparent to existing applications, allowing them to continue using the current reactivity system while providing an easy path to switch to change streams via settings.json file or environment variable. - > Change Streams is the official way to listen to changes in MongoDB. We should provide a way to use it seamlessly in Meteor. It has been planned for a long time, and now we’re in a position to do it. +### Phase 2: Configurable implementation + feedbacks -- Improve TypeScript support for Meteor and packages ([Discussion](https://github.com/meteor/meteor/discussions/12080)) +**Target Release:** 3.5.x ⏳ - > Should be an ongoing effort to improve the TypeScript support in Meteor and packages. We should provide a better experience for TypeScript users, including better type definitions and support for TypeScript features. +**Goal:** Make MongoDB Change Streams more configurable to bring better performance in specific scenarios for real-time updates in Meteor, while gathering feedback from the community to refine and improve the implementation based on real-world usage. -- Improve release CI/CD speed and reliability (optimized build times will help) - > Our CI/CD takes too long to run, causing long queues and delays in our release process and feedback loop; we need to improve that. +## Next priorities -### Candidate items +The priorities listed below represent tasks that are large enough to be considered major items we want to pursue next, similar to bundler optimizations and change streams. -We need to discuss further to decide whether to proceed with these implementations. +* Mobile/Capacitor Support +> Capacitor is a modern alternative to Cordova; we should provide an easy way to build mobile apps using Capacitor. -- Performance improvements (Async Hooks/Async Local Storage optimization) -- HTTP/3 Support -- Improve DDP Client -- Improve Passwordless package ([Discussion](https://github.com/meteor/meteor/discussions/12075)) -- Integrate with Tauri, it might replace Cordova and Electron in a single tool -- Bring Redis-oplog to core ([Repository](https://github.com/Meteor-Community-Packages/redis-oplog)) -- Better file upload support via DDP ([Discussion](https://github.com/meteor/meteor/discussions/11523)) -- Improve usage in Windows environments +* Release CI/CD Speed & Reliability +> Improve the speed and reliability of our release process, so we can improve the contribution experience by decreasing the time to run the CI/CD for PRs and releases. -### Finished items +* Open telemetry & Observability support ([PR](https://github.com/meteor/meteor/pull/14086)) +> Provide built-in support for OpenTelemetry in Meteor, allowing developers to easily instrument their applications for observability and monitoring. This will be divided in 2 phases: +> - Phase 1: Basic OpenTelemetry support with metrics & tracing for DDP methods and publications. +> - Phase 2: Advanced OpenTelemetry support with logging, and integration with mongo instrumentation. -- Change how Meteor executes Async code ([Discussion](https://github.com/meteor/meteor/discussions/11505)) - - Provide new async APIs where Fibers are required - - Mongo package with Async API ([PR](https://github.com/meteor/meteor/pull/12028)) - - Provide async versions for Accounts and core packages - - Adapt Meteor Promise implementation -- Enable Top-Level Await (TLA) on Meteor server-side ([PR](https://github.com/meteor/meteor/pull/12095)) -- Support Top-Level Await (TLA) on Reify -- Remove Fibers dependency from Meteor Public APIs -- Remove Fibers entirely -- Update Cordova integration to Meteor 3.0 -- Run Meteor on Node.js v20 -- Change web engine from Connect to Express +* TypeScript Improvements +> Enhance TypeScript support in Meteor, including better type definitions, improved integration with the build system, and enhanced developer experience. + +* Test toolkit Improvements +> Improve the testing toolkit in Meteor, including better integration with popular testing frameworks, improved test runner performance, and enhanced developer experience. + +Beyond these, we also track smaller tasks delivered in each release. These focus on improving existing areas in Meteor (such as Node 24, Express Auth integration, and more), enforcing Meteor core code quality (linting and standards), easing contributions through documentation and engagement programs, and reviewing and validating existing and new community contributions. --- -For more completed items, refer to our [changelog](https://docs.meteor.com/history.html). +For more completed items, refer to our [changelog](/history). diff --git a/v3-docs/docs/about/web-apps.md b/v3-docs/docs/about/web-apps.md index b39454e0b1..629746e616 100644 --- a/v3-docs/docs/about/web-apps.md +++ b/v3-docs/docs/about/web-apps.md @@ -49,6 +49,7 @@ If you want detailed help about a specific command, run `meteor help `. ## Next Steps -- Follow the [React](/tutorials/react/index.html) or [Vue](/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.html) tutorials. New tutorials are coming soon. +- Follow the [React](/tutorials/react/index.html) or [Vue](/tutorials/vue/meteorjs3-vue3.html) tutorials. New tutorials are coming soon. +- Learn about [Modern Build Stack](/about/modern-build-stack.md) for faster development, smaller bundle sizes, and more. - Read about [Cordova for Mobile Apps](/about/cordova.html). -- Explore the [Meteor Guide](https://guide.meteor.com/). +- Explore the [Tutorials](/tutorials/react/index). diff --git a/v3-docs/docs/about/what-is.md b/v3-docs/docs/about/what-is.md index b87832fa90..a87934062c 100644 --- a/v3-docs/docs/about/what-is.md +++ b/v3-docs/docs/about/what-is.md @@ -29,7 +29,7 @@ Meteor is a full-stack JavaScript platform for developing modern web and mobile - Start by learning how to install Meteor in the [Installation Section](/about/install.html). -- The tutorials are the perfect place to start. Build a simple app to manage a task list! Available for [React](/tutorials/react/index.html), and [Vue 3](/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.html). Blaze and Svelte tutorials are coming soon. +- The tutorials are the perfect place to start. Build a simple app to manage a task list! Available for [React](/tutorials/react/index.html), [Vue 3](/tutorials/vue/meteorjs3-vue3.html), [Solid](/tutorials/solid/index.html), [Blaze](/tutorials/blaze/index.html) and [Svelte 5](/tutorials/svelte/index.html). - Participate in Meteor's fully professional, engaging and interactive online school. Join [Meteor University](https://university.meteor.com/). Our courses cover Meteor 2 but most of the content is still relevant. diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index f52207a4d9..a9e1d08cfe 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -15,7 +15,7 @@ login provider packages: `accounts-password`, `accounts-facebook`, `accounts-github`, `accounts-google`, `accounts-meetup`, `accounts-twitter`, or `accounts-weibo`. -Read more about customizing user accounts in the [Accounts](http://guide.meteor.com/accounts.html) article in the Meteor Guide. +Read more about customizing user accounts in the [Accounts](/tutorials/accounts/accounts) article in the Meteor Guide. ### Accounts with Session Storage {#accounts-session-storage} @@ -176,12 +176,16 @@ Meteor.users.deny({ update: () => true }); For example, [the `accounts-ui` package](../packages/accounts-ui.md) uses this to display an + + animation while the login request is being processed. + + For example, when called in a user's browser, connections in that browser @@ -203,6 +207,35 @@ This method can fail throwing one of the following errors: This function is provided by the `accounts-password` package. See the [Passwords](#passwords) section below. + + +Logs the user in using a valid Meteor login token (also called a resume token). This is typically used to restore a user's session across browser reloads, between tabs, or across DDP connections (such as in multi-server setups). + +**Arguments:** +- `token` (`String`): The login token to use for authentication. Usually obtained from `Accounts._storedLoginToken()` or from a previous login session. +- `callback` (`Function`, optional): Called with no arguments on success, or with a single `Error` argument on failure. + +**Returns:** +- `void` + +**Usage example:** +```js +import { Meteor } from "meteor/meteor"; +const token = Accounts._storedLoginToken(); +Meteor.loginWithToken(token, (error) => { + if (error) { + console.error("Login with token failed", error); + } else { + console.log("Logged in with token!"); + } +}); +``` + +**Notes:** +- If the token is invalid, expired, or revoked, the callback will be called with an error and the user will not be logged in. +- This method is used internally by Meteor to automatically restore login state on page reload and across tabs. +- Can be used with custom DDP connections to authenticate across multiple Meteor servers sharing the same database. + Available functions are: @@ -263,7 +296,7 @@ Then, inside the server of your app (this example is for the Weebo service), imp ```js import { ServiceConfiguration } from "meteor/service-configuration"; -ServiceConfiguration.configurations.upsert( +ServiceConfiguration.configurations.upsertAsync( { service: "weibo" }, { $set: { @@ -958,12 +991,173 @@ be called. To customize the contents of the email, see [`Accounts.emailTemplates`](#Accounts-emailTemplates). +## Email Link Callbacks and URL Customization + +When Meteor sends account-related emails, those emails contain URLs that users click +to complete actions like password reset. This section explains how these URLs work +and how to customize them. + +### How Email URLs Work + +By default, Meteor generates URLs using hash fragments: + +- `https://yourapp.com/#/reset-password/TOKEN` +- `https://yourapp.com/#/verify-email/TOKEN` +- `https://yourapp.com/#/enroll-account/TOKEN` + +**Security Note:** Hash fragments (the part after `#`) are intentionally used because +they are never sent to the server in HTTP requests. This prevents sensitive tokens +from appearing in server logs, proxy logs, or HTTP referrer headers. + +When a user clicks these links, Meteor's client-side code automatically parses +`window.location.hash` and triggers the appropriate callback registered with +the functions below. + +### Complete Example: Custom Password Reset Flow + +Here's how to implement password reset without `accounts-ui`: + +```js +// client/accounts-hooks.js +import { Accounts } from 'meteor/accounts-base'; + +// Register at top level, NOT inside Meteor.startup() +let doneCallback; + +Accounts.onResetPasswordLink((token, done) => { + // Store token and done callback for your UI + Session.set('resetPasswordToken', token); + doneCallback = done; + + // Show your password reset form + // The login process is suspended until done() is called +}); + +// In your password reset form submit handler: +function submitNewPassword(newPassword) { + const token = Session.get('resetPasswordToken'); + + Accounts.resetPassword(token, newPassword, (error) => { + if (error) { + alert('Reset failed: ' + error.reason); + } else { + Session.set('resetPasswordToken', null); + doneCallback(); // Re-enables auto-login + } + }); +} +``` + +### Customizing Email URLs + + + +`Accounts.urls` is a server-side object containing functions that generate URLs +for account emails. Override these to customize the URL format. + +| Property | Signature | Description | +|----------|-----------|-------------| +| `resetPassword` | `(token, extraParams?) => string` | Password reset URL | +| `verifyEmail` | `(token, extraParams?) => string` | Email verification URL | +| `enrollAccount` | `(token, extraParams?) => string` | Account enrollment URL | +| `loginToken` | `(selector, token, extraParams?) => string` | Login token URL | + +#### Async URL Generation + +The URL methods can also return **Promises** that resolve to strings. This is useful when +URL generation requires asynchronous operations, such as: +- Looking up user data from the database +- Calling external services (e.g., URL shorteners) +- Generating signed URLs from cloud providers + +The email-sending functions (`Accounts.sendResetPasswordEmail`, `Accounts.sendEnrollmentEmail`, +and `Accounts.sendVerificationEmail`) handle both synchronous and asynchronous URL methods +transparently. + +**Example: Async URL with database lookup** + +```js +// Server-side +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; + +Accounts.urls.resetPassword = async (token, extraParams) => { + // Example: Look up user preference for custom domain + const user = await Meteor.users.findOneAsync({ 'services.password.reset.token': token }); + const domain = user?.profile?.preferredDomain || Meteor.absoluteUrl(); + + return `${domain}reset-password/${token}`; +}; +``` + +**Example: Using a URL shortener service** + +```js +// Server-side +Accounts.urls.verifyEmail = async (token) => { + const longUrl = Meteor.absoluteUrl(`verify-email/${token}`); + + // Shorten the URL using an external service + const shortUrl = await shortenUrl(longUrl); + return shortUrl; +}; +``` + +**Example: Using Clean URLs Instead of Hash Fragments** + +If your router doesn't handle hash fragments well, you can override `Accounts.urls` +to use clean URLs: + +```js +// Server-side +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; + +Accounts.urls.resetPassword = (token) => { + return Meteor.absoluteUrl(`reset-password/${token}`); +}; + +Accounts.urls.verifyEmail = (token) => { + return Meteor.absoluteUrl(`verify-email/${token}`); +}; + +Accounts.urls.enrollAccount = (token) => { + return Meteor.absoluteUrl(`enroll-account/${token}`); +}; +``` + +**Important:** When using clean URLs (without `#/`), the built-in +`Accounts.onResetPasswordLink`, `Accounts.onEnrollmentLink`, and +`Accounts.onEmailVerificationLink` callbacks won't work automatically. +Handle tokens in your router instead: + +```js +// Example with a router +Router.route('/reset-password/:token', function() { + const token = this.params.token; + // Show password reset UI, call Accounts.resetPassword(token, newPassword) +}); +``` + +### Router Integration + +You have three options when integrating with client-side routers: + +1. **Keep default hash URLs** - Works out of the box + with `Accounts.on*Link` callbacks. No router configuration needed. + +2. **Override `Accounts.urls` for clean URLs** - More "modern" looking URLs, + but requires handling tokens in your router. + +3. **Use hashbang mode** - Some routers support `#!/` routes. Configure your + router accordingly and update `Accounts.urls` to use `#!/` instead of `#/`. + This is an `Object` with several fields that are used to generate text/html @@ -989,6 +1183,7 @@ Set the fields of the object by assigning to them: returns the body text for a reset password email. - `html`: An optional `Function` that takes a user object and a url, and returns the body html for a reset password email. + - `enrollAccount`: Same as `resetPassword`, but for initial password setup for new accounts. - `verifyEmail`: Same as `resetPassword`, but for verifying the users email @@ -996,7 +1191,10 @@ Set the fields of the object by assigning to them: Example: + ```js + + import { Accounts } from "meteor/accounts-base"; Accounts.emailTemplates.siteName = "AwesomeSite"; diff --git a/v3-docs/docs/api/check.md b/v3-docs/docs/api/check.md index d515902838..c8892f9c2b 100644 --- a/v3-docs/docs/api/check.md +++ b/v3-docs/docs/api/check.md @@ -114,6 +114,22 @@ result = Match.test(value, Match.Integer); console.log(result); // true ``` +### `Match.NonEmptyString` + +Matches a non-empty string. + +```js +import { Match } from "meteor/check"; +let result; +// Will return true if `value` is a non-empty string. +result = Match.test("hello", Match.NonEmptyString); +console.log(result); // true + +// Will return false for empty strings. +result = Match.test("", Match.NonEmptyString); +console.log(result); // false +``` + ### `[pattern]` { #arraypattern } A one-element array matches an array of elements, each of which match @@ -224,10 +240,11 @@ import { Match, check } from "meteor/check"; check(buffer, Match.Where(EJSON.isBinary)); -const NonEmptyString = Match.Where((x) => { - check(x, String); - return x.length > 0; +// Example: creating a custom pattern for positive numbers +const PositiveNumber = Match.Where((x) => { + check(x, Number); + return x > 0; }); -check(arg, NonEmptyString); +check(arg, PositiveNumber); ``` diff --git a/v3-docs/docs/api/collections.md b/v3-docs/docs/api/collections.md index 15e17dcd6d..70ce0492fd 100644 --- a/v3-docs/docs/api/collections.md +++ b/v3-docs/docs/api/collections.md @@ -198,7 +198,7 @@ Greetings.findOne({ name: 'John' }); // 🧾 Data is available (Optimistic-UI) Read more about server and stub promises on calling methods, [please refer to the docs](./meteor.md#Meteor-callAsync). -Read more about collections and how to use them in the [Collections](http://guide.meteor.com/collections.html) article in the Meteor Guide. +Read more about collections and how to use them in the [Collections](/tutorials/collections/collections) article in the Meteor Guide. @@ -508,7 +508,7 @@ While `allow` and `deny` make it easy to get started building an app, it's harder than it seems to write secure `allow` and `deny` rules. We recommend that developers avoid `allow` and `deny`, and switch directly to custom methods once they are ready to remove `insecure` mode from their app. See -[the Meteor Guide on security](https://guide.meteor.com/security.html#allow-deny) +[the Meteor Guide on security](/tutorials/security/security#allow-deny) for more details. ::: @@ -649,7 +649,7 @@ While `allow` and `deny` make it easy to get started building an app, it's harder than it seems to write secure `allow` and `deny` rules. We recommend that developers avoid `allow` and `deny`, and switch directly to custom methods once they are ready to remove `insecure` mode from their app. See -[the Meteor Guide on security](https://guide.meteor.com/security.html#allow-deny) +[the Meteor Guide on security](/tutorials/security/security#allow-deny) for more details. ::: @@ -669,6 +669,93 @@ The methods (like `update` or `insert`) you call on the resulting _raw_ collecti +## Collection Extensions + +**Integrated into core in Meteor 3.4** ([PR#13830](https://github.com/meteor/meteor/pull/13830)) + +Meteor provides a powerful Collection Extensions API that allows you to extend the functionality of all collection instances. These static methods on `Mongo.Collection` let you add constructor extensions, prototype methods, and static methods to customize collection behavior. + +These APIs were previously available through the community package [lai:collection-extensions](https://github.com/Meteor-Community-Packages/meteor-collection-extensions) and are now integrated directly into Meteor core. The same APIs are exported under `CollectionExtensions` for backwards compatibility. + + + +Add a constructor extension function that runs when collections are created. The extension function is called with `(name, options)` and `this` bound to the collection instance. + +Example: +```js +Mongo.Collection.addExtension(function(name, options) { + this._customProperty = 'value'; + console.log(`Collection ${name} was created`); +}); +``` + + + +Add a prototype method to all collection instances. The method is bound to the collection instance and available on all collections. + +Example: +```js +Mongo.Collection.addPrototypeMethod('customMethod', function() { + return `${this._name} is awesome`; +}); + +// Now available on all collections +const Users = new Mongo.Collection('users'); +console.log(Users.customMethod()); // "users is awesome" +``` + + + +Add a static method to the `Mongo.Collection` constructor itself. + +Example: +```js +Mongo.Collection.addStaticMethod('getAllCollections', function() { + return Array.from(Mongo._collections.values()); +}); + +// Now available as static method +const allCollections = Mongo.Collection.getAllCollections(); +``` + + + +Remove a constructor extension (useful for testing). + + + +Remove a prototype method from all collection instances. + + + +Remove a static method from the `Mongo.Collection` constructor. + + + +Clear all extensions, prototype methods, and static methods. This is useful for testing to ensure a clean state. + + + +Get all registered constructor extensions. Returns an array of extension functions. Useful for debugging. + + + +Get all registered prototype methods. Returns a Map of method names to functions. Useful for debugging. + + + +Get all registered static methods. Returns a Map of method names to functions. Useful for debugging. + +### Legacy Aliases + +### Mongo.Collection.addPrototype + +> **Deprecated** — backwards compatibility alias for [`addPrototypeMethod`](#Mongo-Collection-addPrototypeMethod). Use `addPrototypeMethod` instead. + +### Mongo.Collection.removePrototype + +> **Deprecated** — backwards compatibility alias for [`removePrototypeMethod`](#Mongo-Collection-removePrototypeMethod). Use `removePrototypeMethod` instead. + ## Cursors {#mongo_cursor} diff --git a/v3-docs/docs/api/meteor.md b/v3-docs/docs/api/meteor.md index 01d7010f93..68f37fd31a 100644 --- a/v3-docs/docs/api/meteor.md +++ b/v3-docs/docs/api/meteor.md @@ -54,6 +54,89 @@ Meteor.startup(() => { + + +This helper function allows you to defer the execution of a function based on the environment. + +::: code-group + +```js [with-deferrable.js] +import { Meteor } from "meteor/meteor"; + +Meteor.startup(async () => { + await Meteor.deferrable(connectToExternalDB, { + on: ["development"], + }); +}); +``` + +```js [without-deferrable.js] +import { Meteor } from "meteor/meteor"; + +Meteor.startup(async () => { + if (Meteor.isDevelopment) { + Meteor.defer(connectToExternalDB); + } else { + await connectToExternalDB(); + } +}); +``` + +::: + +Using this pattern can get some performance gains on the defined environments as sometimes we do not need to wait for this function, +this can increase the speed of startup. + + + +**Introduced in Meteor 3.4** ([PR#14006](https://github.com/meteor/meteor/pull/14006)) + +This helper function allows you to defer the execution of a function only in development environments, significantly improving server startup times in development by deferring non-critical setup code. + +::: code-group + +```js [with-deferrable.js] +import { Meteor } from "meteor/meteor"; +Meteor.startup(async () => { + await Meteor.deferDev(connectToExternalDB); +}); +``` + +```js [without-deferrable.js] +import { Meteor } from "meteor/meteor"; +Meteor.startup(async () => { + if (Meteor.isTest || Meteor.isDevelopment) { + Meteor.defer(connectToExternalDB); + } else { + await connectToExternalDB(); + } +}); +``` + + + +This helper function allows you to defer the execution of a function only in production environments. +::: code-group + +```js [with-deferrable.js] +import { Meteor } from "meteor/meteor"; +Meteor.startup(async () => { + await Meteor.deferProd(loadDevTools); +}); +``` + +```js [without-deferrable.js] +import { Meteor } from "meteor/meteor"; + +Meteor.startup(async () => { + if (Meteor.isProduction) { + Meteor.defer(loadDevTools); + } else { + await loadDevTools(); + } +}); +``` + @@ -203,7 +286,7 @@ to each method call on the client, and checking on the server whether a call with this ID has already been made. Alternatively, you can use [`Meteor.apply`](#Meteor-apply) with the noRetry option set to true. -Read more about methods and how to use them in the [Methods](http://guide.meteor.com/methods.html) article in the Meteor Guide. +Read more about methods and how to use them in the [Methods](/tutorials/methods/methods) article in the Meteor Guide. @@ -650,7 +733,7 @@ will still work. ::: Read more about publications and how to use them in the -[Data Loading](http://guide.meteor.com/data-loading.html) article in the Meteor Guide. +[Data Loading](/tutorials/data-loading/data-loading) article in the Meteor Guide. diff --git a/v3-docs/docs/api/package.md b/v3-docs/docs/api/package.md index 606f0c8f1a..6229b94c33 100644 --- a/v3-docs/docs/api/package.md +++ b/v3-docs/docs/api/package.md @@ -57,6 +57,11 @@ Npm.depends({ simplesmtp: '0.3.10', 'stream-buffers': '0.2.5' }); + +// This lets you use npm packages for development only in your package +Npm.devDepends({ + eslint: "8.36.0" +}); ``` `api.mainModule` is documented in the [modules](../packages/modules.md#modular-application-structure) section. @@ -124,6 +129,7 @@ Meteor packages can include NPM packages and Cordova plugins by using `Npm.depends` and `Cordova.depends` in the `package.js` file. + @@ -161,6 +167,10 @@ This way we avoid having to call a specific code before another specific code in ## Build Plugins API {#build-plugin-api} +::: warning +Starting with Meteor 3.4+, most build plugins won't work for application code when [the Rspack bundler](../about/modern-build-stack/rspack-bundler-integration.md) is enabled (which is the default). However, they will still work for Atmosphere package code and any code specifically set for Meteor bundler processing. You can still use build plugins for scripting independent app processing. Many of these functionalities can now be handled more effectively through Rspack's plugin system and lifecycle management, which offers more modern and flexible ways to process your app source code. +::: + Meteor packages can provide build plugins - programs that integrate with the build tool Isobuild used to compile and bundle your application. @@ -196,6 +206,10 @@ process. Commonly, such files have the following methods: ## Linters {#build-plugin-linters} +::: warning +In Meteor 3.4+ [with Rspack enabled](../about/modern-build-stack/rspack-bundler-integration.md), linter build plugins for application code have the same limitations as other build plugins. However, you can still use Rspack's ecosystem of linting tools and plugins, which offer more integration with modern JavaScript tooling and better performance. +::: + Linters are programs that check the code for undeclared variables or find code that doesn't correspond to certain style guidelines. Some of the popular examples of linters are [JSHint](http://jshint.com/about/) and @@ -255,6 +269,10 @@ See an example of a linting plugin implemented in Core: [jshint](https://github. ## Compilers {#build-plugin-compilers} +::: warning +With Meteor 3.4+ [with Rspack enabled](../about/modern-build-stack/rspack-bundler-integration.md), compiler build plugins won't process application code by default. Rspack has its own loader system for handling various file types and transformations, which is typically more performant and better integrated with the modern JavaScript ecosystem. You can leverage Rspack's loaders for compiling TypeScript, JSX, CSS preprocessors, and other transformations directly. +::: + Compilers are programs that take the source files and output JavaScript or CSS. They also can output parts of HTML that is added to the `` tag and static assets. Examples for the compiler plugins are: CoffeeScript, Babel.js, @@ -335,6 +353,10 @@ package (compiles ES2015+ to JavaScript that can run in the browsers). ## Minifiers {#build-plugin-minifiers} +::: warning +In Meteor 3.4+ [with Rspack enabled](../about/modern-build-stack/rspack-bundler-integration.md), minifier build plugins won't be used for application code by default. Rspack includes its own optimization and minification capabilities through plugins like SwcJsMinimizerRspackPlugin for JavaScript and SwcJsMinimizerRspackPlugin for CSS. These provide efficient minification with modern optimizations and are integrated directly into the Rspack build process. +::: + Minifiers run last after the sources has been compiled and JavaScript code has been linked. Minifiers are only ran for the client programs (`web.browser` and `web.cordova`). diff --git a/v3-docs/docs/api/packages-listing.md b/v3-docs/docs/api/packages-listing.md index c4624c061e..2b283d0acf 100644 --- a/v3-docs/docs/api/packages-listing.md +++ b/v3-docs/docs/api/packages-listing.md @@ -106,6 +106,7 @@ ### [mongo-id](https://github.com/meteor/meteor/tree/devel/packages/mongo-id) {#mongo-id} ### [mongo-livedata](https://github.com/meteor/meteor/tree/devel/packages/mongo-livedata) {#mongo-livedata} ### [npm-mongo](https://github.com/meteor/meteor/tree/devel/packages/npm-mongo) {#npm-mongo} +### [npm-mongo-legacy](https://github.com/meteor/meteor/tree/devel/packages/npm-mongo-legacy) {#npm-mongo-legacy} ### [oauth](https://github.com/meteor/meteor/tree/devel/packages/oauth) {#oauth} ### [oauth-encryption](https://github.com/meteor/meteor/tree/devel/packages/oauth-encryption) {#oauth-encryption} ### [oauth1](https://github.com/meteor/meteor/tree/devel/packages/oauth1) {#oauth1} @@ -124,6 +125,7 @@ ### [retry](https://github.com/meteor/meteor/tree/devel/packages/retry) {#retry} ### [roles](https://github.com/meteor/meteor/tree/devel/packages/roles) {#roles} ### [routepolicy](https://github.com/meteor/meteor/tree/devel/packages/routepolicy) {#routepolicy} +### [rspack](https://github.com/meteor/meteor/tree/devel/packages/rspack) {#rspack} ### [server-render](https://github.com/meteor/meteor/tree/devel/packages/server-render) {#server-render} ### [service-configuration](https://github.com/meteor/meteor/tree/devel/packages/service-configuration) {#service-configuration} ### [session](https://github.com/meteor/meteor/tree/devel/packages/session) {#session} @@ -141,11 +143,11 @@ ### [test-server-tests-in-console-once](https://github.com/meteor/meteor/tree/devel/packages/test-server-tests-in-console-once) {#test-server-tests-in-console-once} ### [tinytest](https://github.com/meteor/meteor/tree/devel/packages/tinytest) {#tinytest} ### [tinytest-harness](https://github.com/meteor/meteor/tree/devel/packages/tinytest-harness) {#tinytest-harness} +### [tools-core](https://github.com/meteor/meteor/tree/devel/packages/tools-core) {#tools-core} ### [tracker](https://github.com/meteor/meteor/tree/devel/packages/tracker) {#tracker} ### [twitter-config-ui](https://github.com/meteor/meteor/tree/devel/packages/twitter-config-ui) {#twitter-config-ui} ### [twitter-oauth](https://github.com/meteor/meteor/tree/devel/packages/twitter-oauth) {#twitter-oauth} ### [typescript](https://github.com/meteor/meteor/tree/devel/packages/typescript) {#typescript} -### [underscore](https://github.com/meteor/meteor/tree/devel/packages/underscore) {#underscore} ### [url](https://github.com/meteor/meteor/tree/devel/packages/url) {#url} ### [webapp](https://github.com/meteor/meteor/tree/devel/packages/webapp) {#webapp} ### [webapp-hashing](https://github.com/meteor/meteor/tree/devel/packages/webapp-hashing) {#webapp-hashing} diff --git a/v3-docs/docs/cli/environment-variables.md b/v3-docs/docs/cli/environment-variables.md index bde20413bb..72bfb8924a 100644 --- a/v3-docs/docs/cli/environment-variables.md +++ b/v3-docs/docs/cli/environment-variables.md @@ -59,6 +59,56 @@ When running `meteor build` or `meteor deploy` you can set `METEOR_DISABLE_OPTIM Since optimistic in-memory caching is one of the more memory-intensive parts of the build system, setting the environment variable `METEOR_DISABLE_OPTIMISTIC_CACHING=1` can help improve memory usage during meteor build, which seems to improve the total build times. This configuration is perfectly safe because the whole point of optimistic caching is to keep track of previous results for future rebuilds, but in the case of meteor `build` or `deploy` there's only ever one initial build, so the extra bookkeeping is unnecessary. +## METEOR_IGNORE +(_development_) + +An alternative way to exclude files and directories from the Meteor bundler, working similarly to a [`.meteorignore`](../about/modern-build-stack/meteor-bundler-optimizations.md#meteorignore) file but configured through an environment variable. This is useful when you want to ignore certain paths without modifying your project files. + +The value should be a space-delimited list of patterns (following the same syntax as `.meteorignore`), for example: `METEOR_IGNORE="tests/* private/drafts"`. + +When both `METEOR_IGNORE` and a `.meteorignore` file are present, their patterns are combined, so you can use the environment variable to complement your existing `.meteorignore` rules. + +An interesting use case is to define different `METEOR_IGNORE` patterns per command. Since the variable is set per process, you can tailor what the bundler sees depending on whether you are running the app or running tests: + +```bash +# When developing, ignore test folders to speed up rebuilds +METEOR_IGNORE="tests" meteor run + +# When testing, ignore folders only needed for the running app +# e.g. public/ assets that are not consumed by tests +METEOR_IGNORE="public" meteor test --driver-package meteor/meteortesting:mocha +``` + +You can also exclude heavy `node_modules` sub-dependencies that are only relevant to one workflow. For example, ignore test-only packages when running the app, or ignore app-only packages when running tests: + +```bash +# Ignore test-only dependencies when running the app +METEOR_IGNORE="node_modules/puppeteer node_modules/playwright-core" meteor run + +# Ignore heavy app-only dependencies when running tests +METEOR_IGNORE="node_modules/pdfkit node_modules/sharp" meteor test --driver-package meteor/meteortesting:mocha +``` + +This way each command only processes the files it actually needs, reducing build times on both workflows without requiring changes to your project's `.meteorignore` file. + +::: info +`METEOR_IGNORE` is automatically set when using the [Rspack bundler integration](../about/modern-build-stack/rspack-bundler-integration.md). Since Rspack handles the client and server app bundling, Meteor's bundler should only worry about what it strictly needs for the Meteor-Rspack integration. By using `METEOR_IGNORE` to exclude folders and dependencies that Rspack already manages or that are irrelevant to Meteor's side of the build, you ensure the most speed is gained from the Rspack delegation. +::: + +## METEOR_LOCAL_DIR +(_development_) + +This environment variable allows you to change the location of the `.meteor/local` directory, which Meteor uses to store its build cache and other local state. This is useful for running multiple instances of the same app with different local states or for redirecting the local directory to a different drive or path. + +When using the [Rspack bundler integration](../about/modern-build-stack/rspack-bundler-integration.md), `METEOR_LOCAL_DIR` also influences the Rspack build context. It extracts the name of the folder represented in the path and appends it as a suffix to the following Rspack constants: +- `RSPACK_BUILD_CONTEXT` +- `RSPACK_CHUNKS_CONTEXT` +- `RSPACK_ASSETS_CONTEXT` + +For example, if `METEOR_LOCAL_DIR` is set to `/path/to/.meteor/local-custom`, Rspack will use `_build-local-custom`, `build-chunks-local-custom`, and `build-assets-local-custom` as its context directories, ensuring that build artifacts remain isolated for that specific local environment. + +For more information, see the [Running Multiple Instances](../about/modern-build-stack/rspack-bundler-integration.md#running-multiple-instances) section in the Rspack documentation. + ## METEOR_PROFILE (_development_) @@ -110,9 +160,22 @@ Used to generate URLs to your application by, among others, the accounts package ## TOOL_NODE_FLAGS (_development, production_) -Used to pass flags/variables to Node inside Meteor build. For example you can use this to pass a link to icu data: `TOOL_NODE_FLAGS="--icu-data-dir=node_modules/full-icu"` +Used to pass Node.js flags that Meteor will inherit and spread to other tool processes like Rspack. For example, to increase memory limits for all tools: `TOOL_NODE_FLAGS="--max-old-space-size=4096"` + +By default, these flags are automatically spread to `NODE_OPTIONS` so that tools like Rspack inherit them. This behavior can be controlled using [`TOOL_NODE_FLAGS_INHERIT`](#tool-node-flags-spread). + For full list of available flags see the [Node documentation](https://nodejs.org/dist/latest-v12.x/docs/api/cli.html). +## TOOL_NODE_FLAGS_INHERIT +(_development, production_) + +Controls whether `TOOL_NODE_FLAGS` are prepended to `NODE_OPTIONS`. Enabled by default. + +```bash +# Disable inheritance - Rspack won't inherit the heap limit +TOOL_NODE_FLAGS_INHERIT=0 TOOL_NODE_FLAGS="--max-old-space-size=4096" meteor run +``` + ## UNIX_SOCKET_GROUP (_production_) @@ -127,4 +190,3 @@ Configure Meteor's HTTP server to listen on a UNIX socket file path (e.g. `UNIX_ (_production_) This overrides the default UNIX file permissions on the UNIX socket file configured in `UNIX_SOCKET_PATH`. For example, `UNIX_SOCKET_PERMISSIONS=660` would set read/write permissions for both the user and group. - diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index 00bdfa2916..efb8527bea 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -268,17 +268,20 @@ If you run `meteor create` without arguments, Meteor will launch an interactive ### Application Types -| Option | Description | Tutorial / Example | -|--------|-------------|----------| -| `--react` | Create a React app (default) | [Meteor 3 with React](https://docs.meteor.com/tutorials/react/), [Meteor 2 with React](https://react-tutorial.meteor.com/) | -| `--vue` | Vue 3 + Tailwind CSS + Vite | [Meteor 3 with Vue](https://docs.meteor.com/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.html), [Meteor 2 with Vue](https://vue3-tutorial.meteor.com/) | -| `--svelte` | Svelte | [Meteor 2 with Svelte](https://svelte-tutorial.meteor.com/) | -| `--blaze` | Basic Blaze app | [Meteor 2 with Blaze](https://blaze-tutorial.meteor.com/) | -| `--solid` | Solid + Vite | [Meteor 2 with Solid Example](https://github.com/fredmaiaarantes/meteor-solid-app/releases/tag/milestone-2.0) | -| `--apollo` | React + Apollo (GraphQL) | [Meteor 2 with GraphQL](https://react-tutorial.meteor.com/simple-todos-graphql/) | -| `--typescript` | React + TypeScript | [TypeScript Guide](https://guide.meteor.com/build-tool.html#typescript) | -| `--tailwind` | React + Tailwind CSS | - | -| `--chakra-ui` | React + Chakra UI | [Simple Tasks Example](https://github.com/fredmaiaarantes/simpletasks) | +| Option | Description | Tutorial / Example | +|------------------|-------------------------|----------| +| `--react` | Create a React app (default) | [Meteor 3 with React](https://docs.meteor.com/tutorials/react/), [Meteor 2 with React](https://react-tutorial.meteor.com/) | +| `--vue` | Vue 3 + Tailwind CSS | [Meteor 3 with Vue](https://docs.meteor.com/tutorials/vue/meteorjs3-vue3.html), [Meteor 2 with Vue](https://vue3-tutorial.meteor.com/) | +| `--svelte` | Svelte | [Meteor 2 with Svelte](https://svelte-tutorial.meteor.com/) | +| `--blaze` | Basic Blaze app | [Meteor 2 with Blaze](https://blaze-tutorial.meteor.com/) | +| `--solid` | Solid | [Meteor 2 with Solid Example](https://github.com/fredmaiaarantes/meteor-solid-app/releases/tag/milestone-2.0) | +| `--apollo` | React + Apollo (GraphQL) | [Meteor 2 with GraphQL](https://react-tutorial.meteor.com/simple-todos-graphql/) | +| `--typescript` | React + TypeScript | [TypeScript Guide](/about/build-tool#typescript) | +| `--tailwind` | React + Tailwind CSS | - | +| `--chakra-ui` | React + Chakra UI | [Simple Tasks Example](https://github.com/fredmaiaarantes/simpletasks) | + | `--coffeescript` | CoffeeScript | - | + | `--babel` | React with Babel support | - | +| `--angular` | Angular + Typescript | - | ### Project Structure Options @@ -290,7 +293,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive | `--package` | Create a new package instead of an application | ::: warning Prototype Mode -The `--prototype` option adds packages that make development faster but shouldn't be used in production. See the [security checklist](https://guide.meteor.com/security.html#checklist). +The `--prototype` option adds packages that make development faster but shouldn't be used in production. See the [security checklist](/tutorials/security/security#checklist). ::: ### Included Packages @@ -355,7 +358,7 @@ The `--prototype` option adds packages that make development faster but shouldn' ::: tip File Structure -To learn more about the recommended file structure for Meteor apps, check the [Meteor Guide](https://guide.meteor.com/structure.html#javascript-structure). +To learn more about the recommended file structure for Meteor apps, check the [Meteor Guide](/tutorials/application-structure/#javascript-structure). ::: ## meteor generate {meteorgenerate} diff --git a/v3-docs/docs/community-packages/cluster.md b/v3-docs/docs/community-packages/cluster.md new file mode 100644 index 0000000000..04b8f6fadb --- /dev/null +++ b/v3-docs/docs/community-packages/cluster.md @@ -0,0 +1,152 @@ +# Cluster + +- `Who maintains the package` – [Bertrand Dupont](https://github.com/dupontbertrand) + +[[toc]] + +## What Is It? + +A Meteor 3 compatible fork of [`meteorhacks:cluster`](https://github.com/meteorhacks/cluster), providing multi-core support, load balancing, and service discovery for Meteor apps. + +The original package was abandoned around 2016. This fork ports it to Meteor 3 with async/await, modern dependencies, and updated MongoDB driver — while preserving the same public API. + +::: warning +This is a **compatibility / migration bridge**, not a new recommended scaling architecture. If you only need multi-core CPU utilization, an external process manager like [PM2](https://pm2.keymetrics.io/) is simpler. This package is for apps that relied on `meteorhacks:cluster` features like service discovery, DDP-aware proxying, or `Cluster.discoverConnection()`. +::: + +## How to Download It? + +```bash +meteor add dupontbertrand:cluster +``` + +## How to Use It? + +### Multi-Core (Simplest Use Case) + +Just set the `CLUSTER_WORKERS_COUNT` environment variable: + +```bash +# Use all available CPU cores +CLUSTER_WORKERS_COUNT=auto meteor run + +# Or specify a number +CLUSTER_WORKERS_COUNT=4 meteor run +``` + +The package forks child processes automatically. No code changes needed. + +### Service Discovery + Load Balancing + +For multi-instance deployments with automatic service registration and DDP-aware load balancing: + +```bash +export CLUSTER_DISCOVERY_URL="mongodb://your-mongo-host/cluster" +export CLUSTER_SERVICE="web" +export CLUSTER_WORKERS_COUNT=2 +``` + +Instances register themselves in MongoDB and discover each other automatically. No need to reconfigure a load balancer when adding or removing instances. + +### Microservices + +Connect to other services in the cluster by name: + +```js +// Server +const serviceConnection = Cluster.discoverConnection('payments'); + +// Call methods on the remote service +const result = await serviceConnection.callAsync('processPayment', data); +``` + +## API + +### `Cluster.connect(discoveryUrl, [options])` + +Connect to a discovery backend (currently MongoDB). + +```js +Cluster.connect('mongodb://host/db'); +``` + +Usually configured via the `CLUSTER_DISCOVERY_URL` environment variable instead of calling directly. + +### `Cluster.register(serviceName, [options])` + +Register this instance as a named service. + +```js +Cluster.register('web'); +``` + +Options: +- `endpoint` — the URL other instances use to reach this one (defaults to `ROOT_URL`) +- `balancer` — URL of the balancer (defaults to `CLUSTER_BALANCER_URL`) +- `uiService` — the service that serves the UI (defaults to the registered service name) + +Usually configured via the `CLUSTER_SERVICE` environment variable. + +### `Cluster.discoverConnection(serviceName, [ddpOptions])` + +Get a DDP connection to a named service. The connection automatically tracks healthy instances via the discovery backend. + +```js +const conn = Cluster.discoverConnection('analytics'); +const data = await conn.callAsync('getReport', params); +``` + +### `Cluster.allowPublicAccess(serviceList)` + +Allow public (non-authenticated) access to specific services. + +```js +Cluster.allowPublicAccess(['web', 'api']); +``` + +Usually configured via the `CLUSTER_PUBLIC_SERVICES` environment variable (comma-separated). + +## Environment Variables + +| Variable | Description | Default | +| -------- | ----------- | ------- | +| `CLUSTER_WORKERS_COUNT` | Number of worker processes (`auto` = all cores) | 1 (no clustering) | +| `CLUSTER_DISCOVERY_URL` | MongoDB URL for service discovery | — | +| `CLUSTER_SERVICE` | Name to register this instance as | — | +| `CLUSTER_ENDPOINT_URL` | URL for other instances to reach this one | `ROOT_URL` | +| `CLUSTER_BALANCER_URL` | URL of the load balancer | — | +| `CLUSTER_PUBLIC_SERVICES` | Comma-separated list of public services | — | +| `CLUSTER_UI_SERVICE` | Service that serves the UI | same as `CLUSTER_SERVICE` | + +## What Changed From meteorhacks:cluster + +This fork preserves the original API. Under the hood: + +- MongoDB discovery backend rewritten in **async/await** (was Fibers/wrapAsync) +- MongoDB driver updated from 1.4.x to **6.12.0** +- `underscore` replaced with native ES2015+ +- npm dependencies updated (`cookies`, `http-proxy`, `portscanner`) +- Fixed `Buffer()` deprecation and a pre-existing IPC listener bug +- Balancer made transport-aware for compatibility with pluggable transports ([#14231](https://github.com/meteor/meteor/pull/14231)) +- Test suite adapted for Meteor 3 + +Full changelog: [METEOR3-PORT.md](https://github.com/dupontbertrand/cluster/blob/master/METEOR3-PORT.md) + +## Known Limitations + +- The balancer uses `OverShadowServerEvent` to intercept HTTP/WS traffic at the `httpServer` level. This is invasive and could break if Meteor changes its listener initialization order. +- No Windows testing yet. +- The worker pool uses `child_process.fork` with per-worker ports (not `node:cluster`). + +## Compatibility + +- Meteor 3.4+ +- Node 22 +- Tested on Linux + +## Sources + +- **Atmosphere:** [dupontbertrand:cluster](https://atmospherejs.com/dupontbertrand/cluster) +- **GitHub:** [dupontbertrand/cluster](https://github.com/dupontbertrand/cluster) +- **Original package:** [meteorhacks/cluster](https://github.com/meteorhacks/cluster) +- **Forum Discussion:** [Memory usage and sessions quadruple after upgrading to Meteor 3](https://forums.meteor.com/t/memory-usage-and-sessions-quadruple-after-upgrading-to-meteor-3/64496) diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index ee31736a45..1d900c95de 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -24,6 +24,10 @@ Please bear in mind if you are adding a package to this list, try providing as m ## List of Community Packages +#### AI/LLM helpers + +- [Wormhole](./wormhole.md) Meteor Wormhole, MCP and REST API endpoint creator + #### Method/Subscription helpers - [`meteor-rpc`](./meteor-rpc.md), Meteor Methods Evolved with type checking and runtime validation @@ -36,6 +40,14 @@ Please bear in mind if you are adding a package to this list, try providing as m - [`jam:soft-delete`](./soft-delete.md), An easy way to add soft deletes to your Meteor app - [`jam:archive`](./archive.md), +#### Scaling / Clustering + +- [`dupontbertrand:cluster`](./cluster.md), Meteor 3 fork of meteorhacks:cluster — multi-core, load balancing, service discovery + +#### Developer tools + +- [`dupontbertrand:mail-preview`](./mail-preview.md), Zero-config dev-mode mail preview UI — view captured emails at `/__meteor_mail__/` + #### Utilities - [`jam:offline`](./offline.md), An easy way to give your Meteor app offline capabilities and make it feel instant diff --git a/v3-docs/docs/community-packages/jam-method.md b/v3-docs/docs/community-packages/jam-method.md index 0958974d6c..a8aff2ec54 100644 --- a/v3-docs/docs/community-packages/jam-method.md +++ b/v3-docs/docs/community-packages/jam-method.md @@ -262,7 +262,7 @@ You can also set all methods to be `serverOnly`. See [Configuring](#configuring- 1. Attach methods to its Collection with a dynamic import as shown above [Attach methods to its Collection (optional)](#attach-methods-to-its-collection-optional) -2. Import function(s) from a file within a `/server` folder. Any code imported from a `/server` folder will not be shipped to the client. The `/server` folder can be located anywhere within your project's file structure and you can have multiple `/server` folders. For example, you can co-locate with your collection folder, e.g. `/imports/api/todos/server/`, or it can be at the root of your project. See [Secret server code](https://guide.meteor.com/security.html#secret-code) for more info. +2. Import function(s) from a file within a `/server` folder. Any code imported from a `/server` folder will not be shipped to the client. The `/server` folder can be located anywhere within your project's file structure and you can have multiple `/server` folders. For example, you can co-locate with your collection folder, e.g. `/imports/api/todos/server/`, or it can be at the root of your project. See [Secret server code](/tutorials/security/security#secret-code) for more info. ```js export const create = createMethod({ diff --git a/v3-docs/docs/community-packages/mail-preview.md b/v3-docs/docs/community-packages/mail-preview.md new file mode 100644 index 0000000000..9f654ccc93 --- /dev/null +++ b/v3-docs/docs/community-packages/mail-preview.md @@ -0,0 +1,123 @@ +# Mail Preview + +- `Who maintains the package` – [Bertrand Dupont](https://github.com/dupontbertrand) + +[[toc]] + +## What Is It? + +A zero-config, dev-mode mail preview UI for Meteor. Every email sent via `Email.sendAsync()` is captured and displayed in a browser UI at `/__meteor_mail__/`. + +Inspired by similar features in Rails (Action Mailer Preview), Phoenix (Swoosh), Laravel (Mailtrap), and Django (console backend). + +This is a `devOnly` package — it is **automatically excluded from production builds**. Zero overhead in production, no need to remove it before deploying. + +## How to Download It? + +```bash +meteor add dupontbertrand:mail-preview +``` + +That's it. No configuration needed. + +## How to Use It? + +1. Start your Meteor app in development mode (`meteor run`) +2. Trigger any email — password reset, verification, enrollment, or your own `Email.sendAsync()` calls +3. Open `http://localhost:3000/__meteor_mail__/` in your browser + +### What You Get + +- **Mail list** — live-updating list of all captured emails (polls every 2s, no page reload) +- **Mail detail** — view each email with tabs for **HTML render**, **Plain Text**, and **HTML Source** +- **Clickable links** — verification, password reset, and enrollment links work directly from the preview +- **JSON API** — programmatic access for testing and tooling +- **Clear all** — one-click button to clear captured mails + +### Example: Capturing an Accounts Email + +```js +// Server — nothing special needed, just use Meteor's built-in accounts +import { Accounts } from 'meteor/accounts-base'; + +// When a user registers, Meteor sends a verification email. +// mail-preview captures it automatically. +Accounts.sendVerificationEmail(userId); +``` + +Then open `/__meteor_mail__/` to see the captured email, click the verification link, and it works. + +### Example: Custom Emails with MJML + +```js +// Server +import { Email } from 'meteor/email'; +import mjml2html from 'mjml'; + +const { html } = mjml2html(` + + + + + Hello from Meteor! + + Visit Meteor + + + + + +`); + +await Email.sendAsync({ + from: 'noreply@example.com', + to: 'user@example.com', + subject: 'Welcome!', + html, +}); +``` + +The MJML-rendered email is captured and displayed with full HTML rendering in the preview UI. + +## JSON API + +For programmatic access (useful in tests or tooling): + +| Method | Endpoint | Description | +| -------- | ------------------------------ | ------------------------ | +| `GET` | `/__meteor_mail__/api/mails` | List all captured mails | +| `GET` | `/__meteor_mail__/api/mails/:id` | Get a single mail | +| `DELETE` | `/__meteor_mail__/api/mails` | Clear all captured mails | + +### Example: Using the API in Tests + +```js +// In a test, after triggering an email: +const res = await fetch('http://localhost:3000/__meteor_mail__/api/mails'); +const { mails } = await res.json(); + +assert.equal(mails[0].subject, 'Verify your email'); +assert.ok(mails[0].text.includes('verify-email')); +``` + +## How It Works + +The package uses `Email.hookSend()` to intercept outgoing emails and store them in memory (up to 50 — oldest are evicted). A middleware mounted via `WebApp.rawConnectHandlers` serves the preview UI. + +- **Dev mode only** — guarded by `Meteor.isDevelopment` and `devOnly: true` in `package.js` +- **No SMTP needed** — emails are captured before they reach any transport +- **No external dependencies** — uses only Meteor core packages (`email`, `webapp`, `ecmascript`) +- **Works alongside `MAIL_URL`** — if set, emails are still sent normally; the hook captures a copy + +## Compatibility + +- Meteor 3.4+ +- Works with `accounts-password` emails (verification, reset password, enrollment) +- Works with custom `Email.sendAsync()` / `Email.send()` calls +- Compatible with Rspack bundler + +## Sources + +- **Atmosphere:** [dupontbertrand:mail-preview](https://atmospherejs.com/dupontbertrand/mail-preview) +- **GitHub:** [dupontbertrand/meteor-mail-preview](https://github.com/dupontbertrand/meteor-mail-preview) +- **Forum Discussion:** [Built-in Mail Preview UI for Dev Mode](https://forums.meteor.com/t/built-in-mail-preview-ui-for-dev-mode/64489) diff --git a/v3-docs/docs/community-packages/pub-sub.md b/v3-docs/docs/community-packages/pub-sub.md index 7015efe054..0eca082b2f 100644 --- a/v3-docs/docs/community-packages/pub-sub.md +++ b/v3-docs/docs/community-packages/pub-sub.md @@ -218,7 +218,7 @@ Meteor.subscribe('notes.all', { cache: true }) // turns caching on, overriding t > **Note**: the rest of the [Meteor.subscribe](https://docs.meteor.com/api/pubsub.html#Meteor-subscribe) API (e.g. `onStop`, `onReady`) works just as you'd expect. -> **Note**: Because the data will remain in Minimongo while the subscription is cached, you should be mindful of your Minimongo `.find` selectors. Be sure to use specific selectors to `.find` the data you need for that particular subscription. This is generally considered [best practice](https://guide.meteor.com/data-loading#fetching) so this is mainly a helpful reminder. +> **Note**: Because the data will remain in Minimongo while the subscription is cached, you should be mindful of your Minimongo `.find` selectors. Be sure to use specific selectors to `.find` the data you need for that particular subscription. This is generally considered [best practice](/tutorials/data-loading/data-loading#fetching) so this is mainly a helpful reminder. #### Clearing the cache Each individual subcription will be automatically removed from the cache when its `cacheDuration` elapses. diff --git a/v3-docs/docs/community-packages/wormhole.md b/v3-docs/docs/community-packages/wormhole.md new file mode 100644 index 0000000000..5ddd5a72f4 --- /dev/null +++ b/v3-docs/docs/community-packages/wormhole.md @@ -0,0 +1,138 @@ +# Jam Method + +- `Who maintains the package` – [William Reiske](https://github.com/wreiske/meteor-wormhole/commits?author=wreiske) + +[[toc]] + +## What Is It? + +Meteor Wormhole is a **server-only, Meteor 3.4+ package** that bridges your Meteor methods to the outside world through: + +- **[MCP (Model Context Protocol)](https://modelcontextprotocol.io/)** — The open standard for connecting AI assistants to tools and data. Your methods become MCP tools that Claude, GPT, Cursor, VS Code Copilot, and any MCP-compatible client can discover and invoke. +- **REST API** — Every exposed method also gets a `POST /api/` endpoint. +- **OpenAPI 3.1 spec** — Auto-generated from your method schemas. +- **Swagger UI** — Built-in interactive API docs at `/api/docs`. + +## How It Works + +Two lines to get started: + +```js +import { Wormhole } from 'meteor/wreiske:meteor-wormhole'; + +Wormhole.init(); // That's it — all your methods are now MCP tools +``` + +By default it runs in **"all-in" mode**, which automatically exposes every `Meteor.methods()` call (minus DDP internals, private `_`-prefixed methods, and Accounts methods). You can also run in **"opt-in" mode** for explicit control: + +```js +Wormhole.init({ mode: 'opt-in' }); + +Wormhole.expose('todos.add', { + description: 'Add a new todo item', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'The todo title' }, + priority: { type: 'string', enum: ['low', 'medium', 'high'] } + }, + required: ['title'] + } +}); +``` + +Add richer schemas and descriptions, and AI agents get better context about what your tools do and how to call them. + +## Features at a Glance + +- **Zero-config MCP server** — Streamable HTTP transport at `/mcp`, session management, JSON-RPC 2.0 +- **Optional REST bridge** — Enable with `rest: { enabled: true }` for traditional HTTP clients +- **Auto-generated OpenAPI 3.1 spec** with Swagger UI +- **Optional API key auth** — Covers both MCP and REST endpoints +- **Smart exclusions** — Automatically skips DDP internals, `_private` methods, and Accounts methods; add your own patterns +- **Input validation** — JSON Schema → Zod conversion for parameter validation +- **Error propagation** — `Meteor.Error` details are properly passed through to clients +- **Enrich existing methods** — Add descriptions and schemas to auto-registered methods with `Wormhole.expose()` + +## Configuration Options + +```js +Wormhole.init({ + mode: 'all', // 'all' or 'opt-in' + path: '/mcp', // MCP endpoint path + name: 'my-app', // MCP server name + apiKey: 'secret', // Optional bearer token auth + exclude: [/^admin\./], // Additional exclusion patterns + rest: { + enabled: true, // Enable REST API + path: '/api', // REST base path + docs: true // Swagger UI at /api/docs + } +}); +``` + +## Point Your MCP Client at It + +If you use Claude Desktop, Cursor, VS Code Copilot, or any other MCP-compatible client, you can connect to a Wormhole-enabled app and your AI assistant will immediately see all the exposed methods as callable tools. Just point it at your app's `/mcp` endpoint. + +## API Reference + +### `Wormhole.init(options)` + +Initialize the MCP bridge. + +| Option | Type | Default | Description | +| --------- | ---------------------- | ------------------- | ---------------------------------- | +| `mode` | `'all' \| 'opt-in'` | `'all'` | Exposure mode | +| `path` | `string` | `'/mcp'` | HTTP endpoint path | +| `name` | `string` | `'meteor-wormhole'` | MCP server name | +| `version` | `string` | `'1.0.0'` | MCP server version | +| `apiKey` | `string \| null` | `null` | Bearer token for auth | +| `exclude` | `(string \| RegExp)[]` | `[]` | Methods to exclude (all-in mode) | +| `rest` | `object \| boolean` | `false` | REST API configuration (see below) | + +#### `rest` options + +| Option | Type | Default | Description | +| --------- | ---------------- | ----------- | -------------------------------------------- | +| `enabled` | `boolean` | `false` | Enable REST endpoints | +| `path` | `string` | `'/api'` | Base path for REST endpoints | +| `docs` | `boolean` | `true` | Serve Swagger UI at `/docs` | +| `apiKey` | `string \| null` | _inherited_ | API key for REST (defaults to main `apiKey`) | + +Shorthand: `rest: true` enables REST with all defaults. + +### `Wormhole.expose(methodName, options)` + +Explicitly expose a method as an MCP tool. + +| Option | Type | Description | +| -------------- | -------- | --------------------------------------------------------------------------------------- | +| `description` | `string` | Human-readable tool description | +| `inputSchema` | `object` | JSON Schema for method parameters | +| `outputSchema` | `object` | JSON Schema for the return value (wrapped inside `{ result }` envelope in OpenAPI/REST) | + +### `Wormhole.unexpose(methodName)` + +Remove a method from MCP exposure. + +## How It Works + +1. **Registration**: In all-in mode, the package monkey-patches `Meteor.methods` to intercept every method registration. In opt-in mode, you call `Wormhole.expose()` manually. + +2. **MCP Server**: A Streamable HTTP MCP server is mounted at the configured path (default `/mcp`) on Meteor's `WebApp`. + +3. **Tool Mapping**: Each exposed Meteor method becomes an MCP tool. Method names are sanitized (e.g., `todos.add` → `todos_add`). + +4. **Invocation**: When an AI agent calls a tool, the bridge invokes the corresponding Meteor method via `Meteor.callAsync()` and returns the result. + +5. **REST API** (optional): When enabled, a parallel REST bridge mounts at the configured path. Each method gets a `POST` endpoint. An OpenAPI 3.1 spec is auto-generated from the registry's metadata and input schemas, and Swagger UI provides interactive documentation. + + +## Links + +- **GitHub:** https://github.com/wreiske/meteor-wormhole +- **Live Demo:** https://wormhole.meteorapp.com/ +- **Swagger UI:** https://wormhole.meteorapp.com/api/docs +- **Atmosphere:** [https://atmospherejs.com/wreiske/meteor-wormhole](https://atmospherejs.com/wreiske/meteor-wormhole) +- **Packosphere:** [https://packosphere.com/wreiske/meteor-wormhole](https://packosphere.com/wreiske/meteor-wormhole) diff --git a/v3-docs/docs/about/contributing.md b/v3-docs/docs/community/contributing.md similarity index 100% rename from v3-docs/docs/about/contributing.md rename to v3-docs/docs/community/contributing.md diff --git a/v3-docs/docs/community/contributors.data.ts b/v3-docs/docs/community/contributors.data.ts new file mode 100644 index 0000000000..6edc582ecb --- /dev/null +++ b/v3-docs/docs/community/contributors.data.ts @@ -0,0 +1,60 @@ +export interface Contributor { + login: string + avatar_url: string + html_url: string + contributions: number +} + +declare const data: Contributor[] +export { data } + +export default { + async load(): Promise { + const contributors: Contributor[] = [] + let page = 1 + const perPage = 100 + + while (true) { + const headers: Record = { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'meteor-docs-builder', + } + + if (process.env.GITHUB_TOKEN) { + headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}` + } + + const response = await fetch( + `https://api.github.com/repos/meteor/meteor/contributors?per_page=${perPage}&page=${page}`, + { headers } + ) + + if (!response.ok) { + console.warn( + `[contributors.data] GitHub API error: ${response.status} ${response.statusText}` + ) + break + } + + const batch = (await response.json()) as any[] + + if (!batch.length) break + + contributors.push( + ...batch + .filter((c) => c.type === 'User') + .map((c) => ({ + login: c.login, + avatar_url: c.avatar_url, + html_url: c.html_url, + contributions: c.contributions, + })) + ) + + if (batch.length < perPage) break + page++ + } + + return contributors + }, +} diff --git a/v3-docs/docs/community/contributors.md b/v3-docs/docs/community/contributors.md new file mode 100644 index 0000000000..c5e93f953b --- /dev/null +++ b/v3-docs/docs/community/contributors.md @@ -0,0 +1,43 @@ +--- +outline: false +--- + + + +# Contributors + +## Technical Committee + +The Technical Committee is responsible for the direction and governance of the Meteor project. + + + +## Core Maintainers + +Core Maintainers are experienced contributors who actively maintain key areas of the Meteor codebase. + + + +## All Contributors + +Thank you to all the amazing people who have contributed to Meteor! This list is automatically generated from the [meteor/meteor](https://github.com/meteor/meteor) GitHub repository, sorted by number of commits. + + diff --git a/v3-docs/docs/components/helpers/ParamTable.vue b/v3-docs/docs/components/helpers/ParamTable.vue index 8251d3a7a1..65a6c6d4b9 100644 --- a/v3-docs/docs/components/helpers/ParamTable.vue +++ b/v3-docs/docs/components/helpers/ParamTable.vue @@ -1,14 +1,14 @@ @@ -51,9 +55,7 @@ const sourceCode = `https://github.com/meteor/meteor/blob/devel/packages/${props

    Arguments:

    - - Source code - + Source code
    @@ -71,7 +73,11 @@ const sourceCode = `https://github.com/meteor/meteor/blob/devel/packages/${props