mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.4.1' into fix-skip-lib-check
This commit is contained in:
111
.github/skills/ai-context/SKILL.md
vendored
Normal file
111
.github/skills/ai-context/SKILL.md
vendored
Normal file
@@ -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/<topic>/SKILL.md # On-demand detailed context
|
||||
packages/<name>/AGENTS.md # Package-specific context
|
||||
<any-folder>/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 |
|
||||
|-------|-------------|
|
||||
| [<name>](.github/skills/<name>/SKILL.md) | <description> |
|
||||
```
|
||||
|
||||
Keep in sync with the skills table in `AGENTS.md`.
|
||||
|
||||
## Skills
|
||||
|
||||
### Creating a Skill
|
||||
|
||||
1. Create `.github/skills/<topic>/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: <topic>
|
||||
description: <when an agent should load this — be specific about triggers>
|
||||
---
|
||||
|
||||
# <Title>
|
||||
|
||||
<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
|
||||
110
.github/skills/codebase/SKILL.md
vendored
Normal file
110
.github/skills/codebase/SKILL.md
vendored
Normal file
@@ -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`
|
||||
188
.github/skills/conventions/SKILL.md
vendored
Normal file
188
.github/skills/conventions/SKILL.md
vendored
Normal file
@@ -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
|
||||
246
.github/skills/modern-tools/SKILL.md
vendored
Normal file
246
.github/skills/modern-tools/SKILL.md
vendored
Normal file
@@ -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
|
||||
152
.github/skills/packages/SKILL.md
vendored
Normal file
152
.github/skills/packages/SKILL.md
vendored
Normal file
@@ -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 |
|
||||
144
.github/skills/testing/SKILL.md
vendored
Normal file
144
.github/skills/testing/SKILL.md
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
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)
|
||||
PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh
|
||||
|
||||
# Modern E2E tests (Jest + Playwright)
|
||||
npm run install:modern # Install dependencies
|
||||
npm run test:modern # Run all E2E tests
|
||||
npm run test:modern -- -t="React" # Run specific test
|
||||
```
|
||||
|
||||
## Modern E2E Tests (`tools/modern-tests/`)
|
||||
|
||||
Jest + Playwright suite for verifying modern 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);
|
||||
});
|
||||
```
|
||||
86
.github/workflows/e2e-tests.yml
vendored
Normal file
86
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'meteor'
|
||||
- 'tools/modern-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:
|
||||
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
|
||||
- Library
|
||||
- Monorepo
|
||||
- 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/modern-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:modern
|
||||
|
||||
- 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:modern -- -t="${{ matrix.category }}"
|
||||
2
.github/workflows/inactive-issues.yml
vendored
2
.github/workflows/inactive-issues.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Manage inactive issues
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/inactive-issues.js')
|
||||
|
||||
52
.github/workflows/test-packages.yml
vendored
Normal file
52
.github/workflows/test-packages.yml
vendored
Normal file
@@ -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
|
||||
10
.github/workflows/windows-selftest.yml
vendored
10
.github/workflows/windows-selftest.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
@@ -60,16 +60,10 @@ jobs:
|
||||
~/.npm
|
||||
node_modules/
|
||||
packages/**/.npm
|
||||
key: ${{ runner.os }}-meteor-${{ hashFiles('**/package-lock.json', 'meteor', 'meteor.bat') }}
|
||||
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: |
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ node_modules
|
||||
\#*\#
|
||||
.\#*
|
||||
.idea
|
||||
!.idea/icon.svg
|
||||
*.iml
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
21
.idea/icon.svg
generated
Executable file
21
.idea/icon.svg
generated
Executable file
@@ -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>
|
||||
60
AGENTS.md
Normal file
60
AGENTS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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
|
||||
npm run test:modern # E2E tests (Jest + Playwright)
|
||||
```
|
||||
|
||||
## 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 |
|
||||
| [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
|
||||
14
CLAUDE.md
Normal file
14
CLAUDE.md
Normal file
@@ -0,0 +1,14 @@
|
||||
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 |
|
||||
| [ai-context](.github/skills/ai-context/SKILL.md) | Creating, updating, or maintaining AI documentation files |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
2
meteor
2
meteor
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BUNDLE_VERSION=22.18.0.3
|
||||
BUNDLE_VERSION=22.22.0.3
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
npm-packages/meteor-installer/package-lock.json
generated
4
npm-packages/meteor-installer/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "meteor",
|
||||
"version": "3.3.2",
|
||||
"version": "3.4.0",
|
||||
"description": "Install Meteor",
|
||||
"main": "install.js",
|
||||
"scripts": {
|
||||
|
||||
99
npm-packages/meteor-rspack/index.d.ts
vendored
Normal file
99
npm-packages/meteor-rspack/index.d.ts
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 Rspack SWC loader config.
|
||||
* @returns A config object with SWC loader config
|
||||
*/
|
||||
extendSwcConfig: (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>;
|
||||
}
|
||||
|
||||
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 };
|
||||
28
npm-packages/meteor-rspack/index.js
Normal file
28
npm-packages/meteor-rspack/index.js
Normal file
@@ -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;
|
||||
139
npm-packages/meteor-rspack/lib/ignore.js
Normal file
139
npm-packages/meteor-rspack/lib/ignore.js
Normal file
@@ -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,
|
||||
};
|
||||
325
npm-packages/meteor-rspack/lib/mergeRulesSplitOverlap.js
Normal file
325
npm-packages/meteor-rspack/lib/mergeRulesSplitOverlap.js
Normal file
@@ -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
|
||||
};
|
||||
99
npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js
Normal file
99
npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js
Normal file
@@ -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,
|
||||
};
|
||||
213
npm-packages/meteor-rspack/lib/meteorRspackHelpers.js
Normal file
213
npm-packages/meteor-rspack/lib/meteorRspackHelpers.js
Normal file
@@ -0,0 +1,213 @@
|
||||
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
|
||||
* Usage: extendSwcConfig()
|
||||
*
|
||||
* @returns {Record<string, object>} `{ meteorRspackConfigX: { optimization: { ... } } }`
|
||||
*/
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compileWithMeteor,
|
||||
compileWithRspack,
|
||||
setCache,
|
||||
splitVendorChunk,
|
||||
extendSwcConfig,
|
||||
makeWebNodeBuiltinsAlias,
|
||||
disablePlugins,
|
||||
};
|
||||
67
npm-packages/meteor-rspack/lib/swc.js
Normal file
67
npm-packages/meteor-rspack/lib/swc.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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')) {
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
// Check if the content uses ES module syntax (export default)
|
||||
if (content.includes('export default')) {
|
||||
// Transform ES module syntax to CommonJS
|
||||
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 });
|
||||
return script.runInContext(context);
|
||||
} 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`);
|
||||
|
||||
if (!hasSwcRc && !hasSwcJs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const swcFile = 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
|
||||
};
|
||||
80
npm-packages/meteor-rspack/lib/test.js
Normal file
80
npm-packages/meteor-rspack/lib/test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js");
|
||||
|
||||
/**
|
||||
* 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.extraEntry - Extra entry to load
|
||||
* @returns {string} The path to the generated file
|
||||
*/
|
||||
const generateEagerTestFile = ({
|
||||
isAppTest,
|
||||
projectDir,
|
||||
buildContext,
|
||||
ignoreEntries: inIgnoreEntries = [],
|
||||
prefix: inPrefix = '',
|
||||
extraEntry,
|
||||
}) => {
|
||||
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)
|
||||
);
|
||||
|
||||
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 = `{
|
||||
const ctx = import.meta.webpackContext('/', {
|
||||
recursive: true,
|
||||
regExp: ${regExp},
|
||||
exclude: ${excludeFoldersRegex.toString()},
|
||||
mode: 'eager',
|
||||
});
|
||||
ctx.keys().forEach(ctx);
|
||||
${
|
||||
extraEntry
|
||||
? `const extra = import.meta.webpackContext('${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,
|
||||
};
|
||||
5470
npm-packages/meteor-rspack/package-lock.json
generated
Normal file
5470
npm-packages/meteor-rspack/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
npm-packages/meteor-rspack/package.json
Normal file
19
npm-packages/meteor-rspack/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@meteorjs/rspack",
|
||||
"version": "1.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
40
npm-packages/meteor-rspack/plugins/AssetExternalsPlugin.js
Normal file
40
npm-packages/meteor-rspack/plugins/AssetExternalsPlugin.js
Normal file
@@ -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 };
|
||||
31
npm-packages/meteor-rspack/plugins/HtmlRspackPlugin.js
Normal file
31
npm-packages/meteor-rspack/plugins/HtmlRspackPlugin.js
Normal file
@@ -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;
|
||||
496
npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js
Normal file
496
npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js
Normal file
@@ -0,0 +1,496 @@
|
||||
// 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');
|
||||
|
||||
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('../') ||
|
||||
!!depInfo.ext)
|
||||
) {
|
||||
const module = this.externalsMeta.get(pkg);
|
||||
if (module) {
|
||||
return `${this.backRoot}${module.relativeRequest}`;
|
||||
}
|
||||
return `${this.backRoot}${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: 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 };
|
||||
50
npm-packages/meteor-rspack/plugins/RspackMeteorHtmlPlugin.js
Normal file
50
npm-packages/meteor-rspack/plugins/RspackMeteorHtmlPlugin.js
Normal file
@@ -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;
|
||||
811
npm-packages/meteor-rspack/rspack.config.js
Normal file
811
npm-packages/meteor-rspack/rspack.config.js
Normal file
@@ -0,0 +1,811 @@
|
||||
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 { generateEagerTestFile } = require("./lib/test.js");
|
||||
const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
|
||||
const { mergeMeteorRspackFragments } = require("./lib/meteorRspackConfigFactory.js");
|
||||
const {
|
||||
compileWithMeteor,
|
||||
compileWithRspack,
|
||||
setCache,
|
||||
splitVendorChunk,
|
||||
extendSwcConfig,
|
||||
makeWebNodeBuiltinsAlias,
|
||||
disablePlugins,
|
||||
} = require('./lib/meteorRspackHelpers.js');
|
||||
const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
|
||||
|
||||
// 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 } = {}) {
|
||||
// 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 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);
|
||||
|
||||
// Build dependencies array
|
||||
const buildDependencies = [
|
||||
...(projectConfigPath ? [projectConfigPath] : []),
|
||||
...(configPath ? [configPath] : []),
|
||||
...(hasTsconfig ? [tsconfigPath] : []),
|
||||
...(hasBabelRcConfig ? [babelRcConfig] : []),
|
||||
...(hasBabelJsConfig ? [babelJsConfig] : []),
|
||||
...(hasSwcrcConfig ? [swcrcPath] : []),
|
||||
...(hasSwcJsConfig ? [swcJsPath] : []),
|
||||
...(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${(side && `/${side}`) || ""}`,
|
||||
},
|
||||
...(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 isProd = !!Meteor.isProduction || argv.mode === 'production';
|
||||
const isDev = !!Meteor.isDevelopment || !isProd;
|
||||
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 isTestLike = !!Meteor.isTestLike;
|
||||
const swcExternalHelpers = !!Meteor.swcExternalHelpers;
|
||||
const isNative = !!Meteor.isNative;
|
||||
const mode = isProd ? 'production' : 'development';
|
||||
const projectDir = process.cwd();
|
||||
const projectConfigPath = Meteor.projectConfigPath || path.resolve(projectDir, 'rspack.config.js');
|
||||
const configPath = Meteor.configPath;
|
||||
const testEntry = Meteor.testEntry;
|
||||
const testClientEntry = Meteor.testClientEntry;
|
||||
const testServerEntry = Meteor.testServerEntry;
|
||||
|
||||
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;
|
||||
|
||||
// Determine entry points
|
||||
const entryPath = Meteor.entryPath;
|
||||
|
||||
// Determine output points
|
||||
const outputPath = Meteor.outputPath;
|
||||
const outputDir = path.dirname(Meteor.outputPath || '');
|
||||
|
||||
const outputFilename = Meteor.outputFilename;
|
||||
|
||||
// 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');
|
||||
|
||||
// Determine context for bundles and assets
|
||||
const buildContext = Meteor.buildContext || '_build';
|
||||
const assetsContext = Meteor.assetsContext || 'build-assets';
|
||||
const chunksContext = Meteor.chunksContext || 'build-chunks';
|
||||
|
||||
// Determine build output and pass to Meteor
|
||||
const buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
|
||||
Meteor.buildOutputDir = buildOutputDir;
|
||||
|
||||
const cacheStrategy = createCacheStrategy(
|
||||
mode,
|
||||
(Meteor.isClient && 'client') || 'server',
|
||||
{ projectConfigPath, configPath }
|
||||
);
|
||||
|
||||
// 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(customSwcConfig);
|
||||
Meteor.extendConfig = (...configs) => mergeSplitOverlap(...configs);
|
||||
Meteor.disablePlugins = matchers => prepareMeteorRspackConfig({
|
||||
disablePlugins: matchers,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
};
|
||||
|
||||
// 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 enableSwcExternalHelpers = !isServer && swcExternalHelpers;
|
||||
const isDevEnvironment = isRun && isDev && !isTest && !isNative;
|
||||
const swcConfigRule = createSwcConfig({
|
||||
isTypescriptEnabled,
|
||||
isReactEnabled,
|
||||
isJsxEnabled,
|
||||
isTsxEnabled,
|
||||
externalHelpers: enableSwcExternalHelpers,
|
||||
isDevEnvironment,
|
||||
isClient,
|
||||
isAngularEnabled,
|
||||
});
|
||||
// Expose swc config to use in custom configs
|
||||
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 clientEntry =
|
||||
isTest && isTestEager && isTestFullApp
|
||||
? generateEagerTestFile({
|
||||
isAppTest: true,
|
||||
projectDir,
|
||||
buildContext,
|
||||
ignoreEntries: [...meteorIgnoreEntries, "**/server/**"],
|
||||
prefix: "client",
|
||||
extraEntry: path.resolve(process.cwd(), Meteor.mainClientEntry),
|
||||
})
|
||||
: isTest && isTestEager
|
||||
? generateEagerTestFile({
|
||||
isAppTest: false,
|
||||
isClient: true,
|
||||
projectDir,
|
||||
buildContext,
|
||||
ignoreEntries: [...meteorIgnoreEntries, "**/server/**"],
|
||||
prefix: "client",
|
||||
})
|
||||
: isTest && testEntry
|
||||
? path.resolve(process.cwd(), testEntry)
|
||||
: isTest && testClientEntry
|
||||
? path.resolve(process.cwd(), testClientEntry)
|
||||
: 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),
|
||||
'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: Meteor.devServerPort || 8080,
|
||||
devMiddleware: {
|
||||
writeToDisk: filePath =>
|
||||
/\.(html)$/.test(filePath) && !filePath.includes('.hot-update.'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
...merge(cacheStrategy, { experiments: { css: true } }),
|
||||
...lazyCompilationConfig,
|
||||
};
|
||||
|
||||
const serverEntry =
|
||||
isTest && isTestEager && isTestFullApp
|
||||
? generateEagerTestFile({
|
||||
isAppTest: true,
|
||||
projectDir,
|
||||
buildContext,
|
||||
ignoreEntries: [...meteorIgnoreEntries, "**/client/**"],
|
||||
prefix: "server",
|
||||
})
|
||||
: isTest && isTestEager
|
||||
? generateEagerTestFile({
|
||||
isAppTest: false,
|
||||
projectDir,
|
||||
buildContext,
|
||||
ignoreEntries: [...meteorIgnoreEntries, "**/client/**"],
|
||||
prefix: "server",
|
||||
})
|
||||
: isTest && testEntry
|
||||
? path.resolve(process.cwd(), testEntry)
|
||||
: isTest && testServerEntry
|
||||
? path.resolve(process.cwd(), testServerEntry)
|
||||
: 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),
|
||||
'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),
|
||||
'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,
|
||||
};
|
||||
|
||||
// Helper function to load and process config files
|
||||
async function loadAndProcessConfig(configPath, configType, Meteor, argv, isAngularEnabled) {
|
||||
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 (isAngularEnabled) 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Load and apply project-level overrides for the selected build
|
||||
// Check if we're in a Meteor package directory by looking at the path
|
||||
const isMeteorPackageConfig = projectDir.includes('/packages/rspack');
|
||||
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;
|
||||
}
|
||||
|
||||
const nextUserConfig = await loadAndProcessConfig(
|
||||
projectConfigPathToUse,
|
||||
'rspack.config.js',
|
||||
Meteor,
|
||||
argv,
|
||||
isAngularEnabled
|
||||
);
|
||||
|
||||
if (nextUserConfig) {
|
||||
if (Meteor.isClient) {
|
||||
clientConfig = mergeSplitOverlap(clientConfig, nextUserConfig);
|
||||
}
|
||||
if (Meteor.isServer) {
|
||||
serverConfig = mergeSplitOverlap(serverConfig, nextUserConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Establish Angular overrides to ensure proper integration
|
||||
const angularExpandConfig = isAngularEnabled
|
||||
? {
|
||||
mode: isProd ? "production" : "development",
|
||||
devServer: { port: Meteor.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()],
|
||||
}
|
||||
: {};
|
||||
|
||||
let config = mergeSplitOverlap(
|
||||
isClient ? clientConfig : serverConfig,
|
||||
angularExpandConfig
|
||||
);
|
||||
config = mergeSplitOverlap(config, testClientExpandConfig);
|
||||
|
||||
// Check for override config file (extra file to override everything)
|
||||
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)) {
|
||||
const nextOverrideConfig = await loadAndProcessConfig(
|
||||
overrideConfigPath,
|
||||
configNameFull,
|
||||
Meteor,
|
||||
argv,
|
||||
isAngularEnabled
|
||||
);
|
||||
|
||||
if (nextOverrideConfig) {
|
||||
// Apply override config as the last step
|
||||
config = mergeSplitOverlap(config, nextOverrideConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldDisablePlugins = config?.disablePlugins != null;
|
||||
if (shouldDisablePlugins) {
|
||||
config = disablePlugins(config, config.disablePlugins);
|
||||
delete config.disablePlugins;
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
return [config];
|
||||
}
|
||||
1100
package-lock.json
generated
1100
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,9 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js"
|
||||
"install:modern": "cd tools/modern-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell",
|
||||
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js",
|
||||
"test:modern": "cd tools/modern-tests && npm test -- "
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 11
|
||||
|
||||
@@ -123,7 +123,7 @@ Meteor.methods({
|
||||
);
|
||||
},
|
||||
async has2faEnabled() {
|
||||
return await Accounts._is2faEnabledForUser();
|
||||
return Accounts._is2faEnabledForUser();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
6
packages/accounts-base/accounts-base.d.ts
vendored
6
packages/accounts-base/accounts-base.d.ts
vendored
@@ -362,11 +362,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 +387,7 @@ export namespace Accounts {
|
||||
function _checkPasswordAsync(
|
||||
user: Meteor.User,
|
||||
password: Password
|
||||
): Promise<{ userId: string; error?: any }>
|
||||
): Promise<{ userId: string; error?: any }>;
|
||||
}
|
||||
|
||||
export namespace Accounts {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -537,7 +531,7 @@ export class AccountsServer extends AccountsCommon {
|
||||
type,
|
||||
fn
|
||||
) {
|
||||
return await this._attemptLogin(
|
||||
return this._attemptLogin(
|
||||
methodInvocation,
|
||||
methodName,
|
||||
methodArgs,
|
||||
@@ -668,7 +662,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 +678,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 +691,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 +731,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 +965,8 @@ export class AccountsServer extends AccountsCommon {
|
||||
_clearAllLoginTokens(userId) {
|
||||
this.users.updateAsync(userId, {
|
||||
$set: {
|
||||
'services.resume.loginTokens': []
|
||||
}
|
||||
'services.resume.loginTokens': [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1565,9 +1569,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");
|
||||
|
||||
@@ -32,7 +32,7 @@ Meteor.methods({
|
||||
},
|
||||
}
|
||||
);
|
||||
return await getTokenFromSecret({ selector, secret });
|
||||
return getTokenFromSecret({ selector, secret });
|
||||
},
|
||||
getTokenFromSecret,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "A user account system",
|
||||
version: "3.1.2",
|
||||
version: "3.2.0",
|
||||
});
|
||||
|
||||
Package.onUse((api) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Accounts._connectionCloseDelayMsForTests = 1000;
|
||||
Accounts._options.ambiguousErrorMessages = false;
|
||||
|
||||
const makeTestConnAsync =
|
||||
(test) =>
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -1415,9 +1417,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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
551
packages/accounts-ui-unstyled/login_buttons.import.css
vendored
Normal file
551
packages/accounts-ui-unstyled/login_buttons.import.css
vendored
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
2
packages/accounts-ui/login_buttons.css
Normal file
2
packages/accounts-ui/login_buttons.css
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Import the CSS from accounts-ui-unstyled */
|
||||
@import url("{accounts-ui-unstyled}/login_buttons.import.css");
|
||||
@@ -1 +0,0 @@
|
||||
@import "{accounts-ui-unstyled}/login_buttons.import.less";
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -142,16 +142,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 +186,6 @@ BCp.processFilesForTarget = function (inputFiles) {
|
||||
|
||||
this.initializeMeteorAppConfig();
|
||||
this.initializeMeteorAppSwcrc();
|
||||
this.initializeMeteorAppLegacyConfig();
|
||||
this.initializeMeteorAppSwcHelpersAvailable();
|
||||
|
||||
inputFiles.forEach(function (inputFile) {
|
||||
@@ -242,6 +231,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 +281,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 +357,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 +371,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 +419,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;
|
||||
@@ -1096,14 +1140,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 +1172,7 @@ function logTranspilation({
|
||||
: color(originPaddedRaw, 35);
|
||||
const cacheStatus = errorMessage
|
||||
? color('⚠️ Fallback', 33)
|
||||
: usedSwc
|
||||
: usedSwc || usedRspack
|
||||
? cacheHit
|
||||
? color('🟢 Cache hit', 32)
|
||||
: color('🔴 Cache miss', 31)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>',
|
||||
|
||||
1
packages/check/check.d.ts
vendored
1
packages/check/check.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "The Meteor command-line tool",
|
||||
version: "3.3.1",
|
||||
version: "3.4.0",
|
||||
});
|
||||
|
||||
Package.includeTool();
|
||||
|
||||
57
packages/meteor/meteor.d.ts
vendored
57
packages/meteor/meteor.d.ts
vendored
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Core Meteor environment",
|
||||
version: '2.1.1',
|
||||
version: '2.2.0',
|
||||
});
|
||||
|
||||
Package.registerBuildPlugin({
|
||||
|
||||
@@ -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"] });
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "JavaScript minifier",
|
||||
version: '3.0.4',
|
||||
version: '3.1.0',
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
|
||||
@@ -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]]}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
|
||||
146
packages/mongo/collection/collection_extensions.js
Normal file
146
packages/mongo/collection/collection_extensions.js
Normal file
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
209
packages/mongo/mongo.d.ts
vendored
209
packages/mongo/mongo.d.ts
vendored
@@ -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: {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
233
packages/mongo/tests/collection_extensions_tests.js
Normal file
233
packages/mongo/tests/collection_extensions_tests.js
Normal file
@@ -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();
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
3
packages/react-fast-refresh/package.js
vendored
3
packages/react-fast-refresh/package.js
vendored
@@ -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({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Authorization package for Meteor",
|
||||
version: "1.0.1",
|
||||
version: "1.0.2",
|
||||
name: "roles",
|
||||
documentation: null,
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1596,6 +1596,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) {
|
||||
|
||||
3
packages/rspack/README.md
Normal file
3
packages/rspack/README.md
Normal file
@@ -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.
|
||||
609
packages/rspack/lib/build-context.js
Normal file
609
packages/rspack/lib/build-context.js
Normal file
@@ -0,0 +1,609 @@
|
||||
/**
|
||||
* @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,
|
||||
} = 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;
|
||||
}
|
||||
}
|
||||
|
||||
addGitignoreEntries(
|
||||
appDir,
|
||||
[
|
||||
RSPACK_BUILD_CONTEXT,
|
||||
`*/${RSPACK_ASSETS_CONTEXT}`,
|
||||
`*/${RSPACK_CHUNKS_CONTEXT}`,
|
||||
RSPACK_DOCTOR_CONTEXT,
|
||||
],
|
||||
'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 }),
|
||||
};
|
||||
const testServerFiles = {
|
||||
entryFile: initialEntrypoints.testServer || '',
|
||||
outputFile: getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output, onlyFilename: true }),
|
||||
};
|
||||
|
||||
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, isTestModule, isClient: true, ...commandRole })]:
|
||||
getBuildFileContent({ isTest: true, isTestModule, isClient: true, ...commandRole, ...testClientFiles }),
|
||||
[getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.entry })]:
|
||||
getBuildFileContent({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.entry, ...testClientFiles }),
|
||||
[getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output })]:
|
||||
getBuildFileContent({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output, ...testClientFiles }),
|
||||
[getBuildFilePath({ isTest: true, isTestModule, isServer: true, ...commandRole })]:
|
||||
getBuildFileContent({ isTest: true, isTestModule, isServer: true, ...commandRole, ...testServerFiles }),
|
||||
[getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.entry })]:
|
||||
getBuildFileContent({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.entry, ...testServerFiles }),
|
||||
[getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output })]:
|
||||
getBuildFileContent({ isTest: true, 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) {
|
||||
// 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) {
|
||||
// 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) {
|
||||
// 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) {
|
||||
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) {
|
||||
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) {
|
||||
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 (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 && role === FILE_ROLE.entry) {
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user