mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.4.1' into typescript-tailwind-skeleton
This commit is contained in:
50
.github/skills/e2e-coverage/SKILL.md
vendored
Normal file
50
.github/skills/e2e-coverage/SKILL.md
vendored
Normal 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
|
||||
12
.github/skills/testing/SKILL.md
vendored
12
.github/skills/testing/SKILL.md
vendored
@@ -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}`
|
||||
|
||||
|
||||
15
.github/workflows/e2e-tests.yml
vendored
15
.github/workflows/e2e-tests.yml
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/inactive-issues.yml
vendored
2
.github/workflows/inactive-issues.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Manage inactive issues
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/inactive-issues.js')
|
||||
|
||||
52
.github/workflows/test-packages.yml
vendored
Normal file
52
.github/workflows/test-packages.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Package tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-packages:
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
CXX: g++-12
|
||||
phantom: false
|
||||
PUPPETEER_DOWNLOAD_PATH: /home/runner/.npm/chromium
|
||||
TEST_PACKAGES_EXCLUDE: stylus
|
||||
METEOR_MODERN: true
|
||||
NODE_ENV: CI
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.17.0
|
||||
|
||||
- name: Restore caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
.meteor
|
||||
.babel-cache
|
||||
dev_bundle
|
||||
/home/runner/.npm/chromium
|
||||
key: ${{ runner.os }}-node-22.17-${{ hashFiles('meteor', '**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-22.17-
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y g++-12 libnss3
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run test-in-console suite
|
||||
run: ./packages/test-in-console/run.sh
|
||||
2
.github/workflows/windows-selftest.yml
vendored
2
.github/workflows/windows-selftest.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ node_modules
|
||||
\#*\#
|
||||
.\#*
|
||||
.idea
|
||||
!.idea/icon.svg
|
||||
*.iml
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
21
.idea/icon.svg
generated
Executable file
21
.idea/icon.svg
generated
Executable file
@@ -0,0 +1,21 @@
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg2" preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 160.10664 156.98515" height="156.98515" width="160.10664">
|
||||
<metadata id="metadata26">
|
||||
<rdf:rdf xmlns="http://www.w3.org/1999/xhtml">
|
||||
<cc:work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></dc:type>
|
||||
<dc:title></dc:title>
|
||||
</cc:work>
|
||||
</rdf:rdf>
|
||||
</metadata>
|
||||
<defs id="defs24"/>
|
||||
<g transform="matrix(0.62649123,0,0,0.62649123,-0.27477954,-0.27455194)" id="g6" style="fill:#de4f4f">
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 0.43860078,0.43823749 219.30039,232.26587 c 0,0 7.45622,5.25885 13.15803,-0.87647 5.70181,-6.13533 1.3158,-12.27065 1.3158,-12.27065 L 0.43860078,0.43823749 l 0,0 z" id="path8"/>
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 69.737525,22.350112 236.40582,202.02749 c 0,0 7.45622,5.25884 13.15803,-0.87648 5.70181,-6.13533 1.3158,-12.27065 1.3158,-12.27065 L 69.737525,22.350112 l 0,0 z" id="path10"/>
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 21.052838,69.241524 187.72114,248.9189 c 0,0 7.45621,5.25885 13.15802,-0.87648 5.70181,-6.13532 1.3158,-12.27065 1.3158,-12.27065 L 21.052838,69.241524 l 0,0 z" id="path12"/>
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 128.32077,41.194324 244.76195,166.72418 c 0,0 5.20922,3.67404 9.19273,-0.61234 3.98351,-4.28639 0.91927,-8.57278 0.91927,-8.57278 L 128.32077,41.194324 l 0,0 z" id="path14"/>
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="M 37.091803,123.58297 153.53299,249.11282 c 0,0 5.20921,3.67405 9.19273,-0.61234 3.98351,-4.28638 0.91927,-8.57277 0.91927,-8.57277 L 37.091803,123.58297 l 0,0 z" id="path16"/>
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="m 188.15974,68.365049 52.77506,57.067161 c 0,0 2.57683,1.72156 4.54735,-0.28693 1.97051,-2.00849 0.45473,-4.01699 0.45473,-4.01699 l -57.77714,-52.763241 0,0 z" id="path18"/>
|
||||
<path style="fill:#de4f4f; fill-opacity:1; stroke:none" d="m 66.228719,181.43032 52.775071,57.06716 c 0,0 2.57682,1.72156 4.54734,-0.28693 1.97051,-2.00849 0.45473,-4.01698 0.45473,-4.01698 l -57.777141,-52.76325 0,0 z" id="path20"/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
290
dev/modern-tools/rspack/E2E_COVERAGE.md
Normal file
290
dev/modern-tools/rspack/E2E_COVERAGE.md
Normal 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 | |
|
||||
19
npm-packages/meteor-rspack/index.d.ts
vendored
19
npm-packages/meteor-rspack/index.d.ts
vendored
@@ -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 = (
|
||||
|
||||
@@ -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);
|
||||
|
||||
121
npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js
Normal file
121
npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);`
|
||||
: ''
|
||||
: ""
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
20
npm-packages/meteor-rspack/package-lock.json
generated
20
npm-packages/meteor-rspack/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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"
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,7 +123,7 @@ Meteor.methods({
|
||||
);
|
||||
},
|
||||
async has2faEnabled() {
|
||||
return await Accounts._is2faEnabledForUser();
|
||||
return Accounts._is2faEnabledForUser();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ Meteor.methods({
|
||||
},
|
||||
}
|
||||
);
|
||||
return await getTokenFromSecret({ selector, secret });
|
||||
return getTokenFromSecret({ selector, secret });
|
||||
},
|
||||
getTokenFromSecret,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -1120,27 +1120,6 @@ export class Connection {
|
||||
return Object.values(invokers).some((invoker) => !!invoker.sentMessage);
|
||||
}
|
||||
|
||||
async _processOneDataMessage(msg, updates) {
|
||||
const messageType = msg.msg;
|
||||
|
||||
// msg is one of ['added', 'changed', 'removed', 'ready', 'updated']
|
||||
if (messageType === 'added') {
|
||||
await this._process_added(msg, updates);
|
||||
} else if (messageType === 'changed') {
|
||||
this._process_changed(msg, updates);
|
||||
} else if (messageType === 'removed') {
|
||||
this._process_removed(msg, updates);
|
||||
} else if (messageType === 'ready') {
|
||||
this._process_ready(msg, updates);
|
||||
} else if (messageType === 'updated') {
|
||||
this._process_updated(msg, updates);
|
||||
} else if (messageType === 'nosub') {
|
||||
// ignore this
|
||||
} else {
|
||||
Meteor._debug('discarding unknown livedata data message type', msg);
|
||||
}
|
||||
}
|
||||
|
||||
_prepareBuffersToFlush() {
|
||||
const self = this;
|
||||
if (self._bufferedWritesFlushHandle) {
|
||||
|
||||
@@ -168,9 +168,24 @@ Tinytest.addAsync('livedata server - async publish cursor', function(
|
||||
connection: clientConn,
|
||||
});
|
||||
clientConn.subscribe('asyncPublishCursor', async () => {
|
||||
const actual = await remoteCollection.find().fetch();
|
||||
test.equal(actual[0].name, 'async');
|
||||
onComplete();
|
||||
// Wait for data to arrive - the subscription is ready but data may still be in transit
|
||||
// This can happen when a previous test run was interrupted (page reload) and the
|
||||
// server is still processing the old session's grace period
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50; // 5 seconds max wait
|
||||
const checkData = async () => {
|
||||
const actual = await remoteCollection.find().fetch();
|
||||
if (actual.length > 0) {
|
||||
test.equal(actual[0].name, 'async');
|
||||
onComplete();
|
||||
} else if (attempts++ < maxAttempts) {
|
||||
setTimeout(checkData, 100);
|
||||
} else {
|
||||
test.fail('Timed out waiting for data in async publish cursor test');
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
await checkData();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
try {
|
||||
Symbol = exports.Symbol = require("core-js/es/symbol");
|
||||
Map = exports.Map = require("core-js/es/map");
|
||||
Set = exports.Set = require("core-js/es/set");
|
||||
|
||||
Symbol = exports.Symbol = global.Symbol || require("core-js/es/symbol");
|
||||
Map = exports.Map = global.Map || require("core-js/es/map");
|
||||
Set = exports.Set = global.Set || require("core-js/es/set");
|
||||
} catch (e) {
|
||||
throw new Error([
|
||||
"The core-js npm package could not be found in your node_modules ",
|
||||
|
||||
@@ -7,46 +7,25 @@
|
||||
Visit <a href="https://developers.facebook.com/apps" target="_blank">https://developers.facebook.com/apps</a>
|
||||
</li>
|
||||
<li>
|
||||
Click "Add a New App".
|
||||
Click "Create App" and fill out the required information.
|
||||
</li>
|
||||
<li>
|
||||
Add a "Display Name" for your app and click on "Create App ID".
|
||||
In "Use cases" select "Authenticate and request data from users with Facebook Login".
|
||||
</li>
|
||||
<li>
|
||||
Answer the "Security Check" CAPTCHA and click on "Submit".
|
||||
In the app dashboard, click "Add Product" and find "Facebook Login", then click "Set Up".
|
||||
</li>
|
||||
<li>
|
||||
When the new app dashboard loads, click on "Settings" in the left hand menu.
|
||||
Select "Web" as your platform.
|
||||
</li>
|
||||
<li>
|
||||
From the top of the "Basic" settings page, note down your "App ID" and "App Secret" (you will be asked for them at the bottom of this popup).
|
||||
In the "Facebook Login > Settings" from the left sidebar, set "Valid OAuth Redirect URIs" to <span class="url">{{siteUrl}}_oauth/facebook</span> and click "Save Changes".
|
||||
</li>
|
||||
<li>
|
||||
Click on the "Add Platform" button, and select "Website".
|
||||
Go to "Settings > Basic" in the left sidebar.
|
||||
</li>
|
||||
<li>
|
||||
In the "Website" section, set the "Site URL" to <span class="url">{{siteUrl}}</span> and click on "Save Changes".
|
||||
</li>
|
||||
<li>
|
||||
Click on "Add Product" in the left hand menu.
|
||||
</li>
|
||||
<li>
|
||||
Hover over "Facebook Login", click on "Set Up".
|
||||
</li>
|
||||
<li>
|
||||
Click on "Facebook Login > Settings" from the left hand menu.
|
||||
</li>
|
||||
<li>
|
||||
Set "Valid OAuth redirect URIs" to <span class="url">{{siteUrl}}_oauth/facebook</span> and click on "Save Changes".
|
||||
</li>
|
||||
<li>
|
||||
Select "App Review" from the left hand menu.
|
||||
</li>
|
||||
<li>
|
||||
Toggle the "Make app public" switch to "Yes".
|
||||
</li>
|
||||
<li>
|
||||
Select a "Category" in the "Make app public" popup and click on "Confirm".
|
||||
Note down your "App ID" and "App Secret" (click "Show" to reveal the App Secret). You'll need these for configuration.
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
@@ -4,28 +4,37 @@
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Visit <a href="https://console.developers.google.com/" target="blank">https://console.developers.google.com/</a>
|
||||
Visit <a href="https://console.cloud.google.com/" target="blank">https://console.cloud.google.com/</a>
|
||||
</li>
|
||||
<li>
|
||||
"Create Project", if needed. Wait for Google to finish provisioning.
|
||||
Create a new project or select an existing one.
|
||||
</li>
|
||||
<li>
|
||||
On the left sidebar, go to "Credentials" and, on the right, "OAuth consent screen". Make sure to enter an email address and a product name, and save.
|
||||
In the left sidebar, go to "APIs & Services" > "OAuth consent screen".
|
||||
</li>
|
||||
<li>
|
||||
On the left sidebar, go to "Credentials". Click the "Create credentials" button, then select "OAuth client ID" as the type.
|
||||
Configure the consent screen: select "External" user type, enter your app name, user support email, and developer contact email, then click "Save and Continue".
|
||||
</li>
|
||||
<li>
|
||||
Select "Web application" as your application type.
|
||||
Skip the "Scopes" step (or add scopes if needed) and click "Save and Continue".
|
||||
</li>
|
||||
<li>
|
||||
Set Authorized Javascript Origins to: <span class="url">{{siteUrl}}</span>
|
||||
Add test users if needed, then click "Save and Continue".
|
||||
</li>
|
||||
<li>
|
||||
Set Authorized Redirect URI to: <span class="url">{{siteUrl}}_oauth/google?close</span>
|
||||
In the left sidebar, go to "Credentials" and click "Create Credentials" > "OAuth client ID".
|
||||
</li>
|
||||
<li>
|
||||
Finish by clicking "Create".
|
||||
Select "Web application" as the application type.
|
||||
</li>
|
||||
<li>
|
||||
Add your site URL to "Authorized JavaScript origins": <span class="url">{{siteUrl}}</span>
|
||||
</li>
|
||||
<li>
|
||||
Add to "Authorized redirect URIs": <span class="url">{{siteUrl}}_oauth/google</span>
|
||||
</li>
|
||||
<li>
|
||||
Click "Create" and note down your "Client ID" and "Client Secret" from the popup.
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
@@ -4,20 +4,25 @@
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Visit <a href="http://www.meetup.com/meetup_api/oauth_consumers/create/" target="blank">http://www.meetup.com/meetup_api/oauth_consumers/create/</a>
|
||||
Visit <a href="https://www.meetup.com/api/oauth/list/" target="blank">https://www.meetup.com/api/oauth/list/</a> and sign in.
|
||||
</li>
|
||||
<li>
|
||||
Click on "Create New Consumer".
|
||||
Click "Create new client".
|
||||
</li>
|
||||
<li>
|
||||
Set the Consumer name to the name of your application.
|
||||
Set the "Client name" to the name of your application.
|
||||
</li>
|
||||
<li>
|
||||
Optionally set the Application Website to the URL of your
|
||||
website. You can leave this blank.
|
||||
Set the "Application Website" to your site URL.
|
||||
</li>
|
||||
<li>
|
||||
Set the <b>Redirect URI</b> to: <span class="url">{{siteUrl}}</span> (Do not append a path to this URL.)
|
||||
Set the <b>Redirect URI</b> to: <span class="url">{{siteUrl}}</span> (Do not append a path to this URL.)
|
||||
</li>
|
||||
<li>
|
||||
Fill out all the other required fields.
|
||||
</li>
|
||||
<li>
|
||||
Click "Create" and note down your "Key" (Client ID) and "Secret" (Client Secret).
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
Follow these steps:
|
||||
</p>
|
||||
<ol>
|
||||
<li> Visit <a href="https://www.meteor.com/account-settings" target="_blank">https://www.meteor.com/account-settings</a> and sign in.
|
||||
<li>
|
||||
Visit <a href="https://beta.galaxycloud.app/" target="_blank">https://beta.galaxycloud.app/</a> and sign in.
|
||||
</li>
|
||||
<li> Click "NEW APPLICATION" in the "Meteor Account Services" section
|
||||
and give your app a name.</li>
|
||||
<li> Add
|
||||
<span class="url">
|
||||
{{siteUrl}}_oauth/meteor-developer
|
||||
</span>
|
||||
as the Redirect URL.
|
||||
<li>
|
||||
Go to "Settings" -> "Authorized Domains" and "Add New Domain".
|
||||
</li>
|
||||
<li>
|
||||
Set the "OAuth Redirect URL" to: <span class="url">{{siteUrl}}_oauth/meteor-developer</span>
|
||||
</li>
|
||||
<li>
|
||||
Click "Create" and note down your "Client ID" and "Client Secret".
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
@@ -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]]}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
8
packages/mongo/mongo.d.ts
vendored
8
packages/mongo/mongo.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Package.describe({
|
||||
summary: "Authorization package for Meteor",
|
||||
version: "1.0.1",
|
||||
version: "1.0.2",
|
||||
name: "roles",
|
||||
documentation: null,
|
||||
});
|
||||
|
||||
@@ -1052,8 +1052,13 @@ Object.assign(Roles, {
|
||||
* @return {Promise<Cursor>} Cursor of users in roles.
|
||||
*/
|
||||
getUsersInRoleAsync: async function (roles, options, queryOptions) {
|
||||
options = Roles._normalizeOptions(options)
|
||||
|
||||
const assignmentOptions = { ...options }
|
||||
assignmentOptions.queryOptions = undefined
|
||||
|
||||
const ids = (
|
||||
await Roles.getUserAssignmentsForRole(roles, options).fetchAsync()
|
||||
await Roles.getUserAssignmentsForRole(roles, assignmentOptions).fetchAsync()
|
||||
).map((a) => a.user._id)
|
||||
|
||||
return Meteor.users.find(
|
||||
|
||||
@@ -1596,6 +1596,38 @@ Tinytest.addAsync(
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Tinytest.addAsync(
|
||||
"roles -can get all users in role by scope and passes through mongo query arguments only to the users collection when included in the options",
|
||||
async function (test) {
|
||||
await clearData();
|
||||
await Roles.createRoleAsync("admin");
|
||||
await Roles.createRoleAsync("user");
|
||||
|
||||
await Roles.addUsersToRolesAsync(
|
||||
[users.eve, users.joe],
|
||||
["admin", "user"],
|
||||
"scope1"
|
||||
);
|
||||
await Roles.addUsersToRolesAsync(
|
||||
[users.bob, users.joe],
|
||||
["admin"],
|
||||
"scope2"
|
||||
);
|
||||
|
||||
const cursor = await Roles.getUsersInRoleAsync("admin", { scope: "scope1", queryOptions: {
|
||||
fields: { _id: 1, username: 1 },
|
||||
limit: 1,
|
||||
}});
|
||||
const results = await cursor.fetchAsync();
|
||||
|
||||
test.equal(1, results.length);
|
||||
test.isTrue(hasProp(results[0], "_id"));
|
||||
test.isTrue(hasProp(results[0], "username"));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Tinytest.addAsync(
|
||||
"roles -can use Roles.GLOBAL_SCOPE to assign blanket roles",
|
||||
async function (test) {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
100
packages/rspack/lib/logging.js
Normal file
100
packages/rspack/lib/logging.js
Normal 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}`);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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=$!
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(' '),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
213
packages/tools-core/tests/meteor_tests.js
Normal file
213
packages/tools-core/tests/meteor_tests.js
Normal 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"
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user