Merge branch 'release-3.4.1' into typescript-tailwind-skeleton

This commit is contained in:
Nacho Codoñer
2026-03-19 16:33:19 +01:00
committed by GitHub
417 changed files with 4215 additions and 1081 deletions

50
.github/skills/e2e-coverage/SKILL.md vendored Normal file
View File

@@ -0,0 +1,50 @@
---
name: e2e-coverage
description: Use when adding, modifying, or reviewing E2E test apps/skeletons to keep the test coverage report up to date.
---
# E2E Test Coverage Report
Guidelines for maintaining `dev/modern-tools/rspack/E2E_COVERAGE.md` — a single-page report of what every E2E app and skeleton tests.
## When to Update
| Trigger | Action |
|---------|--------|
| New app added to `apps/` | Add a subsection under **Apps** with a coverage table |
| New skeleton added to `skeleton.test.js` | Add a row to the **Skeletons** table |
| New npm package imported for compatibility testing | Add an entry under **NPM Package Compatibility** with the package name, file, and reason |
| New custom assertion added to a test file | Add a row to that app's coverage table |
| New feature tested across multiple apps | Add a row to the **Feature Coverage Matrix** |
| App or skeleton removed | Remove its entries from all sections |
## Report Structure
The report has five sections, in this order:
1. **Test Lifecycle** — the phases every app/skeleton goes through (init, run, prod, test, test once, build) and what default assertions apply
2. **Apps** — one subsection per `apps/<name>/` with a short description and a `| What is covered | Phase |` table
3. **Skeletons** — single table with one row per skeleton (`| Skeleton | Port | Language | Extra coverage |`)
4. **NPM Package Compatibility** — grouped by app, each entry has the package name, file path, and why it's included (ESM-only, native bindings, subpath exports, etc.)
5. **Feature Coverage Matrix** — cross-reference table (`| Feature | Apps | Skeletons |`) showing where each capability is tested
## How to Gather Information
For each app or skeleton, check these sources:
| Source | What to look for |
|--------|-----------------|
| `<name>.test.js` | Test helper used, options (`env`, `configFile`, `buildDir`, `testFullApp`, `checkBundleFilePaths`), all `customAssertions` callbacks and what they assert |
| `skeleton.test.js` | The `testMeteorSkeleton({ skeletonName: '<name>' })` block for that skeleton |
| `apps/<name>/server/main.js` | npm imports with comments explaining why (ESM-only, native bindings, etc.) |
| `apps/<name>/imports/` | Shared code with special imports (`node:` protocol, JSX packages) |
| `apps/<name>/rspack.config.*` | Custom config features (`compileWithRspack`, `compileWithMeteor`, `disablePlugins`, custom rules) |
| `apps/<name>/package.json` | Dependencies that exist solely for compatibility testing |
## Writing Guidelines
- Keep descriptions short — one line per table row
- Use the phase names from the lifecycle table: Init, Run, Prod, Test, Build, All
- For npm packages, always state the **reason** (what module format issue it validates)
- Don't duplicate info between the per-app table and the feature matrix — the app table has detail, the matrix has the cross-reference
- When an env var is set in a test file, note it as `All (env prefix)` in the phase column

View File

@@ -28,15 +28,15 @@ PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh
./packages/test-in-console/run.sh # Test all core packages
./packages/test-in-console/run.sh "mongo" # Test specific package
# 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
# E2E tests (Jest + Playwright)
npm run install:e2e # Install dependencies
npm run test:e2e # Run all E2E tests
npm run test:e2e -- -t="React" # Run specific test
```
## Modern E2E Tests (`tools/modern-tests/`)
## E2E Tests (`tools/e2e-tests/`)
Jest + Playwright suite for verifying modern bundler integrations (rspack). Tests cover framework skeletons and build scenarios.
Jest + Playwright suite for verifying bundler integrations (rspack). Tests cover framework skeletons and build scenarios.
**Test apps:** `apps/{react,vue,svelte,solid,blaze,typescript,babel,coffeescript,monorepo}`

View File

@@ -4,7 +4,7 @@ on:
pull_request:
paths:
- 'meteor'
- 'tools/modern-tests/**'
- 'tools/e2e-tests/**'
- 'packages/rspack/**'
- 'packages/tools-core/**'
- 'packages/babel-compiler/**'
@@ -18,6 +18,7 @@ concurrency:
cancel-in-progress: true
env:
TOOL_NODE_FLAGS: "--max_old_space_size=12288"
NODE_OPTIONS: "--max_old_space_size=12288"
jobs:
@@ -32,8 +33,8 @@ jobs:
- Babel
- Blaze
- Coffeescript
- Library
- Monorepo
- Other
- React
- R.Router
- Solid
@@ -51,7 +52,7 @@ jobs:
path: |
~/.npm
node_modules
tools/modern-tests/node_modules
tools/e2e-tests/node_modules
packages/**/.npm
.meteor
dev_bundle
@@ -73,6 +74,14 @@ jobs:
- name: Install test deps
run: npm run install:e2e
- name: Set NPM_LINK_RSPACK=false for release branches
run: |
echo "Current branch: ${{ github.head_ref || github.ref_name }}"
if [[ "${{ github.head_ref || github.ref_name }}" == release-* ]]; then
echo "NPM_LINK_RSPACK=false" >> $GITHUB_ENV
echo "::warning::NPM_LINK_RSPACK=false on release branch. E2E tests will install @meteorjs/rspack from npm — make sure the latest version is published or tests may fail."
fi
- name: Prepare Meteor
run: ./meteor --get-ready

View File

@@ -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
View 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

View File

@@ -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

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ node_modules
\#*\#
.\#*
.idea
!.idea/icon.svg
*.iml
*.sublime-project
*.sublime-workspace

21
.idea/icon.svg generated Executable file
View 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>

View File

@@ -48,6 +48,7 @@ Load these for detailed context on specific topics:
| [testing](.github/skills/testing/SKILL.md) | Writing tests, debugging failures, test infrastructure |
| [packages](.github/skills/packages/SKILL.md) | Finding packages by feature, understanding dependencies |
| [modern-tools](.github/skills/modern-tools/SKILL.md) | tools-core utilities, rspack, modern integrations |
| [e2e-coverage](.github/skills/e2e-coverage/SKILL.md) | Updating the E2E test coverage report when apps/skeletons change |
| [ai-context](.github/skills/ai-context/SKILL.md) | Creating, updating, or maintaining AI documentation files |
## Package Domains

View File

@@ -11,4 +11,5 @@ Load these for detailed context on specific topics:
| [testing](.github/skills/testing/SKILL.md) | Writing tests, debugging failures, test infrastructure |
| [packages](.github/skills/packages/SKILL.md) | Finding packages by feature, understanding dependencies |
| [modern-tools](.github/skills/modern-tools/SKILL.md) | tools-core utilities, rspack, modern integrations |
| [e2e-coverage](.github/skills/e2e-coverage/SKILL.md) | Updating the E2E test coverage report when apps/skeletons change |
| [ai-context](.github/skills/ai-context/SKILL.md) | Creating, updating, or maintaining AI documentation files |

View File

@@ -148,7 +148,7 @@ Place test files next to the module they test using the `*.test.js` naming conve
### E2E tests (Jest + Playwright)
End-to-end tests in `tools/modern-tests/` validate that Meteor skeletons and bundler integrations work correctly. They create real Meteor apps, start dev servers, and assert behavior in a headless Chromium browser.
End-to-end tests in `tools/e2e-tests/` validate that Meteor skeletons and bundler integrations work correctly. They create real Meteor apps, start dev servers, and assert behavior in a headless Chromium browser.
```sh
# Install dependencies (first time)
@@ -161,7 +161,7 @@ npm run test:e2e
npm run test:e2e -- -t="React"
```
Each test has a corresponding app fixture in `tools/modern-tests/apps/`. See that directory for examples when adding new E2E tests.
Each test has a corresponding app fixture in `tools/e2e-tests/apps/`. See that directory for examples when adding new E2E tests.
### Self-tests (Meteor tool)

View File

@@ -0,0 +1,290 @@
# E2E Test Coverage
> To update this report, follow the [e2e-coverage skill](/.github/skills/e2e-coverage/SKILL.md).
End-to-end tests using Jest + Playwright that verify Meteor apps with the Rspack bundler across frameworks, build modes, and features.
Test infrastructure lives in `tools/e2e-tests/`, with app fixtures in `tools/e2e-tests/apps/` and matching test files at `tools/e2e-tests/<name>.test.js`.
## Test Lifecycle
Every app and skeleton goes through these phases (unless skipped):
| Phase | What it does |
|-------|-------------|
| **Init** | Copies app, installs deps, adds rspack, generates config |
| **Run (dev)** | `meteor run` — asserts build artifacts, app loads, client/server hot rebuild |
| **Run (prod)** | `meteor run --production` — same checks in production mode |
| **Test** | `meteor test` — runs mocha test driver, verifies test rebuild |
| **Test once** | `meteor test --once` — runs tests to completion, checks exit code |
| **Build** | `meteor build` — verifies bundle structure (main.js, programs/server, web.browser, web.browser.legacy) |
| **Reset** | `meteor reset` — clears rspack build artifacts, caches, asset/chunk context dirs, and `.meteor/local` subdirectories |
Default assertions on every run phase: build artifacts exist, page title matches, body styles render, `__rspack__` script tag is present.
---
## Apps
Each app lives in `apps/<name>/` and has a matching `<name>.test.js`.
### react
Core React integration with custom Meteor local directory.
| What is covered | Phase |
|----------------|-------|
| Custom `METEOR_LOCAL_DIR` (`.meteor/local-custom`) | All (env prefix) |
| Custom build dir (`_build-local-custom`) created | Run |
| `.gitignore` updated with custom local dir | Run |
| React + JSX environment detection | Run, Prod, Test, Build |
| Image assets load (generated + public + background) | Run, Prod |
| `Meteor.disablePlugins` suppresses rspack plugins | Run, Prod, Test, Build |
| Custom rspack config (`rspack.config.cjs`) | All |
| HMR works in dev, disabled in prod | Run, Prod |
### react-router
Full-featured React Router app with custom packages, Less, and advanced rspack config.
| What is covered | Phase |
|----------------|-------|
| `METEOR_PACKAGE_DIRS` custom packages dir | All (env prefix) |
| `babel-plugin-react-compiler` integration | Init, Prod, Build |
| Compiler output cached in dev (babel.config.js) | Run |
| 404 page routing (renders "Page Not Found") | Run, Prod |
| Less stylesheet support (`white-space: break-spaces`) | Run, Prod |
| Meteor modules config styles (`align-content: center`) | Run, Prod |
| Custom HTML meta tags (`theme-color`) | Run, Prod |
| Default + custom package loading | Run |
| `resolve.extensions` loading (`.jsx`) | Run |
| `rspack.config.override.js` custom plugin loading | Run, Test, Build |
| React + TSX environment detection | Run, Prod, Test, Build |
| Full-app test mode (`--full-app`) | Test |
| Static assets in bundle (png, md) | Build |
| HMR works in dev, disabled in prod | Run, Prod |
### blaze
Blaze templating engine integration.
| What is covered | Phase |
|----------------|-------|
| Blaze environment detection (`isBlazeEnabled`) | Run, Prod, Test, Build |
| HMR disabled (incompatible with Blaze) | Run, Prod |
### full-blaze
Full Blaze app (with `imports/` structure for tests).
| What is covered | Phase |
|----------------|-------|
| Blaze environment detection | Run, Prod, Test, Build |
| `imports/api/` test path structure | Test |
| HMR disabled (incompatible with Blaze) | Run, Prod |
### typescript
TypeScript with SCSS, type checking, `.ts` rspack config, and `.ts` SWC config.
| What is covered | Phase |
|----------------|-------|
| TypeScript rspack config (`rspack.config.ts`) | All |
| TypeScript SWC config (`swc.config.ts`) with automatic JSX runtime | All |
| `@swc/core` type-only import for SWC config typings | All |
| Custom build dir (`build`) | All |
| Custom asset/chunk context dirs (`assets`, `chunks`) | All |
| SCSS styles support (`white-space: break-spaces`) | Run, Prod |
| TypeScript + TSX environment detection | Run, Prod, Test, Build |
| Portable build (Meteor.isDevelopment/isProduction not defined) | Run, Prod, Build |
| `Meteor.extendSwcConfig` with path aliases (`@ui/*`, `@api/*`) | All |
| `TsCheckerRspackPlugin` type checking (no errors) | Run |
| `.meteor/local/types` directory generated | Run |
| Separate client/server test files | Test |
| CI: removes TsCheckerRspackPlugin (resource limits) | Init |
| HMR works in dev, disabled in prod | Run, Prod |
### babel
Babel transpilation with custom module rules and `.mjs` rspack config.
| What is covered | Phase |
|----------------|-------|
| Custom rspack config (`rspack.config.mjs`) | All |
| Custom `NODE_ENV` compilation per phase | All (env prefix) |
| Rspack mode assertion (development/production) | Run, Prod, Test, Build |
| `Meteor.isDevelopment`/`Meteor.isProduction` defines | Run, Prod, Test, Build |
| Module rules for `.js`/`.jsx` files | Run, Prod, Test, Build |
| Module rules for `.tsx`/`.ts`/`.mts`/`.cts`/`.mjs`/`.cjs` | Run, Prod, Test, Build |
| Module rules for `.graphql`/`.gql` files | Run, Prod, Test, Build |
| Default rules negated (custom rules override) | Run, Prod, Test, Build |
| HMR works in dev, disabled in prod | Run, Prod |
### coffeescript
CoffeeScript language support.
| What is covered | Phase |
|----------------|-------|
| `.coffee` file compilation (client + server + test) | All |
| CoffeeScript-specific conditional syntax | Run, Prod |
| HMR works in dev, disabled in prod | Run, Prod |
### vue
Vue.js framework with Tailwind CSS.
| What is covered | Phase |
|----------------|-------|
| Vue single-file components | All |
| Tailwind CSS styles (`.p-8` padding) | Run, Prod |
| HMR works in dev, disabled in prod | Run, Prod |
### solid
SolidJS framework integration.
| What is covered | Phase |
|----------------|-------|
| SolidJS compilation and rendering | All |
| HMR works in dev, disabled in prod | Run, Prod |
### svelte
Svelte framework integration.
| What is covered | Phase |
|----------------|-------|
| Svelte compilation and rendering | All |
| HMR works in dev, disabled in prod | Run, Prod |
### monorepo
Monorepo structure with app in subdirectory.
| What is covered | Phase |
|----------------|-------|
| Monorepo layout (`app/` subdirectory) | All |
| Custom rspack config (`rspack.config.cjs`) | All |
| `rspack.config.override.cjs` custom plugin loading | Run, Test, Build |
| Static assets in bundle (png, md) | Build |
| HMR works in dev, disabled in prod | Run, Prod |
### server-only
Server-only app (no client entry point).
| What is covered | Phase |
|----------------|-------|
| No client bundle (client skipped) | All |
| No client tests (test client skipped) | Test |
| Server entry loads (`server/main.js loaded`) | Run |
---
## Skeletons
Tested via `skeleton.test.js` using `meteor create --<skeleton>`. Each skeleton verifies: app creation, dev run, production run, test once, build, and reset.
| Skeleton | Port | Language | Extra coverage |
|----------|------|----------|----------------|
| angular | 3213 | TypeScript | |
| apollo | 3201 | JSX | |
| babel | 3212 | JSX | |
| bare | 3219 | JS | No title/style checks, no client tests, skip build cache check |
| blaze | 3202 | JS | |
| chakra-ui | 3203 | JSX | No body style checks (custom UI library) |
| coffeescript | 3211 | CoffeeScript | |
| full | 3204 | JS | `imports/api/` test structure |
| react | 3205 | JSX | Custom body styles (Inter font, padding) |
| solid | 3206 | JS | |
| svelte | 3207 | JS | |
| tailwind | 3208 | TypeScript | Tailwind `bg-gray-100` styles (dev + prod color formats) |
| typescript | 3209 | TypeScript | CI: removes TsCheckerRspackPlugin |
| vue | 3210 | JS | |
---
## NPM Package Compatibility
Several apps import specific npm packages to verify that Meteor + Rspack handles different module formats and edge cases without errors. The app boots successfully only if these imports resolve correctly.
### react-router (`apps/react-router/server/main.js`)
| Package | Reason |
|---------|--------|
| `s3mini` | ESM-only package (no CJS fallback) |
| `@modelcontextprotocol/sdk/client/streamableHttp.js` | ESM subpath export (deep path into ESM package) |
| `bcrypt` | Native Node.js bindings (compiled C++ addon) |
| `puppeteer` | Large ESM-compatible package with complex dependency tree (`server/browser-tests/browser.app-test.js`) |
### monorepo (`apps/monorepo/app/`)
| Package | File | Reason |
|---------|------|--------|
| `pino` + `pino-pretty` | `server/main.js` | ESM-first logger; `pino-pretty` uses `thread-stream` which has worker file resolution issues — needs `Meteor.compileWithMeteor(["thread-stream"])` in rspack config |
| `grubba-rpc` | `server/main.js` | Untranspiled npm dependency — needs `Meteor.compileWithRspack(["grubba-rpc"])` to compile it through rspack |
| `node:buffer` | `imports/api/links.js` | Node.js built-in via `node:` protocol in shared client/server code — must be ignored on client without errors |
| `@react-email/components` | `imports/emails/TestEmail.jsx` | JSX-heavy ESM package with many subpath exports |
### babel (`apps/babel/server/apollo.js`)
| Package | Reason |
|---------|--------|
| `@apollo/server` | ESM-first GraphQL server |
| `@apollo/server/express4` | ESM subpath export (middleware from deep path) |
| `graphql` | Peer dependency, dual CJS/ESM package |
### typescript (`apps/typescript/rspack.config.ts`, `apps/typescript/swc.config.ts`)
| Package | Reason |
|---------|--------|
| `node:module` (`createRequire`) | Node.js built-in in a `.ts` config file — tests CJS interop via `createRequire(import.meta.url)` in an ESM context |
| `@swc/core` | Type-only import (`import type { Config }`) — provides typings for `swc.config.ts`, stripped at compile time |
---
## Feature Coverage Matrix
Where each feature is tested across apps and skeletons.
| Feature | Apps | Skeletons |
|---------|------|-----------|
| HMR (dev) | react, react-router, babel, coffeescript, vue, solid, svelte, monorepo, typescript | |
| HMR disabled (prod) | all apps with HMR | |
| HMR incompatible | blaze, full-blaze | |
| Custom rspack config | react (.cjs), react-router, babel (.mjs), monorepo (.cjs), typescript (.ts) | |
| Custom SWC config (.ts) | typescript | |
| Config override file | react-router, monorepo | |
| Custom build dir | react, typescript | |
| Custom asset/chunk context dirs | typescript | |
| Custom env vars | react (METEOR_LOCAL_DIR), react-router (METEOR_PACKAGE_DIRS) | |
| Static asset bundling | react-router, monorepo | |
| Less styles | react-router | |
| SCSS styles | typescript | |
| Tailwind CSS | vue | tailwind |
| Image asset loading | react | |
| 404 routing | react-router | |
| Meta tags | react-router | |
| Babel compiler plugin | react-router | |
| TypeScript type checking | typescript | |
| Meteor.disablePlugins | react | |
| Custom package dirs | react-router | |
| CoffeeScript compilation | coffeescript | coffeescript |
| Server-only (no client) | server-only | |
| Monorepo layout | monorepo | |
| Full-app test mode | react-router | |
| Module rules override | babel | |
| Custom NODE_ENV compilation | babel | |
| Portable build (no isDev/isProd defines) | typescript | |
| `Meteor.extendSwcConfig` (path aliases) | typescript | |
| `meteor reset` cleanup | all apps | all skeletons |
| Skeleton creation | | all 14 skeletons |
| Body style assertions | | react, tailwind (custom); most others (default) |
| Custom .gitignore entries | react | |
| ESM-only packages | react-router, monorepo, babel | |
| ESM subpath exports | react-router, babel | |
| Native bindings (C++ addon) | react-router | |
| `node:` protocol imports | monorepo, typescript | |
| Untranspiled npm deps (`compileWithRspack`) | monorepo | |
| Worker resolution (`compileWithMeteor`) | monorepo | |

View File

@@ -57,10 +57,21 @@ type MeteorEnv = Record<string, any> & {
*/
splitVendorChunk: () => Record<string, object>;
/**
* Extend Rspack SWC loader config.
* Extend the SWC loader config by smart-merging custom options on top of
* Meteor's defaults. Only the properties you specify are overridden;
* everything else is preserved.
* @param swcConfig - SWC loader options to merge with defaults
* @returns A config object with SWC loader config
*/
extendSwcConfig: (swcConfig: SwcLoaderOptions) => Record<string, object>;
/**
* Replace the SWC loader config entirely, discarding Meteor's defaults.
* Use this when you need full control over SWC options and don't want any
* automatic merging with Meteor's built-in configuration.
* @param swcConfig - Complete SWC loader options (replaces defaults)
* @returns A config object with SWC loader config
*/
replaceSwcConfig: (swcConfig: SwcLoaderOptions) => Record<string, object>;
/**
* Extend Rspack configs.
* @returns A config object with merged configs
@@ -75,6 +86,12 @@ type MeteorEnv = Record<string, any> & {
disablePlugins: (
matchers: string | RegExp | ((plugin: any, index: number) => boolean) | Array<string | RegExp | ((plugin: any, index: number) => boolean)>
) => Record<string, any>;
/**
* Omit `Meteor.isDevelopment` and `Meteor.isProduction` from the DefinePlugin so
* the bundle is not tied to a specific Meteor environment (portable / isomorphic builds).
* @returns A config fragment with `meteor.enablePortableBuild: true`
*/
enablePortableBuild: () => Record<string, any>;
}
export type ConfigFactory = (

View File

@@ -117,7 +117,7 @@ function createIgnoreRegex(globPatterns) {
// 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 + '$';
regexPattern = '(?:^|/)' + regexPattern;
return regexPattern;
}).filter(pattern => pattern !== null);

View File

@@ -0,0 +1,121 @@
const path = require('path');
const fs = require('fs');
const { cleanOmittedPaths } = require("./mergeRulesSplitOverlap.js");
const { mergeMeteorRspackFragments } = require("./meteorRspackConfigFactory.js");
// Helper function to load and process config files
async function loadAndProcessConfig(configPath, configType, Meteor, argv, disableWarnings) {
try {
// Load the config file
let config;
if (path.extname(configPath) === '.mjs') {
// For ESM modules, we need to use dynamic import
const fileUrl = `file://${configPath}`;
const module = await import(fileUrl);
config = module.default || module;
} else {
// For CommonJS modules, we can use require
config = require(configPath)?.default || require(configPath);
}
// Process the config
const rawConfig = typeof config === 'function' ? config(Meteor, argv) : config;
const resolvedConfig = await Promise.resolve(rawConfig);
const userConfig = resolvedConfig && '0' in resolvedConfig ? resolvedConfig[0] : resolvedConfig;
// Define omitted paths and warning function
const omitPaths = [
"name",
"target",
"entry",
"output.path",
"output.filename",
...(Meteor.isServer ? ["optimization.splitChunks", "optimization.runtimeChunk"] : []),
].filter(Boolean);
const warningFn = path => {
if (disableWarnings) return;
console.warn(
`[${configType}] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
);
};
// Clean omitted paths and merge Meteor Rspack fragments
let nextConfig = cleanOmittedPaths(userConfig, {
omitPaths,
warningFn,
});
nextConfig = mergeMeteorRspackFragments(nextConfig);
return nextConfig;
} catch (error) {
console.error(`Error loading ${configType} from ${configPath}:`, error);
if (configType === 'rspack.config.js') {
throw error; // Only rethrow for project config
}
return null;
}
}
/**
* Loads both the user's Rspack configuration and its potential override.
*
* @param {string|undefined} projectConfigPath
* @param {object} Meteor
* @param {object} argv
* @returns {Promise<{ nextUserConfig: object|null, nextOverrideConfig: object|null }>}
*/
async function loadUserAndOverrideConfig(projectConfigPath, Meteor, argv) {
let nextUserConfig = null;
let nextOverrideConfig = null;
const projectDir = process.cwd();
const isMeteorPackageConfig = projectDir.includes("/packages/rspack");
if (projectConfigPath) {
const configDir = path.dirname(projectConfigPath);
const configFileName = path.basename(projectConfigPath);
const configExt = path.extname(configFileName);
const configNameWithoutExt = configFileName.replace(configExt, '');
const configNameFull = `${configNameWithoutExt}.override${configExt}`;
const overrideConfigPath = path.join(configDir, configNameFull);
if (fs.existsSync(overrideConfigPath)) {
nextOverrideConfig = await loadAndProcessConfig(
overrideConfigPath,
configNameFull,
Meteor,
argv,
Meteor.isAngularEnabled
);
}
if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) {
// Check if there's a .mjs or .cjs version of the config file
const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs');
const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs');
let projectConfigPathToUse = projectConfigPath;
if (fs.existsSync(mjsConfigPath)) {
projectConfigPathToUse = mjsConfigPath;
} else if (fs.existsSync(cjsConfigPath)) {
projectConfigPathToUse = cjsConfigPath;
}
nextUserConfig = await loadAndProcessConfig(
projectConfigPathToUse,
'rspack.config.js',
Meteor,
argv,
Meteor.isAngularEnabled
);
}
}
return { nextUserConfig, nextOverrideConfig };
}
module.exports = {
loadAndProcessConfig,
loadUserAndOverrideConfig,
};

View File

@@ -132,10 +132,14 @@ function splitVendorChunk() {
}
/**
* Extend SWC loader config
* Usage: extendSwcConfig()
* Extend SWC loader config by smart-merging custom options on top of Meteor's
* defaults (via `mergeSplitOverlap`). Only the properties you specify are
* overridden; everything else is preserved.
*
* @returns {Record<string, object>} `{ meteorRspackConfigX: { optimization: { ... } } }`
* Usage: Meteor.extendSwcConfig({ jsc: { parser: { decorators: true } } })
*
* @param {object} swcConfig - SWC loader options to merge with defaults
* @returns {Record<string, object>} config fragment for spreading into rspack config
*/
function extendSwcConfig(swcConfig) {
return prepareMeteorRspackConfig({
@@ -152,6 +156,44 @@ function extendSwcConfig(swcConfig) {
});
}
/**
* Replace the SWC loader config entirely, discarding Meteor's defaults.
* Use this when you need full control over SWC options and don't want any
* automatic merging with Meteor's built-in configuration.
*
* Usage: Meteor.replaceSwcConfig({ jsc: { parser: { syntax: 'typescript' }, target: 'es2020' } })
*
* @param {object} swcConfig - Complete SWC loader options (replaces defaults)
* @returns {Record<string, object>} config fragment for spreading into rspack config
*/
function replaceSwcConfig(swcConfig) {
return prepareMeteorRspackConfig({
module: {
rules: [
{
test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i,
exclude: /node_modules|\.meteor\/local/,
loader: 'builtin:swc-loader',
options: swcConfig,
},
],
},
});
}
/**
* Signal that `Meteor.isDevelopment` and `Meteor.isProduction` should be omitted
* from DefinePlugin, making the bundle portable across Meteor environments.
* Usage: return Meteor.enablePortableBuild() in your rspack.config.js
*
* @returns {Record<string, object>} config fragment with `meteor.enablePortableBuild: true`
*/
function enablePortableBuild() {
return prepareMeteorRspackConfig({
"meteor.enablePortableBuild": true,
});
}
/**
* Remove plugins from a Rspack config by name, RegExp, predicate, or array of them.
* When using a function predicate, it receives both the plugin and its index in the plugins array.
@@ -202,12 +244,21 @@ function disablePlugins(config, matchers) {
return config;
}
function outputMeteorRspack(data) {
const jsonString = JSON.stringify(data);
const output = `[Meteor-Rspack]${jsonString}[/Meteor-Rspack]`;
console.log(output);
}
module.exports = {
compileWithMeteor,
compileWithRspack,
setCache,
splitVendorChunk,
extendSwcConfig,
replaceSwcConfig,
makeWebNodeBuiltinsAlias,
disablePlugins,
outputMeteorRspack,
enablePortableBuild,
};

View File

@@ -9,11 +9,38 @@ const vm = require('vm');
function getMeteorAppSwcrc(file = '.swcrc') {
try {
const filePath = `${process.cwd()}/${file}`;
if (file.endsWith('.js')) {
if (file.endsWith('.js') || file.endsWith('.ts')) {
let content = fs.readFileSync(filePath, 'utf-8');
// Check if the content uses ES module syntax (export default)
if (file.endsWith('.ts')) {
try {
const swc = require('@swc/core');
const result = swc.transformSync(content, {
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es2015',
},
});
content = result.code;
} catch (swcError) {
content = content
.replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '')
.replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '')
.replace(/import\s+['"][^'"]+['"];?/g, '')
.replace(/export\s+default\s+/, 'module.exports = ')
.replace(/export\s+/g, '')
.replace(/:\s*\w+(\[\])?(\s*=)/g, '$2')
.replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)')
.replace(/\):\s*\w+(\[\])?\s*\{/g, ') {')
.replace(/interface\s+\w+\s*\{[^}]*\}/g, '')
.replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
.replace(/as\s+\w+(\[\])?/g, '');
}
}
if (content.includes('export default')) {
// Transform ES module syntax to CommonJS
content = content.replace(/export\s+default\s+/, 'module.exports = ');
}
const script = new vm.Script(`
@@ -27,7 +54,9 @@ function getMeteorAppSwcrc(file = '.swcrc') {
})()
`);
const context = vm.createContext({ process });
return script.runInContext(context);
const result = script.runInContext(context);
// Handle CJS interop wrapper (e.g. { __esModule: true, default: config })
return result && result.__esModule && result.default ? result.default : result;
} else {
// For .swcrc and other JSON files, parse as JSON
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -45,12 +74,13 @@ function getMeteorAppSwcrc(file = '.swcrc') {
function getMeteorAppSwcConfig() {
const hasSwcRc = fs.existsSync(`${process.cwd()}/.swcrc`);
const hasSwcJs = !hasSwcRc && fs.existsSync(`${process.cwd()}/swc.config.js`);
const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${process.cwd()}/swc.config.ts`);
if (!hasSwcRc && !hasSwcJs) {
if (!hasSwcRc && !hasSwcJs && !hasSwcTs) {
return undefined;
}
const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc';
const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc';
const config = getMeteorAppSwcrc(swcFile);
// Set baseUrl to process.cwd() if it exists

View File

@@ -2,6 +2,10 @@ const fs = require('fs');
const path = require('path');
const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js");
// Normalize a path to always use forward slashes (POSIX style).
// Module identifiers in bundled JS must use '/' regardless of OS.
const toPosix = (p) => p.replace(/\\/g, '/');
/**
* Generates eager test files dynamically
* @param {Object} options - Options for generating the test file
@@ -9,17 +13,20 @@ const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js");
* @param {string} options.projectDir - The project directory
* @param {string} options.buildContext - The build context
* @param {string[]} options.ignoreEntries - Array of ignore patterns
* @param {string[]} options.meteorIgnoreEntries - Array of meteor ignore patterns
* @param {string} options.extraEntry - Extra entry to load
* @returns {string} The path to the generated file
*/
const generateEagerTestFile = ({
isAppTest,
projectDir,
buildContext,
ignoreEntries: inIgnoreEntries = [],
prefix: inPrefix = '',
extraEntry,
}) => {
isAppTest,
projectDir,
buildContext,
ignoreEntries: inIgnoreEntries = [],
meteorIgnoreEntries: inMeteorIgnoreEntries = [],
prefix: inPrefix = '',
extraEntry,
globalImportPath,
}) => {
const distDir = path.resolve(projectDir, ".meteor/local/test");
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
@@ -39,6 +46,11 @@ const generateEagerTestFile = ({
const excludeFoldersRegex = createIgnoreRegex(
createIgnoreGlobConfig(ignoreEntries)
);
console.log("inMeteorIgnoreEntries", inMeteorIgnoreEntries);
// Create regex from meteor ignore entries
const excludeMeteorIgnoreRegex = inMeteorIgnoreEntries.length > 0
? createIgnoreRegex(createIgnoreGlobConfig(inMeteorIgnoreEntries))
: null;
const prefix = (inPrefix && `${inPrefix}-`) || "";
const filename = isAppTest
@@ -49,25 +61,39 @@ const generateEagerTestFile = ({
? "/\\.app-(?:test|spec)s?\\.[^.]+$/"
: "/\\.(?:test|spec)s?\\.[^.]+$/";
const content = `{
const ctx = import.meta.webpackContext('/', {
const content = `${
globalImportPath ? `import '${toPosix(globalImportPath)}';\n\n` : ""
}${
excludeMeteorIgnoreRegex
? `const MeteorIgnoreRegex = ${excludeMeteorIgnoreRegex.toString()};`
: ""
}
{
const ctx = import.meta.webpackContext('${toPosix(projectDir)}', {
recursive: true,
regExp: ${regExp},
exclude: ${excludeFoldersRegex.toString()},
mode: 'eager',
});
ctx.keys().forEach(ctx);
ctx.keys().filter((k) => {
${
excludeMeteorIgnoreRegex
? `// Only exclude based on *relative* path segments.
return !MeteorIgnoreRegex.test(k);`
: "return true;"
}
}).forEach(ctx);
${
extraEntry
? `const extra = import.meta.webpackContext('${path.dirname(
extraEntry
)}', {
? `const extra = import.meta.webpackContext('${toPosix(path.dirname(
extraEntry
))}', {
recursive: false,
regExp: ${new RegExp(`${path.basename(extraEntry)}$`).toString()},
mode: 'eager',
});
extra.keys().forEach(extra);`
: ''
: ""
}
}`;

View File

@@ -1,12 +1,12 @@
{
"name": "@meteorjs/rspack",
"version": "1.0.1",
"version": "1.1.0-beta.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@meteorjs/rspack",
"version": "1.0.1",
"version": "1.1.0-beta.31",
"license": "ISC",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -14,9 +14,6 @@
"node-polyfill-webpack-plugin": "^4.1.0",
"webpack-merge": "^6.0.1"
},
"devDependencies": {
"semver": "^7.7.4"
},
"peerDependencies": {
"@rspack/cli": ">=1.3.0",
"@rspack/core": ">=1.3.0"
@@ -4210,19 +4207,6 @@
"node": ">=10"
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@meteorjs/rspack",
"version": "1.0.1",
"version": "1.1.0-beta.31",
"description": "Configuration logic for using Rspack in Meteor projects",
"main": "index.js",
"type": "commonjs",
@@ -12,15 +12,8 @@
"node-polyfill-webpack-plugin": "^4.1.0",
"webpack-merge": "^6.0.1"
},
"scripts": {
"bump": "node ./scripts/bump-version.js",
"publish:beta": "bash ./scripts/publish-beta.sh"
},
"peerDependencies": {
"@rspack/cli": ">=1.3.0",
"@rspack/core": ">=1.3.0"
},
"devDependencies": {
"semver": "^7.7.4"
}
}

View File

@@ -0,0 +1,36 @@
// MeteorRspackOutputPlugin.js
//
// This plugin outputs a JSON stringified with a tag delimiter each time
// a new Rspack compilation happens. The JSON content is configurable
// via plugin instantiation options.
const { outputMeteorRspack } = require('../lib/meteorRspackHelpers');
class MeteorRspackOutputPlugin {
constructor(options = {}) {
this.pluginName = 'MeteorRspackOutputPlugin';
this.options = options;
this.compilationCount = 0;
// The data to be output as JSON, can be a static object or a function
this.getData =
typeof options.getData === 'function'
? options.getData
: () => options.data || {};
}
apply(compiler) {
// Hook into the 'done' event which fires after each compilation completes
compiler.hooks.done.tap(this.pluginName, stats => {
this.compilationCount++;
const data = {
...(this.getData(stats, {
compilationCount: this.compilationCount,
isRebuild: this.compilationCount > 1,
}) || {}),
};
outputMeteorRspack(data);
});
}
}
module.exports = { MeteorRspackOutputPlugin };

View File

@@ -10,6 +10,10 @@
const fs = require('fs');
const path = require('path');
// Normalize a path to always use forward slashes (POSIX style).
// Module identifiers in bundled JS must use '/' regardless of OS.
const toPosix = (p) => p.replace(/\\/g, '/');
class RequireExternalsPlugin {
constructor({
filePath,
@@ -46,7 +50,7 @@ class RequireExternalsPlugin {
// Prepare paths
this.filePath = path.resolve(process.cwd(), filePath);
this.backRoot = '../'.repeat(
filePath.replace(/^\.?\/+/, '').split('/').length - 1
filePath.replace(/^\.?[/\\]+/, '').split(/[/\\]/).length - 1
);
// Initialize funcCount based on existing helpers in the file
@@ -96,14 +100,16 @@ class RequireExternalsPlugin {
pkg &&
(path.isAbsolute(pkg) ||
pkg.startsWith('./') ||
pkg.startsWith('.\\') ||
pkg.startsWith('../') ||
pkg.startsWith('..\\') ||
!!depInfo.ext)
) {
const module = this.externalsMeta.get(pkg);
if (module) {
return `${this.backRoot}${module.relativeRequest}`;
return `${this.backRoot}${toPosix(module.relativeRequest)}`;
}
return `${this.backRoot}${name}`;
return `${this.backRoot}${toPosix(name)}`;
}
return pkg;
@@ -132,7 +138,7 @@ class RequireExternalsPlugin {
this.externalsMeta.set(externalRequest, {
originalRequest: request,
externalRequest,
relativeRequest: path.join(relContext, request),
relativeRequest: toPosix(path.join(relContext, request)),
});
// tell Rspack "don't bundle this, import it at runtime"

View File

@@ -10,18 +10,22 @@ const { getMeteorAppSwcConfig } = require('./lib/swc.js');
const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js');
const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js');
const { AssetExternalsPlugin } = require('./plugins/AssetExternalsPlugin.js');
const { MeteorRspackOutputPlugin } = require('./plugins/MeteorRspackOutputPlugin.js');
const { generateEagerTestFile } = require("./lib/test.js");
const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
const { mergeMeteorRspackFragments } = require("./lib/meteorRspackConfigFactory.js");
const {
compileWithMeteor,
compileWithRspack,
setCache,
splitVendorChunk,
extendSwcConfig,
replaceSwcConfig,
makeWebNodeBuiltinsAlias,
disablePlugins,
outputMeteorRspack,
enablePortableBuild,
} = require('./lib/meteorRspackHelpers.js');
const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js');
const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
// Safe require that doesn't throw if the module isn't found
@@ -40,7 +44,11 @@ function safeRequire(moduleName) {
}
// Persistent filesystem cache strategy
function createCacheStrategy(mode, side, { projectConfigPath, configPath } = {}) {
function createCacheStrategy(
mode,
side,
{ projectConfigPath, configPath, buildContext } = {},
) {
// Check for configuration files
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
const hasTsconfig = fs.existsSync(tsconfigPath);
@@ -52,6 +60,8 @@ function createCacheStrategy(mode, side, { projectConfigPath, configPath } = {})
const hasSwcrcConfig = fs.existsSync(swcrcPath);
const swcJsPath = path.join(process.cwd(), 'swc.config.js');
const hasSwcJsConfig = fs.existsSync(swcJsPath);
const swcTsPath = path.join(process.cwd(), 'swc.config.ts');
const hasSwcTsConfig = fs.existsSync(swcTsPath);
const postcssConfigPath = path.join(process.cwd(), 'postcss.config.js');
const hasPostcssConfig = fs.existsSync(postcssConfigPath);
const packageLockPath = path.join(process.cwd(), 'package-lock.json');
@@ -68,6 +78,7 @@ function createCacheStrategy(mode, side, { projectConfigPath, configPath } = {})
...(hasBabelJsConfig ? [babelJsConfig] : []),
...(hasSwcrcConfig ? [swcrcPath] : []),
...(hasSwcJsConfig ? [swcJsPath] : []),
...(hasSwcTsConfig ? [swcTsPath] : []),
...(hasPostcssConfig ? [postcssConfigPath] : []),
...(hasPackageLock ? [packageLockPath] : []),
...(hasYarnLock ? [yarnLockPath] : []),
@@ -81,7 +92,9 @@ function createCacheStrategy(mode, side, { projectConfigPath, configPath } = {})
type: "persistent",
storage: {
type: "filesystem",
directory: `node_modules/.cache/rspack${(side && `/${side}`) || ""}`,
directory: `node_modules/.cache/rspack/${
[buildContext, side].filter(Boolean).join('-') || 'default'
}`,
},
...(buildDependencies.length > 0 && {
buildDependencies: buildDependencies,
@@ -204,15 +217,45 @@ module.exports = async function (inMeteor = {}, argv = {}) {
const Meteor = { ...inMeteor };
// Convert string boolean values to actual booleans
for (const key in Meteor) {
if (Meteor[key] === 'true' || Meteor[key] === true) {
if (Meteor[key] === "true" || Meteor[key] === true) {
Meteor[key] = true;
} else if (Meteor[key] === 'false' || Meteor[key] === false) {
} else if (Meteor[key] === "false" || Meteor[key] === false) {
Meteor[key] = false;
}
}
const isProd = !!Meteor.isProduction || argv.mode === 'production';
const isDev = !!Meteor.isDevelopment || !isProd;
const isTestLike = !!Meteor.isTestLike;
const swcExternalHelpers = !!Meteor.swcExternalHelpers;
const isNative = !!Meteor.isNative;
const devServerPort = Meteor.devServerPort || 8080;
const projectDir = process.cwd();
const projectConfigPath =
Meteor.projectConfigPath || path.resolve(projectDir, "rspack.config.js");
// Determine context for bundles and assets
const meteorLocalDirName = process.env.METEOR_LOCAL_DIR
? path.basename(process.env.METEOR_LOCAL_DIR.replace(/\\/g, "/"))
: "";
const buildContext =
Meteor.buildContext ||
process.env.RSPACK_BUILD_CONTEXT ||
`_build${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`;
const assetsContext =
Meteor.assetsContext ||
process.env.RSPACK_ASSETS_CONTEXT ||
`build-assets${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`;
const chunksContext =
Meteor.chunksContext ||
process.env.RSPACK_CHUNKS_CONTEXT ||
`build-chunks${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`;
// Compute build paths before loading user config (needed by Meteor helpers below)
const outputPath = Meteor.outputPath;
const outputDir = path.dirname(Meteor.outputPath || "");
Meteor.buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
// Meteor flags derived purely from input; independent of loaded user/override configs
const isTest = !!Meteor.isTest;
const isClient = !!Meteor.isClient;
const isServer = !!Meteor.isServer;
@@ -222,16 +265,10 @@ module.exports = async function (inMeteor = {}, argv = {}) {
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 isProfile = !!Meteor.isProfile;
const isVerbose = !!Meteor.isVerbose;
const configPath = Meteor.configPath;
const testEntry = Meteor.testEntry;
const testClientEntry = Meteor.testClientEntry;
const testServerEntry = Meteor.testServerEntry;
const isTypescriptEnabled = Meteor.isTypescriptEnabled || false;
const isJsxEnabled =
@@ -240,58 +277,69 @@ module.exports = async function (inMeteor = {}, argv = {}) {
Meteor.isTsxEnabled || (isTypescriptEnabled && isReactEnabled) || false;
const isBundleVisualizerEnabled = Meteor.isBundleVisualizerEnabled || false;
const isAngularEnabled = Meteor.isAngularEnabled || false;
const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
// Determine entry points
const entryPath = Meteor.entryPath;
// Defined here so it can be called both before and after the first config load;
// without loaded configs it falls through to argv/Meteor flags.
const getModeFromConfig = (userConfig, overrideConfig) => {
if (overrideConfig?.mode) return overrideConfig.mode;
if (userConfig?.mode) return userConfig.mode;
if (argv.mode) return argv.mode;
if (Meteor.isProduction) return "production";
if (Meteor.isDevelopment) return "development";
return null;
};
// Determine output points
const outputPath = Meteor.outputPath;
const outputDir = path.dirname(Meteor.outputPath || '');
// Initial mode before user/override configs are loaded
const initialCurrentMode = getModeFromConfig();
const initialIsProd = initialCurrentMode
? initialCurrentMode === "production"
: !!Meteor.isProduction;
const initialIsDev = initialCurrentMode
? initialCurrentMode === "development"
: !!Meteor.isDevelopment || !initialIsProd;
const initialMode = initialIsProd ? "production" : "development";
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 }
// Initialized with pre-load values so helpers work during the first config load;
// reassigned after load once mode is fully resolved.
let cacheStrategy = createCacheStrategy(
initialMode,
(Meteor.isClient && "client") || "server",
{ projectConfigPath, configPath, buildContext }
);
let swcConfigRule = createSwcConfig({
isTypescriptEnabled,
isReactEnabled,
isJsxEnabled,
isTsxEnabled,
externalHelpers: enableSwcExternalHelpers,
isDevEnvironment: isRun && initialIsDev && !isTest && !isNative,
isClient,
isAngularEnabled,
});
Meteor.swcConfigOptions = swcConfigRule.options;
// Expose Meteor's helpers to expand Rspack configs
Meteor.compileWithMeteor = deps => compileWithMeteor(deps);
Meteor.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.setCache = (enabled) =>
setCache(!!enabled, enabled === "memory" ? undefined : cacheStrategy);
Meteor.splitVendorChunk = () => splitVendorChunk();
Meteor.extendSwcConfig = (customSwcConfig) => extendSwcConfig(customSwcConfig);
Meteor.extendSwcConfig = (customSwcConfig) =>
extendSwcConfig(
mergeSplitOverlap(Meteor.swcConfigOptions, customSwcConfig)
);
Meteor.replaceSwcConfig = (customSwcConfig) =>
replaceSwcConfig(customSwcConfig);
Meteor.extendConfig = (...configs) => mergeSplitOverlap(...configs);
Meteor.disablePlugins = matchers => prepareMeteorRspackConfig({
disablePlugins: matchers,
});
Meteor.disablePlugins = (matchers) =>
prepareMeteorRspackConfig({
disablePlugins: matchers,
});
Meteor.enablePortableBuild = () => enablePortableBuild();
// Add HtmlRspackPlugin function to Meteor
Meteor.HtmlRspackPlugin = (options = {}) => {
@@ -315,6 +363,51 @@ module.exports = async function (inMeteor = {}, argv = {}) {
});
};
// First pass: resolve user/override configs early so mode overrides (e.g. "production")
// are available before computing isProd/isDev and the rest of the build flags.
// Skipped for Angular since it manages its own mode via the second pass.
let { nextUserConfig, nextOverrideConfig } = isAngularEnabled
? {}
: await loadUserAndOverrideConfig(projectConfigPath, Meteor, argv);
// Determine the final mode with loaded configs
const currentMode = getModeFromConfig(nextUserConfig, nextOverrideConfig);
const isProd = currentMode
? currentMode === "production"
: !!Meteor.isProduction;
const isDev = currentMode
? currentMode === "development"
: !!Meteor.isDevelopment || !isProd;
const mode = isProd ? "production" : "development";
const isPortableBuild = !!(
nextUserConfig?.["meteor.enablePortableBuild"] ||
nextOverrideConfig?.["meteor.enablePortableBuild"]
);
// Determine entry points
const entryPath = Meteor.entryPath || "";
// Determine output points
const outputFilename = Meteor.outputFilename;
cacheStrategy = createCacheStrategy(
mode,
(Meteor.isClient && "client") || "server",
{ projectConfigPath, configPath }
);
// Determine run point
const runPath = Meteor.runPath || "";
// Determine banner
const bannerOutput = JSON.parse(
Meteor.bannerOutput || process.env.RSPACK_BANNER || '""'
);
// Determine output directories
const clientOutputDir = path.resolve(projectDir, "public");
const serverOutputDir = path.resolve(projectDir, "private");
// Get Meteor ignore entries
const meteorIgnoreEntries = getMeteorIgnoreEntries(projectDir);
@@ -330,21 +423,17 @@ module.exports = async function (inMeteor = {}, argv = {}) {
// Set default watch options
const watchOptions = {
ignored: [
...createIgnoreGlobConfig([
...meteorIgnoreEntries,
...additionalEntries,
]),
...createIgnoreGlobConfig([...meteorIgnoreEntries, ...additionalEntries]),
],
};
if (Meteor.isDebug || Meteor.isVerbose) {
console.log('[i] Rspack mode:', mode);
console.log('[i] Meteor flags:', Meteor);
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({
swcConfigRule = createSwcConfig({
isTypescriptEnabled,
isReactEnabled,
isJsxEnabled,
@@ -354,7 +443,6 @@ module.exports = async function (inMeteor = {}, argv = {}) {
isClient,
isAngularEnabled,
});
// Expose swc config to use in custom configs
Meteor.swcConfigOptions = swcConfigRule.options;
const externals = [
@@ -363,34 +451,34 @@ module.exports = async function (inMeteor = {}, argv = {}) {
...(isServer ? [/^bcrypt$/] : []),
];
const alias = {
'/': path.resolve(process.cwd()),
"/": path.resolve(process.cwd()),
};
const fallback = {
...(isClient && makeWebNodeBuiltinsAlias()),
};
const extensions = [
'.ts',
'.tsx',
'.mts',
'.cts',
'.js',
'.jsx',
'.mjs',
'.cjs',
'.json',
'.wasm',
".ts",
".tsx",
".mts",
".cts",
".js",
".jsx",
".mjs",
".cjs",
".json",
".wasm",
];
const extraRules = [];
const reactRefreshModule = isReactEnabled
? safeRequire('@rspack/plugin-react-refresh')
? safeRequire("@rspack/plugin-react-refresh")
: null;
const requireExternalsPlugin = new RequireExternalsPlugin({
filePath: path.join(buildContext, runPath),
...(Meteor.isBlazeEnabled && {
externals: /\.html$/,
isEagerImport: module => module.endsWith('.html'),
isEagerImport: (module) => module.endsWith(".html"),
...(isProd && {
lastImports: [`./${outputFilename}`],
}),
@@ -400,25 +488,26 @@ module.exports = async function (inMeteor = {}, argv = {}) {
// Handle assets
const assetExternalsPlugin = new AssetExternalsPlugin();
const assetModuleFilename = _fileInfo => {
const assetModuleFilename = (_fileInfo) => {
const filename = _fileInfo.filename;
const isPublic = filename.startsWith('/') || filename.startsWith('public');
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')
? 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 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({
@@ -429,36 +518,42 @@ module.exports = async function (inMeteor = {}, argv = {}) {
: [];
// Not supported in Meteor yet (Rspack 1.7+ is enabled by default)
const lazyCompilationConfig = { lazyCompilation: false };
const shouldLogVerbose = isProfile || isVerbose;
const loggingConfig = shouldLogVerbose
? {}
: { stats: "errors-warnings", infrastructureLogging: { level: "warn" } };
const clientEntry =
isTest && isTestEager && isTestFullApp
isClient && isTest && isTestEager && isTestFullApp
? generateEagerTestFile({
isAppTest: true,
projectDir,
buildContext,
ignoreEntries: [...meteorIgnoreEntries, "**/server/**"],
ignoreEntries: ["**/server/**"],
meteorIgnoreEntries,
prefix: "client",
extraEntry: path.resolve(process.cwd(), Meteor.mainClientEntry),
globalImportPath: path.resolve(projectDir, buildContext, entryPath),
})
: isTest && isTestEager
: isClient && isTest && isTestEager
? generateEagerTestFile({
isAppTest: false,
isClient: true,
projectDir,
buildContext,
ignoreEntries: [...meteorIgnoreEntries, "**/server/**"],
ignoreEntries: ["**/server/**"],
meteorIgnoreEntries,
prefix: "client",
globalImportPath: path.resolve(projectDir, buildContext, entryPath),
})
: isTest && testEntry
: isClient && 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]`;
const clientNameConfig = `[${(isTest && "test-") || ""}client-rspack]`;
// Base client config
let clientConfig = {
name: clientNameConfig,
target: 'web',
target: "web",
mode,
entry: clientEntry,
output: {
@@ -467,7 +562,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
const chunkName = _module.chunk?.name;
const isMainChunk = !chunkName || chunkName === "main";
const chunkSuffix = `${chunksContext}/[id]${
isProd ? '.[chunkhash]' : ''
isProd ? ".[chunkhash]" : ""
}.js`;
if (isDevEnvironment) {
if (isMainChunk) return outputFilename;
@@ -476,21 +571,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
if (isMainChunk) return `../${buildContext}/${outputPath}`;
return chunkSuffix;
},
libraryTarget: 'commonjs2',
publicPath: '/',
chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
libraryTarget: "commonjs2",
publicPath: "/",
chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
assetModuleFilename,
cssFilename: `${chunksContext}/[name]${
isProd ? '.[contenthash]' : ''
isProd ? ".[contenthash]" : ""
}.css`,
cssChunkFilename: `${chunksContext}/[id]${
isProd ? '.[contenthash]' : ''
isProd ? ".[contenthash]" : ""
}.css`,
...(isProd && { clean: { keep: keepOutsideBuild() } }),
},
optimization: {
usedExports: true,
splitChunks: { chunks: 'async' },
splitChunks: { chunks: "async" },
},
module: {
rules: [
@@ -499,7 +594,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
? [
{
test: /\.html$/i,
loader: 'ignore-loader',
loader: "ignore-loader",
},
]
: []),
@@ -517,74 +612,90 @@ module.exports = async function (inMeteor = {}, argv = {}) {
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),
"Meteor.isClient": JSON.stringify(true),
"Meteor.isServer": JSON.stringify(false),
"Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp),
"Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp),
...(!isPortableBuild && {
"Meteor.isDevelopment": JSON.stringify(isDev),
"Meteor.isProduction": JSON.stringify(isProd),
}),
}),
...bannerPluginConfig,
Meteor.HtmlRspackPlugin(),
...doctorPluginConfig,
new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => {
res.request = res.request.replace(/^node:/, '');
res.request = res.request.replace(/^node:/, "");
}),
],
watchOptions,
devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
devtool:
isDevEnvironment || isNative || isTest
? "source-map"
: "hidden-source-map",
...(isDevEnvironment && {
devServer: {
...createRemoteDevServerConfig(),
static: { directory: clientOutputDir, publicPath: '/__rspack__/' },
static: { directory: clientOutputDir, publicPath: "/__rspack__/" },
hot: true,
liveReload: true,
...(Meteor.isBlazeEnabled && { hot: false }),
port: Meteor.devServerPort || 8080,
port: devServerPort,
devMiddleware: {
writeToDisk: filePath =>
/\.(html)$/.test(filePath) && !filePath.includes('.hot-update.'),
writeToDisk: (filePath) =>
/\.(html)$/.test(filePath) && !filePath.includes(".hot-update."),
},
onListening(devServer) {
if (!devServer) return;
const { host, port } = devServer.options;
const protocol =
devServer.options.server?.type === "https" ? "https" : "http";
const devServerUrl = `${protocol}://${host || "localhost"}:${port}`;
outputMeteorRspack({ devServerUrl });
},
},
}),
...merge(cacheStrategy, { experiments: { css: true } }),
...lazyCompilationConfig,
...loggingConfig,
};
const serverEntry =
isTest && isTestEager && isTestFullApp
isServer && isTest && isTestEager && isTestFullApp
? generateEagerTestFile({
isAppTest: true,
projectDir,
buildContext,
ignoreEntries: [...meteorIgnoreEntries, "**/client/**"],
ignoreEntries: ["**/client/**"],
meteorIgnoreEntries,
prefix: "server",
globalImportPath: path.resolve(projectDir, buildContext, entryPath),
})
: isTest && isTestEager
: isServer && isTest && isTestEager
? generateEagerTestFile({
isAppTest: false,
projectDir,
buildContext,
ignoreEntries: [...meteorIgnoreEntries, "**/client/**"],
ignoreEntries: ["**/client/**"],
meteorIgnoreEntries,
prefix: "server",
globalImportPath: path.resolve(projectDir, buildContext, entryPath),
})
: isTest && testEntry
: isServer && 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]`;
const serverNameConfig = `[${(isTest && "test-") || ""}server-rspack]`;
// Base server config
let serverConfig = {
name: serverNameConfig,
target: 'node',
target: "node",
mode,
entry: serverEntry,
output: {
path: serverOutputDir,
filename: () => `../${buildContext}/${outputPath}`,
libraryTarget: 'commonjs2',
chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
libraryTarget: "commonjs2",
chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
assetModuleFilename,
...(isProd && { clean: { keep: keepOutsideBuild() } }),
},
@@ -598,15 +709,15 @@ module.exports = async function (inMeteor = {}, argv = {}) {
parser: {
javascript: {
// Dynamic imports on the server are treated as bundled in the same chunk
dynamicImportMode: 'eager',
dynamicImportMode: "eager",
},
},
},
resolve: {
extensions,
alias,
modules: ['node_modules', path.resolve(projectDir)],
conditionNames: ['import', 'require', 'node', 'default'],
modules: ["node_modules", path.resolve(projectDir)],
conditionNames: ["import", "require", "node", "default"],
},
externals,
externalsPresets: { node: true },
@@ -614,18 +725,22 @@ module.exports = async function (inMeteor = {}, argv = {}) {
new DefinePlugin(
isTest && (isTestModule || isTestEager)
? {
'Meteor.isTest': JSON.stringify(isTest && !isTestFullApp),
'Meteor.isAppTest': JSON.stringify(isTest && isTestFullApp),
'Meteor.isDevelopment': JSON.stringify(isDev),
"Meteor.isTest": JSON.stringify(isTest && !isTestFullApp),
"Meteor.isAppTest": JSON.stringify(isTest && isTestFullApp),
...(!isPortableBuild && {
"Meteor.isDevelopment": JSON.stringify(isDev),
}),
}
: {
'Meteor.isClient': JSON.stringify(false),
'Meteor.isServer': JSON.stringify(true),
'Meteor.isTest': JSON.stringify(isTestLike && !isTestFullApp),
'Meteor.isAppTest': JSON.stringify(isTestLike && isTestFullApp),
'Meteor.isDevelopment': JSON.stringify(isDev),
'Meteor.isProduction': JSON.stringify(isProd),
},
"Meteor.isClient": JSON.stringify(false),
"Meteor.isServer": JSON.stringify(true),
"Meteor.isTest": JSON.stringify(isTestLike && !isTestFullApp),
"Meteor.isAppTest": JSON.stringify(isTestLike && isTestFullApp),
...(!isPortableBuild && {
"Meteor.isDevelopment": JSON.stringify(isDev),
"Meteor.isProduction": JSON.stringify(isProd),
}),
}
),
...bannerPluginConfig,
requireExternalsPlugin,
@@ -633,104 +748,21 @@ module.exports = async function (inMeteor = {}, argv = {}) {
...doctorPluginConfig,
],
watchOptions,
devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
devtool:
isDevEnvironment || isNative || isTest
? "source-map"
: "hidden-source-map",
...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
cacheStrategy),
...lazyCompilationConfig,
...loggingConfig,
};
// 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 },
devServer: { port: devServerPort },
stats: { preset: "normal" },
infrastructureLogging: { level: "info" },
...(isProd && isClient && { output: { module: false } }),
@@ -757,34 +789,31 @@ module.exports = async function (inMeteor = {}, argv = {}) {
}
: {};
let config = mergeSplitOverlap(
isClient ? clientConfig : serverConfig,
angularExpandConfig
);
// Second pass: re-run only when a mode override was detected, so the user config
// can depend on fully-computed Meteor flags and helpers (swcConfigOptions, buildOutputDir, etc.).
if (nextUserConfig?.mode || nextOverrideConfig?.mode || isAngularEnabled) {
({ nextUserConfig, nextOverrideConfig } = await loadUserAndOverrideConfig(
projectConfigPath,
Meteor,
argv
));
}
let statsOverrided = false;
let config = isClient ? clientConfig : serverConfig;
if (nextUserConfig) {
config = mergeSplitOverlap(config, nextUserConfig);
if (nextUserConfig.stats != null) {
statsOverrided = true;
}
}
config = mergeSplitOverlap(config, angularExpandConfig);
config = mergeSplitOverlap(config, testClientExpandConfig);
// 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);
}
if (nextOverrideConfig) {
config = mergeSplitOverlap(config, nextOverrideConfig);
if (nextOverrideConfig.stats != null) {
statsOverrided = true;
}
}
@@ -794,18 +823,38 @@ module.exports = async function (inMeteor = {}, argv = {}) {
delete config.disablePlugins;
}
delete config["meteor.enablePortableBuild"];
if (Meteor.isDebug || Meteor.isVerbose) {
console.log('Config:', inspect(config, { depth: null, colors: true }));
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') {
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',
"\n⚠ Warning: lazyCompilation may not work correctly in the current Meteor-Rspack integration.\n" +
" This feature will be evaluated for support in future Meteor versions.\n" +
" If you encounter any issues, please disable it in your rspack config.\n"
);
}
// Add MeteorRspackOutputPlugin as the last plugin to output compilation info
const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({
getData: (stats, { isRebuild, compilationCount }) => ({
name: config.name,
mode: config.mode,
hasErrors: stats.hasErrors(),
hasWarnings: stats.hasWarnings(),
timestamp: Date.now(),
statsOverrided,
compilationCount,
isRebuild,
}),
});
config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])];
return [config];
}

View File

@@ -37,9 +37,10 @@
"scripts": {
"install:unit": "cd tools/unit-tests && npm install",
"test:unit": "cd tools/unit-tests && npm test",
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js",
"install:e2e": "cd tools/modern-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell",
"test:e2e": "cd tools/modern-tests && npm test -- "
"install:e2e": "cd tools/e2e-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell",
"test:e2e": "cd tools/e2e-tests && npm test -- ",
"create-app:e2e": "cd tools/e2e-tests && node scripts/create-app.js",
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js"
},
"jshintConfig": {
"esversion": 11

View File

@@ -123,7 +123,7 @@ Meteor.methods({
);
},
async has2faEnabled() {
return await Accounts._is2faEnabledForUser();
return Accounts._is2faEnabledForUser();
},
});

View File

@@ -560,7 +560,7 @@ export class AccountsServer extends AccountsCommon {
type,
fn
) {
return await this._attemptLogin(
return this._attemptLogin(
methodInvocation,
methodName,
methodArgs,
@@ -707,7 +707,7 @@ export class AccountsServer extends AccountsCommon {
const result = await accounts._runLoginHandlers(this, options);
//console.log({result});
return await accounts._attemptLogin(this, "login", arguments, result);
return accounts._attemptLogin(this, "login", arguments, result);
};
methods.logout = async function () {
@@ -760,7 +760,7 @@ export class AccountsServer extends AccountsCommon {
const newStampedToken = accounts._generateStampedLoginToken();
newStampedToken.when = currentStampedToken.when;
await accounts._insertLoginToken(this.userId, newStampedToken);
return await accounts._loginUser(this, this.userId, newStampedToken);
return accounts._loginUser(this, this.userId, newStampedToken);
};
// Removes all tokens except the token associated with the current

View File

@@ -32,7 +32,7 @@ Meteor.methods({
},
}
);
return await getTokenFromSecret({ selector, secret });
return getTokenFromSecret({ selector, secret });
},
getTokenFromSecret,
});

View File

@@ -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);
}
}
);

View File

@@ -290,7 +290,7 @@ Accounts._checkPasswordAsync = checkPasswordAsync;
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')
}
@@ -472,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 };
@@ -507,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");
}

View File

@@ -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;

View File

@@ -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');

View File

@@ -101,17 +101,18 @@ let lastModifiedSwcConfigTime;
BCp.initializeMeteorAppSwcrc = function () {
const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`);
const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`);
if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) {
const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${getMeteorAppDir()}/swc.config.ts`);
if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs && !hasSwcTs) {
return;
}
const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc';
const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc';
const filePath = `${getMeteorAppDir()}/${swcFile}`;
const fileStats = fs.statSync(filePath);
const fileModTime = fileStats?.mtime?.getTime();
let currentLastModifiedConfigTime;
if (hasSwcJs) {
// For dynamic JS files, first get the resolved configuration
if (hasSwcJs || hasSwcTs) {
// For dynamic JS/TS files, first get the resolved configuration
const resolvedConfig = lastModifiedSwcConfigTime?.includes(`${fileModTime}`)
? lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile)
: getMeteorAppSwcrc(swcFile);
@@ -1073,8 +1074,37 @@ function getMeteorAppPackageJson() {
function getMeteorAppSwcrc(file = '.swcrc') {
try {
const filePath = `${getMeteorAppDir()}/${file}`;
if (file.endsWith('.js')) {
if (file.endsWith('.js') || file.endsWith('.ts')) {
let content = fs.readFileSync(filePath, 'utf-8');
if (file.endsWith('.ts')) {
try {
const swc = require('@meteorjs/swc-core');
const result = swc.transformSync(content, {
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es2015',
},
});
content = result.code;
} catch (swcError) {
content = content
.replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '')
.replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '')
.replace(/import\s+['"][^'"]+['"];?/g, '')
.replace(/export\s+default\s+/, 'module.exports = ')
.replace(/export\s+/g, '')
.replace(/:\s*\w+(\[\])?(\s*=)/g, '$2')
.replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)')
.replace(/\):\s*\w+(\[\])?\s*\{/g, ') {')
.replace(/interface\s+\w+\s*\{[^}]*\}/g, '')
.replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
.replace(/as\s+\w+(\[\])?/g, '');
}
}
// Check if the content uses ES module syntax (export default)
if (content.includes('export default')) {
// Transform ES module syntax to CommonJS
@@ -1091,7 +1121,9 @@ function getMeteorAppSwcrc(file = '.swcrc') {
})()
`);
const context = vm.createContext({ process });
return script.runInContext(context);
const result = script.runInContext(context);
// Handle CJS interop wrapper (e.g. { __esModule: true, default: config })
return result && result.__esModule && result.default ? result.default : result;
} else {
// For .swcrc and other JSON files, parse as JSON
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));

View File

@@ -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) {

View File

@@ -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();
});
});
});

View File

@@ -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 ",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {
@@ -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]]}`);
}
}

View File

@@ -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 => {

View File

@@ -122,14 +122,14 @@ export namespace Mongo {
* 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 = any, U = T>(extension: (this: Collection<T, U>, name: string | null, options?: CollectionOptions<T, U>) => void): void;
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 = any, U = T>(name: string, method: (this: Collection<T, U>, ...args: any[]) => any): void;
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.
@@ -573,14 +573,14 @@ export namespace Mongo {
* 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 = any, U = T>(extension: (this: Collection<T, U>, name: string | null, options?: CollectionOptions<T, U>) => void): void;
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 = any, U = T>(name: string, method: (this: Collection<T, U>, ...args: any[]) => any): void;
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.

View File

@@ -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.

View File

@@ -2,7 +2,7 @@
Package.describe({
summary: "Authorization package for Meteor",
version: "1.0.1",
version: "1.0.2",
name: "roles",
documentation: null,
});

View File

@@ -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(

View File

@@ -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) {

View File

@@ -21,6 +21,7 @@ const {
isMeteorAppBuild,
isMeteorBlazeProject,
isMeteorAppNative,
isMeteorAppTestFullApp,
} = require('meteor/tools-core/lib/meteor');
const {
@@ -81,15 +82,26 @@ export function ensureRspackBuildContextExists() {
}
}
const commonBuildEntries = [
RSPACK_BUILD_CONTEXT,
`*/${RSPACK_ASSETS_CONTEXT}`,
`*/${RSPACK_CHUNKS_CONTEXT}`,
RSPACK_DOCTOR_CONTEXT,
];
if (process.env.METEOR_LOCAL_DIR) {
addGitignoreEntries(
appDir,
[process.env.METEOR_LOCAL_DIR, ...commonBuildEntries],
"Meteor custom local directory (METEOR_LOCAL_DIR)"
);
return buildContextPath;
}
addGitignoreEntries(
appDir,
[
RSPACK_BUILD_CONTEXT,
`*/${RSPACK_ASSETS_CONTEXT}`,
`*/${RSPACK_CHUNKS_CONTEXT}`,
RSPACK_DOCTOR_CONTEXT,
],
'Meteor Modern-Tools build context directories',
commonBuildEntries,
"Meteor Modern-Tools build context directories"
);
return buildContextPath;
@@ -129,11 +141,14 @@ export function ensureModuleFilesExist() {
const testClientFiles = {
entryFile: initialEntrypoints.testClient || '',
outputFile: getBuildFilePath({ isTest: true, isTestModule, isClient: true, role: FILE_ROLE.output, onlyFilename: true }),
mainEntryFile: mainClientFiles.entryFile,
};
const testServerFiles = {
entryFile: initialEntrypoints.testServer || '',
outputFile: getBuildFilePath({ isTest: true, isTestModule, isServer: true, role: FILE_ROLE.output, onlyFilename: true }),
mainEntryFile: mainServerFiles.entryFile,
};
const isTestFullApp = isMeteorAppTestFullApp();
const moduleFiles = {
/* Main module files for client and server */
@@ -150,18 +165,18 @@ export function ensureModuleFilesExist() {
[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 }),
[getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isClient: true, ...commandRole })]:
getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isClient: true, ...commandRole, ...testClientFiles }),
[getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.entry })]:
getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.entry, ...testClientFiles }),
[getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.output })]:
getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isClient: true, role: FILE_ROLE.output, ...testClientFiles }),
[getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isServer: true, ...commandRole })]:
getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isServer: true, ...commandRole, ...testServerFiles }),
[getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.entry })]:
getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.entry, ...testServerFiles }),
[getBuildFilePath({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.output })]:
getBuildFileContent({ isTest: true, isTestFullApp, isTestModule, isServer: true, role: FILE_ROLE.output, ...testServerFiles }),
};
Object.entries(moduleFiles).forEach(([filename, defaultContent]) => {
@@ -280,6 +295,20 @@ function getBanner(config, side, env, module, role) {
if (module === 'test') {
// Test file banners
if (role === FILE_ROLE.entry) {
if (!config?.entryFile) {
return `/**
* @file ${side}-entry.js
* @description No code generated
* --------------------------------------------------------------------------
* ⚡ Rspack Test ${sideDisplay} Entry (${envDisplay})
* --------------------------------------------------------------------------
* • [■ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ]
*
* This file is empty because \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` is not set in package.json.
*
${AUTO_GENERATED_WARNING}
*/`;
}
// For test mode, if side is client or server, include it in the title
const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`;
return `/**
@@ -299,6 +328,20 @@ ${AUTO_GENERATED_WARNING}
}
if (role === FILE_ROLE.output) {
if (!config?.entryFile) {
return `/**
* @file ${side}-rspack.js
* @description No code generated
* --------------------------------------------------------------------------
* ⚡ Rspack Test ${sideDisplay} App (${envDisplay})
* --------------------------------------------------------------------------
* • [ ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ]
*
* This file is empty because \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` is not set in package.json.
*
${AUTO_GENERATED_WARNING}
*/`;
}
// For test mode, if side is client or server, include it in the title
const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`;
return `/**
@@ -318,6 +361,20 @@ ${AUTO_GENERATED_WARNING}
}
if (role === FILE_ROLE.run || role === FILE_ROLE.build) {
if (!config?.entryFile) {
return `/**
* @file ${side}-meteor.js
* @description No code generated
* --------------------------------------------------------------------------
* ☄️ Meteor Test ${sideDisplay} App (${envDisplay})
* --------------------------------------------------------------------------
* • [ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ]
*
* This file is empty because \`meteor.testModule${side === 'test' ? '' : `.${side}`}\` is not set in package.json.
*
${AUTO_GENERATED_WARNING}
*/`;
}
// For test mode, if side is client or server, include it in the title
const testType = side === 'test' ? 'Test' : `Test ${sideDisplay}`;
return `/**
@@ -341,6 +398,20 @@ ${AUTO_GENERATED_WARNING}
// For main modules (not test mode), use the new templates
// Entry files
if (role === FILE_ROLE.entry) {
if (!config?.entryFile) {
return `/**
* @file ${side}-entry.js
* @description No code generated
* --------------------------------------------------------------------------
* 🔌 Rspack ${sideDisplay} Entry (${envDisplay})
* --------------------------------------------------------------------------
* • [■ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ]
*
* This file is empty because \`meteor.mainModule.${side}\` is not set in package.json.
*
${AUTO_GENERATED_WARNING}
*/`;
}
return `/**
* @file ${side}-entry.js
* @description Entry point for Rspack build process
@@ -360,6 +431,20 @@ ${AUTO_GENERATED_WARNING}
// Rspack output files
if (role === FILE_ROLE.output) {
if (!config?.entryFile) {
return `/**
* @file ${side}-rspack.js
* @description No code generated
* --------------------------------------------------------------------------
* ⚡ Rspack ${sideDisplay} App (${envDisplay})
* --------------------------------------------------------------------------
* • [ ${side}-entry.js ] ──▶ [■ ${side}-rspack.js ] ──▶ [ ${side}-meteor.js ]
*
* This file is empty because \`meteor.mainModule.${side}\` is not set in package.json.
*
${AUTO_GENERATED_WARNING}
*/`;
}
return `/**
* @file ${side}-rspack.js
* @description Bundled output generated by Rspack
@@ -379,6 +464,20 @@ ${AUTO_GENERATED_WARNING}
// Meteor files (run or build role)
if (role === FILE_ROLE.run || role === FILE_ROLE.build) {
if (!config?.entryFile) {
return `/**
* @file ${side}-meteor.js
* @description No code generated
* --------------------------------------------------------------------------
* ☄️ Meteor ${sideDisplay} App (${envDisplay})
* --------------------------------------------------------------------------
* • [ ${side}-entry.js ] ──▶ [ ${side}-rspack.js ] ──▶ [■ ${side}-meteor.js ]
*
* This file is empty because \`meteor.mainModule.${side}\` is not set in package.json.
*
${AUTO_GENERATED_WARNING}
*/`;
}
return `/**
* @file ${side}-meteor.js
* @description Meteor runtime file that imports the Rspack bundle
@@ -404,6 +503,10 @@ ${AUTO_GENERATED_WARNING}
* @returns {string} The HMR code or empty string
*/
function getHmrCode(config, role) {
if (!config?.entryFile && !config?.isTest) {
return '';
}
if (role === FILE_ROLE.entry && config?.isClient && !config?.isTest) {
return `/* Enables HMR */
if (module.hot) {
@@ -418,9 +521,33 @@ if (module.hot) {
* @returns {string} The import content
*/
function getImportContent(config, side, role) {
if (config?.entryFile && role === FILE_ROLE.entry) {
return `/* Link to 🔌 Meteor ${capitalizeFirstLetter(side)} Entry */
if (!config?.entryFile && !config?.isTest) {
return '';
}
if (role === FILE_ROLE.entry) {
if (config?.isTest) {
return `${
config?.isTestFullApp && config?.mainEntryFile
? `/* Link to 🔌 Meteor ${capitalizeFirstLetter(
side
)} Main Entry (--full-app mode) */
import '../../${config.mainEntryFile}';`
: ""
}
${
config?.entryFile
? `
/* Link to 🔌 Meteor ${capitalizeFirstLetter(side)} Test Entry */
import '../../${config.entryFile}';`
: ""
}`;
}
if (config?.entryFile) {
return `/* Link to 🔌 Meteor ${capitalizeFirstLetter(side)} Entry */
import '../../${config?.entryFile}';`;
}
}
if (config?.outputFile &&

View File

@@ -15,8 +15,10 @@ const {
isMeteorAppDevelopment,
isMeteorAppRun,
isMeteorAppBuild,
isMeteorAppNative,
isMeteorAppDebug,
isMeteorAppTest,
isMeteorAppTestFullApp,
isMeteorAppConfigModernVerbose,
isMeteorBlazeProject,
isMeteorLessProject,
@@ -154,7 +156,9 @@ export function configureMeteorForRspack() {
const initialEntrypointContexts = [
initialEntrypoints.mainClient,
initialEntrypoints.mainServer,
].map(entrypoint => path.dirname(entrypoint));
]
.filter(Boolean)
.map(entrypoint => path.dirname(entrypoint));
const includedDirs = ['public', 'private', '.meteor', RSPACK_BUILD_CONTEXT];
const ignoredDirs = projectRootFilesAndFolders.directories.filter(
dir => !includedDirs.includes(dir),
@@ -338,7 +342,7 @@ export function configureMeteorForRspack() {
isServer: true,
});
const appEntrypoints = {
let appEntrypoints = {
mainClient: `${RSPACK_BUILD_CONTEXT}/${mainClientModule}`,
mainServer: `${RSPACK_BUILD_CONTEXT}/${mainServerModule}`,
...((isTestModule && {
@@ -349,6 +353,13 @@ export function configureMeteorForRspack() {
testServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`,
}),
};
if (isMeteorAppTestFullApp()) {
appEntrypoints = {
...appEntrypoints,
mainClient: `${RSPACK_BUILD_CONTEXT}/${testClientModule}`,
mainServer: `${RSPACK_BUILD_CONTEXT}/${testServerModule}`,
};
}
// Set entry points in environment variables if they exist
setMeteorAppEntrypoints(appEntrypoints);
@@ -360,7 +371,7 @@ export function configureMeteorForRspack() {
ensureModuleFilesExist();
// Write content to module files
if (isMeteorAppRun() && isMeteorAppDevelopment()) {
if (isMeteorAppRun() && isMeteorAppDevelopment() && !isMeteorAppNative()) {
const customScriptUrl = `/__rspack__/${getBuildFilePath({
...env,
isMain: true,

View File

@@ -3,9 +3,11 @@
* @description Constants and global state keys for Rspack plugin
*/
import path from 'path';
export const DEFAULT_RSPACK_VERSION = '1.7.1';
export const DEFAULT_METEOR_RSPACK_VERSION = '1.0.1';
export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.31';
export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3';
@@ -46,6 +48,10 @@ export const GLOBAL_STATE_KEYS = {
const meteorConfig = typeof Plugin !== 'undefined' ? Plugin?.getMeteorConfig() : null;
const meteorLocalDirName = process.env.METEOR_LOCAL_DIR
? path.basename(process.env.METEOR_LOCAL_DIR.replace(/\\/g, '/'))
: '';
/**
* Directory name for Rspack build context
* Can be overridden with RSPACK_BUILD_CONTEXT environment variable
@@ -54,7 +60,7 @@ const meteorConfig = typeof Plugin !== 'undefined' ? Plugin?.getMeteorConfig() :
export const RSPACK_BUILD_CONTEXT =
meteorConfig?.buildContext ||
process.env.RSPACK_BUILD_CONTEXT ||
'_build';
`_build${(meteorLocalDirName && `-${meteorLocalDirName}`) || ''}`;
process.env.RSPACK_BUILD_CONTEXT = RSPACK_BUILD_CONTEXT;
@@ -66,7 +72,7 @@ process.env.RSPACK_BUILD_CONTEXT = RSPACK_BUILD_CONTEXT;
export const RSPACK_ASSETS_CONTEXT =
meteorConfig?.assetsContext ||
process.env.RSPACK_ASSETS_CONTEXT ||
'build-assets';
`build-assets${(meteorLocalDirName && `-${meteorLocalDirName}`) || ''}`;
process.env.RSPACK_ASSETS_CONTEXT = RSPACK_ASSETS_CONTEXT;
@@ -78,7 +84,7 @@ process.env.RSPACK_ASSETS_CONTEXT = RSPACK_ASSETS_CONTEXT;
export const RSPACK_CHUNKS_CONTEXT =
meteorConfig?.chunksContext ||
process.env.RSPACK_CHUNKS_CONTEXT ||
'build-chunks';
`build-chunks${(meteorLocalDirName && `-${meteorLocalDirName}`) || ''}`;
process.env.RSPACK_CHUNKS_CONTEXT = RSPACK_CHUNKS_CONTEXT;

View File

@@ -75,14 +75,11 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
let regularDepsStrings = [];
// Display a header for the installation process
logProgress(`┌─────────────────────────────────────────────────`);
logProgress(`${packageName} Dependencies Installation`);
logProgress(`└─────────────────────────────────────────────────`);
logProgress(`=> 📦 ${packageName} Dependencies`);
// Show what dependencies will be installed
logInfo(`The following ${packageName} dependencies need to be installed:`);
dependencyStrings.forEach(dep => {
logInfo(`${dep}`);
logInfo(` ${dep}`);
});
// Check if this is a Yarn project
@@ -95,7 +92,7 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
// Log progress for dev dependencies
logProgress(
`🔧 Installing ${devDepsToInstall.length} dev dependenc${
`=> 🔧 Installing ${devDepsToInstall.length} dev dependenc${
devDepsToInstall.length === 1 ? "y" : "ies"
}...`
);
@@ -114,7 +111,7 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
// Log progress for regular dependencies
logProgress(
`🔧 Installing ${regularDepsToInstall.length} dependenc${
`=> 🔧 Installing ${regularDepsToInstall.length} dependenc${
regularDepsToInstall.length === 1 ? "y" : "ies"
}...`
);
@@ -131,22 +128,20 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
if (!success) {
const isYarnProj = process.env.YARN_ENABLED === 'true';
logError(`\n┌─────────────────────────────────────────────────`);
logError(`│ ❌ ${packageName} Installation Failed`);
logError(`└─────────────────────────────────────────────────`);
logError(`=> ❌ Failed to install ${packageName}`);
if (!devDepsSuccess && devDepsStrings.length > 0) {
const devInstallCommand = isYarnProj
? `yarn add --dev ${devDepsStrings.join(' ').trim()}`
: `meteor npm install -D ${devDepsStrings.join(' ').trim()}`;
logError(`For dev dependencies, run: ${devInstallCommand}`);
logError(` For dev dependencies, run: ${devInstallCommand}`);
}
if (!regularDepsSuccess && regularDepsStrings.length > 0) {
const regularInstallCommand = isYarnProj
? `yarn add ${regularDepsStrings.join(' ').trim()}`
: `meteor npm install ${regularDepsStrings.join(' ').trim()}`;
logError(`For regular dependencies, run: ${regularInstallCommand}`);
logError(` For regular dependencies, run: ${regularInstallCommand}`);
}
const allFailedDeps = [];
@@ -158,20 +153,14 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
);
}
logSuccess(` ${packageName} dependencies installed`);
logSuccess(`=> ✅ Installed ${packageName} dependencies`);
if (isMeteorAppUpdate()) {
const isYarnProj = process.env.YARN_ENABLED === 'true';
const installCommand = isYarnProj ? 'yarn install' : 'npm install';
logInfo(`\n┌───────────────────────────────────────────────────────────────────────┐`);
logInfo(`│ 🔔 IMPORTANT: Project Stability Reminder │`);
logInfo(`├───────────────────────────────────────────────────────────────────────┤`);
logInfo(`│ After the Meteor update finishes, please run \`${installCommand}\` in your │`);
logInfo(`│ project directory. │`);
logInfo(`│ │`);
logInfo(`│ This helps keep your dependencies correct and your project stable. │`);
logInfo(`└───────────────────────────────────────────────────────────────────────┘`);
logInfo(`=> 🔔 Remember: Run \`${installCommand}\` after the Meteor update finishes.`);
logInfo(` This helps keep your dependencies correct and your project stable.`);
}
}

View File

@@ -0,0 +1,100 @@
/**
* @module logging
* @description Functions for logging Rspack processes
*/
const { logRaw } = require("meteor/tools-core/lib/log");
const {
isMeteorAppConfigModernVerbose,
isMeteorAppProfile,
} = require("meteor/tools-core/lib/meteor");
/**
* Checks if the logs should be verbose for Rspack processes.
* @returns {boolean} True if profiling or verbose mode is enabled, false otherwise.
*/
export function shouldLogVerbose() {
return isMeteorAppProfile() || isMeteorAppConfigModernVerbose();
}
/**
* Strips the leading label line (e.g. "[server-rspack]:\n") from Rspack output.
* @param {string} output - The raw output from an Rspack process
* @returns {string} The output without the leading label line, trimmed
*/
export function stripRspackLabel(output) {
return output.replace(/^\[.*?]:\s*\n/, "").trim();
}
/**
* Parses and extracts [Meteor-Rspack]{}[/Meteor-Rspack] content from data.
* Returns the cleaned data (without the tag content) and the parsed JSON config.
* @param {string} data - The raw data that may contain Meteor-Rspack tags
* @returns {{ cleanedData: string, config: Object|null }} Object with cleaned data and parsed config
*/
export function parseMeteorRspackOutput(data) {
const tagRegex = /\[Meteor-Rspack\](.*?)\[\/Meteor-Rspack\]/g;
let config = null;
let match;
// Find all matches and parse the last one (in case of multiple)
while ((match = tagRegex.exec(data)) !== null) {
try {
config = JSON.parse(match[1]);
} catch (e) {
// If JSON parsing fails, keep config as null
config = null;
}
}
// Remove all [Meteor-Rspack]...[/Meteor-Rspack] tags from the data
const cleanedData = data.replace(tagRegex, "").trim();
return { cleanedData, config };
}
const compilationCount = {};
let hmrServerLogged = false;
/**
* Logs "=> Started Rspack HMR server at <devServerUrl>" if devServerUrl exists in config.
* Only logs once per session.
* @param {Object|null} config - The parsed config from MeteorRspackOutputPlugin
*/
export function logHmrServerStarted(config) {
if (hmrServerLogged) return;
if (!config?.devServerUrl) return;
hmrServerLogged = true;
logRaw(`=> Started Rspack HMR server at ${config.devServerUrl}/`);
}
/**
* Logs a friendly Meteor-style message with the raw Rspack output appended.
* Strips the leading label and logs as:
* "=> Compiled your client app compiled successfully in 342 ms"
* Adds a leading newline from the second compilation onwards per target.
* @param {string} output - The raw stdout line from an Rspack process
* @param {string} target - The build target label (e.g. "client", "server")
* @param {boolean} statsOverrided - If true, skip cleaning and use \n separator
*/
export function logCompilationOutput(output, target, statsOverrided = false) {
let cleaned;
let separator;
// Logs original Rspack logging when stats overrided by user
if (statsOverrided) {
cleaned = stripRspackLabel(output);
separator = "\n";
} else {
cleaned = stripRspackLabel(output)
.replace(/^.*\[.*?]\s*/g, "")
.trim()
.replace(/\s*compiled\s*/g, "");
separator = cleaned.includes("\n") ? ":\n" : " ";
// Ignore successful logs on default Meteor-Rspack logging
if (/\s*successfully\s*/g.test(cleaned)) return;
}
compilationCount[target] = (compilationCount[target] || 0) + 1;
const prefix = compilationCount[target] > 1 ? "\n=>" : "=>";
logRaw(`${prefix} Compiled Rspack ${target} app${separator}${cleaned}`);
}

View File

@@ -3,8 +3,8 @@
* @description Functions for managing Rspack processes
*/
import fs from 'fs';
import path from 'path';
import fs from "fs";
import path from "path";
const {
spawnProcess,
@@ -15,7 +15,9 @@ const {
const {
logError,
logInfo,
} = require('meteor/tools-core/lib/log');
logRaw,
getRunLog,
} = require("meteor/tools-core/lib/log");
const {
getMeteorAppDir,
@@ -33,6 +35,7 @@ const {
isMeteorAppConfigModernVerbose,
isMeteorBundleVisualizerProject,
getMeteorAppPort,
inheritMeteorToolNodeFlags,
} = require('meteor/tools-core/lib/meteor');
const {
@@ -58,6 +61,15 @@ const {
getBuildFileContent,
} = require('./build-context');
import {
logCompilationOutput,
logHmrServerStarted,
parseMeteorRspackOutput,
shouldLogVerbose,
stripRspackLabel,
} from "./logging";
import { isMeteorAppProfile } from "../../tools-core/lib/meteor";
/**
* Calculates the devServerPort based on process.env.PORT
* Base port is 8077, and we add the sum of the digits of process.env.PORT
@@ -221,29 +233,52 @@ export function getRspackEnv({ isClient, isServer, isTest: inIsTest, isTestLike:
const isBlazeHotEnabled = isMeteorBlazeHotProject();
const isBundleVisualizerEnabled = isMeteorBundleVisualizerProject();
const isProfile = isMeteorAppProfile();
const swcExternalHelpers = checkNpmDependencyExists('@swc/helpers');
const configPath = getConfigFilePath();
const projectConfigPath = getCustomConfigFilePath();
const pairs = [
['isDevelopment', isMeteorAppDevelopment()],
['isProduction', isMeteorAppProduction()],
['isDebug', isMeteorAppDebug()],
['isVerbose', isMeteorAppConfigModernVerbose()],
['isTest', isTest],
...(isTestLike ? [['isTestLike', isTestLike || isTest]] : []),
...(isTestLike && isTestFullApp && [['isTestFullApp', isTestFullApp]] || []),
...(isTestLike && isTestModule && [['isTestModule', isTestModule]] || []),
...(isTestLike && isTestEager && [['isTestEager', isTestEager]] || []),
['isRun', isMeteorAppRun()],
['isBuild', isMeteorAppBuild()],
['isNative', isMeteorAppNative()],
['isClient', isClient],
['isServer', isServer],
['entryPath', getBuildFilePath({ ...module, ...env, ...side, isTestModule, role: FILE_ROLE.entry }) ],
['outputPath', getBuildFilePath({ ...module, ...env, ...side, isTestModule, role: FILE_ROLE.output }) ],
['outputFilename',
["isDevelopment", isMeteorAppDevelopment()],
["isProduction", isMeteorAppProduction()],
["isDebug", isMeteorAppDebug()],
["isVerbose", isMeteorAppConfigModernVerbose()],
...((isProfile && [["isProfile", isMeteorAppProfile()]]) || []),
["isTest", isTest],
...(isTestLike ? [["isTestLike", isTestLike || isTest]] : []),
...((isTestLike && isTestFullApp && [["isTestFullApp", isTestFullApp]]) ||
[]),
...((isTestLike && isTestModule && [["isTestModule", isTestModule]]) || []),
...((isTestLike && isTestEager && [["isTestEager", isTestEager]]) || []),
["isRun", isMeteorAppRun()],
["isBuild", isMeteorAppBuild()],
["isNative", isMeteorAppNative()],
["isClient", isClient],
["isServer", isServer],
[
"entryPath",
getBuildFilePath({
...module,
...env,
...side,
isTestModule,
role: FILE_ROLE.entry,
}),
],
[
"outputPath",
getBuildFilePath({
...module,
...env,
...side,
isTestModule,
role: FILE_ROLE.output,
}),
],
[
"outputFilename",
getBuildFilePath({
...env,
...side,
@@ -252,41 +287,49 @@ export function getRspackEnv({ isClient, isServer, isTest: inIsTest, isTestLike:
onlyFilename: true,
}),
],
['runPath', getBuildFilePath({ ...module, ...env, ...side, ...commandRole }) ],
['buildContext', RSPACK_BUILD_CONTEXT],
['chunksContext', RSPACK_CHUNKS_CONTEXT],
['assetsContext', RSPACK_ASSETS_CONTEXT],
['devServerPort', process.env.RSPACK_DEVSERVER_PORT],
['projectConfigPath', projectConfigPath],
['configPath', configPath],
[
"runPath",
getBuildFilePath({ ...module, ...env, ...side, ...commandRole }),
],
["buildContext", RSPACK_BUILD_CONTEXT],
["chunksContext", RSPACK_CHUNKS_CONTEXT],
["assetsContext", RSPACK_ASSETS_CONTEXT],
["devServerPort", process.env.RSPACK_DEVSERVER_PORT],
["projectConfigPath", projectConfigPath],
["configPath", configPath],
...((isTest &&
initialEntrypoints.testClient &&
initialEntrypoints.testServer && [
['testClientEntry', initialEntrypoints.testClient],
['testServerEntry', initialEntrypoints.testServer],
["testClientEntry", initialEntrypoints.testClient],
["testServerEntry", initialEntrypoints.testServer],
]) ||
(isTest &&
initialEntrypoints.testModule && [
['testEntry', initialEntrypoints.testModule],
]) || [
['mainClientEntry', initialEntrypoints.mainClient],
['mainClientHtmlEntry', initialEntrypoints.mainClientHtml],
['mainServerEntry', initialEntrypoints.mainServer],
]),
...(swcExternalHelpers && [['swcExternalHelpers', swcExternalHelpers]] || []),
...(isReactEnabled && [['isReactEnabled', isReactEnabled]] || []),
...(isBlazeEnabled && [['isBlazeEnabled', isBlazeEnabled]] || []),
...(isBlazeHotEnabled && [['isBlazeHotEnabled', isBlazeHotEnabled]] || []),
...(isTypescriptEnabled && [['isTypescriptEnabled', isTypescriptEnabled]] || []),
...(isAngularEnabled && [['isAngularEnabled', isAngularEnabled]] || []),
...(isTsxEnabled && [['isTsxEnabled', isTsxEnabled]] || []),
...(isJsxEnabled && [['isJsxEnabled', isJsxEnabled]] || []),
...(isBundleVisualizerEnabled && [
['isBundleVisualizerEnabled', isBundleVisualizerEnabled],
['rsdoctorClientPort', process.env.RSDOCTOR_CLIENT_PORT],
['rsdoctorServerPort', process.env.RSDOCTOR_SERVER_PORT],
] || []),
["testEntry", initialEntrypoints.testModule],
]) || [
["mainClientEntry", initialEntrypoints.mainClient],
["mainClientHtmlEntry", initialEntrypoints.mainClientHtml],
["mainServerEntry", initialEntrypoints.mainServer],
]),
...((swcExternalHelpers && [["swcExternalHelpers", swcExternalHelpers]]) ||
[]),
...((isReactEnabled && [["isReactEnabled", isReactEnabled]]) || []),
...((isBlazeEnabled && [["isBlazeEnabled", isBlazeEnabled]]) || []),
...((isBlazeHotEnabled && [["isBlazeHotEnabled", isBlazeHotEnabled]]) ||
[]),
...((isTypescriptEnabled && [
["isTypescriptEnabled", isTypescriptEnabled],
]) ||
[]),
...((isAngularEnabled && [["isAngularEnabled", isAngularEnabled]]) || []),
...((isTsxEnabled && [["isTsxEnabled", isTsxEnabled]]) || []),
...((isJsxEnabled && [["isJsxEnabled", isJsxEnabled]]) || []),
...((isBundleVisualizerEnabled && [
["isBundleVisualizerEnabled", isBundleVisualizerEnabled],
["rsdoctorClientPort", process.env.RSDOCTOR_CLIENT_PORT],
["rsdoctorServerPort", process.env.RSDOCTOR_SERVER_PORT],
]) ||
[]),
].filter(Boolean);
// Create environment variables object with bannerOutput
@@ -327,37 +370,76 @@ export function startRspackClientServe(options = {}) {
command,
args, {
cwd: appDir,
env: { ...process.env, ...envs },
env: inheritMeteorToolNodeFlags({ ...process.env, ...envs }),
onStdout: (data) => {
logInfo(`[Rspack Client] ${data}`);
if (onCompile && data.trim().includes("compiled")) {
onCompile(data);
const { cleanedData, config } = parseMeteorRspackOutput(data);
if (config && !!config?.devServerUrl) {
logHmrServerStarted(config);
}
if (onCompile && config && (config?.compilationCount || 0) > 0) {
onCompile(cleanedData, config);
if (
config?.name?.includes("client") &&
!config?.hasErrors &&
config?.isRebuild
) {
getRunLog()?.logClientRestart();
}
}
if (!cleanedData) return;
if (shouldLogVerbose()) {
logInfo(`[Rspack Client] ${cleanedData}`);
} else {
logCompilationOutput(cleanedData, 'client', config?.statsOverrided);
}
},
onStderr: (data) => {
const { cleanedData } = parseMeteorRspackOutput(data);
if (!cleanedData) return;
// Check if this is an EADDRINUSE error in development mode (which we want to completely ignore)
if (isMeteorAppDevelopment() && data.includes('EADDRINUSE')) {
logError(`[Rspack Client Error] ${data}`);
if (isMeteorAppDevelopment() && cleanedData.includes('EADDRINUSE')) {
if (shouldLogVerbose()) {
logError(`[Rspack Client Error] ${cleanedData}`);
} else {
logError(stripRspackLabel(cleanedData));
}
return;
}
// Check if this is actually an informational message (like webpack-dev-server messages)
if (data.includes('Loopback:') || data.includes('Project is running at:')) {
logInfo(`[Rspack Client] ${data}`);
if (cleanedData.includes('Loopback:') || cleanedData.includes('Project is running at:')) {
if (shouldLogVerbose()) {
logInfo(`[Rspack Client] ${cleanedData}`);
} else {
logRaw(stripRspackLabel(cleanedData));
}
} else {
// Check if this is the "npm error could not determine executable to run" error
if (data.includes('npm error could not determine executable to run')) {
if (cleanedData.includes('npm error could not determine executable to run')) {
const errorMsg = '[Rspack Client Error] Try running "meteor npm install" to ensure rspack is available';
logError(errorMsg);
if (shouldLogVerbose()) {
logError(errorMsg);
} else {
logError('Try running "meteor npm install" to ensure rspack is available');
}
throw new Error(errorMsg);
}
logError(`[Rspack Client Error] ${data}`);
if (shouldLogVerbose()) {
logError(`[Rspack Client Error] ${cleanedData}`);
} else {
logError(stripRspackLabel(cleanedData));
}
}
},
onError: (err) => {
const errorMsg = `Rspack Error: ${err.message}`;
logError(errorMsg);
if (shouldLogVerbose()) {
logError(errorMsg);
} else {
logError(err.message);
}
throw new Error(errorMsg);
}
},
});
// Store the new process in global state
@@ -390,30 +472,54 @@ export function startRspackServerWatch(options = {}) {
command,
args, {
cwd: appDir,
env: { ...process.env, ...envs },
env: inheritMeteorToolNodeFlags({ ...process.env, ...envs }),
onStdout: (data) => {
logInfo(`[Rspack Server] ${data}`);
if (onCompile && data.trim().includes("compiled")) {
onCompile(data);
const { cleanedData, config } = parseMeteorRspackOutput(data);
if (onCompile && config && (config?.compilationCount || 0) > 0) {
onCompile(cleanedData, config);
}
if (!cleanedData) return;
if (shouldLogVerbose()) {
logInfo(`[Rspack Server] ${cleanedData}`);
} else {
logCompilationOutput(cleanedData, 'server', config?.statsOverrided);
}
},
onStderr: (data) => {
const { cleanedData } = parseMeteorRspackOutput(data);
if (!cleanedData) return;
// Check if this is actually an informational message (like webpack-dev-server messages)
if (data.includes('Project is running at:')) {
logInfo(`[Rspack Server] ${data}`);
if (cleanedData.includes('Project is running at:')) {
if (shouldLogVerbose()) {
logInfo(`[Rspack Server] ${cleanedData}`);
} else {
logRaw(stripRspackLabel(cleanedData));
}
} else {
// Check if this is the "npm error could not determine executable to run" error
if (data.includes('npm error could not determine executable to run')) {
if (cleanedData.includes('npm error could not determine executable to run')) {
const errorMsg = '[Rspack Server Error] Try running "meteor npm install" to ensure rspack is available';
logError(errorMsg);
if (shouldLogVerbose()) {
logError(errorMsg);
} else {
logError('Try running "meteor npm install" to ensure rspack is available');
}
throw new Error(errorMsg);
}
logError(`[Rspack Server Error] ${data}`);
if (shouldLogVerbose()) {
logError(`[Rspack Server Error] ${cleanedData}`);
} else {
logError(stripRspackLabel(cleanedData));
}
}
},
onError: (err) => {
const errorMsg = `Rspack Error: ${err.message}`;
logError(errorMsg);
if (shouldLogVerbose()) {
logError(errorMsg);
} else {
logError(err.message);
}
throw new Error(errorMsg);
}
});
@@ -457,25 +563,45 @@ export async function runRspackBuild({ isClient, isServer, isTest, isTestModule,
args,
{
cwd: appDir,
env: { ...process.env, ...envs },
env: inheritMeteorToolNodeFlags({ ...process.env, ...envs }),
onStdout: (data) => {
logInfo(`[Rspack ${label} ${endpoint}] ${data}`);
if (onCompile && data.trim().includes("compiled")) {
onCompile(data);
const { cleanedData, config } = parseMeteorRspackOutput(data);
if (onCompile && config && (config?.compilationCount || 0) > 0) {
onCompile(cleanedData, config);
}
if (!cleanedData) return;
if (shouldLogVerbose()) {
logInfo(`[Rspack ${label} ${endpoint}] ${cleanedData}`);
} else {
logCompilationOutput(cleanedData, endpoint.toLowerCase(), config?.statsOverrided);
}
},
onStderr: (data) => {
const { cleanedData } = parseMeteorRspackOutput(data);
if (!cleanedData) return;
// Check if this is actually an informational message (like webpack-dev-server messages)
if (data.includes('Project is running at:')) {
logInfo(`[Rspack ${label} ${endpoint}] ${data}`);
if (cleanedData.includes('Project is running at:')) {
if (shouldLogVerbose()) {
logInfo(`[Rspack ${label} ${endpoint}] ${cleanedData}`);
} else {
logRaw(stripRspackLabel(cleanedData));
}
} else {
// Check if this is the "npm error could not determine executable to run" error
if (data.includes('npm error could not determine executable to run')) {
if (cleanedData.includes('npm error could not determine executable to run')) {
const errorMsg = `[Rspack ${label} Error ${endpoint}] Try running "meteor npm install" to ensure rspack is available`;
logError(errorMsg);
if (shouldLogVerbose()) {
logError(errorMsg);
} else {
logError(`Try running "meteor npm install" to ensure rspack is available`);
}
throw new Error(errorMsg);
}
logError(`[Rspack ${label} Error ${endpoint}] ${data}`);
if (shouldLogVerbose()) {
logError(`[Rspack ${label} Error ${endpoint}] ${cleanedData}`);
} else {
logError(stripRspackLabel(cleanedData));
}
}
},
onExit: (code) => {
@@ -483,12 +609,20 @@ export async function runRspackBuild({ isClient, isServer, isTest, isTestModule,
resolve();
} else {
const error = new Error(`Rspack ${label} failed in ${endpoint} with exit code ${code}`);
logError(error.message);
if (shouldLogVerbose()) {
logError(error.message);
} else {
logError(`Rspack ${label} failed with exit code ${code}`);
}
reject(error);
}
},
onError: (err) => {
logError(`Rspack ${label} ${endpoint} error: ${err.message}`);
if (shouldLogVerbose()) {
logError(`Rspack ${label} ${endpoint} error: ${err.message}`);
} else {
logError(err.message);
}
reject(err);
}
});

View File

@@ -67,7 +67,6 @@ const {
getMeteorAppEntrypoints,
isMeteorAppTest,
isMeteorAppTestWatch,
isMeteorAppTestFullApp,
isMeteorAppDevelopment,
isMeteorAppProduction,
isMeteorAppDebug,
@@ -89,12 +88,13 @@ const {
} = require('meteor/tools-core/lib/npm');
const { hasMeteorAppConfigAutoInstallDeps } = require("../tools-core/lib/meteor");
// Get entry points from Meteor configuration
let initialEntrypoints;
if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest() || isMeteorAppUpdate()) {
// Get entry points from Meteor configuration
const initialEntrypoints = getMeteorInitialAppEntrypoints();
initialEntrypoints = getMeteorInitialAppEntrypoints();
// Check if mainClient and mainServer exist
if (!initialEntrypoints.mainClient || !initialEntrypoints.mainServer) {
if (!initialEntrypoints?.mainServer) {
logError(`\n┌─────────────────────────────────────────────────`);
logError(`│ ❌ Missing Required Entry Points`);
logError(`└─────────────────────────────────────────────────`);
@@ -177,6 +177,11 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
// Configure Meteor settings for Rspack
configureMeteorForRspack();
// Set native mode flag so the server module can skip dev proxy setup
if (isMeteorAppNative()) {
process.env.RSPACK_NATIVE = 'true';
}
// Calculate and set the devServerPort at boot
if (!process.env.RSPACK_DEVSERVER_PORT) {
process.env.RSPACK_DEVSERVER_PORT = calculateDevServerPort();
@@ -230,25 +235,43 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
// For 'run' command, start Rspack in appropriate modes with distinct callbacks
if (isMeteorAppDevelopment() && !isMeteorAppNative()) {
startRspackClientServe({ onCompile: onCompileClient });
startRspackServerWatch({ onCompile: onCompileServer });
if (initialEntrypoints?.mainClient) {
startRspackClientServe({ onCompile: onCompileClient });
}
if (initialEntrypoints?.mainServer) {
startRspackServerWatch({ onCompile: onCompileServer });
}
} else if (isMeteorAppProduction() || isMeteorAppNative()) {
runRspackBuild({
isClient: true,
isServer: false,
watch: true,
onCompile: onCompileClient,
});
runRspackBuild({
isServer: true,
isClient: false,
watch: true,
onCompile: onCompileServer,
});
if (initialEntrypoints?.mainClient) {
runRspackBuild({
isClient: true,
isServer: false,
watch: true,
onCompile: onCompileClient,
});
}
if (initialEntrypoints?.mainServer) {
runRspackBuild({
isServer: true,
isClient: false,
watch: true,
onCompile: onCompileServer,
});
}
}
// Wait for first compilation to complete
await waitForFirstCompilation(clientFirstCompile, serverFirstCompile, clientFirstCompilePromise, serverFirstCompilePromise);
const waitTarget =
initialEntrypoints?.mainClient && initialEntrypoints?.mainServer
? 'both'
: 'server';
await waitForFirstCompilation(
clientFirstCompile,
serverFirstCompile,
clientFirstCompilePromise,
serverFirstCompilePromise,
{ target: waitTarget },
);
// When running `meteor test` command
} else if (isMeteorAppTest()) {
@@ -264,61 +287,56 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
onCompileServer,
} = setupCompilationTracking();
// When run test for full app, run Rspack app server as well
// isTestLike ensures the app runtime environment inherit test envs
if (isMeteorAppTestFullApp()) {
await runRspackBuild({
isTest: false,
isTestLike: true,
isServer: true,
isClient: false,
});
if (isMeteorAppTestWatch()) {
runRspackBuild({
isServer: true,
isClient: false,
isTest: false,
isTestLike: true,
watch: true,
});
}
}
// When testModule is specified for client or server, run Rspack considering those files
if (initialEntrypoints?.testClient || initialEntrypoints?.testServer) {
runRspackBuild({
isTest: true,
isClient: true,
isServer: false,
watch: isMeteorAppTestWatch(),
onCompile: onCompileClient,
label: 'Test',
});
if (initialEntrypoints?.testClient) {
runRspackBuild({
isTest: true,
isClient: true,
isServer: false,
watch: isMeteorAppTestWatch(),
onCompile: onCompileClient,
label: 'Test',
});
}
runRspackBuild({
isTest: true,
isClient: false,
isServer: true,
watch: isMeteorAppTestWatch(),
onCompile: onCompileServer,
label: 'Test',
});
if (initialEntrypoints?.testServer) {
runRspackBuild({
isTest: true,
isClient: false,
isServer: true,
watch: isMeteorAppTestWatch(),
onCompile: onCompileServer,
label: 'Test',
});
}
// Wait for first compilation to complete
await waitForFirstCompilation(clientFirstCompile, serverFirstCompile, clientFirstCompilePromise, serverFirstCompilePromise);
const waitTarget =
initialEntrypoints?.testClient && initialEntrypoints?.testServer
? 'both'
: 'server';
await waitForFirstCompilation(
clientFirstCompile,
serverFirstCompile,
clientFirstCompilePromise,
serverFirstCompilePromise,
{ target: waitTarget },
);
// When testModule is specified as a single file or not specified
} else {
runRspackBuild({
isTest: true,
isTestModule: true,
isClient: true,
isServer: false,
watch: isMeteorAppTestWatch(),
onCompile: onCompileClient,
label: 'Test',
});
if (initialEntrypoints?.testModule) {
runRspackBuild({
isTest: true,
isTestModule: true,
isClient: true,
isServer: false,
watch: isMeteorAppTestWatch(),
onCompile: onCompileClient,
label: 'Test',
});
}
runRspackBuild({
isTest: true,
isTestModule: true,
@@ -328,17 +346,28 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
onCompile: onCompileServer,
label: 'Test',
});
await waitForFirstCompilation(clientFirstCompile, serverFirstCompile, clientFirstCompilePromise, serverFirstCompilePromise, { target: 'server' });
const waitTarget = initialEntrypoints?.testModule ? 'both' : 'server';
await waitForFirstCompilation(
clientFirstCompile,
serverFirstCompile,
clientFirstCompilePromise,
serverFirstCompilePromise,
{ target: waitTarget }
);
}
// When running `meteor build` command
} else if (isMeteorAppBuild()) {
// For 'build' command, run Rspack build without watch mode
// Run client and server builds in parallel and wait for both to complete
await Promise.all([
runRspackBuild({ isClient: true, isServer: false }),
runRspackBuild({ isServer: true, isClient: false }),
]);
const targetsToBuild = [
initialEntrypoints?.mainClient &&
runRspackBuild({ isClient: true, isServer: false }),
initialEntrypoints?.mainServer &&
runRspackBuild({ isServer: true, isClient: false }),
].filter(Boolean);
await Promise.all(targetsToBuild);
}
} catch (error) {
logError(`Rspack plugin error: ${error.message}`);

View File

@@ -28,7 +28,11 @@ const RSPACK_ASSETS_REGEX = new RegExp(
`^\/${rspackAssetsContext}\/(.+)$`,
);
if (Meteor.isDevelopment) {
const shouldEnableDevHMRProxy =
global?.Package?.["tools-core"] != null &&
Meteor.isDevelopment &&
!process.env.RSPACK_NATIVE;
if (shouldEnableDevHMRProxy) {
const { shuffleString } = require('meteor/tools-core/lib/string');
const { createProxyMiddleware } = require('http-proxy-middleware');

View File

@@ -210,7 +210,9 @@ runTests = function () {
// Also log xUnit output
xunit('<testsuite errors="" failures="" name="meteor" skips="" tests="" time="">');
resultSet.forEach(function (result, name) {
Object.keys(resultSet).forEach(function (name) {
let result = resultSet[name];
var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client");
var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client");
var time = "";

View File

@@ -6,6 +6,7 @@ Package.describe({
Package.onUse(function(api) {
api.use(['tinytest', 'random', 'ejson', 'check', 'ecmascript']);
api.use('fetch', 'server');
api.use('jquery', 'client');
api.export('TEST_STATUS', 'client');

View File

@@ -15,6 +15,7 @@ export PATH=$METEOR_HOME:$PATH
export URL='http://127.0.0.1:4096/'
export METEOR_PACKAGE_DIRS='packages/deprecated'
export METEOR_NO_DEPRECATION=true
exec 3< <(./meteor test-packages --driver-package test-in-console -p 4096 --exclude ${TEST_PACKAGES_EXCLUDE:-''} $1)
EXEC_PID=$!

View File

@@ -272,7 +272,13 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t
### Assertions without optional fail messages
`test.throws(func, expected);`
`test.throws(func, expected[, message]);`
`test.throwsAsync(func, expected[, message]);`
`test.doesNotThrows(func[, failureMessage]);`
`test.doesNotThrowsAsync(func[, failureMessage]);`
`expected` can be:
@@ -281,6 +287,8 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t
- `regexp`: pass if the exception message passes the regexp.
- `function`: call the function as a predicate with the exception.
`doesNotThrows` and `doesNotThrowsAsync` assert that the function does not throw. If the function throws, the assertion fails. The optional `failureMessage` is only used to annotate the failure.
Note: Node's `assert.throws` also accepts a constructor to test whether the error is of the expected class. But since JavaScript can't distinguish between constructors and plain functions and Node's `assert.throws` also accepts a predicate function, if the error fails the `instanceof` test with the constructor then the constructor is then treated as a predicate and called (!)
The upshot is, if you want to test whether an error is of a particular class, use a predicate function.

View File

@@ -244,6 +244,19 @@ export class TestCaseResults {
// The upshot is, if you want to test whether an error is of a
// particular class, use a predicate function.
//
/**
* Assert that `f` throws.
*
* `expected` can be:
* - undefined: accept any exception.
* - string: pass if the string is a substring of the exception message.
* - regexp: pass if the exception message passes the regexp.
* - function: call the function as a predicate with the exception.
*
* @param {Function} f
* @param {*} expected
* @param {String} message
*/
throws(f, expected, message) {
let actual;
const predicate = this._guessPredicate(expected);
@@ -258,10 +271,34 @@ export class TestCaseResults {
}
/**
* Same as throw, but accepts an async function as a parameter.
* @param f
* @param expected
* @param message
* Assert that `f` does not throw.
* @param {Function} f
* @param {String} failureMessage
*/
doesNotThrows(f, failureMessage) {
let actual;
try {
f();
} catch (exception) {
actual = exception;
}
if (!actual) {
this.ok();
} else {
this.fail({
type: "throws",
message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""),
});
}
}
/**
* Same as `throws`, but accepts an async function as a parameter.
* @param {Function} f
* @param {*} expected
* @param {String} message
* @returns {Promise<void>}
*/
async throwsAsync(f, expected, message) {
@@ -276,6 +313,31 @@ export class TestCaseResults {
this._assertActual(actual, predicate, message);
}
/**
* Same as `doesNotThrows`, but accepts an async function as a parameter.
* @param {Function} f
* @param {String} failureMessage
* @returns {Promise<void>}
*/
async doesNotThrowsAsync(f, failureMessage) {
let actual;
try {
await f();
} catch (exception) {
actual = exception;
}
if (!actual) {
this.ok();
} else {
this.fail({
type: "throws",
message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""),
});
}
}
isTrue(v, msg) {
if (v)
this.ok();

View File

@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import { logProgress, logSuccess, logInfo, logError } from './log';
import { logError, logProgress, logSuccess } from './log';
/**
* Checks if the given directory is a git repository
@@ -45,7 +45,7 @@ export function ensureGitignoreExists(dir, initialEntries = []) {
fs.writeFileSync(gitignorePath, content, 'utf8');
return true;
} catch (error) {
console.error(`Error creating .gitignore file: ${error.message}`);
logError(`=> Failed to create .gitignore: ${error.message}`);
return false;
}
}
@@ -71,7 +71,7 @@ export function getMissingGitignoreEntries(dir, entries) {
return entries.filter(entry => !lines.includes(entry));
} catch (error) {
console.error(`Error reading .gitignore file: ${error.message}`);
logError(`=> Failed to read .gitignore: ${error.message}`);
return entries;
}
}
@@ -96,16 +96,7 @@ export function addGitignoreEntries(dir, entries, context = '') {
return true; // All entries already exist
}
// Display a header for the gitignore entries addition
logProgress(`┌─────────────────────────────────────────────────`);
logProgress(`│ Adding Gitignore Entries${context ? ` for ${context}` : ''}`);
logProgress(`└─────────────────────────────────────────────────`);
// Show what entries will be added
logInfo(`The following entries will be added to .gitignore:`);
missingEntries.forEach(entry => {
logInfo(`${entry}`);
});
logProgress(`=> Adding gitignore entries${context ? ` for ${context}` : ''}: ${missingEntries.join(', ')}`);
try {
const gitignorePath = path.join(dir, '.gitignore');
@@ -126,13 +117,10 @@ export function addGitignoreEntries(dir, entries, context = '') {
content += missingEntries.join('\n') + '\n';
fs.writeFileSync(gitignorePath, content, 'utf8');
logSuccess(`✅ Gitignore entries${context ? ` for ${context}` : ''} added`);
logSuccess(`=> Added gitignore entries${context ? ` for ${context}` : ''}`);
return true;
} catch (error) {
logError(`\n┌─────────────────────────────────────────────────`);
logError(`│ ❌ Failed to Add Gitignore Entries${context ? ` for ${context}` : ''}`);
logError(`└─────────────────────────────────────────────────`);
logError(`Error: ${error.message}`);
logError(`=> Failed to add gitignore entries${context ? ` for ${context}` : ''}: ${error.message}`);
return false;
}
}

View File

@@ -1,21 +1,38 @@
// Check if colors should be disabled
const shouldDisableColors = !!process.env.METEOR_DISABLE_COLORS;
// Minimum message length for consistent log formatting
const MIN_MESSAGE_LENGTH = 80;
// ANSI color codes
const colors = {
reset: shouldDisableColors ? '' : '\x1b[0m',
blue: shouldDisableColors ? '' : '\x1b[34m',
red: shouldDisableColors ? '' : '\x1b[31m',
purple: shouldDisableColors ? '' : '\x1b[35m',
green: shouldDisableColors ? '' : '\x1b[32m'
reset: shouldDisableColors ? "" : "\x1b[0m",
blue: shouldDisableColors ? "" : "\x1b[34m",
red: shouldDisableColors ? "" : "\x1b[31m",
purple: shouldDisableColors ? "" : "\x1b[35m",
green: shouldDisableColors ? "" : "\x1b[32m",
cyan: shouldDisableColors ? "" : "\x1b[36m",
};
/**
* Pad a message to ensure it has a minimum length
* @param {string} message - The message to pad
* @param {number} minLength - The minimum length (default: MIN_MESSAGE_LENGTH)
* @returns {string} The padded message
*/
export function padMessage(message, minLength = MIN_MESSAGE_LENGTH) {
if (message.length >= minLength) {
return message;
}
return message.padEnd(minLength);
}
/**
* Log a progress message in blue
* @param {string} message - The message to log
*/
export function logProgress(message) {
console.log(`${colors.blue}${message}${colors.reset}`);
console.log(`${colors.blue}${padMessage(message)}${colors.reset}`);
}
/**
@@ -23,15 +40,23 @@ export function logProgress(message) {
* @param {string} message - The message to log
*/
export function logError(message) {
console.error(`${colors.red}${message}${colors.reset}`);
console.error(`${colors.red}${padMessage(message)}${colors.reset}`);
}
/**
* Log an info message in purple
* Log an info message in cyan
* @param {string} message - The message to log
*/
export function logInfo(message) {
console.log(`${colors.purple}${message}${colors.reset}`);
console.log(`${colors.cyan}${padMessage(message)}${colors.reset}`);
}
/**
* Log a raw message without any color
* @param {string} message - The message to log
*/
export function logRaw(message) {
console.log(padMessage(message));
}
/**
@@ -39,5 +64,16 @@ export function logInfo(message) {
* @param {string} message - The message to log
*/
export function logSuccess(message) {
console.log(`${colors.green}${message}${colors.reset}`);
console.log(`${colors.green}${padMessage(message)}${colors.reset}`);
}
/**
* Get the runLogInstance from the Plugin object if it exists
* @returns {Object|undefined} The runLogInstance or undefined
*/
export function getRunLog() {
if (typeof Plugin !== 'undefined') {
return Plugin.runLogInstance;
}
return undefined;
}

View File

@@ -1,6 +1,12 @@
const fs = require('fs');
const path = require('path');
const { logError } = require("./log");
// Normalize a path to always use forward slashes (POSIX style).
// Module identifiers must use '/' regardless of OS.
const toPosix = (p) => p.replace(/\\/g, '/');
/**
* Returns the current working directory of the Meteor application.
* @returns {string} The absolute path to the Meteor application directory.
@@ -106,13 +112,13 @@ export function getMeteorInitialAppEntrypoints() {
);
if (fs.existsSync(htmlPath)) {
mainClientHtml = path.join(clientDir, `${clientBasename}.html`);
mainClientHtml = toPosix(path.join(clientDir, `${clientBasename}.html`));
} else {
// Find first html in entry folder
const files = fs.readdirSync(path.join(getMeteorAppDir(), clientDir));
const htmlFile = files.find((file) => path.extname(file) === ".html");
if (htmlFile) {
mainClientHtml = path.join(clientDir, htmlFile);
mainClientHtml = toPosix(path.join(clientDir, htmlFile));
}
}
}
@@ -267,6 +273,9 @@ export function isMeteorAppNative() {
* @returns {boolean} True if the application is in development mode, false otherwise.
*/
export function isMeteorAppDevelopment() {
if (process.env.NODE_ENV) {
return process.env.NODE_ENV !== 'production';
}
return Package.meteor?.Meteor.isDevelopment && !isMeteorAppBuild();
}
@@ -275,6 +284,9 @@ export function isMeteorAppDevelopment() {
* @returns {boolean} True if the application is in production mode, false otherwise.
*/
export function isMeteorAppProduction() {
if (process.env.NODE_ENV) {
return process.env.NODE_ENV === 'production';
}
return Package.meteor?.Meteor.isProduction || isMeteorAppBuild();
}
@@ -292,6 +304,14 @@ export function isMeteorAppDebug() {
);
}
/**
* Checks if the Meteor application is running with METEOR_PROFILE enabled.
* @returns {boolean} True if METEOR_PROFILE is set, false otherwise.
*/
export function isMeteorAppProfile() {
return !!process.env.METEOR_PROFILE;
}
/**
* Sets a custom script URL for the Meteor application in the environment variable.
* @param {string} scriptUrl - The URL of the custom script.
@@ -383,11 +403,11 @@ export function getMeteorAppFilesAndFolders(options = {}) {
}
} catch (error) {
// Skip items that can't be accessed
console.error(`Error accessing ${itemPath}: ${error.message}`);
logError(`=> Failed to access ${itemPath}: ${error.message}`);
}
}
} catch (error) {
console.error(`Error reading directory ${dirPath}: ${error.message}`);
logError(`=> Failed to read directory ${dirPath}: ${error.message}`);
}
return result;
@@ -487,3 +507,36 @@ export function getMeteorEnvPackageDirs() {
...(packageDirsFromEnvVar('PACKAGE_DIRS', ':')),
];
}
/**
* Spreads Meteor's TOOL_NODE_FLAGS to NODE_OPTIONS for proper inheritance
* of Meteor-specific tool environment process variables.
* Only spreads if TOOL_NODE_FLAGS_INHERIT is truthy (enabled by default).
* @param {Object} env - The current environment variables
* @returns {Object} The updated environment variables with NODE_OPTIONS
*/
export function inheritMeteorToolNodeFlags(env = {}) {
const toolFlags = env.TOOL_NODE_FLAGS;
if (!toolFlags) {
return env;
}
// Check if spreading is enabled (default: true)
// Only disable if TOOL_NODE_FLAGS_INHERIT is explicitly set to a falsy value
// Treat "0" as falsy for this specific case
const shouldSpread = env.TOOL_NODE_FLAGS_INHERIT !== undefined
? (env.TOOL_NODE_FLAGS_INHERIT !== "0" && !!env.TOOL_NODE_FLAGS_INHERIT)
: true;
if (!shouldSpread) {
return env;
}
return {
...env,
NODE_OPTIONS: [toolFlags, env.NODE_OPTIONS]
.filter(Boolean)
.map(s => s.trim())
.join(' '),
};
}

View File

@@ -1,5 +1,6 @@
const { spawn } = require('child_process');
const net = require('net');
const { logError } = require('./log');
/**
* Spawns a new OS process with the given command and arguments.
@@ -55,7 +56,7 @@ export function spawnProcess(command, args, options = {}) {
proc.on('error', (err) => {
proc.isRunning = false;
if (options.onError) options.onError(err);
else console.error(`Process error: ${err.message}`);
else logError(`=> Process error: ${err.message}`);
});
// This happens sometimes when we write to stdin after the app

View File

@@ -9,3 +9,14 @@ Package.onUse(function (api) {
api.mainModule('tools-core.js', 'server');
});
Package.onTest(function (api) {
api.use(['ecmascript', 'tinytest']);
api.use('tools-core');
// Add test files for each lib/ module
// This structure allows easy addition of tests for other lib/ categories
api.addFiles([
'tests/meteor_tests.js',
], 'server');
});

View File

@@ -0,0 +1,213 @@
import { inheritMeteorToolNodeFlags } from "../lib/meteor.js";
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - no TOOL_NODE_FLAGS",
function (test) {
const env = {
NODE_OPTIONS: "--inspect",
TOOL_NODE_FLAGS_INHERIT: "true",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result,
env,
"Should return input unchanged when no TOOL_NODE_FLAGS"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - default behavior (inherit enabled)",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
NODE_OPTIONS: "--inspect",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.TOOL_NODE_FLAGS,
"--max-old-space-size=4096",
"TOOL_NODE_FLAGS should be preserved"
);
test.equal(
result.NODE_OPTIONS,
"--max-old-space-size=4096 --inspect",
"NODE_OPTIONS should contain both flags"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - inherit explicitly enabled",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
NODE_OPTIONS: "--inspect",
TOOL_NODE_FLAGS_INHERIT: "true",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--max-old-space-size=4096 --inspect",
"NODE_OPTIONS should contain both flags when explicitly enabled"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - inherit explicitly enabled with truthy value",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
NODE_OPTIONS: "--inspect",
TOOL_NODE_FLAGS_INHERIT: "1",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--max-old-space-size=4096 --inspect",
"NODE_OPTIONS should contain both flags with truthy string"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - inherit disabled with empty string",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
NODE_OPTIONS: "--inspect",
TOOL_NODE_FLAGS_INHERIT: "",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--inspect",
"NODE_OPTIONS should remain unchanged when inherit disabled with empty string"
);
test.equal(
result.TOOL_NODE_FLAGS,
"--max-old-space-size=4096",
"TOOL_NODE_FLAGS should be preserved"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - inherit disabled with false",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
NODE_OPTIONS: "--inspect",
TOOL_NODE_FLAGS_INHERIT: false,
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--inspect",
"NODE_OPTIONS should remain unchanged when inherit disabled with false"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - inherit disabled with zero",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
NODE_OPTIONS: "--inspect",
TOOL_NODE_FLAGS_INHERIT: "0",
};
const result = inheritMeteorToolNodeFlags(env);
console.log("--> (meteor_tests.js-Line: 128)\n result: ", result);
test.equal(
result.NODE_OPTIONS,
"--inspect",
'NODE_OPTIONS should remain unchanged when inherit disabled with "0"'
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - no existing NODE_OPTIONS",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--max-old-space-size=4096",
"NODE_OPTIONS should be set to TOOL_NODE_FLAGS when no existing NODE_OPTIONS"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - whitespace handling",
function (test) {
const env = {
TOOL_NODE_FLAGS: " --max-old-space-size=4096 ",
NODE_OPTIONS: " --inspect ",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--max-old-space-size=4096 --inspect",
"Should handle whitespace correctly"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - multiple flags",
function (test) {
const env = {
TOOL_NODE_FLAGS: "--max-old-space-size=4096 --expose-gc",
NODE_OPTIONS: "--inspect --trace-warnings",
};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result.NODE_OPTIONS,
"--max-old-space-size=4096 --expose-gc --inspect --trace-warnings",
"Should handle multiple flags correctly"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - empty environment",
function (test) {
const env = {};
const result = inheritMeteorToolNodeFlags(env);
test.equal(
result,
env,
"Should return input unchanged for empty environment"
);
}
);
Tinytest.add(
"tools-core - inheritMeteorToolNodeFlags - undefined environment",
function (test) {
const result = inheritMeteorToolNodeFlags();
test.equal(
Object.keys(result).length,
0,
"Should return empty object for undefined input"
);
}
);

View File

@@ -4,22 +4,28 @@
</p>
<ol>
<li>
Visit <a href="https://developer.twitter.com/en/portal/projects/new" target="_blank">https://developer.twitter.com/en/portal/projects/new</a>
Visit <a href="https://developer.x.com/en/portal/dashboard" target="_blank">https://developer.x.com/en/portal/dashboard</a> and sign in.
</li>
<li>
Select "Add project".
Create a new project and app (or select an existing one).
</li>
<li>
Save the API keys.
In your app settings, click on "Set up" under "User authentication settings".
</li>
<li>
Once you create your project, click "Set up" under "User authentication settings"
Enable "OAuth 1.0a" (required for Meteor).
</li>
<li>
Set Callback URI to: <span class="url">{{siteUrl}}_oauth/twitter</span>
Set "Callback URI / Redirect URL" to: <span class="url">{{siteUrl}}_oauth/twitter</span>
</li>
<li>
Set Website to: <span class="url">{{siteUrl}}</span>
Set "Website URL" to: <span class="url">{{siteUrl}}</span>
</li>
<li>
Click "Save".
</li>
<li>
Go to the "Keys and tokens" tab and note down your "API Key" (Consumer Key) and "API Key Secret" (Consumer Secret).
</li>
</ol>
</template>

View File

@@ -26,6 +26,49 @@ const isMacOS = () => {
return platform() === 'darwin';
};
const getGroupNameForGid = (gid) => {
try {
const data = readFileSync('/etc/group', 'utf8');
const line = data
.trim()
.split('\n')
.find((groupLine) => {
const [, , groupGid] = groupLine.trim().split(':');
return Number(groupGid) === gid;
});
if (!line) return null;
const [name] = line.trim().split(':');
return name || null;
} catch {
return null;
}
};
const getWritableGroupName = () => {
const { gid, uid } = userInfo();
const gidsToTry = new Set();
if (typeof gid === 'number') {
gidsToTry.add(gid);
}
if (typeof process.getgroups === 'function') {
process.getgroups().forEach((groupId) => gidsToTry.add(groupId));
}
for (const groupId of gidsToTry) {
const groupName = getGroupNameForGid(groupId);
if (groupName) {
return groupName;
}
}
if (Boolean(process.env.TRAVIS)) return 'travis';
if (isMacOS()) return 'staff';
return uid === 0 ? 'root' : null;
};
const removeTestSocketFile = () => {
try {
unlinkSync(testSocketFile);
@@ -131,9 +174,17 @@ testAsyncMulti(
},
async (test) => {
// use UNIX_SOCKET_PATH and UNIX_SOCKET_GROUP
const groupToUse = getWritableGroupName();
if (!groupToUse) {
// Skip when no writable group could be determined for the current user.
// test.isTrue(true);
test.fail(`fail test: no writable group could be determined for the current user.`);
return;
}
const { httpServer, server } = prepareServer();
const groupToUse = Boolean(process.env.TRAVIS) && 'travis' || (isMacOS() ? 'staff' : 'root');
process.env.UNIX_SOCKET_PATH = testSocketFile;
process.env.UNIX_SOCKET_GROUP = groupToUse;
process.env.UNIX_SOCKET_PERMISSIONS = '777';

View File

@@ -1,25 +1,31 @@
<template name="configureLoginServiceDialogForWeibo">
<p>
<strong>Note:</strong> Weibo is currently deprecated and the team is looking for maintainers for this package.
</p>
<p>
First, you'll need to register your app on Weibo. Follow these steps:
</p>
<ol>
<li>
Visit <a href="http://open.weibo.com/development" target="_blank">http://open.weibo.com/development</a> (Google Chrome's automatic translation works well here)
Visit <a href="https://open.weibo.com/apps" target="_blank">https://open.weibo.com/apps</a> and sign in (Google Chrome's automatic translation works well here).
</li>
<li>
Click the orange "立即创建微连接" button
Click "创建应用" (Create Application) and select "网页应用" (Web Application).
</li>
<li>
Select the third option, APP / 网页应用 (Web Applications)
Complete the registration form with your app details.
</li>
<li>
Complete the registration process. An email confirmation will be send. No SMS confirmation required.
Complete the email verification process.
</li>
<li>
Open 应用信息 (Application) -> 高级信息 (Advanced Information)
In your app dashboard, go to "应用信息" > "高级信息" (Application Info > Advanced Information).
</li>
<li>
Set OAuth2.0 授权回调页 (authorized callback page) to: <span class="url">{{siteUrl}}_oauth/weibo</span>
Set "OAuth2.0 授权回调页" (OAuth2.0 Redirect URI) to: <span class="url">{{siteUrl}}_oauth/weibo</span>
</li>
<li>
In "应用信息" > "基本信息" (Application Info > Basic Information), note down your "App Key" (Client ID) and "App Secret" (Client Secret).
</li>
</ol>
</template>

View File

@@ -1832,7 +1832,7 @@ main.registerCommand({
}
// Compile the app to resolve NPM dependencies bump coming from plugins
if (!files.inCheckout() && options["npm"]) {
if (options["npm"]) {
await compileMeteorApp(options);
return 0;
}

View File

@@ -1868,6 +1868,15 @@ main.registerCommand({
"MONGO_URL will NOT be reset.");
}
// Always clean the default .meteor/local directory to prevent regressions.
// When METEOR_LOCAL_DIR is set, also clean the custom local directory.
const defaultLocalRelative = files.pathJoin('.meteor', 'local');
const customLocalRelative = process.env.METEOR_LOCAL_DIR || null;
const localDirs = [defaultLocalRelative];
if (customLocalRelative && customLocalRelative !== defaultLocalRelative) {
localDirs.push(customLocalRelative);
}
const resetMeteorNpmCachePromise = options['skip-cache'] ? Promise.resolve() : files.rm_recursive_async(
files.pathJoin(options.appDir, "node_modules", ".cache", "meteor")
);
@@ -1882,19 +1891,23 @@ main.registerCommand({
// XXX detect the case where Meteor is running the app, but
// MONGO_URL was set, so we don't see a Mongo process
var findMongoPort = require('../runners/run-mongo.js').findMongoPort;
var isRunning = !! await findMongoPort(files.pathJoin(options.appDir, ".meteor", "local", "db"));
if (isRunning) {
Console.error("reset: Meteor is running.");
Console.error();
Console.error(
"This command does not work while Meteor is running your application.",
"Exit the running Meteor development server.");
return 1;
// Check all local dirs for a running Mongo instance
for (const localRelative of localDirs) {
const localDir = files.pathResolve(options.appDir, localRelative);
var isRunning = !! await findMongoPort(files.pathJoin(localDir, "db"));
if (isRunning) {
Console.error("reset: Meteor is running.");
Console.error();
Console.error(
"This command does not work while Meteor is running your application.",
"Exit the running Meteor development server.");
return 1;
}
}
await Promise.all([
files.rm_recursive_async(
files.pathJoin(options.appDir, ".meteor", "local")
...localDirs.map((rel) =>
files.rm_recursive_async(files.pathResolve(options.appDir, rel))
),
resetMeteorNpmCachePromise,
...resetRspackPromises,
@@ -1904,11 +1917,19 @@ main.registerCommand({
return;
}
var allExceptDb = files.getPathsInDir(files.pathJoin('.meteor', 'local'), {
cwd: options.appDir,
maxDepth: 1,
}).filter(function (path) {
return !path.includes('.meteor/local/db');
// Collect all paths inside each local dir except db
var allExceptDb = localDirs.flatMap((rel) => {
try {
return files.getPathsInDir(rel, {
cwd: options.appDir,
maxDepth: 1,
}).filter(function (p) {
return !p.includes('/db');
});
} catch (e) {
// Directory may not exist (e.g. default dir when only custom is used)
return [];
}
});
var allRemovePromises = [

Some files were not shown because too many files have changed in this diff Show More