diff --git a/.github/skills/e2e-coverage/SKILL.md b/.github/skills/e2e-coverage/SKILL.md new file mode 100644 index 0000000000..c43b00ba2e --- /dev/null +++ b/.github/skills/e2e-coverage/SKILL.md @@ -0,0 +1,50 @@ +--- +name: e2e-coverage +description: Use when adding, modifying, or reviewing E2E test apps/skeletons to keep the test coverage report up to date. +--- + +# E2E Test Coverage Report + +Guidelines for maintaining `dev/modern-tools/rspack/E2E_COVERAGE.md` — a single-page report of what every E2E app and skeleton tests. + +## When to Update + +| Trigger | Action | +|---------|--------| +| New app added to `apps/` | Add a subsection under **Apps** with a coverage table | +| New skeleton added to `skeleton.test.js` | Add a row to the **Skeletons** table | +| New npm package imported for compatibility testing | Add an entry under **NPM Package Compatibility** with the package name, file, and reason | +| New custom assertion added to a test file | Add a row to that app's coverage table | +| New feature tested across multiple apps | Add a row to the **Feature Coverage Matrix** | +| App or skeleton removed | Remove its entries from all sections | + +## Report Structure + +The report has five sections, in this order: + +1. **Test Lifecycle** — the phases every app/skeleton goes through (init, run, prod, test, test once, build) and what default assertions apply +2. **Apps** — one subsection per `apps//` 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 | +|--------|-----------------| +| `.test.js` | Test helper used, options (`env`, `configFile`, `buildDir`, `testFullApp`, `checkBundleFilePaths`), all `customAssertions` callbacks and what they assert | +| `skeleton.test.js` | The `testMeteorSkeleton({ skeletonName: '' })` block for that skeleton | +| `apps//server/main.js` | npm imports with comments explaining why (ESM-only, native bindings, etc.) | +| `apps//imports/` | Shared code with special imports (`node:` protocol, JSX packages) | +| `apps//rspack.config.*` | Custom config features (`compileWithRspack`, `compileWithMeteor`, `disablePlugins`, custom rules) | +| `apps//package.json` | Dependencies that exist solely for compatibility testing | + +## Writing Guidelines + +- Keep descriptions short — one line per table row +- Use the phase names from the lifecycle table: Init, Run, Prod, Test, Build, All +- For npm packages, always state the **reason** (what module format issue it validates) +- Don't duplicate info between the per-app table and the feature matrix — the app table has detail, the matrix has the cross-reference +- When an env var is set in a test file, note it as `All (env prefix)` in the phase column diff --git a/.github/skills/testing/SKILL.md b/.github/skills/testing/SKILL.md index b4275b99a3..d54d674e95 100644 --- a/.github/skills/testing/SKILL.md +++ b/.github/skills/testing/SKILL.md @@ -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}` diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 72c3789bc8..ce29d07dfb 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -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 diff --git a/.github/workflows/inactive-issues.yml b/.github/workflows/inactive-issues.yml index 2d8cba7a3f..1ea6d88be6 100644 --- a/.github/workflows/inactive-issues.yml +++ b/.github/workflows/inactive-issues.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v3 - name: Manage inactive issues - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const script = require('./.github/scripts/inactive-issues.js') diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml new file mode 100644 index 0000000000..29b8c48646 --- /dev/null +++ b/.github/workflows/test-packages.yml @@ -0,0 +1,52 @@ +name: Package tests + +on: + pull_request: + +jobs: + test-packages: + runs-on: ubuntu-22.04 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + timeout-minutes: 90 + env: + CXX: g++-12 + phantom: false + PUPPETEER_DOWNLOAD_PATH: /home/runner/.npm/chromium + TEST_PACKAGES_EXCLUDE: stylus + METEOR_MODERN: true + NODE_ENV: CI + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.17.0 + + - name: Restore caches + uses: actions/cache@v4 + with: + path: | + ~/.npm + .meteor + .babel-cache + dev_bundle + /home/runner/.npm/chromium + key: ${{ runner.os }}-node-22.17-${{ hashFiles('meteor', '**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-22.17- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y g++-12 libnss3 + + - name: Install npm dependencies + run: npm install + + - name: Run test-in-console suite + run: ./packages/test-in-console/run.sh diff --git a/.github/workflows/windows-selftest.yml b/.github/workflows/windows-selftest.yml index 04b3abc05d..97a22ad4cb 100644 --- a/.github/workflows/windows-selftest.yml +++ b/.github/workflows/windows-selftest.yml @@ -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 diff --git a/.gitignore b/.gitignore index 4742e13056..36de1287c6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules \#*\# .\#* .idea +!.idea/icon.svg *.iml *.sublime-project *.sublime-workspace diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100755 index 0000000000..16ecae9b10 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,21 @@ + + + + + image/svg+xml + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 2b774d8e66..6e04b3ad4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index cfdb92933f..87a39b6cd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 | diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 58eda23621..d14f9c746b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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) diff --git a/dev/modern-tools/rspack/E2E_COVERAGE.md b/dev/modern-tools/rspack/E2E_COVERAGE.md new file mode 100644 index 0000000000..1199c73a50 --- /dev/null +++ b/dev/modern-tools/rspack/E2E_COVERAGE.md @@ -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/.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//` and has a matching `.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 --`. 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 | | diff --git a/npm-packages/meteor-rspack/index.d.ts b/npm-packages/meteor-rspack/index.d.ts index 896b9703df..20a17a1e7d 100644 --- a/npm-packages/meteor-rspack/index.d.ts +++ b/npm-packages/meteor-rspack/index.d.ts @@ -57,10 +57,21 @@ type MeteorEnv = Record & { */ splitVendorChunk: () => Record; /** - * 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; + /** + * 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; /** * Extend Rspack configs. * @returns A config object with merged configs @@ -75,6 +86,12 @@ type MeteorEnv = Record & { disablePlugins: ( matchers: string | RegExp | ((plugin: any, index: number) => boolean) | Array boolean)> ) => Record; + /** + * 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; } export type ConfigFactory = ( diff --git a/npm-packages/meteor-rspack/lib/ignore.js b/npm-packages/meteor-rspack/lib/ignore.js index 17058cce19..7abc3ba959 100644 --- a/npm-packages/meteor-rspack/lib/ignore.js +++ b/npm-packages/meteor-rspack/lib/ignore.js @@ -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); diff --git a/npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js b/npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js new file mode 100644 index 0000000000..8cac1656c3 --- /dev/null +++ b/npm-packages/meteor-rspack/lib/meteorRspackConfigHelpers.js @@ -0,0 +1,121 @@ +const path = require('path'); +const fs = require('fs'); +const { cleanOmittedPaths } = require("./mergeRulesSplitOverlap.js"); +const { mergeMeteorRspackFragments } = require("./meteorRspackConfigFactory.js"); + +// Helper function to load and process config files +async function loadAndProcessConfig(configPath, configType, Meteor, argv, disableWarnings) { + try { + // Load the config file + let config; + if (path.extname(configPath) === '.mjs') { + // For ESM modules, we need to use dynamic import + const fileUrl = `file://${configPath}`; + const module = await import(fileUrl); + config = module.default || module; + } else { + // For CommonJS modules, we can use require + config = require(configPath)?.default || require(configPath); + } + + // Process the config + const rawConfig = typeof config === 'function' ? config(Meteor, argv) : config; + const resolvedConfig = await Promise.resolve(rawConfig); + const userConfig = resolvedConfig && '0' in resolvedConfig ? resolvedConfig[0] : resolvedConfig; + + // Define omitted paths and warning function + const omitPaths = [ + "name", + "target", + "entry", + "output.path", + "output.filename", + ...(Meteor.isServer ? ["optimization.splitChunks", "optimization.runtimeChunk"] : []), + ].filter(Boolean); + + const warningFn = path => { + if (disableWarnings) return; + console.warn( + `[${configType}] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`, + ); + }; + + // Clean omitted paths and merge Meteor Rspack fragments + let nextConfig = cleanOmittedPaths(userConfig, { + omitPaths, + warningFn, + }); + nextConfig = mergeMeteorRspackFragments(nextConfig); + + return nextConfig; + } catch (error) { + console.error(`Error loading ${configType} from ${configPath}:`, error); + if (configType === 'rspack.config.js') { + throw error; // Only rethrow for project config + } + return null; + } +} + +/** + * Loads both the user's Rspack configuration and its potential override. + * + * @param {string|undefined} projectConfigPath + * @param {object} Meteor + * @param {object} argv + * @returns {Promise<{ nextUserConfig: object|null, nextOverrideConfig: object|null }>} + */ +async function loadUserAndOverrideConfig(projectConfigPath, Meteor, argv) { + let nextUserConfig = null; + let nextOverrideConfig = null; + + const projectDir = process.cwd(); + const isMeteorPackageConfig = projectDir.includes("/packages/rspack"); + + if (projectConfigPath) { + const configDir = path.dirname(projectConfigPath); + const configFileName = path.basename(projectConfigPath); + const configExt = path.extname(configFileName); + const configNameWithoutExt = configFileName.replace(configExt, ''); + const configNameFull = `${configNameWithoutExt}.override${configExt}`; + const overrideConfigPath = path.join(configDir, configNameFull); + + if (fs.existsSync(overrideConfigPath)) { + nextOverrideConfig = await loadAndProcessConfig( + overrideConfigPath, + configNameFull, + Meteor, + argv, + Meteor.isAngularEnabled + ); + } + + if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) { + // Check if there's a .mjs or .cjs version of the config file + const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs'); + const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs'); + + let projectConfigPathToUse = projectConfigPath; + if (fs.existsSync(mjsConfigPath)) { + projectConfigPathToUse = mjsConfigPath; + } else if (fs.existsSync(cjsConfigPath)) { + projectConfigPathToUse = cjsConfigPath; + } + + nextUserConfig = await loadAndProcessConfig( + projectConfigPathToUse, + 'rspack.config.js', + Meteor, + argv, + Meteor.isAngularEnabled + ); + } + } + + return { nextUserConfig, nextOverrideConfig }; +} + +module.exports = { + loadAndProcessConfig, + loadUserAndOverrideConfig, +}; diff --git a/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js b/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js index 1b77f977cb..2fbbac3027 100644 --- a/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js +++ b/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js @@ -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} `{ meteorRspackConfigX: { optimization: { ... } } }` + * Usage: Meteor.extendSwcConfig({ jsc: { parser: { decorators: true } } }) + * + * @param {object} swcConfig - SWC loader options to merge with defaults + * @returns {Record} 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} 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} 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, }; diff --git a/npm-packages/meteor-rspack/lib/swc.js b/npm-packages/meteor-rspack/lib/swc.js index 8b43703059..e113ad1626 100644 --- a/npm-packages/meteor-rspack/lib/swc.js +++ b/npm-packages/meteor-rspack/lib/swc.js @@ -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 diff --git a/npm-packages/meteor-rspack/lib/test.js b/npm-packages/meteor-rspack/lib/test.js index 3c2644b337..1780d114bb 100644 --- a/npm-packages/meteor-rspack/lib/test.js +++ b/npm-packages/meteor-rspack/lib/test.js @@ -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);` - : '' + : "" } }`; diff --git a/npm-packages/meteor-rspack/package-lock.json b/npm-packages/meteor-rspack/package-lock.json index cbe90a99f1..7792758d69 100644 --- a/npm-packages/meteor-rspack/package-lock.json +++ b/npm-packages/meteor-rspack/package-lock.json @@ -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", diff --git a/npm-packages/meteor-rspack/package.json b/npm-packages/meteor-rspack/package.json index 12f5e629c4..2bcbb679c2 100644 --- a/npm-packages/meteor-rspack/package.json +++ b/npm-packages/meteor-rspack/package.json @@ -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" } } diff --git a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js new file mode 100644 index 0000000000..a4494ae49e --- /dev/null +++ b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js @@ -0,0 +1,36 @@ +// MeteorRspackOutputPlugin.js +// +// This plugin outputs a JSON stringified with a tag delimiter each time +// a new Rspack compilation happens. The JSON content is configurable +// via plugin instantiation options. + +const { outputMeteorRspack } = require('../lib/meteorRspackHelpers'); + +class MeteorRspackOutputPlugin { + constructor(options = {}) { + this.pluginName = 'MeteorRspackOutputPlugin'; + this.options = options; + this.compilationCount = 0; + // The data to be output as JSON, can be a static object or a function + this.getData = + typeof options.getData === 'function' + ? options.getData + : () => options.data || {}; + } + + apply(compiler) { + // Hook into the 'done' event which fires after each compilation completes + compiler.hooks.done.tap(this.pluginName, stats => { + this.compilationCount++; + const data = { + ...(this.getData(stats, { + compilationCount: this.compilationCount, + isRebuild: this.compilationCount > 1, + }) || {}), + }; + outputMeteorRspack(data); + }); + } +} + +module.exports = { MeteorRspackOutputPlugin }; diff --git a/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js b/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js index fb400a4382..51cf0958fd 100644 --- a/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js +++ b/npm-packages/meteor-rspack/plugins/RequireExtenalsPlugin.js @@ -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" diff --git a/npm-packages/meteor-rspack/rspack.config.js b/npm-packages/meteor-rspack/rspack.config.js index 09138424a7..ea1fa3da79 100644 --- a/npm-packages/meteor-rspack/rspack.config.js +++ b/npm-packages/meteor-rspack/rspack.config.js @@ -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]; } diff --git a/package.json b/package.json index 9c096fcd0a..6508b0c3ef 100644 --- a/package.json +++ b/package.json @@ -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 diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index 801233d294..673ce77c15 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -123,7 +123,7 @@ Meteor.methods({ ); }, async has2faEnabled() { - return await Accounts._is2faEnabledForUser(); + return Accounts._is2faEnabledForUser(); }, }); diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index e8a884c87b..cd4e816460 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -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 diff --git a/packages/accounts-base/accounts_tests_setup.js b/packages/accounts-base/accounts_tests_setup.js index 0e211828c0..7f8cf2a40d 100644 --- a/packages/accounts-base/accounts_tests_setup.js +++ b/packages/accounts-base/accounts_tests_setup.js @@ -32,7 +32,7 @@ Meteor.methods({ }, } ); - return await getTokenFromSecret({ selector, secret }); + return getTokenFromSecret({ selector, secret }); }, getTokenFromSecret, }); diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index fe393fb663..577f4555fb 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -55,7 +55,7 @@ Meteor.methods( check(email, String); const userId = await Accounts.createUser({ email }); await Accounts.sendEnrollmentEmail(userId); - return await Meteor.users.findOneAsync(userId); + return Meteor.users.findOneAsync(userId); } } ); diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index cb7cb96112..711c163fe6 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -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"); } diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 49f94544a0..f409516c59 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1,4 +1,6 @@ Accounts._connectionCloseDelayMsForTests = 1000; +Accounts._options.ambiguousErrorMessages = false; + const makeTestConnAsync = (test) => new Promise((resolve, reject) => { @@ -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; diff --git a/packages/accounts-passwordless/server_tests.js b/packages/accounts-passwordless/server_tests.js index 768023b952..9855e1c020 100644 --- a/packages/accounts-passwordless/server_tests.js +++ b/packages/accounts-passwordless/server_tests.js @@ -28,6 +28,9 @@ const getData = ({ createdAt }) => { }; Tinytest.add('passwordless - time expired', test => { + // The test suite for accounts-passwordless includes testing whether it gets the right error messages from the server. + // So, we need this disabled, otherwise those tests incorrectly fail when you run them. + Accounts._options.ambiguousErrorMessages = false; const createdAt = new Date('July 17, 2022 13:00:00'); const currentDate = new Date('July 17, 2022 14:01:00'); diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 0de0637df2..2fdb836de0 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -101,17 +101,18 @@ let lastModifiedSwcConfigTime; BCp.initializeMeteorAppSwcrc = function () { const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`); const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`); - if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) { + const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${getMeteorAppDir()}/swc.config.ts`); + if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs && !hasSwcTs) { return; } - const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc'; + const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc'; const filePath = `${getMeteorAppDir()}/${swcFile}`; const fileStats = fs.statSync(filePath); const fileModTime = fileStats?.mtime?.getTime(); let currentLastModifiedConfigTime; - if (hasSwcJs) { - // For dynamic JS files, first get the resolved configuration + if (hasSwcJs || hasSwcTs) { + // For dynamic JS/TS files, first get the resolved configuration const resolvedConfig = lastModifiedSwcConfigTime?.includes(`${fileModTime}`) ? lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile) : getMeteorAppSwcrc(swcFile); @@ -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')); diff --git a/packages/ddp-client/common/livedata_connection.js b/packages/ddp-client/common/livedata_connection.js index 9755a8012a..03385b7b68 100644 --- a/packages/ddp-client/common/livedata_connection.js +++ b/packages/ddp-client/common/livedata_connection.js @@ -1120,27 +1120,6 @@ export class Connection { return Object.values(invokers).some((invoker) => !!invoker.sentMessage); } - async _processOneDataMessage(msg, updates) { - const messageType = msg.msg; - - // msg is one of ['added', 'changed', 'removed', 'ready', 'updated'] - if (messageType === 'added') { - await this._process_added(msg, updates); - } else if (messageType === 'changed') { - this._process_changed(msg, updates); - } else if (messageType === 'removed') { - this._process_removed(msg, updates); - } else if (messageType === 'ready') { - this._process_ready(msg, updates); - } else if (messageType === 'updated') { - this._process_updated(msg, updates); - } else if (messageType === 'nosub') { - // ignore this - } else { - Meteor._debug('discarding unknown livedata data message type', msg); - } - } - _prepareBuffersToFlush() { const self = this; if (self._bufferedWritesFlushHandle) { diff --git a/packages/ddp-server/livedata_server_async_tests.js b/packages/ddp-server/livedata_server_async_tests.js index 4ca4ca0864..3606b66044 100644 --- a/packages/ddp-server/livedata_server_async_tests.js +++ b/packages/ddp-server/livedata_server_async_tests.js @@ -168,9 +168,24 @@ Tinytest.addAsync('livedata server - async publish cursor', function( connection: clientConn, }); clientConn.subscribe('asyncPublishCursor', async () => { - const actual = await remoteCollection.find().fetch(); - test.equal(actual[0].name, 'async'); - onComplete(); + // Wait for data to arrive - the subscription is ready but data may still be in transit + // This can happen when a previous test run was interrupted (page reload) and the + // server is still processing the old session's grace period + let attempts = 0; + const maxAttempts = 50; // 5 seconds max wait + const checkData = async () => { + const actual = await remoteCollection.find().fetch(); + if (actual.length > 0) { + test.equal(actual[0].name, 'async'); + onComplete(); + } else if (attempts++ < maxAttempts) { + setTimeout(checkData, 100); + } else { + test.fail('Timed out waiting for data in async publish cursor test'); + onComplete(); + } + }; + await checkData(); }); }); }); diff --git a/packages/ecmascript-runtime-client/legacy.js b/packages/ecmascript-runtime-client/legacy.js index 20a6d8fb3a..3d482c5438 100644 --- a/packages/ecmascript-runtime-client/legacy.js +++ b/packages/ecmascript-runtime-client/legacy.js @@ -1,8 +1,7 @@ try { - Symbol = exports.Symbol = require("core-js/es/symbol"); - Map = exports.Map = require("core-js/es/map"); - Set = exports.Set = require("core-js/es/set"); - + Symbol = exports.Symbol = global.Symbol || require("core-js/es/symbol"); + Map = exports.Map = global.Map || require("core-js/es/map"); + Set = exports.Set = global.Set || require("core-js/es/set"); } catch (e) { throw new Error([ "The core-js npm package could not be found in your node_modules ", diff --git a/packages/facebook-config-ui/facebook_configure.html b/packages/facebook-config-ui/facebook_configure.html index 6eca279ec5..678004abd0 100644 --- a/packages/facebook-config-ui/facebook_configure.html +++ b/packages/facebook-config-ui/facebook_configure.html @@ -7,46 +7,25 @@ Visit https://developers.facebook.com/apps
  • - Click "Add a New App". + Click "Create App" and fill out the required information.
  • - 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".
  • - Answer the "Security Check" CAPTCHA and click on "Submit". + In the app dashboard, click "Add Product" and find "Facebook Login", then click "Set Up".
  • - When the new app dashboard loads, click on "Settings" in the left hand menu. + Select "Web" as your platform.
  • - 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 {{siteUrl}}_oauth/facebook and click "Save Changes".
  • - Click on the "Add Platform" button, and select "Website". + Go to "Settings > Basic" in the left sidebar.
  • - In the "Website" section, set the "Site URL" to {{siteUrl}} and click on "Save Changes". -
  • -
  • - Click on "Add Product" in the left hand menu. -
  • -
  • - Hover over "Facebook Login", click on "Set Up". -
  • -
  • - Click on "Facebook Login > Settings" from the left hand menu. -
  • -
  • - Set "Valid OAuth redirect URIs" to {{siteUrl}}_oauth/facebook and click on "Save Changes". -
  • -
  • - Select "App Review" from the left hand menu. -
  • -
  • - Toggle the "Make app public" switch to "Yes". -
  • -
  • - 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.
  • diff --git a/packages/google-config-ui/google_configure.html b/packages/google-config-ui/google_configure.html index c8b4b2fb3a..a20a0e4ec2 100644 --- a/packages/google-config-ui/google_configure.html +++ b/packages/google-config-ui/google_configure.html @@ -4,28 +4,37 @@

    1. - Visit https://console.developers.google.com/ + Visit https://console.cloud.google.com/
    2. - "Create Project", if needed. Wait for Google to finish provisioning. + Create a new project or select an existing one.
    3. - 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".
    4. - 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".
    5. - Select "Web application" as your application type. + Skip the "Scopes" step (or add scopes if needed) and click "Save and Continue".
    6. - Set Authorized Javascript Origins to: {{siteUrl}} + Add test users if needed, then click "Save and Continue".
    7. - Set Authorized Redirect URI to: {{siteUrl}}_oauth/google?close + In the left sidebar, go to "Credentials" and click "Create Credentials" > "OAuth client ID".
    8. - Finish by clicking "Create". + Select "Web application" as the application type. +
    9. +
    10. + Add your site URL to "Authorized JavaScript origins": {{siteUrl}} +
    11. +
    12. + Add to "Authorized redirect URIs": {{siteUrl}}_oauth/google +
    13. +
    14. + Click "Create" and note down your "Client ID" and "Client Secret" from the popup.
    diff --git a/packages/meetup-config-ui/meetup_configure.html b/packages/meetup-config-ui/meetup_configure.html index 5433697dbd..a0a6b36d82 100644 --- a/packages/meetup-config-ui/meetup_configure.html +++ b/packages/meetup-config-ui/meetup_configure.html @@ -4,20 +4,25 @@

    1. - Visit http://www.meetup.com/meetup_api/oauth_consumers/create/ + Visit https://www.meetup.com/api/oauth/list/ and sign in.
    2. - Click on "Create New Consumer". + Click "Create new client".
    3. - Set the Consumer name to the name of your application. + Set the "Client name" to the name of your application.
    4. - Optionally set the Application Website to the URL of your - website. You can leave this blank. + Set the "Application Website" to your site URL.
    5. - Set the Redirect URI to: {{siteUrl}} (Do not append a path to this URL.) + Set the Redirect URI to: {{siteUrl}} (Do not append a path to this URL.) +
    6. +
    7. + Fill out all the other required fields. +
    8. +
    9. + Click "Create" and note down your "Key" (Client ID) and "Secret" (Client Secret).
    diff --git a/packages/meteor-developer-config-ui/meteor_developer_configure.html b/packages/meteor-developer-config-ui/meteor_developer_configure.html index 6d34d867e6..592a11b8be 100644 --- a/packages/meteor-developer-config-ui/meteor_developer_configure.html +++ b/packages/meteor-developer-config-ui/meteor_developer_configure.html @@ -4,15 +4,17 @@ Follow these steps:

      -
    1. Visit https://www.meteor.com/account-settings and sign in. +
    2. + Visit https://beta.galaxycloud.app/ and sign in.
    3. -
    4. Click "NEW APPLICATION" in the "Meteor Account Services" section - and give your app a name.
    5. -
    6. Add - - {{siteUrl}}_oauth/meteor-developer - - as the Redirect URL. +
    7. + Go to "Settings" -> "Authorized Domains" and "Add New Domain". +
    8. +
    9. + Set the "OAuth Redirect URL" to: {{siteUrl}}_oauth/meteor-developer +
    10. +
    11. + Click "Create" and note down your "Client ID" and "Client Secret".
    diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index a345d4938c..a532337674 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -176,7 +176,7 @@ export default class LocalCollection { const queriesToRecompute = []; // trigger live queries that match - for (const qid of Object.keys(this.queries)) { + for (const qid in this.queries) { const query = this.queries[qid]; if (query.dirty) { @@ -828,7 +828,7 @@ export default class LocalCollection { LocalCollection._modify(doc, mod, {arrayIndices}); const recomputeQids = {}; - for (const qid of Object.keys(this.queries)) { + for (const qid in this.queries) { const query = this.queries[qid]; if (query.dirty) { @@ -2293,11 +2293,12 @@ const NO_CREATE_MODIFIERS = { }; // Make sure field names do not contain Mongo restricted -// characters ('.', '$', '\0'). +// characters ('$', '\0') or invalid dot usage (leading/trailing/consecutive '.'). // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names const invalidCharMsg = { $: 'start with \'$\'', - '.': 'contain \'.\'', + '.': 'start or end with \'.\'', + '..': 'contain consecutive dots', '\0': 'contain null bytes' }; @@ -2313,7 +2314,7 @@ function assertHasValidFieldNames(doc) { function assertIsValidFieldName(key) { let match; - if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { + if (typeof key === 'string' && (match = key.match(/^\$|^\.|\.\.|\.$|^\.$|\0/))) { throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); } } diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index c1405552a5..a0c5525bf6 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -2496,12 +2496,13 @@ Tinytest.addAsync('minimongo - modify', async test => { await modify({a: 12}, {}, {}); // tested against mongodb await modify({a: 12}, {a: 13}, {a: 13}); await modify({a: 12, b: 99}, {a: 13}, {a: 13}); + await modify({a: 12}, {b: {'a.b': 13}}, {b: {'a.b': 13}}); + await modify({_id: 1, a: 1}, {_id: 1, 'a.b': 2}, {_id: 1, 'a.b': 2}); await exception({a: 12}, {a: 13, $set: {b: 13}}); await exception({a: 12}, {$set: {b: 13}, a: 13}); await exception({a: 12}, {$a: 13}); // invalid operator await exception({a: 12}, {b: {$a: 13}}); - await exception({a: 12}, {b: {'a.b': 13}}); await exception({a: 12}, {b: {'\0a': 13}}); // keys @@ -2740,11 +2741,11 @@ Tinytest.addAsync('minimongo - modify', async test => { await exception({_id: 1}, {$set: {_id: 4}}); await modify({_id: 4}, {$set: {_id: 4}}, {_id: 4}); // not-changing _id is not bad // restricted field names + await modify({a: {}}, {$set: {a: {'a.b': 1}}}, {a: {'a.b': 1}}); await exception({a: {}}, {$set: {a: {$a: 1}}}); await exception({ a: {} }, { $set: { a: { c: [{ b: { $a: 1 } }] } } }); await exception({a: {}}, {$set: {a: {'\0a': 1}}}); - await exception({a: {}}, {$set: {a: {'a.b': 1}}}); // $unset await modify({}, {$unset: {a: 1}}, {}); @@ -2822,8 +2823,8 @@ Tinytest.addAsync('minimongo - modify', async test => { await exception({}, {$push: {'\0a': 1}}); await exception({}, {$push: {a: {$a: 1}}}); await exception({}, {$push: {a: {$each: [{$a: 1}]}}}); - await exception({}, {$push: {a: {$each: [{'a.b': 1}]}}}); await exception({}, {$push: {a: {$each: [{'\0a': 1}]}}}); + await modify({}, {$push: {a: {$each: [{'a.b': 1}]}}}, {a: [{'a.b': 1}]}); await modify({}, {$push: {a: {$each: [{'': 1}]}}}, {a: [ { '': 1 } ]}); await modify({}, {$push: {a: {$each: [{' ': 1}]}}}, {a: [ { ' ': 1 } ]}); await exception({}, {$push: {a: {$each: [{'.': 1}]}}}); @@ -2857,7 +2858,7 @@ Tinytest.addAsync('minimongo - modify', async test => { await modify({a: {}}, {$pushAll: {'a.x': []}}, {a: {x: []}}); await exception({a: [1]}, {$pushAll: {a: [{$a: 1}]}}); await exception({a: [1]}, {$pushAll: {a: [{'\0a': 1}]}}); - await exception({a: [1]}, {$pushAll: {a: [{'a.b': 1}]}}); + await modify({a: [1]}, {$pushAll: {a: [{'a.b': 1}]}}, {a: [1, {'a.b': 1}]}); // $addToSet await modify({}, {$addToSet: {a: 1}}, {a: [1]}); @@ -2883,14 +2884,16 @@ Tinytest.addAsync('minimongo - modify', async test => { // invalid field names await exception({}, {$addToSet: {a: {$b: 1}}}); - await exception({}, {$addToSet: {a: {'a.b': 1}}}); + await modify({}, {$addToSet: {a: {'a.b': 1}}}, {a: [{'a.b': 1}]}); await exception({}, {$addToSet: {a: {'a.': 1}}}); await exception({}, {$addToSet: {a: {'\u0000a': 1}}}); await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, {$a: 1}]}}}); await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, {'\0a': 1}]}}}); await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{$a: 1}]]}}}); - await exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}); - await exception({a: [1, 2]}, {$addToSet: {a: {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}); + await modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}, + {a: [1, 2, 3, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}); + await modify({a: [1, 2]}, {$addToSet: {a: {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}, + {a: [1, 2, {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}]}); // $each is first element and thus an operator await modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}}, {a: [ 1, 2, 3, 4 ]}); // this should fail because $each is now a field name (not first in object) and thus invalid field name with $ @@ -2983,7 +2986,7 @@ Tinytest.addAsync('minimongo - modify', async test => { await upsertException({a: 0}, {$setOnInsert: {'\0a': 12}}); await upsert({a: 0}, {$setOnInsert: {b: {a: 1}}}, {a: 0, b: {a: 1}}); await upsertException({a: 0}, {$setOnInsert: {b: {$a: 1}}}); - await upsertException({a: 0}, {$setOnInsert: {b: {'a.b': 1}}}); + await upsert({a: 0}, {$setOnInsert: {b: {'a.b': 1}}}, {a: 0, b: {'a.b': 1}}); await upsertException({a: 0}, {$setOnInsert: {b: {'\0a': 1}}}); // Test for https://github.com/meteor/meteor/issues/8775. @@ -3919,7 +3922,7 @@ Tinytest.add('minimongo - reactive skip/limit count while updating', test => { }); // Makes sure inserts cannot be performed using field names that have -// Mongo restricted characters in them ('.', '$', '\0'): +// Mongo restricted characters in them ('$', '\0'): // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names Tinytest.add('minimongo - cannot insert using invalid field names', test => { const collection = new LocalCollection(); @@ -3930,18 +3933,13 @@ Tinytest.add('minimongo - cannot insert using invalid field names', test => { // Quick test to make sure field values with dots are allowed collection.insert({ a: 'b.c' }); - // Verify top level dot-field inserts are prohibited - ['a.b', '.b', 'a.', 'a.b.c'].forEach((field) => { + // Verify invalid dot patterns are rejected: leading dot, trailing dot, consecutive dots + ['.b', 'a.', '.', 'a..b', '. ', ' .', '...', 'a...b'].forEach((field) => { test.throws(() => { collection.insert({ [field]: 'c' }); - }, `Key ${field} must not contain '.'`); + }, `Key ${field}`); }); - // Verify nested dot-field inserts are prohibited - test.throws(() => { - collection.insert({ a: { b: { 'c.d': 'e' } } }); - }, "Key c.d must not contain '.'"); - // Verify field names starting with $ are prohibited test.throws(() => { collection.insert({ $a: 'b' }); @@ -3965,6 +3963,49 @@ Tinytest.add('minimongo - cannot insert using invalid field names', test => { }, 'Key \0c must not contain null bytes'); }); +// Verify that valid dotted field names are allowed (MongoDB 3.6+) +Tinytest.add('minimongo - can insert using valid dotted field names', test => { + const collection = new LocalCollection(); + + // Verify dotted field names work + ['a.b', 'a.b.c'].forEach((field) => { + const id = collection.insert({ [field]: 'd' }); + const doc = collection.findOne(id); + test.equal(doc[field], 'd', `Field ${field} should be allowed`); + collection.remove(id); + }); + + // Verify dotted fields in nested objects work + const id2 = collection.insert({ + nested: { + 'a.b': 'c', + 'a.b.c': 'd' + } + }); + const doc2 = collection.findOne(id2); + test.equal(doc2.nested['a.b'], 'c'); + test.equal(doc2.nested['a.b.c'], 'd'); + + // Verify update operations work with dotted field names in values + const id3 = collection.insert({ a: 'b' }); + collection.update(id3, { $set: { b: { 'a.b': 'c' } } }); + const doc3 = collection.findOne(id3); + test.equal(doc3.b['a.b'], 'c'); + + // Verify distinction: path semantics vs literal dotted keys + const id4 = collection.insert({ x: {} }); + // This uses path semantics - creates nested structure + collection.update(id4, { $set: { 'x.y': 1 } }); + const doc4a = collection.findOne(id4); + test.equal(doc4a.x.y, 1, 'Path semantics should create nested structure'); + + // This uses literal key - dot is part of the key name + collection.update(id4, { $set: { z: { 'x.y': 2 } } }); + const doc4b = collection.findOne(id4); + test.equal(doc4b.z['x.y'], 2, 'Literal key should store dot as part of name'); + test.equal(doc4b.z.x, undefined, 'Literal key should not create nesting'); +}); + // Makes sure $set's cannot be performed using null bytes // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names Tinytest.add('minimongo - cannot $set with null bytes', test => { diff --git a/packages/mongo/mongo.d.ts b/packages/mongo/mongo.d.ts index 0d499ee588..662fee5e72 100644 --- a/packages/mongo/mongo.d.ts +++ b/packages/mongo/mongo.d.ts @@ -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(extension: (this: Collection, name: string | null, options?: CollectionOptions) => void): void; + addExtension(extension: (this: Collection, name: string | null, options?: CollectionOptions) => 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(name: string, method: (this: Collection, ...args: any[]) => any): void; + addPrototypeMethod(name: string, method: (this: Collection, ...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(extension: (this: Collection, name: string | null, options?: CollectionOptions) => void): void; + addExtension(extension: (this: Collection, name: string | null, options?: CollectionOptions) => 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(name: string, method: (this: Collection, ...args: any[]) => any): void; + addPrototypeMethod(name: string, method: (this: Collection, ...args: any[]) => any): void; /** * Add a static method to the Mongo.Collection constructor. diff --git a/packages/mongo/tests/observe_changes_tests.js b/packages/mongo/tests/observe_changes_tests.js index 8dd50eed5d..ac7a671cac 100644 --- a/packages/mongo/tests/observe_changes_tests.js +++ b/packages/mongo/tests/observe_changes_tests.js @@ -519,8 +519,8 @@ if (Meteor.isServer) { const [resolver1, promise1] = getPromiseAndResolver(); const [resolver2, promise2] = getPromiseAndResolver(); - await self.insert({x: 2, y: 3}); self.expects.push(resolver1, resolver2); + await self.insert({x: 2, y: 3}); await self.insert({x: 3, y: 7}); // filtered out by the query await self.insert({x: 4}); // Expect two added calls to happen. diff --git a/packages/roles/package.js b/packages/roles/package.js index dbcc7ac855..d0943ce407 100644 --- a/packages/roles/package.js +++ b/packages/roles/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Authorization package for Meteor", - version: "1.0.1", + version: "1.0.2", name: "roles", documentation: null, }); diff --git a/packages/roles/roles_common_async.js b/packages/roles/roles_common_async.js index dbd46d5b71..1d5aa19a0f 100644 --- a/packages/roles/roles_common_async.js +++ b/packages/roles/roles_common_async.js @@ -1052,8 +1052,13 @@ Object.assign(Roles, { * @return {Promise} Cursor of users in roles. */ getUsersInRoleAsync: async function (roles, options, queryOptions) { + options = Roles._normalizeOptions(options) + + const assignmentOptions = { ...options } + assignmentOptions.queryOptions = undefined + const ids = ( - await Roles.getUserAssignmentsForRole(roles, options).fetchAsync() + await Roles.getUserAssignmentsForRole(roles, assignmentOptions).fetchAsync() ).map((a) => a.user._id) return Meteor.users.find( diff --git a/packages/roles/tests/serverAsync.js b/packages/roles/tests/serverAsync.js index 1e844550fc..043af5fdf1 100644 --- a/packages/roles/tests/serverAsync.js +++ b/packages/roles/tests/serverAsync.js @@ -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) { diff --git a/packages/rspack/lib/build-context.js b/packages/rspack/lib/build-context.js index 765f841f41..5ff8a7d08c 100644 --- a/packages/rspack/lib/build-context.js +++ b/packages/rspack/lib/build-context.js @@ -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 && diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 23ef8005a3..5771d25fb2 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -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, diff --git a/packages/rspack/lib/constants.js b/packages/rspack/lib/constants.js index 218b70a22f..1341f54ce7 100644 --- a/packages/rspack/lib/constants.js +++ b/packages/rspack/lib/constants.js @@ -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; diff --git a/packages/rspack/lib/dependencies.js b/packages/rspack/lib/dependencies.js index 20ebca4271..81195fb7cc 100644 --- a/packages/rspack/lib/dependencies.js +++ b/packages/rspack/lib/dependencies.js @@ -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.`); } } diff --git a/packages/rspack/lib/logging.js b/packages/rspack/lib/logging.js new file mode 100644 index 0000000000..6c938843d5 --- /dev/null +++ b/packages/rspack/lib/logging.js @@ -0,0 +1,100 @@ +/** + * @module logging + * @description Functions for logging Rspack processes + */ + +const { logRaw } = require("meteor/tools-core/lib/log"); + +const { + isMeteorAppConfigModernVerbose, + isMeteorAppProfile, +} = require("meteor/tools-core/lib/meteor"); + +/** + * Checks if the logs should be verbose for Rspack processes. + * @returns {boolean} True if profiling or verbose mode is enabled, false otherwise. + */ +export function shouldLogVerbose() { + return isMeteorAppProfile() || isMeteorAppConfigModernVerbose(); +} + +/** + * Strips the leading label line (e.g. "[server-rspack]:\n") from Rspack output. + * @param {string} output - The raw output from an Rspack process + * @returns {string} The output without the leading label line, trimmed + */ +export function stripRspackLabel(output) { + return output.replace(/^\[.*?]:\s*\n/, "").trim(); +} + +/** + * Parses and extracts [Meteor-Rspack]{}[/Meteor-Rspack] content from data. + * Returns the cleaned data (without the tag content) and the parsed JSON config. + * @param {string} data - The raw data that may contain Meteor-Rspack tags + * @returns {{ cleanedData: string, config: Object|null }} Object with cleaned data and parsed config + */ +export function parseMeteorRspackOutput(data) { + const tagRegex = /\[Meteor-Rspack\](.*?)\[\/Meteor-Rspack\]/g; + let config = null; + let match; + + // Find all matches and parse the last one (in case of multiple) + while ((match = tagRegex.exec(data)) !== null) { + try { + config = JSON.parse(match[1]); + } catch (e) { + // If JSON parsing fails, keep config as null + config = null; + } + } + + // Remove all [Meteor-Rspack]...[/Meteor-Rspack] tags from the data + const cleanedData = data.replace(tagRegex, "").trim(); + + return { cleanedData, config }; +} + +const compilationCount = {}; +let hmrServerLogged = false; + +/** + * Logs "=> Started Rspack HMR server at " if devServerUrl exists in config. + * Only logs once per session. + * @param {Object|null} config - The parsed config from MeteorRspackOutputPlugin + */ +export function logHmrServerStarted(config) { + if (hmrServerLogged) return; + if (!config?.devServerUrl) return; + hmrServerLogged = true; + logRaw(`=> Started Rspack HMR server at ${config.devServerUrl}/`); +} + +/** + * Logs a friendly Meteor-style message with the raw Rspack output appended. + * Strips the leading label and logs as: + * "=> Compiled your client app compiled successfully in 342 ms" + * Adds a leading newline from the second compilation onwards per target. + * @param {string} output - The raw stdout line from an Rspack process + * @param {string} target - The build target label (e.g. "client", "server") + * @param {boolean} statsOverrided - If true, skip cleaning and use \n separator + */ +export function logCompilationOutput(output, target, statsOverrided = false) { + let cleaned; + let separator; + // Logs original Rspack logging when stats overrided by user + if (statsOverrided) { + cleaned = stripRspackLabel(output); + separator = "\n"; + } else { + cleaned = stripRspackLabel(output) + .replace(/^.*\[.*?]\s*/g, "") + .trim() + .replace(/\s*compiled\s*/g, ""); + separator = cleaned.includes("\n") ? ":\n" : " "; + // Ignore successful logs on default Meteor-Rspack logging + if (/\s*successfully\s*/g.test(cleaned)) return; + } + compilationCount[target] = (compilationCount[target] || 0) + 1; + const prefix = compilationCount[target] > 1 ? "\n=>" : "=>"; + logRaw(`${prefix} Compiled Rspack ${target} app${separator}${cleaned}`); +} diff --git a/packages/rspack/lib/processes.js b/packages/rspack/lib/processes.js index d97d731257..d453c5144f 100644 --- a/packages/rspack/lib/processes.js +++ b/packages/rspack/lib/processes.js @@ -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); } }); diff --git a/packages/rspack/rspack_plugin.js b/packages/rspack/rspack_plugin.js index e852a22a6c..73c6a6eb5c 100644 --- a/packages/rspack/rspack_plugin.js +++ b/packages/rspack/rspack_plugin.js @@ -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}`); diff --git a/packages/rspack/rspack_server.js b/packages/rspack/rspack_server.js index 53598b8de5..9c55294738 100644 --- a/packages/rspack/rspack_server.js +++ b/packages/rspack/rspack_server.js @@ -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'); diff --git a/packages/test-in-console/driver.js b/packages/test-in-console/driver.js index 4f66667d0d..a4533f1256 100644 --- a/packages/test-in-console/driver.js +++ b/packages/test-in-console/driver.js @@ -210,7 +210,9 @@ runTests = function () { // Also log xUnit output xunit(''); - resultSet.forEach(function (result, name) { + Object.keys(resultSet).forEach(function (name) { + let result = resultSet[name]; + var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client"); var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client"); var time = ""; diff --git a/packages/test-in-console/package.js b/packages/test-in-console/package.js index ebf1b6eff9..fe96041fd6 100644 --- a/packages/test-in-console/package.js +++ b/packages/test-in-console/package.js @@ -6,6 +6,7 @@ Package.describe({ Package.onUse(function(api) { api.use(['tinytest', 'random', 'ejson', 'check', 'ecmascript']); api.use('fetch', 'server'); + api.use('jquery', 'client'); api.export('TEST_STATUS', 'client'); diff --git a/packages/test-in-console/run.sh b/packages/test-in-console/run.sh index 5a88e9065d..5bdfb466eb 100755 --- a/packages/test-in-console/run.sh +++ b/packages/test-in-console/run.sh @@ -15,6 +15,7 @@ export PATH=$METEOR_HOME:$PATH export URL='http://127.0.0.1:4096/' export METEOR_PACKAGE_DIRS='packages/deprecated' +export METEOR_NO_DEPRECATION=true exec 3< <(./meteor test-packages --driver-package test-in-console -p 4096 --exclude ${TEST_PACKAGES_EXCLUDE:-''} $1) EXEC_PID=$! diff --git a/packages/tinytest/README.md b/packages/tinytest/README.md index d93c5f1222..514ae883a7 100644 --- a/packages/tinytest/README.md +++ b/packages/tinytest/README.md @@ -272,7 +272,13 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t ### Assertions without optional fail messages -`test.throws(func, expected);` +`test.throws(func, expected[, message]);` + +`test.throwsAsync(func, expected[, message]);` + +`test.doesNotThrows(func[, failureMessage]);` + +`test.doesNotThrowsAsync(func[, failureMessage]);` `expected` can be: @@ -281,6 +287,8 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t - `regexp`: pass if the exception message passes the regexp. - `function`: call the function as a predicate with the exception. +`doesNotThrows` and `doesNotThrowsAsync` assert that the function does not throw. If the function throws, the assertion fails. The optional `failureMessage` is only used to annotate the failure. + Note: Node's `assert.throws` also accepts a constructor to test whether the error is of the expected class. But since JavaScript can't distinguish between constructors and plain functions and Node's `assert.throws` also accepts a predicate function, if the error fails the `instanceof` test with the constructor then the constructor is then treated as a predicate and called (!) The upshot is, if you want to test whether an error is of a particular class, use a predicate function. diff --git a/packages/tinytest/tinytest.js b/packages/tinytest/tinytest.js index b66097aafb..f8c29bab74 100644 --- a/packages/tinytest/tinytest.js +++ b/packages/tinytest/tinytest.js @@ -244,6 +244,19 @@ export class TestCaseResults { // The upshot is, if you want to test whether an error is of a // particular class, use a predicate function. // + /** + * Assert that `f` throws. + * + * `expected` can be: + * - undefined: accept any exception. + * - string: pass if the string is a substring of the exception message. + * - regexp: pass if the exception message passes the regexp. + * - function: call the function as a predicate with the exception. + * + * @param {Function} f + * @param {*} expected + * @param {String} message + */ throws(f, expected, message) { let actual; const predicate = this._guessPredicate(expected); @@ -258,10 +271,34 @@ export class TestCaseResults { } /** - * Same as throw, but accepts an async function as a parameter. - * @param f - * @param expected - * @param message + * Assert that `f` does not throw. + * @param {Function} f + * @param {String} failureMessage + */ + doesNotThrows(f, failureMessage) { + let actual; + + try { + f(); + } catch (exception) { + actual = exception; + } + + if (!actual) { + this.ok(); + } else { + this.fail({ + type: "throws", + message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""), + }); + } + } + + /** + * Same as `throws`, but accepts an async function as a parameter. + * @param {Function} f + * @param {*} expected + * @param {String} message * @returns {Promise} */ 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} + */ + async doesNotThrowsAsync(f, failureMessage) { + let actual; + + try { + await f(); + } catch (exception) { + actual = exception; + } + + if (!actual) { + this.ok(); + } else { + this.fail({ + type: "throws", + message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""), + }); + } + } + isTrue(v, msg) { if (v) this.ok(); diff --git a/packages/tools-core/lib/git.js b/packages/tools-core/lib/git.js index 71afcdb0e0..51021ce95b 100644 --- a/packages/tools-core/lib/git.js +++ b/packages/tools-core/lib/git.js @@ -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; } } diff --git a/packages/tools-core/lib/log.js b/packages/tools-core/lib/log.js index 80e3f2a9af..6225d0da06 100644 --- a/packages/tools-core/lib/log.js +++ b/packages/tools-core/lib/log.js @@ -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; } diff --git a/packages/tools-core/lib/meteor.js b/packages/tools-core/lib/meteor.js index 1556a1ddde..c603101f29 100644 --- a/packages/tools-core/lib/meteor.js +++ b/packages/tools-core/lib/meteor.js @@ -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(' '), + }; +} diff --git a/packages/tools-core/lib/process.js b/packages/tools-core/lib/process.js index 88db5b7cc6..211870eee7 100644 --- a/packages/tools-core/lib/process.js +++ b/packages/tools-core/lib/process.js @@ -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 diff --git a/packages/tools-core/package.js b/packages/tools-core/package.js index 71a14de26c..c12ebf8b89 100644 --- a/packages/tools-core/package.js +++ b/packages/tools-core/package.js @@ -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'); +}); diff --git a/packages/tools-core/tests/meteor_tests.js b/packages/tools-core/tests/meteor_tests.js new file mode 100644 index 0000000000..2615172da9 --- /dev/null +++ b/packages/tools-core/tests/meteor_tests.js @@ -0,0 +1,213 @@ +import { inheritMeteorToolNodeFlags } from "../lib/meteor.js"; + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - no TOOL_NODE_FLAGS", + function (test) { + const env = { + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "true", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result, + env, + "Should return input unchanged when no TOOL_NODE_FLAGS" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - default behavior (inherit enabled)", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.TOOL_NODE_FLAGS, + "--max-old-space-size=4096", + "TOOL_NODE_FLAGS should be preserved" + ); + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "NODE_OPTIONS should contain both flags" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit explicitly enabled", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "true", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "NODE_OPTIONS should contain both flags when explicitly enabled" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit explicitly enabled with truthy value", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "1", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "NODE_OPTIONS should contain both flags with truthy string" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit disabled with empty string", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--inspect", + "NODE_OPTIONS should remain unchanged when inherit disabled with empty string" + ); + test.equal( + result.TOOL_NODE_FLAGS, + "--max-old-space-size=4096", + "TOOL_NODE_FLAGS should be preserved" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit disabled with false", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: false, + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--inspect", + "NODE_OPTIONS should remain unchanged when inherit disabled with false" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - inherit disabled with zero", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + NODE_OPTIONS: "--inspect", + TOOL_NODE_FLAGS_INHERIT: "0", + }; + const result = inheritMeteorToolNodeFlags(env); + console.log("--> (meteor_tests.js-Line: 128)\n result: ", result); + + test.equal( + result.NODE_OPTIONS, + "--inspect", + 'NODE_OPTIONS should remain unchanged when inherit disabled with "0"' + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - no existing NODE_OPTIONS", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096", + "NODE_OPTIONS should be set to TOOL_NODE_FLAGS when no existing NODE_OPTIONS" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - whitespace handling", + function (test) { + const env = { + TOOL_NODE_FLAGS: " --max-old-space-size=4096 ", + NODE_OPTIONS: " --inspect ", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --inspect", + "Should handle whitespace correctly" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - multiple flags", + function (test) { + const env = { + TOOL_NODE_FLAGS: "--max-old-space-size=4096 --expose-gc", + NODE_OPTIONS: "--inspect --trace-warnings", + }; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result.NODE_OPTIONS, + "--max-old-space-size=4096 --expose-gc --inspect --trace-warnings", + "Should handle multiple flags correctly" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - empty environment", + function (test) { + const env = {}; + const result = inheritMeteorToolNodeFlags(env); + + test.equal( + result, + env, + "Should return input unchanged for empty environment" + ); + } +); + +Tinytest.add( + "tools-core - inheritMeteorToolNodeFlags - undefined environment", + function (test) { + const result = inheritMeteorToolNodeFlags(); + + test.equal( + Object.keys(result).length, + 0, + "Should return empty object for undefined input" + ); + } +); diff --git a/packages/twitter-config-ui/twitter_configure.html b/packages/twitter-config-ui/twitter_configure.html index 85b2f3c2d1..b3fb304d65 100644 --- a/packages/twitter-config-ui/twitter_configure.html +++ b/packages/twitter-config-ui/twitter_configure.html @@ -4,22 +4,28 @@

    1. - Visit https://developer.twitter.com/en/portal/projects/new + Visit https://developer.x.com/en/portal/dashboard and sign in.
    2. - Select "Add project". + Create a new project and app (or select an existing one).
    3. - Save the API keys. + In your app settings, click on "Set up" under "User authentication settings".
    4. - Once you create your project, click "Set up" under "User authentication settings" + Enable "OAuth 1.0a" (required for Meteor).
    5. - Set Callback URI to: {{siteUrl}}_oauth/twitter + Set "Callback URI / Redirect URL" to: {{siteUrl}}_oauth/twitter
    6. - Set Website to: {{siteUrl}} + Set "Website URL" to: {{siteUrl}} +
    7. +
    8. + Click "Save". +
    9. +
    10. + Go to the "Keys and tokens" tab and note down your "API Key" (Consumer Key) and "API Key Secret" (Consumer Secret).
    diff --git a/packages/webapp/socket_file_tests.js b/packages/webapp/socket_file_tests.js index 2dbde1927f..856fe7329e 100644 --- a/packages/webapp/socket_file_tests.js +++ b/packages/webapp/socket_file_tests.js @@ -26,6 +26,49 @@ const isMacOS = () => { return platform() === 'darwin'; }; +const getGroupNameForGid = (gid) => { + try { + const data = readFileSync('/etc/group', 'utf8'); + const line = data + .trim() + .split('\n') + .find((groupLine) => { + const [, , groupGid] = groupLine.trim().split(':'); + return Number(groupGid) === gid; + }); + + if (!line) return null; + const [name] = line.trim().split(':'); + return name || null; + } catch { + return null; + } +}; + +const getWritableGroupName = () => { + const { gid, uid } = userInfo(); + const gidsToTry = new Set(); + + if (typeof gid === 'number') { + gidsToTry.add(gid); + } + + if (typeof process.getgroups === 'function') { + process.getgroups().forEach((groupId) => gidsToTry.add(groupId)); + } + + for (const groupId of gidsToTry) { + const groupName = getGroupNameForGid(groupId); + if (groupName) { + return groupName; + } + } + + if (Boolean(process.env.TRAVIS)) return 'travis'; + if (isMacOS()) return 'staff'; + return uid === 0 ? 'root' : null; +}; + const removeTestSocketFile = () => { try { unlinkSync(testSocketFile); @@ -131,9 +174,17 @@ testAsyncMulti( }, async (test) => { // use UNIX_SOCKET_PATH and UNIX_SOCKET_GROUP + const groupToUse = getWritableGroupName(); + + if (!groupToUse) { + // Skip when no writable group could be determined for the current user. + // test.isTrue(true); + test.fail(`fail test: no writable group could be determined for the current user.`); + return; + } + const { httpServer, server } = prepareServer(); - const groupToUse = Boolean(process.env.TRAVIS) && 'travis' || (isMacOS() ? 'staff' : 'root'); process.env.UNIX_SOCKET_PATH = testSocketFile; process.env.UNIX_SOCKET_GROUP = groupToUse; process.env.UNIX_SOCKET_PERMISSIONS = '777'; diff --git a/packages/weibo-config-ui/weibo_configure.html b/packages/weibo-config-ui/weibo_configure.html index e48ff8e4e9..b956b8e407 100644 --- a/packages/weibo-config-ui/weibo_configure.html +++ b/packages/weibo-config-ui/weibo_configure.html @@ -1,25 +1,31 @@ diff --git a/tools/cli/commands-packages.js b/tools/cli/commands-packages.js index 02f896676f..395c26ba36 100644 --- a/tools/cli/commands-packages.js +++ b/tools/cli/commands-packages.js @@ -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; } diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 76a6081cb2..a888036a88 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -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 = [ diff --git a/tools/modern-tests/README.md b/tools/e2e-tests/README.md similarity index 100% rename from tools/modern-tests/README.md rename to tools/e2e-tests/README.md diff --git a/tools/modern-tests/apps/babel/.babelrc b/tools/e2e-tests/apps/babel/.babelrc similarity index 100% rename from tools/modern-tests/apps/babel/.babelrc rename to tools/e2e-tests/apps/babel/.babelrc diff --git a/tools/modern-tests/apps/babel/.gitignore b/tools/e2e-tests/apps/babel/.gitignore similarity index 100% rename from tools/modern-tests/apps/babel/.gitignore rename to tools/e2e-tests/apps/babel/.gitignore diff --git a/tools/modern-tests/apps/babel/.meteor/.gitignore b/tools/e2e-tests/apps/babel/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/babel/.meteor/.gitignore rename to tools/e2e-tests/apps/babel/.meteor/.gitignore diff --git a/tools/modern-tests/apps/babel/.meteor/.id b/tools/e2e-tests/apps/babel/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/babel/.meteor/.id rename to tools/e2e-tests/apps/babel/.meteor/.id diff --git a/tools/modern-tests/apps/babel/.meteor/packages b/tools/e2e-tests/apps/babel/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/babel/.meteor/packages rename to tools/e2e-tests/apps/babel/.meteor/packages diff --git a/tools/modern-tests/apps/babel/.meteor/platforms b/tools/e2e-tests/apps/babel/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/babel/.meteor/platforms rename to tools/e2e-tests/apps/babel/.meteor/platforms diff --git a/tools/modern-tests/apps/babel/.meteor/release b/tools/e2e-tests/apps/babel/.meteor/release similarity index 100% rename from tools/modern-tests/apps/babel/.meteor/release rename to tools/e2e-tests/apps/babel/.meteor/release diff --git a/tools/modern-tests/apps/babel/.meteor/versions b/tools/e2e-tests/apps/babel/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/babel/.meteor/versions rename to tools/e2e-tests/apps/babel/.meteor/versions diff --git a/tools/modern-tests/apps/babel/.swcrc b/tools/e2e-tests/apps/babel/.swcrc similarity index 100% rename from tools/modern-tests/apps/babel/.swcrc rename to tools/e2e-tests/apps/babel/.swcrc diff --git a/tools/modern-tests/apps/babel/client/main.css b/tools/e2e-tests/apps/babel/client/main.css similarity index 100% rename from tools/modern-tests/apps/babel/client/main.css rename to tools/e2e-tests/apps/babel/client/main.css diff --git a/tools/modern-tests/apps/babel/client/main.html b/tools/e2e-tests/apps/babel/client/main.html similarity index 100% rename from tools/modern-tests/apps/babel/client/main.html rename to tools/e2e-tests/apps/babel/client/main.html diff --git a/tools/modern-tests/apps/babel/client/main.jsx b/tools/e2e-tests/apps/babel/client/main.jsx similarity index 100% rename from tools/modern-tests/apps/babel/client/main.jsx rename to tools/e2e-tests/apps/babel/client/main.jsx diff --git a/tools/modern-tests/apps/babel/imports/api/links.js b/tools/e2e-tests/apps/babel/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/babel/imports/api/links.js rename to tools/e2e-tests/apps/babel/imports/api/links.js diff --git a/tools/modern-tests/apps/babel/imports/apollo/schema.graphql b/tools/e2e-tests/apps/babel/imports/apollo/schema.graphql similarity index 100% rename from tools/modern-tests/apps/babel/imports/apollo/schema.graphql rename to tools/e2e-tests/apps/babel/imports/apollo/schema.graphql diff --git a/tools/modern-tests/apps/babel/imports/ui/App.jsx b/tools/e2e-tests/apps/babel/imports/ui/App.jsx similarity index 100% rename from tools/modern-tests/apps/babel/imports/ui/App.jsx rename to tools/e2e-tests/apps/babel/imports/ui/App.jsx diff --git a/tools/modern-tests/apps/babel/imports/ui/Hello.jsx b/tools/e2e-tests/apps/babel/imports/ui/Hello.jsx similarity index 100% rename from tools/modern-tests/apps/babel/imports/ui/Hello.jsx rename to tools/e2e-tests/apps/babel/imports/ui/Hello.jsx diff --git a/tools/modern-tests/apps/babel/imports/ui/Info.jsx b/tools/e2e-tests/apps/babel/imports/ui/Info.jsx similarity index 100% rename from tools/modern-tests/apps/babel/imports/ui/Info.jsx rename to tools/e2e-tests/apps/babel/imports/ui/Info.jsx diff --git a/tools/modern-tests/apps/babel/package.json b/tools/e2e-tests/apps/babel/package.json similarity index 100% rename from tools/modern-tests/apps/babel/package.json rename to tools/e2e-tests/apps/babel/package.json diff --git a/tools/modern-tests/apps/babel/rspack.config.mjs b/tools/e2e-tests/apps/babel/rspack.config.mjs similarity index 100% rename from tools/modern-tests/apps/babel/rspack.config.mjs rename to tools/e2e-tests/apps/babel/rspack.config.mjs diff --git a/tools/modern-tests/apps/babel/server/apollo.js b/tools/e2e-tests/apps/babel/server/apollo.js similarity index 100% rename from tools/modern-tests/apps/babel/server/apollo.js rename to tools/e2e-tests/apps/babel/server/apollo.js diff --git a/tools/modern-tests/apps/babel/server/main.js b/tools/e2e-tests/apps/babel/server/main.js similarity index 100% rename from tools/modern-tests/apps/babel/server/main.js rename to tools/e2e-tests/apps/babel/server/main.js diff --git a/tools/modern-tests/apps/babel/tests/main.js b/tools/e2e-tests/apps/babel/tests/main.js similarity index 100% rename from tools/modern-tests/apps/babel/tests/main.js rename to tools/e2e-tests/apps/babel/tests/main.js diff --git a/tools/modern-tests/apps/blaze/.gitignore b/tools/e2e-tests/apps/blaze/.gitignore similarity index 100% rename from tools/modern-tests/apps/blaze/.gitignore rename to tools/e2e-tests/apps/blaze/.gitignore diff --git a/tools/modern-tests/apps/blaze/.meteor/.gitignore b/tools/e2e-tests/apps/blaze/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/blaze/.meteor/.gitignore rename to tools/e2e-tests/apps/blaze/.meteor/.gitignore diff --git a/tools/modern-tests/apps/blaze/.meteor/.id b/tools/e2e-tests/apps/blaze/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/blaze/.meteor/.id rename to tools/e2e-tests/apps/blaze/.meteor/.id diff --git a/tools/modern-tests/apps/blaze/.meteor/packages b/tools/e2e-tests/apps/blaze/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/blaze/.meteor/packages rename to tools/e2e-tests/apps/blaze/.meteor/packages diff --git a/tools/modern-tests/apps/blaze/.meteor/platforms b/tools/e2e-tests/apps/blaze/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/blaze/.meteor/platforms rename to tools/e2e-tests/apps/blaze/.meteor/platforms diff --git a/tools/modern-tests/apps/blaze/.meteor/release b/tools/e2e-tests/apps/blaze/.meteor/release similarity index 100% rename from tools/modern-tests/apps/blaze/.meteor/release rename to tools/e2e-tests/apps/blaze/.meteor/release diff --git a/tools/modern-tests/apps/blaze/.meteor/versions b/tools/e2e-tests/apps/blaze/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/blaze/.meteor/versions rename to tools/e2e-tests/apps/blaze/.meteor/versions diff --git a/tools/modern-tests/apps/blaze/client/main.css b/tools/e2e-tests/apps/blaze/client/main.css similarity index 100% rename from tools/modern-tests/apps/blaze/client/main.css rename to tools/e2e-tests/apps/blaze/client/main.css diff --git a/tools/modern-tests/apps/blaze/client/main.html b/tools/e2e-tests/apps/blaze/client/main.html similarity index 100% rename from tools/modern-tests/apps/blaze/client/main.html rename to tools/e2e-tests/apps/blaze/client/main.html diff --git a/tools/modern-tests/apps/blaze/client/main.js b/tools/e2e-tests/apps/blaze/client/main.js similarity index 100% rename from tools/modern-tests/apps/blaze/client/main.js rename to tools/e2e-tests/apps/blaze/client/main.js diff --git a/tools/modern-tests/apps/blaze/package.json b/tools/e2e-tests/apps/blaze/package.json similarity index 100% rename from tools/modern-tests/apps/blaze/package.json rename to tools/e2e-tests/apps/blaze/package.json diff --git a/tools/modern-tests/apps/blaze/server/main.js b/tools/e2e-tests/apps/blaze/server/main.js similarity index 100% rename from tools/modern-tests/apps/blaze/server/main.js rename to tools/e2e-tests/apps/blaze/server/main.js diff --git a/tools/modern-tests/apps/blaze/tests/main.js b/tools/e2e-tests/apps/blaze/tests/main.js similarity index 100% rename from tools/modern-tests/apps/blaze/tests/main.js rename to tools/e2e-tests/apps/blaze/tests/main.js diff --git a/tools/modern-tests/apps/coffeescript/.gitignore b/tools/e2e-tests/apps/coffeescript/.gitignore similarity index 100% rename from tools/modern-tests/apps/coffeescript/.gitignore rename to tools/e2e-tests/apps/coffeescript/.gitignore diff --git a/tools/modern-tests/apps/coffeescript/.meteor/.gitignore b/tools/e2e-tests/apps/coffeescript/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/coffeescript/.meteor/.gitignore rename to tools/e2e-tests/apps/coffeescript/.meteor/.gitignore diff --git a/tools/modern-tests/apps/coffeescript/.meteor/.id b/tools/e2e-tests/apps/coffeescript/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/coffeescript/.meteor/.id rename to tools/e2e-tests/apps/coffeescript/.meteor/.id diff --git a/tools/modern-tests/apps/coffeescript/.meteor/packages b/tools/e2e-tests/apps/coffeescript/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/coffeescript/.meteor/packages rename to tools/e2e-tests/apps/coffeescript/.meteor/packages diff --git a/tools/modern-tests/apps/coffeescript/.meteor/platforms b/tools/e2e-tests/apps/coffeescript/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/coffeescript/.meteor/platforms rename to tools/e2e-tests/apps/coffeescript/.meteor/platforms diff --git a/tools/modern-tests/apps/coffeescript/.meteor/release b/tools/e2e-tests/apps/coffeescript/.meteor/release similarity index 100% rename from tools/modern-tests/apps/coffeescript/.meteor/release rename to tools/e2e-tests/apps/coffeescript/.meteor/release diff --git a/tools/modern-tests/apps/coffeescript/.meteor/versions b/tools/e2e-tests/apps/coffeescript/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/coffeescript/.meteor/versions rename to tools/e2e-tests/apps/coffeescript/.meteor/versions diff --git a/tools/modern-tests/apps/coffeescript/client/main.coffee b/tools/e2e-tests/apps/coffeescript/client/main.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/client/main.coffee rename to tools/e2e-tests/apps/coffeescript/client/main.coffee diff --git a/tools/modern-tests/apps/coffeescript/client/main.css b/tools/e2e-tests/apps/coffeescript/client/main.css similarity index 100% rename from tools/modern-tests/apps/coffeescript/client/main.css rename to tools/e2e-tests/apps/coffeescript/client/main.css diff --git a/tools/modern-tests/apps/coffeescript/client/main.html b/tools/e2e-tests/apps/coffeescript/client/main.html similarity index 100% rename from tools/modern-tests/apps/coffeescript/client/main.html rename to tools/e2e-tests/apps/coffeescript/client/main.html diff --git a/tools/modern-tests/apps/coffeescript/imports/api/links.coffee b/tools/e2e-tests/apps/coffeescript/imports/api/links.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/imports/api/links.coffee rename to tools/e2e-tests/apps/coffeescript/imports/api/links.coffee diff --git a/tools/modern-tests/apps/coffeescript/imports/ui/App.coffee b/tools/e2e-tests/apps/coffeescript/imports/ui/App.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/imports/ui/App.coffee rename to tools/e2e-tests/apps/coffeescript/imports/ui/App.coffee diff --git a/tools/modern-tests/apps/coffeescript/imports/ui/Hello.coffee b/tools/e2e-tests/apps/coffeescript/imports/ui/Hello.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/imports/ui/Hello.coffee rename to tools/e2e-tests/apps/coffeescript/imports/ui/Hello.coffee diff --git a/tools/modern-tests/apps/coffeescript/imports/ui/Info.coffee b/tools/e2e-tests/apps/coffeescript/imports/ui/Info.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/imports/ui/Info.coffee rename to tools/e2e-tests/apps/coffeescript/imports/ui/Info.coffee diff --git a/tools/modern-tests/apps/coffeescript/package.json b/tools/e2e-tests/apps/coffeescript/package.json similarity index 100% rename from tools/modern-tests/apps/coffeescript/package.json rename to tools/e2e-tests/apps/coffeescript/package.json diff --git a/tools/modern-tests/apps/coffeescript/rspack.config.js b/tools/e2e-tests/apps/coffeescript/rspack.config.js similarity index 100% rename from tools/modern-tests/apps/coffeescript/rspack.config.js rename to tools/e2e-tests/apps/coffeescript/rspack.config.js diff --git a/tools/modern-tests/apps/coffeescript/server/main.coffee b/tools/e2e-tests/apps/coffeescript/server/main.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/server/main.coffee rename to tools/e2e-tests/apps/coffeescript/server/main.coffee diff --git a/tools/modern-tests/apps/coffeescript/tests/main.coffee b/tools/e2e-tests/apps/coffeescript/tests/main.coffee similarity index 100% rename from tools/modern-tests/apps/coffeescript/tests/main.coffee rename to tools/e2e-tests/apps/coffeescript/tests/main.coffee diff --git a/tools/modern-tests/apps/full-blaze/.gitignore b/tools/e2e-tests/apps/full-blaze/.gitignore similarity index 100% rename from tools/modern-tests/apps/full-blaze/.gitignore rename to tools/e2e-tests/apps/full-blaze/.gitignore diff --git a/tools/modern-tests/apps/full-blaze/.meteor/.gitignore b/tools/e2e-tests/apps/full-blaze/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/full-blaze/.meteor/.gitignore rename to tools/e2e-tests/apps/full-blaze/.meteor/.gitignore diff --git a/tools/modern-tests/apps/full-blaze/.meteor/.id b/tools/e2e-tests/apps/full-blaze/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/full-blaze/.meteor/.id rename to tools/e2e-tests/apps/full-blaze/.meteor/.id diff --git a/tools/modern-tests/apps/full-blaze/.meteor/packages b/tools/e2e-tests/apps/full-blaze/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/full-blaze/.meteor/packages rename to tools/e2e-tests/apps/full-blaze/.meteor/packages diff --git a/tools/modern-tests/apps/full-blaze/.meteor/platforms b/tools/e2e-tests/apps/full-blaze/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/full-blaze/.meteor/platforms rename to tools/e2e-tests/apps/full-blaze/.meteor/platforms diff --git a/tools/modern-tests/apps/full-blaze/.meteor/release b/tools/e2e-tests/apps/full-blaze/.meteor/release similarity index 100% rename from tools/modern-tests/apps/full-blaze/.meteor/release rename to tools/e2e-tests/apps/full-blaze/.meteor/release diff --git a/tools/modern-tests/apps/full-blaze/.meteor/versions b/tools/e2e-tests/apps/full-blaze/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/full-blaze/.meteor/versions rename to tools/e2e-tests/apps/full-blaze/.meteor/versions diff --git a/tools/modern-tests/apps/full-blaze/client/head.html b/tools/e2e-tests/apps/full-blaze/client/head.html similarity index 100% rename from tools/modern-tests/apps/full-blaze/client/head.html rename to tools/e2e-tests/apps/full-blaze/client/head.html diff --git a/tools/modern-tests/apps/full-blaze/client/main.js b/tools/e2e-tests/apps/full-blaze/client/main.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/client/main.js rename to tools/e2e-tests/apps/full-blaze/client/main.js diff --git a/tools/modern-tests/apps/full-blaze/client/main.less b/tools/e2e-tests/apps/full-blaze/client/main.less similarity index 100% rename from tools/modern-tests/apps/full-blaze/client/main.less rename to tools/e2e-tests/apps/full-blaze/client/main.less diff --git a/tools/modern-tests/apps/full-blaze/imports/api/links/links.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/links.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/api/links/links.js rename to tools/e2e-tests/apps/full-blaze/imports/api/links/links.js diff --git a/tools/modern-tests/apps/full-blaze/imports/api/links/links.tests.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/links.tests.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/api/links/links.tests.js rename to tools/e2e-tests/apps/full-blaze/imports/api/links/links.tests.js diff --git a/tools/modern-tests/apps/full-blaze/imports/api/links/methods.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/api/links/methods.js rename to tools/e2e-tests/apps/full-blaze/imports/api/links/methods.js diff --git a/tools/modern-tests/apps/full-blaze/imports/api/links/methods.tests.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/methods.tests.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/api/links/methods.tests.js rename to tools/e2e-tests/apps/full-blaze/imports/api/links/methods.tests.js diff --git a/tools/modern-tests/apps/full-blaze/imports/api/links/server/publications.js b/tools/e2e-tests/apps/full-blaze/imports/api/links/server/publications.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/api/links/server/publications.js rename to tools/e2e-tests/apps/full-blaze/imports/api/links/server/publications.js diff --git a/tools/modern-tests/apps/full-blaze/imports/startup/both/index.js b/tools/e2e-tests/apps/full-blaze/imports/startup/both/index.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/startup/both/index.js rename to tools/e2e-tests/apps/full-blaze/imports/startup/both/index.js diff --git a/tools/modern-tests/apps/full-blaze/imports/startup/client/index.js b/tools/e2e-tests/apps/full-blaze/imports/startup/client/index.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/startup/client/index.js rename to tools/e2e-tests/apps/full-blaze/imports/startup/client/index.js diff --git a/tools/modern-tests/apps/full-blaze/imports/startup/client/routes.js b/tools/e2e-tests/apps/full-blaze/imports/startup/client/routes.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/startup/client/routes.js rename to tools/e2e-tests/apps/full-blaze/imports/startup/client/routes.js diff --git a/tools/modern-tests/apps/full-blaze/imports/startup/server/fixtures.js b/tools/e2e-tests/apps/full-blaze/imports/startup/server/fixtures.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/startup/server/fixtures.js rename to tools/e2e-tests/apps/full-blaze/imports/startup/server/fixtures.js diff --git a/tools/modern-tests/apps/full-blaze/imports/startup/server/index.js b/tools/e2e-tests/apps/full-blaze/imports/startup/server/index.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/startup/server/index.js rename to tools/e2e-tests/apps/full-blaze/imports/startup/server/index.js diff --git a/tools/modern-tests/apps/full-blaze/imports/startup/server/register-api.js b/tools/e2e-tests/apps/full-blaze/imports/startup/server/register-api.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/startup/server/register-api.js rename to tools/e2e-tests/apps/full-blaze/imports/startup/server/register-api.js diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/components/hello/hello.html b/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.html similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/components/hello/hello.html rename to tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.html diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/components/hello/hello.js b/tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/components/hello/hello.js rename to tools/e2e-tests/apps/full-blaze/imports/ui/components/hello/hello.js diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/components/info/info.html b/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.html similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/components/info/info.html rename to tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.html diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/components/info/info.js b/tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/components/info/info.js rename to tools/e2e-tests/apps/full-blaze/imports/ui/components/info/info.js diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/layouts/body/body.html b/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.html similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/layouts/body/body.html rename to tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.html diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/layouts/body/body.js b/tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/layouts/body/body.js rename to tools/e2e-tests/apps/full-blaze/imports/ui/layouts/body/body.js diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/pages/home/home.html b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.html similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/pages/home/home.html rename to tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.html diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/pages/home/home.js b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/pages/home/home.js rename to tools/e2e-tests/apps/full-blaze/imports/ui/pages/home/home.js diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html rename to tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.html diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js b/tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js rename to tools/e2e-tests/apps/full-blaze/imports/ui/pages/not-found/not-found.js diff --git a/tools/modern-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less b/tools/e2e-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less similarity index 100% rename from tools/modern-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less rename to tools/e2e-tests/apps/full-blaze/imports/ui/stylesheets/not-found.less diff --git a/tools/modern-tests/apps/full-blaze/package.json b/tools/e2e-tests/apps/full-blaze/package.json similarity index 100% rename from tools/modern-tests/apps/full-blaze/package.json rename to tools/e2e-tests/apps/full-blaze/package.json diff --git a/tools/modern-tests/apps/full-blaze/private/README.md b/tools/e2e-tests/apps/full-blaze/private/README.md similarity index 100% rename from tools/modern-tests/apps/full-blaze/private/README.md rename to tools/e2e-tests/apps/full-blaze/private/README.md diff --git a/tools/modern-tests/apps/full-blaze/public/img/404.svg b/tools/e2e-tests/apps/full-blaze/public/img/404.svg similarity index 100% rename from tools/modern-tests/apps/full-blaze/public/img/404.svg rename to tools/e2e-tests/apps/full-blaze/public/img/404.svg diff --git a/tools/modern-tests/apps/full-blaze/public/img/bg-footer.svg b/tools/e2e-tests/apps/full-blaze/public/img/bg-footer.svg similarity index 100% rename from tools/modern-tests/apps/full-blaze/public/img/bg-footer.svg rename to tools/e2e-tests/apps/full-blaze/public/img/bg-footer.svg diff --git a/tools/modern-tests/apps/full-blaze/rspack.config.js b/tools/e2e-tests/apps/full-blaze/rspack.config.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/rspack.config.js rename to tools/e2e-tests/apps/full-blaze/rspack.config.js diff --git a/tools/modern-tests/apps/full-blaze/server/main.js b/tools/e2e-tests/apps/full-blaze/server/main.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/server/main.js rename to tools/e2e-tests/apps/full-blaze/server/main.js diff --git a/tools/modern-tests/apps/full-blaze/swc.config.js b/tools/e2e-tests/apps/full-blaze/swc.config.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/swc.config.js rename to tools/e2e-tests/apps/full-blaze/swc.config.js diff --git a/tools/modern-tests/apps/full-blaze/tests/main.js b/tools/e2e-tests/apps/full-blaze/tests/main.js similarity index 100% rename from tools/modern-tests/apps/full-blaze/tests/main.js rename to tools/e2e-tests/apps/full-blaze/tests/main.js diff --git a/tools/modern-tests/apps/monorepo/.npmrc b/tools/e2e-tests/apps/monorepo/.npmrc similarity index 100% rename from tools/modern-tests/apps/monorepo/.npmrc rename to tools/e2e-tests/apps/monorepo/.npmrc diff --git a/tools/modern-tests/apps/monorepo/app/.meteor/packages b/tools/e2e-tests/apps/monorepo/app/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/monorepo/app/.meteor/packages rename to tools/e2e-tests/apps/monorepo/app/.meteor/packages diff --git a/tools/modern-tests/apps/monorepo/app/.meteor/platforms b/tools/e2e-tests/apps/monorepo/app/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/monorepo/app/.meteor/platforms rename to tools/e2e-tests/apps/monorepo/app/.meteor/platforms diff --git a/tools/modern-tests/apps/monorepo/app/.meteor/release b/tools/e2e-tests/apps/monorepo/app/.meteor/release similarity index 100% rename from tools/modern-tests/apps/monorepo/app/.meteor/release rename to tools/e2e-tests/apps/monorepo/app/.meteor/release diff --git a/tools/modern-tests/apps/monorepo/app/.meteorignore b/tools/e2e-tests/apps/monorepo/app/.meteorignore similarity index 100% rename from tools/modern-tests/apps/monorepo/app/.meteorignore rename to tools/e2e-tests/apps/monorepo/app/.meteorignore diff --git a/tools/modern-tests/apps/monorepo/app/.swcrc b/tools/e2e-tests/apps/monorepo/app/.swcrc similarity index 100% rename from tools/modern-tests/apps/monorepo/app/.swcrc rename to tools/e2e-tests/apps/monorepo/app/.swcrc diff --git a/tools/modern-tests/apps/monorepo/app/client/client.test.js b/tools/e2e-tests/apps/monorepo/app/client/client.test.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/client/client.test.js rename to tools/e2e-tests/apps/monorepo/app/client/client.test.js diff --git a/tools/modern-tests/apps/monorepo/app/client/main.css b/tools/e2e-tests/apps/monorepo/app/client/main.css similarity index 100% rename from tools/modern-tests/apps/monorepo/app/client/main.css rename to tools/e2e-tests/apps/monorepo/app/client/main.css diff --git a/tools/modern-tests/apps/monorepo/app/client/main.html b/tools/e2e-tests/apps/monorepo/app/client/main.html similarity index 100% rename from tools/modern-tests/apps/monorepo/app/client/main.html rename to tools/e2e-tests/apps/monorepo/app/client/main.html diff --git a/tools/modern-tests/apps/monorepo/app/client/main.jsx b/tools/e2e-tests/apps/monorepo/app/client/main.jsx similarity index 100% rename from tools/modern-tests/apps/monorepo/app/client/main.jsx rename to tools/e2e-tests/apps/monorepo/app/client/main.jsx diff --git a/tools/modern-tests/apps/monorepo/app/ignored/ignore.test.js b/tools/e2e-tests/apps/monorepo/app/ignored/ignore.test.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/ignored/ignore.test.js rename to tools/e2e-tests/apps/monorepo/app/ignored/ignore.test.js diff --git a/tools/modern-tests/apps/monorepo/app/imports/api/links.js b/tools/e2e-tests/apps/monorepo/app/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/imports/api/links.js rename to tools/e2e-tests/apps/monorepo/app/imports/api/links.js diff --git a/tools/modern-tests/apps/monorepo/app/imports/emails/TestEmail.jsx b/tools/e2e-tests/apps/monorepo/app/imports/emails/TestEmail.jsx similarity index 100% rename from tools/modern-tests/apps/monorepo/app/imports/emails/TestEmail.jsx rename to tools/e2e-tests/apps/monorepo/app/imports/emails/TestEmail.jsx diff --git a/tools/modern-tests/apps/monorepo/app/imports/ui/App.jsx b/tools/e2e-tests/apps/monorepo/app/imports/ui/App.jsx similarity index 100% rename from tools/modern-tests/apps/monorepo/app/imports/ui/App.jsx rename to tools/e2e-tests/apps/monorepo/app/imports/ui/App.jsx diff --git a/tools/modern-tests/apps/monorepo/app/imports/ui/Hello.jsx b/tools/e2e-tests/apps/monorepo/app/imports/ui/Hello.jsx similarity index 100% rename from tools/modern-tests/apps/monorepo/app/imports/ui/Hello.jsx rename to tools/e2e-tests/apps/monorepo/app/imports/ui/Hello.jsx diff --git a/tools/modern-tests/apps/monorepo/app/imports/ui/Info.jsx b/tools/e2e-tests/apps/monorepo/app/imports/ui/Info.jsx similarity index 100% rename from tools/modern-tests/apps/monorepo/app/imports/ui/Info.jsx rename to tools/e2e-tests/apps/monorepo/app/imports/ui/Info.jsx diff --git a/tools/modern-tests/apps/monorepo/app/package.json b/tools/e2e-tests/apps/monorepo/app/package.json similarity index 100% rename from tools/modern-tests/apps/monorepo/app/package.json rename to tools/e2e-tests/apps/monorepo/app/package.json diff --git a/tools/modern-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js b/tools/e2e-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js rename to tools/e2e-tests/apps/monorepo/app/plugins/CustomConsoleLogPlugin.js diff --git a/tools/modern-tests/apps/monorepo/app/public/1x1.png b/tools/e2e-tests/apps/monorepo/app/public/1x1.png similarity index 100% rename from tools/modern-tests/apps/monorepo/app/public/1x1.png rename to tools/e2e-tests/apps/monorepo/app/public/1x1.png diff --git a/tools/modern-tests/apps/monorepo/app/public/docs/text.md b/tools/e2e-tests/apps/monorepo/app/public/docs/text.md similarity index 100% rename from tools/modern-tests/apps/monorepo/app/public/docs/text.md rename to tools/e2e-tests/apps/monorepo/app/public/docs/text.md diff --git a/tools/modern-tests/apps/monorepo/app/public/images/1x1.png b/tools/e2e-tests/apps/monorepo/app/public/images/1x1.png similarity index 100% rename from tools/modern-tests/apps/monorepo/app/public/images/1x1.png rename to tools/e2e-tests/apps/monorepo/app/public/images/1x1.png diff --git a/tools/modern-tests/apps/monorepo/app/rspack.config.cjs b/tools/e2e-tests/apps/monorepo/app/rspack.config.cjs similarity index 100% rename from tools/modern-tests/apps/monorepo/app/rspack.config.cjs rename to tools/e2e-tests/apps/monorepo/app/rspack.config.cjs diff --git a/tools/modern-tests/apps/monorepo/app/rspack.config.override.cjs b/tools/e2e-tests/apps/monorepo/app/rspack.config.override.cjs similarity index 100% rename from tools/modern-tests/apps/monorepo/app/rspack.config.override.cjs rename to tools/e2e-tests/apps/monorepo/app/rspack.config.override.cjs diff --git a/tools/modern-tests/apps/monorepo/app/server/main.js b/tools/e2e-tests/apps/monorepo/app/server/main.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/server/main.js rename to tools/e2e-tests/apps/monorepo/app/server/main.js diff --git a/tools/modern-tests/apps/monorepo/app/server/server.test.js b/tools/e2e-tests/apps/monorepo/app/server/server.test.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/server/server.test.js rename to tools/e2e-tests/apps/monorepo/app/server/server.test.js diff --git a/tools/modern-tests/apps/monorepo/app/tests/ignore-test.test.js b/tools/e2e-tests/apps/monorepo/app/tests/ignore-test.test.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/tests/ignore-test.test.js rename to tools/e2e-tests/apps/monorepo/app/tests/ignore-test.test.js diff --git a/tools/modern-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js b/tools/e2e-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js rename to tools/e2e-tests/apps/monorepo/app/tests/ignored/ignore-nested.test.js diff --git a/tools/modern-tests/apps/monorepo/app/tests/main.test.js b/tools/e2e-tests/apps/monorepo/app/tests/main.test.js similarity index 100% rename from tools/modern-tests/apps/monorepo/app/tests/main.test.js rename to tools/e2e-tests/apps/monorepo/app/tests/main.test.js diff --git a/tools/modern-tests/apps/monorepo/package.json b/tools/e2e-tests/apps/monorepo/package.json similarity index 100% rename from tools/modern-tests/apps/monorepo/package.json rename to tools/e2e-tests/apps/monorepo/package.json diff --git a/tools/modern-tests/apps/react-router/.gitignore b/tools/e2e-tests/apps/react-router/.gitignore similarity index 100% rename from tools/modern-tests/apps/react-router/.gitignore rename to tools/e2e-tests/apps/react-router/.gitignore diff --git a/tools/modern-tests/apps/react-router/.meteor/.gitignore b/tools/e2e-tests/apps/react-router/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/react-router/.meteor/.gitignore rename to tools/e2e-tests/apps/react-router/.meteor/.gitignore diff --git a/tools/modern-tests/apps/react-router/.meteor/.id b/tools/e2e-tests/apps/react-router/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/react-router/.meteor/.id rename to tools/e2e-tests/apps/react-router/.meteor/.id diff --git a/tools/modern-tests/apps/react-router/.meteor/packages b/tools/e2e-tests/apps/react-router/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/react-router/.meteor/packages rename to tools/e2e-tests/apps/react-router/.meteor/packages diff --git a/tools/modern-tests/apps/react-router/.meteor/platforms b/tools/e2e-tests/apps/react-router/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/react-router/.meteor/platforms rename to tools/e2e-tests/apps/react-router/.meteor/platforms diff --git a/tools/modern-tests/apps/react-router/.meteor/release b/tools/e2e-tests/apps/react-router/.meteor/release similarity index 100% rename from tools/modern-tests/apps/react-router/.meteor/release rename to tools/e2e-tests/apps/react-router/.meteor/release diff --git a/tools/modern-tests/apps/react-router/.meteor/versions b/tools/e2e-tests/apps/react-router/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/react-router/.meteor/versions rename to tools/e2e-tests/apps/react-router/.meteor/versions diff --git a/tools/e2e-tests/apps/react-router/.meteorignore b/tools/e2e-tests/apps/react-router/.meteorignore new file mode 100644 index 0000000000..269a4e9cc4 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/.meteorignore @@ -0,0 +1,16 @@ +# Folder combination patterns +react-router* +tests/ignored/react-router*-ignored +tests/ignored/folder-to-ignored/ +tests/ignored/file-to-ignored.app-test.js +**/glob-ignored/*.app-test.js +tests/ignored/prefix-*-ignored +tests/ignored/*-ignored-suffix + +# File combination patterns +tests/ignored/specific-file-ignored.app-test.js +tests/ignored/*.temp-ignored.app-test.js +**/unit/*.app-test.js +tests/ignored/integration/*-ignored.app-test.js +tests/ignored/test-*-ignored.app-test.js +!important.app-test.js diff --git a/tools/modern-tests/apps/react-router/babel.config.js b/tools/e2e-tests/apps/react-router/babel.config.js similarity index 100% rename from tools/modern-tests/apps/react-router/babel.config.js rename to tools/e2e-tests/apps/react-router/babel.config.js diff --git a/tools/modern-tests/apps/react-router/client/client.app-test.js b/tools/e2e-tests/apps/react-router/client/client.app-test.js similarity index 100% rename from tools/modern-tests/apps/react-router/client/client.app-test.js rename to tools/e2e-tests/apps/react-router/client/client.app-test.js diff --git a/tools/modern-tests/apps/react-router/client/main.css b/tools/e2e-tests/apps/react-router/client/main.css similarity index 100% rename from tools/modern-tests/apps/react-router/client/main.css rename to tools/e2e-tests/apps/react-router/client/main.css diff --git a/tools/modern-tests/apps/react-router/client/main.html b/tools/e2e-tests/apps/react-router/client/main.html similarity index 100% rename from tools/modern-tests/apps/react-router/client/main.html rename to tools/e2e-tests/apps/react-router/client/main.html diff --git a/tools/modern-tests/apps/react-router/client/main.jsx b/tools/e2e-tests/apps/react-router/client/main.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/client/main.jsx rename to tools/e2e-tests/apps/react-router/client/main.jsx diff --git a/tools/e2e-tests/apps/react-router/important.app-test.js b/tools/e2e-tests/apps/react-router/important.app-test.js new file mode 100644 index 0000000000..a9cb841908 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/important.app-test.js @@ -0,0 +1,7 @@ +import assert from "assert"; + +describe("!important.app-test.js pattern", () => { + it("should run as it's not ignored (negation)", () => { + assert(true, "should run"); + }); +}); diff --git a/tools/modern-tests/apps/react-router/imports/api/links.js b/tools/e2e-tests/apps/react-router/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/react-router/imports/api/links.js rename to tools/e2e-tests/apps/react-router/imports/api/links.js diff --git a/tools/modern-tests/apps/react-router/imports/helpers/alias.js b/tools/e2e-tests/apps/react-router/imports/helpers/alias.js similarity index 100% rename from tools/modern-tests/apps/react-router/imports/helpers/alias.js rename to tools/e2e-tests/apps/react-router/imports/helpers/alias.js diff --git a/tools/modern-tests/apps/react-router/imports/ui/App.jsx b/tools/e2e-tests/apps/react-router/imports/ui/App.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/imports/ui/App.jsx rename to tools/e2e-tests/apps/react-router/imports/ui/App.jsx diff --git a/tools/modern-tests/apps/react-router/imports/ui/Global.less b/tools/e2e-tests/apps/react-router/imports/ui/Global.less similarity index 100% rename from tools/modern-tests/apps/react-router/imports/ui/Global.less rename to tools/e2e-tests/apps/react-router/imports/ui/Global.less diff --git a/tools/modern-tests/apps/react-router/imports/ui/Hello.jsx b/tools/e2e-tests/apps/react-router/imports/ui/Hello.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/imports/ui/Hello.jsx rename to tools/e2e-tests/apps/react-router/imports/ui/Hello.jsx diff --git a/tools/modern-tests/apps/react-router/imports/ui/Home.jsx b/tools/e2e-tests/apps/react-router/imports/ui/Home.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/imports/ui/Home.jsx rename to tools/e2e-tests/apps/react-router/imports/ui/Home.jsx diff --git a/tools/modern-tests/apps/react-router/imports/ui/Info.jsx b/tools/e2e-tests/apps/react-router/imports/ui/Info.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/imports/ui/Info.jsx rename to tools/e2e-tests/apps/react-router/imports/ui/Info.jsx diff --git a/tools/modern-tests/apps/react-router/imports/ui/NotFound.jsx b/tools/e2e-tests/apps/react-router/imports/ui/NotFound.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/imports/ui/NotFound.jsx rename to tools/e2e-tests/apps/react-router/imports/ui/NotFound.jsx diff --git a/tools/modern-tests/apps/react-router/my-packages/custom-package/custom-package.js b/tools/e2e-tests/apps/react-router/my-packages/custom-package/custom-package.js similarity index 100% rename from tools/modern-tests/apps/react-router/my-packages/custom-package/custom-package.js rename to tools/e2e-tests/apps/react-router/my-packages/custom-package/custom-package.js diff --git a/tools/modern-tests/apps/react-router/my-packages/custom-package/package.js b/tools/e2e-tests/apps/react-router/my-packages/custom-package/package.js similarity index 100% rename from tools/modern-tests/apps/react-router/my-packages/custom-package/package.js rename to tools/e2e-tests/apps/react-router/my-packages/custom-package/package.js diff --git a/tools/modern-tests/apps/react-router/package.json b/tools/e2e-tests/apps/react-router/package.json similarity index 100% rename from tools/modern-tests/apps/react-router/package.json rename to tools/e2e-tests/apps/react-router/package.json diff --git a/tools/modern-tests/apps/react-router/packages/default-package/default-package.js b/tools/e2e-tests/apps/react-router/packages/default-package/default-package.js similarity index 100% rename from tools/modern-tests/apps/react-router/packages/default-package/default-package.js rename to tools/e2e-tests/apps/react-router/packages/default-package/default-package.js diff --git a/tools/modern-tests/apps/react-router/packages/default-package/package.js b/tools/e2e-tests/apps/react-router/packages/default-package/package.js similarity index 100% rename from tools/modern-tests/apps/react-router/packages/default-package/package.js rename to tools/e2e-tests/apps/react-router/packages/default-package/package.js diff --git a/tools/modern-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js b/tools/e2e-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js similarity index 100% rename from tools/modern-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js rename to tools/e2e-tests/apps/react-router/plugins/CustomConsoleLogPlugin.js diff --git a/tools/modern-tests/apps/react-router/public/1x1.png b/tools/e2e-tests/apps/react-router/public/1x1.png similarity index 100% rename from tools/modern-tests/apps/react-router/public/1x1.png rename to tools/e2e-tests/apps/react-router/public/1x1.png diff --git a/tools/modern-tests/apps/react-router/public/docs/text.md b/tools/e2e-tests/apps/react-router/public/docs/text.md similarity index 100% rename from tools/modern-tests/apps/react-router/public/docs/text.md rename to tools/e2e-tests/apps/react-router/public/docs/text.md diff --git a/tools/modern-tests/apps/react-router/public/images/1x1.png b/tools/e2e-tests/apps/react-router/public/images/1x1.png similarity index 100% rename from tools/modern-tests/apps/react-router/public/images/1x1.png rename to tools/e2e-tests/apps/react-router/public/images/1x1.png diff --git a/tools/e2e-tests/apps/react-router/react-router-wxyz-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/react-router-wxyz-ignored/ignore.app-test.js new file mode 100644 index 0000000000..bf67b299e7 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/react-router-wxyz-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("react-router*-ignored pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/modern-tests/apps/react-router/rspack.config.js b/tools/e2e-tests/apps/react-router/rspack.config.js similarity index 100% rename from tools/modern-tests/apps/react-router/rspack.config.js rename to tools/e2e-tests/apps/react-router/rspack.config.js diff --git a/tools/modern-tests/apps/react-router/rspack.config.override.js b/tools/e2e-tests/apps/react-router/rspack.config.override.js similarity index 100% rename from tools/modern-tests/apps/react-router/rspack.config.override.js rename to tools/e2e-tests/apps/react-router/rspack.config.override.js diff --git a/tools/modern-tests/apps/react-router/server/browser-tests/browser.app-test.js b/tools/e2e-tests/apps/react-router/server/browser-tests/browser.app-test.js similarity index 100% rename from tools/modern-tests/apps/react-router/server/browser-tests/browser.app-test.js rename to tools/e2e-tests/apps/react-router/server/browser-tests/browser.app-test.js diff --git a/tools/modern-tests/apps/react-router/server/main.js b/tools/e2e-tests/apps/react-router/server/main.js similarity index 100% rename from tools/modern-tests/apps/react-router/server/main.js rename to tools/e2e-tests/apps/react-router/server/main.js diff --git a/tools/modern-tests/apps/react-router/server/resolve-extensions/first.jsx b/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.jsx similarity index 100% rename from tools/modern-tests/apps/react-router/server/resolve-extensions/first.jsx rename to tools/e2e-tests/apps/react-router/server/resolve-extensions/first.jsx diff --git a/tools/modern-tests/apps/react-router/server/resolve-extensions/first.tsx b/tools/e2e-tests/apps/react-router/server/resolve-extensions/first.tsx similarity index 100% rename from tools/modern-tests/apps/react-router/server/resolve-extensions/first.tsx rename to tools/e2e-tests/apps/react-router/server/resolve-extensions/first.tsx diff --git a/tools/modern-tests/apps/react-router/server/server.app-test.js b/tools/e2e-tests/apps/react-router/server/server.app-test.js similarity index 100% rename from tools/modern-tests/apps/react-router/server/server.app-test.js rename to tools/e2e-tests/apps/react-router/server/server.app-test.js diff --git a/tools/modern-tests/apps/react-router/server/ts/helpers.ts b/tools/e2e-tests/apps/react-router/server/ts/helpers.ts similarity index 100% rename from tools/modern-tests/apps/react-router/server/ts/helpers.ts rename to tools/e2e-tests/apps/react-router/server/ts/helpers.ts diff --git a/tools/modern-tests/apps/react-router/styles/module.css b/tools/e2e-tests/apps/react-router/styles/module.css similarity index 100% rename from tools/modern-tests/apps/react-router/styles/module.css rename to tools/e2e-tests/apps/react-router/styles/module.css diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/file-to-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/file-to-ignored.app-test.js new file mode 100644 index 0000000000..c67d43d5d9 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/file-to-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/file-to-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/folder-to-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/folder-to-ignored/ignore.app-test.js new file mode 100644 index 0000000000..5878c51755 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/folder-to-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/folder-to-ignored/ pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/integration/test-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/integration/test-ignored.app-test.js new file mode 100644 index 0000000000..6c85dda4f6 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/integration/test-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/integration/*-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/prefix-test-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/prefix-test-ignored/ignore.app-test.js new file mode 100644 index 0000000000..a8a1c30c5a --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/prefix-test-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/prefix-*-ignored pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/react-router-wxyz-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/react-router-wxyz-ignored/ignore.app-test.js new file mode 100644 index 0000000000..217570f7b1 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/react-router-wxyz-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/react-router*-ignored pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/some-test-ignored-suffix/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/some-test-ignored-suffix/ignore.app-test.js new file mode 100644 index 0000000000..ad7167d7c5 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/some-test-ignored-suffix/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/*-ignored-suffix pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/some/nested/glob-ignored/ignore.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/some/nested/glob-ignored/ignore.app-test.js new file mode 100644 index 0000000000..403124d0b5 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/some/nested/glob-ignored/ignore.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/some/nested/glob-ignored/ignore.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/some/unit/test-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/some/unit/test-ignored.app-test.js new file mode 100644 index 0000000000..3d8cab2346 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/some/unit/test-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/some/unit/test-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/specific-file-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/specific-file-ignored.app-test.js new file mode 100644 index 0000000000..4eda02edf5 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/specific-file-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/specific-file-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/test-example-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/test-example-ignored.app-test.js new file mode 100644 index 0000000000..9fdbbda403 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/test-example-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/test-*-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/e2e-tests/apps/react-router/tests/ignored/test.temp-ignored.app-test.js b/tools/e2e-tests/apps/react-router/tests/ignored/test.temp-ignored.app-test.js new file mode 100644 index 0000000000..72d4d3d0e1 --- /dev/null +++ b/tools/e2e-tests/apps/react-router/tests/ignored/test.temp-ignored.app-test.js @@ -0,0 +1,5 @@ +describe("tests/ignored/*.temp-ignored.app-test.js pattern", () => { + it("should not run as ignored", () => { + throw new Error("test should be ignored by eager test loading"); + }); +}); diff --git a/tools/modern-tests/apps/react-router/tests/main.app-test.js b/tools/e2e-tests/apps/react-router/tests/main.app-test.js similarity index 100% rename from tools/modern-tests/apps/react-router/tests/main.app-test.js rename to tools/e2e-tests/apps/react-router/tests/main.app-test.js diff --git a/tools/modern-tests/apps/react/.gitignore b/tools/e2e-tests/apps/react/.gitignore similarity index 100% rename from tools/modern-tests/apps/react/.gitignore rename to tools/e2e-tests/apps/react/.gitignore diff --git a/tools/modern-tests/apps/react/.meteor/.gitignore b/tools/e2e-tests/apps/react/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/react/.meteor/.gitignore rename to tools/e2e-tests/apps/react/.meteor/.gitignore diff --git a/tools/modern-tests/apps/react/.meteor/.id b/tools/e2e-tests/apps/react/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/react/.meteor/.id rename to tools/e2e-tests/apps/react/.meteor/.id diff --git a/tools/modern-tests/apps/react/.meteor/packages b/tools/e2e-tests/apps/react/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/react/.meteor/packages rename to tools/e2e-tests/apps/react/.meteor/packages diff --git a/tools/modern-tests/apps/react/.meteor/platforms b/tools/e2e-tests/apps/react/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/react/.meteor/platforms rename to tools/e2e-tests/apps/react/.meteor/platforms diff --git a/tools/modern-tests/apps/react/.meteor/release b/tools/e2e-tests/apps/react/.meteor/release similarity index 100% rename from tools/modern-tests/apps/react/.meteor/release rename to tools/e2e-tests/apps/react/.meteor/release diff --git a/tools/modern-tests/apps/react/.meteor/versions b/tools/e2e-tests/apps/react/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/react/.meteor/versions rename to tools/e2e-tests/apps/react/.meteor/versions diff --git a/tools/modern-tests/apps/react/client/main.html b/tools/e2e-tests/apps/react/client/main.html similarity index 100% rename from tools/modern-tests/apps/react/client/main.html rename to tools/e2e-tests/apps/react/client/main.html diff --git a/tools/modern-tests/apps/react/client/main.jsx b/tools/e2e-tests/apps/react/client/main.jsx similarity index 100% rename from tools/modern-tests/apps/react/client/main.jsx rename to tools/e2e-tests/apps/react/client/main.jsx diff --git a/tools/modern-tests/apps/react/imports/api/links.js b/tools/e2e-tests/apps/react/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/react/imports/api/links.js rename to tools/e2e-tests/apps/react/imports/api/links.js diff --git a/tools/modern-tests/apps/react/imports/ui/App.jsx b/tools/e2e-tests/apps/react/imports/ui/App.jsx similarity index 100% rename from tools/modern-tests/apps/react/imports/ui/App.jsx rename to tools/e2e-tests/apps/react/imports/ui/App.jsx diff --git a/tools/modern-tests/apps/react/imports/ui/Hello.jsx b/tools/e2e-tests/apps/react/imports/ui/Hello.jsx similarity index 100% rename from tools/modern-tests/apps/react/imports/ui/Hello.jsx rename to tools/e2e-tests/apps/react/imports/ui/Hello.jsx diff --git a/tools/modern-tests/apps/react/imports/ui/Info.jsx b/tools/e2e-tests/apps/react/imports/ui/Info.jsx similarity index 100% rename from tools/modern-tests/apps/react/imports/ui/Info.jsx rename to tools/e2e-tests/apps/react/imports/ui/Info.jsx diff --git a/tools/modern-tests/apps/react/imports/ui/images/1x1-js.png b/tools/e2e-tests/apps/react/imports/ui/images/1x1-js.png similarity index 100% rename from tools/modern-tests/apps/react/imports/ui/images/1x1-js.png rename to tools/e2e-tests/apps/react/imports/ui/images/1x1-js.png diff --git a/tools/modern-tests/apps/react/imports/ui/main.css b/tools/e2e-tests/apps/react/imports/ui/main.css similarity index 100% rename from tools/modern-tests/apps/react/imports/ui/main.css rename to tools/e2e-tests/apps/react/imports/ui/main.css diff --git a/tools/modern-tests/apps/react/package.json b/tools/e2e-tests/apps/react/package.json similarity index 100% rename from tools/modern-tests/apps/react/package.json rename to tools/e2e-tests/apps/react/package.json diff --git a/tools/modern-tests/apps/react/plugins/CustomConsoleLogPlugin.js b/tools/e2e-tests/apps/react/plugins/CustomConsoleLogPlugin.js similarity index 100% rename from tools/modern-tests/apps/react/plugins/CustomConsoleLogPlugin.js rename to tools/e2e-tests/apps/react/plugins/CustomConsoleLogPlugin.js diff --git a/tools/modern-tests/apps/react/public/1x1-css.png b/tools/e2e-tests/apps/react/public/1x1-css.png similarity index 100% rename from tools/modern-tests/apps/react/public/1x1-css.png rename to tools/e2e-tests/apps/react/public/1x1-css.png diff --git a/tools/modern-tests/apps/react/public/1x1-public.jpg b/tools/e2e-tests/apps/react/public/1x1-public.jpg similarity index 100% rename from tools/modern-tests/apps/react/public/1x1-public.jpg rename to tools/e2e-tests/apps/react/public/1x1-public.jpg diff --git a/tools/modern-tests/apps/react/rspack.config.cjs b/tools/e2e-tests/apps/react/rspack.config.cjs similarity index 100% rename from tools/modern-tests/apps/react/rspack.config.cjs rename to tools/e2e-tests/apps/react/rspack.config.cjs diff --git a/tools/modern-tests/apps/react/server/main.js b/tools/e2e-tests/apps/react/server/main.js similarity index 100% rename from tools/modern-tests/apps/react/server/main.js rename to tools/e2e-tests/apps/react/server/main.js diff --git a/tools/modern-tests/apps/react/tests/main.js b/tools/e2e-tests/apps/react/tests/main.js similarity index 100% rename from tools/modern-tests/apps/react/tests/main.js rename to tools/e2e-tests/apps/react/tests/main.js diff --git a/tools/modern-tests/apps/solid/.meteor/.gitignore b/tools/e2e-tests/apps/server-only/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/solid/.meteor/.gitignore rename to tools/e2e-tests/apps/server-only/.meteor/.gitignore diff --git a/tools/e2e-tests/apps/server-only/.meteor/.id b/tools/e2e-tests/apps/server-only/.meteor/.id new file mode 100644 index 0000000000..33386e121f --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +l2d6s2mpmf9d.hefjva6hrzxq diff --git a/tools/e2e-tests/apps/server-only/.meteor/packages b/tools/e2e-tests/apps/server-only/.meteor/packages new file mode 100644 index 0000000000..0743c87631 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/packages @@ -0,0 +1,20 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base@1.5.2 # Packages every Meteor app needs to have +mobile-experience@1.1.2 # Packages for a great mobile UX +mongo@2.2.0 # The database Meteor supports right now +static-html@1.5.0 # Define static page content in .html files +reactive-var@1.0.13 # Reactive variable for tracker +tracker@1.3.4 # Meteor's client-side reactive programming library + +standard-minifier-css@1.10.0 # CSS minifier run for production mode +standard-minifier-js@3.2.0 # JS minifier run for production mode +es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers +ecmascript@0.17.0 # Enable ECMAScript2015+ syntax in app code +typescript@5.9.3 # Enable TypeScript syntax in .ts and .tsx modules +shell-server@0.7.0 # Server-side component of the `meteor shell` command +rspack diff --git a/tools/modern-tests/apps/solid/.meteor/platforms b/tools/e2e-tests/apps/server-only/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/solid/.meteor/platforms rename to tools/e2e-tests/apps/server-only/.meteor/platforms diff --git a/tools/modern-tests/apps/solid/.meteor/release b/tools/e2e-tests/apps/server-only/.meteor/release similarity index 100% rename from tools/modern-tests/apps/solid/.meteor/release rename to tools/e2e-tests/apps/server-only/.meteor/release diff --git a/tools/e2e-tests/apps/server-only/.meteor/versions b/tools/e2e-tests/apps/server-only/.meteor/versions new file mode 100644 index 0000000000..7e24fc06e4 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/.meteor/versions @@ -0,0 +1,66 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.13.0 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.1.0 +caching-compiler@2.0.1 +callback-hook@1.6.1 +check@1.5.0 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.17.0 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +id-map@1.2.0 +inter-process-messaging@0.1.2 +launch-screen@2.0.1 +logging@1.3.6 +meteor@2.2.0 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.1.0 +minimongo@2.0.5 +mobile-experience@1.1.2 +mobile-status-bar@1.1.1 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +mongo@2.2.0 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.1 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.3.0 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +rspack@1.0.0 +shell-server@0.7.0 +socket-stream-client@0.6.1 +standard-minifier-css@1.10.0 +standard-minifier-js@3.2.0 +static-html@1.5.0 +static-html-tools@1.0.0 +tools-core@1.0.0 +tracker@1.3.4 +typescript@5.9.3 +webapp@2.1.0 +webapp-hashing@1.1.2 diff --git a/tools/e2e-tests/apps/server-only/package.json b/tools/e2e-tests/apps/server-only/package.json new file mode 100644 index 0000000000..2b9704d1f7 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/package.json @@ -0,0 +1,17 @@ +{ + "name": "server-only", + "private": true, + "scripts": { + "start": "meteor run" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "meteor-node-stubs": "^1.2.12" + }, + "meteor": { + "mainModule": { + "server": "server/main.js" + }, + "modern": true + } +} diff --git a/tools/e2e-tests/apps/server-only/rspack.config.js b/tools/e2e-tests/apps/server-only/rspack.config.js new file mode 100644 index 0000000000..a6434b4e45 --- /dev/null +++ b/tools/e2e-tests/apps/server-only/rspack.config.js @@ -0,0 +1,5 @@ +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(() => { + return {}; +}); diff --git a/tools/e2e-tests/apps/server-only/server/main.js b/tools/e2e-tests/apps/server-only/server/main.js new file mode 100644 index 0000000000..f24ca7f09f --- /dev/null +++ b/tools/e2e-tests/apps/server-only/server/main.js @@ -0,0 +1 @@ +console.log('server/main.js loaded'); \ No newline at end of file diff --git a/tools/modern-tests/apps/solid/.gitignore b/tools/e2e-tests/apps/solid/.gitignore similarity index 100% rename from tools/modern-tests/apps/solid/.gitignore rename to tools/e2e-tests/apps/solid/.gitignore diff --git a/tools/modern-tests/apps/svelte/.meteor/.gitignore b/tools/e2e-tests/apps/solid/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/svelte/.meteor/.gitignore rename to tools/e2e-tests/apps/solid/.meteor/.gitignore diff --git a/tools/modern-tests/apps/solid/.meteor/.id b/tools/e2e-tests/apps/solid/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/solid/.meteor/.id rename to tools/e2e-tests/apps/solid/.meteor/.id diff --git a/tools/modern-tests/apps/solid/.meteor/packages b/tools/e2e-tests/apps/solid/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/solid/.meteor/packages rename to tools/e2e-tests/apps/solid/.meteor/packages diff --git a/tools/modern-tests/apps/svelte/.meteor/platforms b/tools/e2e-tests/apps/solid/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/svelte/.meteor/platforms rename to tools/e2e-tests/apps/solid/.meteor/platforms diff --git a/tools/modern-tests/apps/svelte/.meteor/release b/tools/e2e-tests/apps/solid/.meteor/release similarity index 100% rename from tools/modern-tests/apps/svelte/.meteor/release rename to tools/e2e-tests/apps/solid/.meteor/release diff --git a/tools/modern-tests/apps/solid/.meteor/versions b/tools/e2e-tests/apps/solid/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/solid/.meteor/versions rename to tools/e2e-tests/apps/solid/.meteor/versions diff --git a/tools/modern-tests/apps/solid/client/main.html b/tools/e2e-tests/apps/solid/client/main.html similarity index 100% rename from tools/modern-tests/apps/solid/client/main.html rename to tools/e2e-tests/apps/solid/client/main.html diff --git a/tools/modern-tests/apps/solid/client/main.js b/tools/e2e-tests/apps/solid/client/main.js similarity index 100% rename from tools/modern-tests/apps/solid/client/main.js rename to tools/e2e-tests/apps/solid/client/main.js diff --git a/tools/modern-tests/apps/solid/imports/api/links.js b/tools/e2e-tests/apps/solid/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/solid/imports/api/links.js rename to tools/e2e-tests/apps/solid/imports/api/links.js diff --git a/tools/modern-tests/apps/solid/imports/ui/App.jsx b/tools/e2e-tests/apps/solid/imports/ui/App.jsx similarity index 100% rename from tools/modern-tests/apps/solid/imports/ui/App.jsx rename to tools/e2e-tests/apps/solid/imports/ui/App.jsx diff --git a/tools/modern-tests/apps/solid/imports/ui/Hello.jsx b/tools/e2e-tests/apps/solid/imports/ui/Hello.jsx similarity index 100% rename from tools/modern-tests/apps/solid/imports/ui/Hello.jsx rename to tools/e2e-tests/apps/solid/imports/ui/Hello.jsx diff --git a/tools/modern-tests/apps/solid/imports/ui/Info.jsx b/tools/e2e-tests/apps/solid/imports/ui/Info.jsx similarity index 100% rename from tools/modern-tests/apps/solid/imports/ui/Info.jsx rename to tools/e2e-tests/apps/solid/imports/ui/Info.jsx diff --git a/tools/modern-tests/apps/solid/imports/ui/main.css b/tools/e2e-tests/apps/solid/imports/ui/main.css similarity index 100% rename from tools/modern-tests/apps/solid/imports/ui/main.css rename to tools/e2e-tests/apps/solid/imports/ui/main.css diff --git a/tools/modern-tests/apps/solid/imports/ui/main.jsx b/tools/e2e-tests/apps/solid/imports/ui/main.jsx similarity index 100% rename from tools/modern-tests/apps/solid/imports/ui/main.jsx rename to tools/e2e-tests/apps/solid/imports/ui/main.jsx diff --git a/tools/modern-tests/apps/solid/package.json b/tools/e2e-tests/apps/solid/package.json similarity index 95% rename from tools/modern-tests/apps/solid/package.json rename to tools/e2e-tests/apps/solid/package.json index 104f5258ef..5ce93de638 100644 --- a/tools/modern-tests/apps/solid/package.json +++ b/tools/e2e-tests/apps/solid/package.json @@ -22,7 +22,7 @@ "modern": true }, "devDependencies": { - "@meteorjs/rspack": "^0.0.29", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "babel-loader": "10.0.0", diff --git a/tools/modern-tests/apps/solid/rspack.config.js b/tools/e2e-tests/apps/solid/rspack.config.js similarity index 100% rename from tools/modern-tests/apps/solid/rspack.config.js rename to tools/e2e-tests/apps/solid/rspack.config.js diff --git a/tools/modern-tests/apps/solid/server/main.js b/tools/e2e-tests/apps/solid/server/main.js similarity index 100% rename from tools/modern-tests/apps/solid/server/main.js rename to tools/e2e-tests/apps/solid/server/main.js diff --git a/tools/modern-tests/apps/solid/tests/main.js b/tools/e2e-tests/apps/solid/tests/main.js similarity index 100% rename from tools/modern-tests/apps/solid/tests/main.js rename to tools/e2e-tests/apps/solid/tests/main.js diff --git a/tools/modern-tests/apps/svelte/.gitignore b/tools/e2e-tests/apps/svelte/.gitignore similarity index 100% rename from tools/modern-tests/apps/svelte/.gitignore rename to tools/e2e-tests/apps/svelte/.gitignore diff --git a/tools/modern-tests/apps/typescript/.meteor/.gitignore b/tools/e2e-tests/apps/svelte/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/typescript/.meteor/.gitignore rename to tools/e2e-tests/apps/svelte/.meteor/.gitignore diff --git a/tools/modern-tests/apps/svelte/.meteor/.id b/tools/e2e-tests/apps/svelte/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/svelte/.meteor/.id rename to tools/e2e-tests/apps/svelte/.meteor/.id diff --git a/tools/modern-tests/apps/svelte/.meteor/packages b/tools/e2e-tests/apps/svelte/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/svelte/.meteor/packages rename to tools/e2e-tests/apps/svelte/.meteor/packages diff --git a/tools/modern-tests/apps/typescript/.meteor/platforms b/tools/e2e-tests/apps/svelte/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/typescript/.meteor/platforms rename to tools/e2e-tests/apps/svelte/.meteor/platforms diff --git a/tools/modern-tests/apps/typescript/.meteor/release b/tools/e2e-tests/apps/svelte/.meteor/release similarity index 100% rename from tools/modern-tests/apps/typescript/.meteor/release rename to tools/e2e-tests/apps/svelte/.meteor/release diff --git a/tools/modern-tests/apps/svelte/.meteor/versions b/tools/e2e-tests/apps/svelte/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/svelte/.meteor/versions rename to tools/e2e-tests/apps/svelte/.meteor/versions diff --git a/tools/modern-tests/apps/svelte/client/main.css b/tools/e2e-tests/apps/svelte/client/main.css similarity index 100% rename from tools/modern-tests/apps/svelte/client/main.css rename to tools/e2e-tests/apps/svelte/client/main.css diff --git a/tools/modern-tests/apps/svelte/client/main.html b/tools/e2e-tests/apps/svelte/client/main.html similarity index 100% rename from tools/modern-tests/apps/svelte/client/main.html rename to tools/e2e-tests/apps/svelte/client/main.html diff --git a/tools/modern-tests/apps/svelte/client/main.js b/tools/e2e-tests/apps/svelte/client/main.js similarity index 100% rename from tools/modern-tests/apps/svelte/client/main.js rename to tools/e2e-tests/apps/svelte/client/main.js diff --git a/tools/modern-tests/apps/svelte/imports/api/links.js b/tools/e2e-tests/apps/svelte/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/svelte/imports/api/links.js rename to tools/e2e-tests/apps/svelte/imports/api/links.js diff --git a/tools/modern-tests/apps/svelte/imports/ui/App.svelte b/tools/e2e-tests/apps/svelte/imports/ui/App.svelte similarity index 100% rename from tools/modern-tests/apps/svelte/imports/ui/App.svelte rename to tools/e2e-tests/apps/svelte/imports/ui/App.svelte diff --git a/tools/modern-tests/apps/svelte/package.json b/tools/e2e-tests/apps/svelte/package.json similarity index 95% rename from tools/modern-tests/apps/svelte/package.json rename to tools/e2e-tests/apps/svelte/package.json index a9873d4aa5..cba0d4e7a0 100644 --- a/tools/modern-tests/apps/svelte/package.json +++ b/tools/e2e-tests/apps/svelte/package.json @@ -13,7 +13,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^0.0.28", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "playwright": "1.58.0", diff --git a/tools/modern-tests/apps/svelte/rspack.config.js b/tools/e2e-tests/apps/svelte/rspack.config.js similarity index 100% rename from tools/modern-tests/apps/svelte/rspack.config.js rename to tools/e2e-tests/apps/svelte/rspack.config.js diff --git a/tools/modern-tests/apps/svelte/server/main.js b/tools/e2e-tests/apps/svelte/server/main.js similarity index 100% rename from tools/modern-tests/apps/svelte/server/main.js rename to tools/e2e-tests/apps/svelte/server/main.js diff --git a/tools/modern-tests/apps/svelte/tests/main.js b/tools/e2e-tests/apps/svelte/tests/main.js similarity index 100% rename from tools/modern-tests/apps/svelte/tests/main.js rename to tools/e2e-tests/apps/svelte/tests/main.js diff --git a/tools/modern-tests/apps/svelte/tsconfig.json b/tools/e2e-tests/apps/svelte/tsconfig.json similarity index 100% rename from tools/modern-tests/apps/svelte/tsconfig.json rename to tools/e2e-tests/apps/svelte/tsconfig.json diff --git a/tools/modern-tests/apps/typescript/.gitignore b/tools/e2e-tests/apps/typescript/.gitignore similarity index 100% rename from tools/modern-tests/apps/typescript/.gitignore rename to tools/e2e-tests/apps/typescript/.gitignore diff --git a/tools/modern-tests/apps/vue/.meteor/.gitignore b/tools/e2e-tests/apps/typescript/.meteor/.gitignore similarity index 100% rename from tools/modern-tests/apps/vue/.meteor/.gitignore rename to tools/e2e-tests/apps/typescript/.meteor/.gitignore diff --git a/tools/modern-tests/apps/typescript/.meteor/.id b/tools/e2e-tests/apps/typescript/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/typescript/.meteor/.id rename to tools/e2e-tests/apps/typescript/.meteor/.id diff --git a/tools/modern-tests/apps/typescript/.meteor/packages b/tools/e2e-tests/apps/typescript/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/typescript/.meteor/packages rename to tools/e2e-tests/apps/typescript/.meteor/packages diff --git a/tools/modern-tests/apps/vue/.meteor/platforms b/tools/e2e-tests/apps/typescript/.meteor/platforms similarity index 100% rename from tools/modern-tests/apps/vue/.meteor/platforms rename to tools/e2e-tests/apps/typescript/.meteor/platforms diff --git a/tools/modern-tests/apps/vue/.meteor/release b/tools/e2e-tests/apps/typescript/.meteor/release similarity index 100% rename from tools/modern-tests/apps/vue/.meteor/release rename to tools/e2e-tests/apps/typescript/.meteor/release diff --git a/tools/modern-tests/apps/typescript/.meteor/versions b/tools/e2e-tests/apps/typescript/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/typescript/.meteor/versions rename to tools/e2e-tests/apps/typescript/.meteor/versions diff --git a/tools/modern-tests/apps/typescript/client/main.html b/tools/e2e-tests/apps/typescript/client/main.html similarity index 100% rename from tools/modern-tests/apps/typescript/client/main.html rename to tools/e2e-tests/apps/typescript/client/main.html diff --git a/tools/modern-tests/apps/typescript/client/main.scss b/tools/e2e-tests/apps/typescript/client/main.scss similarity index 100% rename from tools/modern-tests/apps/typescript/client/main.scss rename to tools/e2e-tests/apps/typescript/client/main.scss diff --git a/tools/modern-tests/apps/typescript/client/main.tsx b/tools/e2e-tests/apps/typescript/client/main.tsx similarity index 90% rename from tools/modern-tests/apps/typescript/client/main.tsx rename to tools/e2e-tests/apps/typescript/client/main.tsx index e576e1b803..4eb40f49d1 100644 --- a/tools/modern-tests/apps/typescript/client/main.tsx +++ b/tools/e2e-tests/apps/typescript/client/main.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createRoot } from 'react-dom/client'; import { Meteor } from 'meteor/meteor'; import { App } from '@ui/App'; diff --git a/tools/modern-tests/apps/typescript/imports/api/links.ts b/tools/e2e-tests/apps/typescript/imports/api/links.ts similarity index 100% rename from tools/modern-tests/apps/typescript/imports/api/links.ts rename to tools/e2e-tests/apps/typescript/imports/api/links.ts diff --git a/tools/modern-tests/apps/typescript/imports/ui/App.tsx b/tools/e2e-tests/apps/typescript/imports/ui/App.tsx similarity index 87% rename from tools/modern-tests/apps/typescript/imports/ui/App.tsx rename to tools/e2e-tests/apps/typescript/imports/ui/App.tsx index 52f71448cc..94679c5547 100644 --- a/tools/modern-tests/apps/typescript/imports/ui/App.tsx +++ b/tools/e2e-tests/apps/typescript/imports/ui/App.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import './Global.scss'; import { Hello } from './Hello'; import { Info } from './Info'; diff --git a/tools/modern-tests/apps/typescript/imports/ui/Global.scss b/tools/e2e-tests/apps/typescript/imports/ui/Global.scss similarity index 100% rename from tools/modern-tests/apps/typescript/imports/ui/Global.scss rename to tools/e2e-tests/apps/typescript/imports/ui/Global.scss diff --git a/tools/modern-tests/apps/typescript/imports/ui/Hello.tsx b/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx similarity index 87% rename from tools/modern-tests/apps/typescript/imports/ui/Hello.tsx rename to tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx index 15e0f185ac..527d5af607 100644 --- a/tools/modern-tests/apps/typescript/imports/ui/Hello.tsx +++ b/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; export const Hello = () => { const [counter, setCounter] = useState(0); diff --git a/tools/modern-tests/apps/typescript/imports/ui/Info.tsx b/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx similarity index 95% rename from tools/modern-tests/apps/typescript/imports/ui/Info.tsx rename to tools/e2e-tests/apps/typescript/imports/ui/Info.tsx index 23cb8f07a3..809fbc6716 100644 --- a/tools/modern-tests/apps/typescript/imports/ui/Info.tsx +++ b/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useFind, useSubscribe } from "meteor/react-meteor-data"; import { LinksCollection, Link } from "../api/links"; diff --git a/tools/modern-tests/apps/typescript/package.json b/tools/e2e-tests/apps/typescript/package.json similarity index 97% rename from tools/modern-tests/apps/typescript/package.json rename to tools/e2e-tests/apps/typescript/package.json index d1ac310541..f0359b6094 100644 --- a/tools/modern-tests/apps/typescript/package.json +++ b/tools/e2e-tests/apps/typescript/package.json @@ -15,6 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@swc/core": "^1.15.18", "@types/meteor": "^2.9.9", "@types/mocha": "^8.2.3", "@types/node": "^22.10.6", diff --git a/tools/modern-tests/apps/typescript/rspack.config.ts b/tools/e2e-tests/apps/typescript/rspack.config.ts similarity index 96% rename from tools/modern-tests/apps/typescript/rspack.config.ts rename to tools/e2e-tests/apps/typescript/rspack.config.ts index 35ec5ffdad..f4be271f64 100644 --- a/tools/modern-tests/apps/typescript/rspack.config.ts +++ b/tools/e2e-tests/apps/typescript/rspack.config.ts @@ -16,6 +16,7 @@ const require = createRequire(import.meta.url); */ export default defineConfig(Meteor => { return { + ...Meteor.enablePortableBuild(), ...Meteor.extendSwcConfig({ jsc: { baseUrl: process.cwd(), diff --git a/tools/modern-tests/apps/typescript/server/main.ts b/tools/e2e-tests/apps/typescript/server/main.ts similarity index 100% rename from tools/modern-tests/apps/typescript/server/main.ts rename to tools/e2e-tests/apps/typescript/server/main.ts diff --git a/tools/e2e-tests/apps/typescript/swc.config.ts b/tools/e2e-tests/apps/typescript/swc.config.ts new file mode 100644 index 0000000000..699a33a40d --- /dev/null +++ b/tools/e2e-tests/apps/typescript/swc.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "@swc/core"; + +const config: Config = { + jsc: { + transform: { + react: { + runtime: "automatic", + }, + }, + }, +}; + +export default config; diff --git a/tools/modern-tests/apps/typescript/tests/client.ts b/tools/e2e-tests/apps/typescript/tests/client.ts similarity index 100% rename from tools/modern-tests/apps/typescript/tests/client.ts rename to tools/e2e-tests/apps/typescript/tests/client.ts diff --git a/tools/modern-tests/apps/typescript/tests/server.ts b/tools/e2e-tests/apps/typescript/tests/server.ts similarity index 100% rename from tools/modern-tests/apps/typescript/tests/server.ts rename to tools/e2e-tests/apps/typescript/tests/server.ts diff --git a/tools/modern-tests/apps/typescript/tsconfig.json b/tools/e2e-tests/apps/typescript/tsconfig.json similarity index 100% rename from tools/modern-tests/apps/typescript/tsconfig.json rename to tools/e2e-tests/apps/typescript/tsconfig.json diff --git a/tools/modern-tests/apps/vue/.gitignore b/tools/e2e-tests/apps/vue/.gitignore similarity index 100% rename from tools/modern-tests/apps/vue/.gitignore rename to tools/e2e-tests/apps/vue/.gitignore diff --git a/tools/e2e-tests/apps/vue/.meteor/.gitignore b/tools/e2e-tests/apps/vue/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/modern-tests/apps/vue/.meteor/.id b/tools/e2e-tests/apps/vue/.meteor/.id similarity index 100% rename from tools/modern-tests/apps/vue/.meteor/.id rename to tools/e2e-tests/apps/vue/.meteor/.id diff --git a/tools/modern-tests/apps/vue/.meteor/packages b/tools/e2e-tests/apps/vue/.meteor/packages similarity index 100% rename from tools/modern-tests/apps/vue/.meteor/packages rename to tools/e2e-tests/apps/vue/.meteor/packages diff --git a/tools/e2e-tests/apps/vue/.meteor/platforms b/tools/e2e-tests/apps/vue/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/e2e-tests/apps/vue/.meteor/release b/tools/e2e-tests/apps/vue/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/e2e-tests/apps/vue/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/modern-tests/apps/vue/.meteor/versions b/tools/e2e-tests/apps/vue/.meteor/versions similarity index 100% rename from tools/modern-tests/apps/vue/.meteor/versions rename to tools/e2e-tests/apps/vue/.meteor/versions diff --git a/tools/modern-tests/apps/vue/.meteorignore b/tools/e2e-tests/apps/vue/.meteorignore similarity index 100% rename from tools/modern-tests/apps/vue/.meteorignore rename to tools/e2e-tests/apps/vue/.meteorignore diff --git a/tools/modern-tests/apps/vue/client/main.css b/tools/e2e-tests/apps/vue/client/main.css similarity index 100% rename from tools/modern-tests/apps/vue/client/main.css rename to tools/e2e-tests/apps/vue/client/main.css diff --git a/tools/modern-tests/apps/vue/client/main.html b/tools/e2e-tests/apps/vue/client/main.html similarity index 100% rename from tools/modern-tests/apps/vue/client/main.html rename to tools/e2e-tests/apps/vue/client/main.html diff --git a/tools/modern-tests/apps/vue/client/main.js b/tools/e2e-tests/apps/vue/client/main.js similarity index 100% rename from tools/modern-tests/apps/vue/client/main.js rename to tools/e2e-tests/apps/vue/client/main.js diff --git a/tools/modern-tests/apps/vue/client/meteor.css b/tools/e2e-tests/apps/vue/client/meteor.css similarity index 100% rename from tools/modern-tests/apps/vue/client/meteor.css rename to tools/e2e-tests/apps/vue/client/meteor.css diff --git a/tools/modern-tests/apps/vue/imports/api/links.js b/tools/e2e-tests/apps/vue/imports/api/links.js similarity index 100% rename from tools/modern-tests/apps/vue/imports/api/links.js rename to tools/e2e-tests/apps/vue/imports/api/links.js diff --git a/tools/modern-tests/apps/vue/imports/ui/App.vue b/tools/e2e-tests/apps/vue/imports/ui/App.vue similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/App.vue rename to tools/e2e-tests/apps/vue/imports/ui/App.vue diff --git a/tools/modern-tests/apps/vue/imports/ui/components/AppMenu.vue b/tools/e2e-tests/apps/vue/imports/ui/components/AppMenu.vue similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/components/AppMenu.vue rename to tools/e2e-tests/apps/vue/imports/ui/components/AppMenu.vue diff --git a/tools/modern-tests/apps/vue/imports/ui/components/Hello.vue b/tools/e2e-tests/apps/vue/imports/ui/components/Hello.vue similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/components/Hello.vue rename to tools/e2e-tests/apps/vue/imports/ui/components/Hello.vue diff --git a/tools/modern-tests/apps/vue/imports/ui/components/Info.vue b/tools/e2e-tests/apps/vue/imports/ui/components/Info.vue similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/components/Info.vue rename to tools/e2e-tests/apps/vue/imports/ui/components/Info.vue diff --git a/tools/modern-tests/apps/vue/imports/ui/main.css b/tools/e2e-tests/apps/vue/imports/ui/main.css similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/main.css rename to tools/e2e-tests/apps/vue/imports/ui/main.css diff --git a/tools/modern-tests/apps/vue/imports/ui/main.js b/tools/e2e-tests/apps/vue/imports/ui/main.js similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/main.js rename to tools/e2e-tests/apps/vue/imports/ui/main.js diff --git a/tools/modern-tests/apps/vue/imports/ui/router.js b/tools/e2e-tests/apps/vue/imports/ui/router.js similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/router.js rename to tools/e2e-tests/apps/vue/imports/ui/router.js diff --git a/tools/modern-tests/apps/vue/imports/ui/views/About.vue b/tools/e2e-tests/apps/vue/imports/ui/views/About.vue similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/views/About.vue rename to tools/e2e-tests/apps/vue/imports/ui/views/About.vue diff --git a/tools/modern-tests/apps/vue/imports/ui/views/Home.vue b/tools/e2e-tests/apps/vue/imports/ui/views/Home.vue similarity index 100% rename from tools/modern-tests/apps/vue/imports/ui/views/Home.vue rename to tools/e2e-tests/apps/vue/imports/ui/views/Home.vue diff --git a/tools/modern-tests/apps/vue/package.json b/tools/e2e-tests/apps/vue/package.json similarity index 96% rename from tools/modern-tests/apps/vue/package.json rename to tools/e2e-tests/apps/vue/package.json index 3f7c62a14e..31c39838ad 100644 --- a/tools/modern-tests/apps/vue/package.json +++ b/tools/e2e-tests/apps/vue/package.json @@ -17,7 +17,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "@tailwindcss/postcss": "^4.1.12", diff --git a/tools/modern-tests/apps/vue/postcss.config.js b/tools/e2e-tests/apps/vue/postcss.config.js similarity index 100% rename from tools/modern-tests/apps/vue/postcss.config.js rename to tools/e2e-tests/apps/vue/postcss.config.js diff --git a/tools/modern-tests/apps/vue/rspack.config.js b/tools/e2e-tests/apps/vue/rspack.config.js similarity index 100% rename from tools/modern-tests/apps/vue/rspack.config.js rename to tools/e2e-tests/apps/vue/rspack.config.js diff --git a/tools/modern-tests/apps/vue/server/entry-meteor.js b/tools/e2e-tests/apps/vue/server/entry-meteor.js similarity index 100% rename from tools/modern-tests/apps/vue/server/entry-meteor.js rename to tools/e2e-tests/apps/vue/server/entry-meteor.js diff --git a/tools/modern-tests/apps/vue/server/main.js b/tools/e2e-tests/apps/vue/server/main.js similarity index 100% rename from tools/modern-tests/apps/vue/server/main.js rename to tools/e2e-tests/apps/vue/server/main.js diff --git a/tools/modern-tests/apps/vue/tests/main.js b/tools/e2e-tests/apps/vue/tests/main.js similarity index 100% rename from tools/modern-tests/apps/vue/tests/main.js rename to tools/e2e-tests/apps/vue/tests/main.js diff --git a/tools/modern-tests/assertions.js b/tools/e2e-tests/assertions.js similarity index 88% rename from tools/modern-tests/assertions.js rename to tools/e2e-tests/assertions.js index 4b873f6d9e..5abaad4daf 100644 --- a/tools/modern-tests/assertions.js +++ b/tools/e2e-tests/assertions.js @@ -102,7 +102,7 @@ export async function assertFileExist(tempDir, filePath, options = {}) { return checkFile(); } // If we've exceeded the timeout, fail the test - expect(fileExists).toBe(true); + throw new Error(`Expected file to exist but it was not found: ${fullPath}`); return false; } @@ -145,6 +145,43 @@ export async function assertFileExist(tempDir, filePath, options = {}) { await checkFile(); } +/** + * Helper function to assert that a path does NOT exist + * Retries until the path is gone or the timeout is exceeded + * @param {string} basePath - Base directory path + * @param {string} relPath - Relative path from basePath to check + * @param {Object} options - Additional options + * @param {number} options.timeout - Maximum time to wait in milliseconds (default: 5000) + * @param {number} options.checkInterval - Interval between checks in milliseconds (default: 100) + * @returns {Promise} + */ +export async function assertPathNotExist(basePath, relPath, options = {}) { + const { timeout = 5000, checkInterval = 100 } = options; + const fullPath = path.join(basePath, relPath); + const startTime = Date.now(); + + const check = async () => { + const exists = await fs.pathExists(fullPath); + if (exists && Date.now() - startTime < timeout) { + await new Promise(r => setTimeout(r, checkInterval)); + return check(); + } + if (exists) { + const stat = await fs.stat(fullPath); + const isDir = stat.isDirectory(); + let contents = ''; + if (isDir) { + const entries = await fs.readdir(fullPath); + contents = ` (contains: ${entries.join(', ')})`; + } + console.error(`assertPathNotExist FAILED: ${relPath} still exists at ${fullPath} [${isDir ? 'dir' : 'file'}, ${stat.size} bytes]${contents}`); + } + expect(exists).toBe(false); + }; + + await check(); +} + /** * Helper function to evaluate JavaScript code in the browser console and assert the result * @param {string} code - JavaScript code to evaluate in the browser console diff --git a/tools/e2e-tests/babel.config.js b/tools/e2e-tests/babel.config.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/modern-tests/babel.test.js b/tools/e2e-tests/babel.test.js similarity index 50% rename from tools/modern-tests/babel.test.js rename to tools/e2e-tests/babel.test.js index a414d7bbb0..4565410d1f 100644 --- a/tools/modern-tests/babel.test.js +++ b/tools/e2e-tests/babel.test.js @@ -7,15 +7,27 @@ describe('Babel App Bundling /', () => { describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ appName: 'babel', port: 3122, - filePaths: { - client: 'client/main.jsx', + filePaths: { + client: 'client/main.jsx', server: 'server/main.js', test: 'tests/main.js' }, configFile: 'rspack.config.mjs', + skipEnvCheck: true, + // Test custom NODE_ENV compilation + env: { + meteorRun: { NODE_ENV: 'development' }, + meteorRunProduction: { NODE_ENV: 'production' }, + meteorTest: { NODE_ENV: 'development' }, + meteorTestOnce: { NODE_ENV: 'test' }, + meteorBuild: { NODE_ENV: 'development' }, + }, customAssertions: { afterRun: async ({ result }) => { await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); }, afterRunRebuildClient: async ({ allConsoleLogs }) => { // Check for HMR output as enabled by default @@ -23,6 +35,15 @@ describe('Babel App Bundling /', () => { }, afterRunProduction: async ({ result }) => { await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: production/); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/ + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/ + ); }, afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { // Check for HMR to not be enabled in production-like mode @@ -30,12 +51,22 @@ describe('Babel App Bundling /', () => { }, afterTest: async ({ result }) => { await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); }, afterTestOnce: async ({ result }) => { await assertFileExtensionModuleRules(result.outputLines); + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); }, afterBuild: async ({ result }) => { await assertFileExtensionModuleRules(result.outputLines); + // Force development mode on build + await waitForMeteorOutput(result.outputLines, /\[i\] Rspack mode: development/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*true[^ ]*/); + await waitForMeteorOutput(result.outputLines, /[^ ]*Meteor.isProduction[^ ]*: [^ ]*false[^ ]*/); }, } })); diff --git a/tools/modern-tests/blaze.test.js b/tools/e2e-tests/blaze.test.js similarity index 100% rename from tools/modern-tests/blaze.test.js rename to tools/e2e-tests/blaze.test.js diff --git a/tools/modern-tests/coffeescript.test.js b/tools/e2e-tests/coffeescript.test.js similarity index 100% rename from tools/modern-tests/coffeescript.test.js rename to tools/e2e-tests/coffeescript.test.js diff --git a/tools/modern-tests/full-blaze.test.js b/tools/e2e-tests/full-blaze.test.js similarity index 100% rename from tools/modern-tests/full-blaze.test.js rename to tools/e2e-tests/full-blaze.test.js diff --git a/tools/modern-tests/helpers.js b/tools/e2e-tests/helpers.js similarity index 96% rename from tools/modern-tests/helpers.js rename to tools/e2e-tests/helpers.js index cbacc9e7d0..dd62124f30 100644 --- a/tools/modern-tests/helpers.js +++ b/tools/e2e-tests/helpers.js @@ -86,7 +86,7 @@ export async function setupMeteorApp(appName, options = {}) { * @returns {Object} - The meteor process and output lines */ export async function runMeteorApp(tempDir, port, options = {}) { - const { isMonorepo = false } = options; + const { isMonorepo = false, env = {} } = options; // Start Meteor CLI in dev mode console.log(`Starting Meteor app on port ${port}...`); @@ -105,11 +105,12 @@ export async function runMeteorApp(tempDir, port, options = {}) { // Run the meteor command const { meteorProcess, outputLines } = await runMeteorCommand( - 'run', - args, + 'run', + args, appDir, { - captureOutput + captureOutput, + execaOptions: { env: { ...process.env, ...env } } } ); @@ -123,11 +124,13 @@ export async function runMeteorApp(tempDir, port, options = {}) { } // Wait for server to be up - console.log(`Waiting for app to be available on port ${port}...`); - await waitOn({ - resources: [`http-get://localhost:${port}`], - timeout: 90000 - }); + if (!options.skipWaitOn) { + console.log(`Waiting for app to be available on port ${port}...`); + await waitOn({ + resources: [`http-get://localhost:${port}`], + timeout: 90000 + }); + } return { meteorProcess, outputLines }; } @@ -179,7 +182,7 @@ async function killSingleProcessByPort(port) { // Different commands based on OS const command = process.platform === 'win32' ? `FOR /F "tokens=5" %a in ('netstat -ano ^| find "LISTENING" ^| find ":${port}"') do taskkill /F /PID %a` - : `lsof -i :${port} -t | xargs -r kill -9`; + : `lsof -i :${port} -t | grep -v ^${process.pid}$ | xargs -r kill -9`; console.log(`Killing process on port ${port}...`); try { @@ -216,10 +219,14 @@ async function killSingleProcessByPort(port) { export async function runMeteorCommand(command, args = [], cwd, options = {}) { console.log(`Running Meteor command: ${command} ${args.join(' ')}...`); - const { captureOutput = false, checkExitCode = false, execaOptions: extraExecaOptions = {} } = options; + const { captureOutput = false, checkExitCode = false, execaOptions: extraExecaOptions = {}, env = {} } = options; const execaOptions = { cwd, + env: { + ...process.env, + ...env + }, ...extraExecaOptions }; @@ -523,12 +530,13 @@ export async function appendFileContent(tempDir, filePath, options = {}) { * @param {string|RegExp} options.waitForOutput - Output pattern to wait for * @param {Object} options.waitOptions - Options for waitForMeteorOutput * @param {string[]} options.commandOptions - Additional command line options for the test command + * @param {boolean} options.testClient - Whether to enable client-side tests with a browser driver * @param {boolean} options.checkTestResults - Whether to check test results and propagate failures to Jest * @param {boolean} options.isMonorepo - Whether the app is a monorepo * @returns {Object} - The meteor process and output lines */ export async function runMeteorTests(tempDir, port, options = {}) { - const { isMonorepo = false } = options; + const { isMonorepo = false, env = {} } = options; // Start Meteor tests console.log(`Starting Meteor tests on port ${port}...`); @@ -547,14 +555,15 @@ export async function runMeteorTests(tempDir, port, options = {}) { // Run the meteor test command const { meteorProcess, outputLines, processResult } = await runMeteorCommand( - 'test', - args, + 'test', + args, appDir, { execaOptions: { env: { ...process.env, - TEST_BROWSER_DRIVER: 'playwright' + ...(options.testClient ? { TEST_BROWSER_DRIVER: 'playwright' } : { TEST_CLIENT: 0 }), + ...env, } }, captureOutput, @@ -705,7 +714,7 @@ export async function waitForPlaywrightConsole(pattern, options = {}) { * @returns {Object} - The build output directory and the meteor process result */ export async function buildMeteorApp(tempDir, options = {}) { - const { isMonorepo = false } = options; + const { isMonorepo = false, env = {} } = options; // Create a unique temporary directory for the build output const randomSuffix = Math.random().toString(36).substring(2, 10); @@ -729,11 +738,11 @@ export async function buildMeteorApp(tempDir, options = {}) { // Run the meteor build command with automatic exit code checking const result = await runMeteorCommand( - 'build', - args, + 'build', + args, appDir, { - execaOptions: options.execaOptions || {}, + execaOptions: { ...(options.execaOptions || {}), env: { ...process.env, ...(options.execaOptions?.env || {}), ...env } }, captureOutput: options.captureOutput !== undefined ? options.captureOutput : true, checkExitCode: true // Automatically check exit code } diff --git a/tools/modern-tests/jest.config.js b/tools/e2e-tests/jest.config.js similarity index 100% rename from tools/modern-tests/jest.config.js rename to tools/e2e-tests/jest.config.js diff --git a/tools/modern-tests/jest.setup.js b/tools/e2e-tests/jest.setup.js similarity index 81% rename from tools/modern-tests/jest.setup.js rename to tools/e2e-tests/jest.setup.js index 1f0cf6e1fa..8b3f830a1f 100644 --- a/tools/modern-tests/jest.setup.js +++ b/tools/e2e-tests/jest.setup.js @@ -7,6 +7,9 @@ if (isCI) { console.log('Set 2 retries on Jest level'); } +// Clear NODE_ENV so meteor commands don't inherit any value from the test runner environment +process.env.NODE_ENV = ''; + // Set fixed ports for all tests process.env.RSPACK_DEVSERVER_PORT = '8080'; process.env.RSDOCTOR_CLIENT_PORT = '8888'; diff --git a/tools/modern-tests/monorepo.test.js b/tools/e2e-tests/monorepo.test.js similarity index 100% rename from tools/modern-tests/monorepo.test.js rename to tools/e2e-tests/monorepo.test.js diff --git a/tools/modern-tests/package-lock.json b/tools/e2e-tests/package-lock.json similarity index 99% rename from tools/modern-tests/package-lock.json rename to tools/e2e-tests/package-lock.json index eee71dcd5c..341c3bf556 100644 --- a/tools/modern-tests/package-lock.json +++ b/tools/e2e-tests/package-lock.json @@ -1,11 +1,11 @@ { - "name": "meteor-modern-tests", + "name": "meteor-e2e-tests", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "meteor-modern-tests", + "name": "meteor-e2e-tests", "version": "1.0.0", "devDependencies": { "@swc/core": "^1.15.18", @@ -15,7 +15,7 @@ "fs-extra": "^11.3.1", "jest": "^29.0.0", "jest-playwright-preset": "^3.0.1", - "playwright": "1.58.0", + "playwright": "^1.58.0", "semver": "^7.7.4", "underscore": "^1.13.8", "wait-on": "^7.0.0" diff --git a/tools/modern-tests/package.json b/tools/e2e-tests/package.json similarity index 88% rename from tools/modern-tests/package.json rename to tools/e2e-tests/package.json index 3a116c0bd9..1893ece2e4 100644 --- a/tools/modern-tests/package.json +++ b/tools/e2e-tests/package.json @@ -1,5 +1,5 @@ { - "name": "meteor-modern-tests", + "name": "meteor-e2e-tests", "version": "1.0.0", "description": "Isolated Jest + Playwright environment for Meteor E2E tests", "scripts": { @@ -13,7 +13,7 @@ "fs-extra": "^11.3.1", "jest": "^29.0.0", "jest-playwright-preset": "^3.0.1", - "playwright": "1.58.0", + "playwright": "^1.58.0", "semver": "^7.7.4", "underscore": "^1.13.8", "wait-on": "^7.0.0" diff --git a/tools/modern-tests/react-router.test.js b/tools/e2e-tests/react-router.test.js similarity index 100% rename from tools/modern-tests/react-router.test.js rename to tools/e2e-tests/react-router.test.js diff --git a/tools/modern-tests/react.test.js b/tools/e2e-tests/react.test.js similarity index 59% rename from tools/modern-tests/react.test.js rename to tools/e2e-tests/react.test.js index 896d587af3..f10ececa9c 100644 --- a/tools/modern-tests/react.test.js +++ b/tools/e2e-tests/react.test.js @@ -9,7 +9,7 @@ import { import { testMeteorBundler, testMeteorRspackBundler } from './test-helpers'; import fs from 'fs-extra'; import path from 'path'; -import { assertMeteorReactApp, assertConsoleEval } from "./assertions"; +import { assertMeteorReactApp, assertConsoleEval, assertFileExist } from "./assertions"; describe('React App Bundling /', () => { @@ -64,62 +64,104 @@ describe('React App Bundling /', () => { }); }); - describe('Meteor+Rspack Bundler /', testMeteorRspackBundler({ - appName: 'react', - port: 3102, - filePaths: { - client: 'client/main.jsx', - server: 'server/main.js', - test: 'tests/main.js' - }, - configFile: 'rspack.config.cjs', - customAssertions: { - afterRun: async ({ result }) => { - await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); - - // Check if images exist and return 200 status code - await assertImagesExistAndLoad(); - - // Check custom plugin is disabled with Meteor.disablePlugins - await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/, { negate: true }); + describe( + "Meteor+Rspack Bundler /", + testMeteorRspackBundler({ + appName: "react", + port: 3102, + filePaths: { + client: "client/main.jsx", + server: "server/main.js", + test: "tests/main.js", }, - afterRunRebuildClient: async ({ allConsoleLogs }) => { - // Check for HMR output as enabled by default - await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:.*/); - }, - afterRunProduction: async ({ result }) => { - await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + configFile: "rspack.config.cjs", + buildDir: "_build-local-custom", + env: { METEOR_LOCAL_DIR: ".meteor/local-custom" }, + customAssertions: { + afterRun: async ({ result, tempDir }) => { + const appDir = tempDir; // testMeteorRspackBundler uses tempDir as appDir if not monorepo - // Check if images exist and return 200 status code - await assertImagesExistAndLoad(); + const localDir = path.join(appDir, ".meteor", "local-custom"); + const buildDir = path.join(appDir, "_build-local-custom"); - // Check custom plugin is disabled with Meteor.disablePlugins - await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/, { negate: true }); - }, - afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { - // Check for HMR to not be enabled in production-like mode - await waitForMeteorOutput(allConsoleLogs, /.*HMR.*Updated modules:*/, { negate: true }); - }, - afterTest: async ({ result }) => { - await waitForReactEnvs(result.outputLines); + expect(await fs.pathExists(buildDir)).toBe(true); + expect(await fs.pathExists(localDir)).toBe(true); - // Check custom plugin is disabled with Meteor.disablePlugins - await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/, { negate: true }); - }, - afterTestOnce: async ({ result }) => { - await waitForReactEnvs(result.outputLines); + await assertFileExist(appDir, '.gitignore', { content: '.meteor/local-custom' }); - // Check custom plugin is disabled with Meteor.disablePlugins - await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/, { negate: true }); - }, - afterBuild: async ({ result }) => { - await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); - // Check custom plugin is disabled with Meteor.disablePlugins - await waitForMeteorOutput(result.outputLines, /.*CustomConsoleLogPlugin.*/, { negate: true }); + // Check if images exist and return 200 status code + await assertImagesExistAndLoad(); + + // Check custom plugin is disabled with Meteor.disablePlugins + await waitForMeteorOutput( + result.outputLines, + /.*CustomConsoleLogPlugin.*/, + { negate: true } + ); + }, + afterRunRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR output as enabled by default + await waitForMeteorOutput( + allConsoleLogs, + /.*HMR.*Updated modules:.*/ + ); + }, + afterRunProduction: async ({ result }) => { + await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + + // Check if images exist and return 200 status code + await assertImagesExistAndLoad(); + + // Check custom plugin is disabled with Meteor.disablePlugins + await waitForMeteorOutput( + result.outputLines, + /.*CustomConsoleLogPlugin.*/, + { negate: true } + ); + }, + afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { + // Check for HMR to not be enabled in production-like mode + await waitForMeteorOutput( + allConsoleLogs, + /.*HMR.*Updated modules:*/, + { negate: true } + ); + }, + afterTest: async ({ result }) => { + await waitForReactEnvs(result.outputLines); + + // Check custom plugin is disabled with Meteor.disablePlugins + await waitForMeteorOutput( + result.outputLines, + /.*CustomConsoleLogPlugin.*/, + { negate: true } + ); + }, + afterTestOnce: async ({ result }) => { + await waitForReactEnvs(result.outputLines); + + // Check custom plugin is disabled with Meteor.disablePlugins + await waitForMeteorOutput( + result.outputLines, + /.*CustomConsoleLogPlugin.*/, + { negate: true } + ); + }, + afterBuild: async ({ result }) => { + await waitForReactEnvs(result.outputLines, { isJsxEnabled: true }); + + // Check custom plugin is disabled with Meteor.disablePlugins + await waitForMeteorOutput( + result.outputLines, + /.*CustomConsoleLogPlugin.*/, + { negate: true } + ); + }, }, - } - })); + }) + ); }); /** diff --git a/tools/e2e-tests/scripts/create-app.js b/tools/e2e-tests/scripts/create-app.js new file mode 100644 index 0000000000..fc11504356 --- /dev/null +++ b/tools/e2e-tests/scripts/create-app.js @@ -0,0 +1,483 @@ +#!/usr/bin/env node + +/** + * Script to create a Meteor test app for manual testing without automatic cleanup. + * + * Sources apps from: + * - tools/e2e-tests/apps/ (use --app flag) + * - meteor create -- (use --skeleton flag) + * + * Usage: + * npm run create-app:e2e -- --app react + * npm run create-app:e2e -- --app react --output ./dist/my-react-app + * npm run create-app:e2e -- --app monorepo --monorepo + * npm run create-app:e2e -- --skeleton react + * npm run create-app:e2e -- --skeleton react --output ./my-apps/custom-name + */ + +const path = require('path'); +const fs = require('fs-extra'); +const execa = require('execa'); +const { linkLocalRspack } = require('./link-rspack'); + +const REPO_ROOT = path.resolve(__dirname, '../../..'); +const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor'); +const E2E_TESTS_DIR = path.join(__dirname, '..'); +const APPS_DIR = path.join(E2E_TESTS_DIR, 'apps'); +const DEFAULT_OUTPUT_DIR = path.join(REPO_ROOT, 'dist'); + +// ANSI color helpers +const c = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + red: '\x1b[31m', + magenta: '\x1b[35m', +}; +const log = { + step: (msg) => console.log(`${c.cyan}>${c.reset} ${msg}`), + success: (msg) => console.log(`${c.green}>${c.reset} ${msg}`), + info: (msg) => console.log(`${c.blue}>${c.reset} ${msg}`), + warn: (msg) => console.log(`${c.yellow}>${c.reset} ${msg}`), + error: (msg) => console.error(`${c.red}>${c.reset} ${msg}`), + detail: (msg) => console.log(` ${c.dim}${msg}${c.reset}`), +}; + +function parseArgs(argv) { + const args = { monorepo: false }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--app') { + args.app = argv[++i]; + } else if (argv[i] === '--skeleton') { + args.skeleton = argv[++i]; + } else if (argv[i] === '--output') { + args.output = argv[++i]; + } else if (argv[i] === '--monorepo') { + args.monorepo = true; + } else if (argv[i] === '--force' || argv[i] === '-f') { + args.force = true; + } else if (argv[i] === '--help' || argv[i] === '-h') { + args.help = true; + } + } + return args; +} + +function printHelp() { + const availableApps = fs.existsSync(APPS_DIR) + ? fs.readdirSync(APPS_DIR).join(', ') + : '(none found)'; + + console.log(` +${c.bold}Usage:${c.reset} npm run create-app:e2e -- [options] + +${c.bold}Options:${c.reset} + ${c.cyan}--app${c.reset} Copy an existing app from tools/e2e-tests/apps/ + ${c.cyan}--skeleton${c.reset} Create a new app via "meteor create --" + ${c.cyan}--output${c.reset} Full destination path for the app (default: ./dist/) + ${c.cyan}--monorepo${c.reset} Treat the app as a monorepo (runs npm install at both root and app/ levels) + ${c.cyan}--force${c.reset}, ${c.cyan}-f${c.reset} Remove the destination directory if it already exists before creating the app + ${c.cyan}--help${c.reset}, ${c.cyan}-h${c.reset} Show this help message + +${c.bold}Available apps:${c.reset} ${c.green}${availableApps}${c.reset} + +${c.bold}Examples:${c.reset} + ${c.dim}npm run create-app:e2e -- --app react${c.reset} + ${c.dim}npm run create-app:e2e -- --app react --output ./dist/my-react-app${c.reset} + ${c.dim}npm run create-app:e2e -- --app monorepo --monorepo${c.reset} + ${c.dim}npm run create-app:e2e -- --skeleton react${c.reset} + ${c.dim}npm run create-app:e2e -- --skeleton react --output ./my-apps/custom-name${c.reset} +`); +} + +/** + * Find a test-helper function call block (e.g., testMeteorSkeleton({ skeletonName: 'react', ... })) + * that contains a matching name key/value, and return the content of its options object. + */ +function findTestHelperBlock(content, fnName, nameKey, nameValue) { + let searchStart = 0; + + while (searchStart < content.length) { + const fnIdx = content.indexOf(fnName + '(', searchStart); + if (fnIdx === -1) return null; + + const braceIdx = content.indexOf('{', fnIdx + fnName.length); + if (braceIdx === -1) return null; + + // Find matching closing brace + let depth = 0; + let endIdx = -1; + for (let i = braceIdx; i < content.length; i++) { + if (content[i] === '{') depth++; + else if (content[i] === '}') { + depth--; + if (depth === 0) { + endIdx = i; + break; + } + } + } + + if (endIdx !== -1) { + const block = content.substring(braceIdx, endIdx + 1); + const namePattern = new RegExp(`${nameKey}:\\s*['"]${nameValue}['"]`); + if (namePattern.test(block)) { + return block; + } + } + + searchStart = endIdx !== -1 ? endIdx + 1 : fnIdx + 1; + } + + return null; +} + +/** + * Parse environment variable patterns from a code string. + * Matches: + * - process.env.KEY = 'value' (non-empty string values only) + * - env: { KEY: 'value', ... } + * Only includes envs that have an actual non-empty value. + */ +function parseEnvVars(code) { + const envVars = {}; + + // Pattern 1: process.env.KEY = 'value' or "value" (non-empty values only) + const processEnvRegex = /process\.env\.(\w+)\s*=\s*['"]([^'"]+)['"]/g; + let match; + while ((match = processEnvRegex.exec(code)) !== null) { + envVars[match[1]] = match[2]; + } + + // Pattern 2: env: { KEY: 'value', ... } + const envObjRegex = /\benv:\s*\{([^}]+)\}/g; + while ((match = envObjRegex.exec(code)) !== null) { + const envContent = match[1]; + const kvRegex = /(\w+)\s*:\s*['"]([^'"]+)['"]/g; + let kvMatch; + while ((kvMatch = kvRegex.exec(envContent)) !== null) { + envVars[kvMatch[1]] = kvMatch[2]; + } + } + + return envVars; +} + +/** + * Read the corresponding test file for an app or skeleton and extract + * environment variables that the tests set (so the manually created app + * behaves the same way). + * + * For --app : reads tools/e2e-tests/.test.js (whole file) + * For --skeleton : reads tools/e2e-tests/skeleton.test.js and + * scopes to the testMeteorSkeleton({ skeletonName: '' }) block. + */ +function extractEnvVarsFromTestFile(sourceName, isApp) { + const testFile = isApp + ? path.join(E2E_TESTS_DIR, `${sourceName}.test.js`) + : path.join(E2E_TESTS_DIR, 'skeleton.test.js'); + + if (!fs.existsSync(testFile)) return {}; + + const content = fs.readFileSync(testFile, 'utf8'); + let scope; + + if (isApp) { + scope = content; + } else { + scope = findTestHelperBlock(content, 'testMeteorSkeleton', 'skeletonName', sourceName); + if (!scope) return {}; + } + + return parseEnvVars(scope); +} + +/** + * Build a shell env prefix string from an env vars object. + * e.g., { METEOR_LOCAL_DIR: '.meteor/local-custom' } => "METEOR_LOCAL_DIR=.meteor/local-custom" + */ +function buildEnvPrefix(envVars) { + const entries = Object.entries(envVars); + if (entries.length === 0) return ''; + return entries.map(([key, value]) => `${key}=${value}`).join(' '); +} + +/** + * Replace bare `meteor` command occurrences in a script string with the full + * checkout path. Matches `meteor` only as a standalone command word: + * - not preceded by `/` or a word character (avoids already-resolved paths + * and things like `something-meteor`) + * - not followed by a word character (avoids `meteor-node-stubs` etc.) + */ +function rewriteMeteorCmd(scriptValue, meteorExecutable) { + return scriptValue.replace(/(? { + const inv = n === 'start' ? 'npm start' : `npm run ${n}`; + return Math.max(max, inv.length); + }, 0); + + const line = `${c.dim}${'─'.repeat(55)}${c.reset}`; + + console.log(''); + console.log(line); + console.log(` ${c.green}${c.bold}App ready at:${c.reset} ${c.cyan}${destDir}${c.reset}`); + console.log(line); + console.log(''); + console.log(` ${c.yellow}cd ${destDir}${c.reset}`); + console.log(''); + console.log(` ${c.bold}Run commands${c.reset} ${c.dim}(meteor checkout binary):${c.reset}`); + console.log(` ${c.dim}${m} run${c.reset}`); + console.log(` ${c.dim}${m} run --production${c.reset}`); + + if (hasTestModule) { + console.log(` ${c.dim}${m} test --driver-package meteortesting:mocha${c.reset}`); + console.log(` ${c.dim}${m} test --once --driver-package meteortesting:mocha${c.reset}`); + if (hasClient) { + console.log(` ${c.dim}${m} test --full-app --driver-package meteortesting:mocha${c.reset}`); + console.log(` ${c.dim}${m} test --full-app --once --driver-package meteortesting:mocha${c.reset}`); + } + } + + console.log(` ${c.dim}${m} build ./_build --directory${c.reset}`); + console.log(''); + console.log(` ${c.bold}npm scripts${c.reset} ${c.dim}(run from the app directory):${c.reset}`); + + for (const [name, cmd] of Object.entries(scripts)) { + const invocation = name === 'start' ? 'npm start' : `npm run ${name}`; + const padding = ' '.repeat(maxLen - invocation.length); + console.log(` ${c.cyan}${invocation}${c.reset}${padding} ${c.dim}# ${cmd}${c.reset}`); + } + + console.log(line); + console.log(''); +} + +async function setupFromApp(appName, destDir, { isMonorepo = false, force = false } = {}) { + const sourceDir = path.join(APPS_DIR, appName); + + if (!fs.existsSync(sourceDir)) { + const available = fs.readdirSync(APPS_DIR).join(', '); + throw new Error( + `App '${appName}' not found in tools/e2e-tests/apps/\nAvailable apps: ${available}` + ); + } + + if (fs.existsSync(destDir)) { + if (force) { + log.warn(`Removing existing destination: ${c.cyan}${destDir}${c.reset}`); + await fs.remove(destDir); + } else { + log.error(`Destination already exists: ${c.cyan}${destDir}${c.reset}`); + log.detail('Remove it first or use --force to replace it.'); + process.exit(1); + } + } + + await fs.ensureDir(path.dirname(destDir)); + + log.step(`Copying app ${c.bold}${appName}${c.reset} to ${c.cyan}${destDir}${c.reset}`); + await fs.copy(sourceDir, destDir, { + dereference: true, + preserveTimestamps: true, + overwrite: true, + }); + log.success('Copy complete.'); + + const appPackageJsonPath = isMonorepo + ? path.join(destDir, 'app', 'package.json') + : path.join(destDir, 'package.json'); + + const envVars = extractEnvVarsFromTestFile(appName, true); + if (Object.keys(envVars).length > 0) { + log.detail(`env from test file: ${c.magenta}${Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join(' ')}${c.reset}`); + } + + const meteorAppDir = isMonorepo ? path.join(destDir, 'app') : destDir; + const execEnv = Object.keys(envVars).length > 0 ? { env: { ...process.env, ...envVars } } : {}; + + log.step('Adding rspack package...'); + await execa(METEOR_EXECUTABLE, ['add', 'rspack'], { + cwd: meteorAppDir, + stdio: 'inherit', + ...execEnv, + }); + + log.step('Linking local @meteorjs/rspack...'); + await linkLocalRspack(meteorAppDir, { env: envVars }); + + if (isMonorepo) { + log.step('Running meteor npm install at root level...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { cwd: destDir, stdio: 'inherit', ...execEnv }); + log.step('Running meteor npm install at app level...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { + cwd: path.join(destDir, 'app'), + stdio: 'inherit', + ...execEnv, + }); + } else { + log.step('Running meteor npm install...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { cwd: destDir, stdio: 'inherit', ...execEnv }); + } + + if (fs.existsSync(appPackageJsonPath)) { + log.step('Injecting npm scripts into package.json...'); + await injectNpmScripts(appPackageJsonPath, envVars); + } + + return { destDir, appPackageJsonPath }; +} + +async function setupFromSkeleton(skeletonName, destDir, { force = false } = {}) { + if (fs.existsSync(destDir)) { + if (force) { + log.warn(`Removing existing destination: ${c.cyan}${destDir}${c.reset}`); + await fs.remove(destDir); + } else { + log.error(`Destination already exists: ${c.cyan}${destDir}${c.reset}`); + log.detail('Remove it first or use --force to replace it.'); + process.exit(1); + } + } + + const parentDir = path.dirname(destDir); + const appDirName = path.basename(destDir); + + await fs.ensureDir(parentDir); + + log.step(`Creating Meteor app ${c.bold}${appDirName}${c.reset} via ${c.dim}meteor create --${skeletonName}${c.reset}`); + await execa(METEOR_EXECUTABLE, ['create', `--${skeletonName}`, appDirName], { + cwd: parentDir, + stdio: 'inherit', + }); + + const appPackageJsonPath = path.join(destDir, 'package.json'); + + const envVars = extractEnvVarsFromTestFile(skeletonName, false); + if (Object.keys(envVars).length > 0) { + log.detail(`env from test file: ${c.magenta}${Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join(' ')}${c.reset}`); + } + const execEnv = Object.keys(envVars).length > 0 ? { env: { ...process.env, ...envVars } } : {}; + + log.step('Adding rspack package...'); + await execa(METEOR_EXECUTABLE, ['add', 'rspack'], { + cwd: destDir, + stdio: 'inherit', + ...execEnv, + }); + + log.step('Linking local @meteorjs/rspack...'); + await linkLocalRspack(destDir, { env: envVars }); + + log.step('Running meteor npm install...'); + await execa(METEOR_EXECUTABLE, ['npm', 'install'], { cwd: destDir, stdio: 'inherit', ...execEnv }); + + if (fs.existsSync(appPackageJsonPath)) { + log.step('Injecting npm scripts into package.json...'); + await injectNpmScripts(appPackageJsonPath, envVars); + } + + return { destDir, appPackageJsonPath }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printHelp(); + process.exit(0); + } + + if (!args.app && !args.skeleton) { + log.error('You must provide --app or --skeleton '); + printHelp(); + process.exit(1); + } + + if (args.app && args.skeleton) { + log.error('--app and --skeleton are mutually exclusive'); + process.exit(1); + } + + const sourceName = args.app || args.skeleton; + + // --output is the full destination path; if omitted, default to ./dist/ + const destDir = args.output + ? path.resolve(REPO_ROOT, args.output) + : path.join(DEFAULT_OUTPUT_DIR, sourceName); + + let result; + if (args.app) { + result = await setupFromApp(args.app, destDir, { isMonorepo: args.monorepo, force: args.force }); + } else { + result = await setupFromSkeleton(args.skeleton, destDir, { force: args.force }); + } + + printCommandSummary(result.destDir, result.appPackageJsonPath); +} + +main().catch(err => { + log.error(err.message); + process.exit(1); +}); diff --git a/tools/e2e-tests/scripts/link-rspack.js b/tools/e2e-tests/scripts/link-rspack.js new file mode 100644 index 0000000000..5e8b6bbb97 --- /dev/null +++ b/tools/e2e-tests/scripts/link-rspack.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +/** + * Links the local npm-packages/meteor-rspack into a Meteor app so it runs + * against the latest dev version. + * + * Steps: + * 1. Run `meteor update --npm` in the app + * 2. Install the matching @rspack/core and @rspack/cli versions into the + * local meteor-rspack package (read from packages/rspack/lib/constants.js) + * 3. Install `ignore-loader` in the app + * 4. `npm link` the local meteor-rspack into the app + * + */ + +const path = require('path'); +const fs = require('fs'); +const execa = require('execa'); + +const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); +const METEOR_EXECUTABLE = path.join(REPO_ROOT, 'meteor'); +const RSPACK_PACKAGE_DIR = path.join(REPO_ROOT, 'npm-packages', 'meteor-rspack'); +const CONSTANTS_PATH = path.join(REPO_ROOT, 'packages', 'rspack', 'lib', 'constants.js'); + +async function linkLocalRspack(appDir, { env } = {}) { + const execOpts = env ? { env: { ...process.env, ...env } } : {}; + + console.log(`Running meteor update --npm in ${appDir}...`); + await execa(METEOR_EXECUTABLE, ['update', '--npm'], { + cwd: appDir, + stdio: 'inherit', + ...execOpts, + }); + + const constantsContent = fs.readFileSync(CONSTANTS_PATH, 'utf8'); + const rspackVersionMatch = constantsContent.match( + /DEFAULT_RSPACK_VERSION\s*=\s*['"]([^'"]+)['"]/ + ); + const rspackVersion = rspackVersionMatch?.[1]; + if (rspackVersion) { + console.log(`Installing @rspack/core@${rspackVersion} and @rspack/cli@${rspackVersion}...`); + await execa( + 'npm', + [ + 'install', + `@rspack/core@${rspackVersion}`, + `@rspack/cli@${rspackVersion}`, + '--no-save', + '--no-package-lock', + ], + { cwd: RSPACK_PACKAGE_DIR } + ); + } + + console.log('Installing ignore-loader in the app...'); + await execa('npm', ['install', 'ignore-loader', '--save'], { cwd: appDir }); + + console.log(`Linking local meteor-rspack from ${RSPACK_PACKAGE_DIR}...`); + await execa('npm', ['link', RSPACK_PACKAGE_DIR], { cwd: appDir }); + + console.log('Local meteor-rspack linked successfully.'); +} + +module.exports = { linkLocalRspack, REPO_ROOT, METEOR_EXECUTABLE, RSPACK_PACKAGE_DIR }; + +// CLI mode +if (require.main === module) { + const appDir = process.argv[2]; + if (!appDir) { + console.error('Usage: node link-rspack.js '); + process.exit(1); + } + linkLocalRspack(path.resolve(appDir)).catch(err => { + console.error(err.message); + process.exit(1); + }); +} diff --git a/tools/e2e-tests/server-only.test.js b/tools/e2e-tests/server-only.test.js new file mode 100644 index 0000000000..53eca14d53 --- /dev/null +++ b/tools/e2e-tests/server-only.test.js @@ -0,0 +1,27 @@ +import { + waitForMeteorOutput, +} from './helpers'; +import { testMeteorRspackBundler } from './test-helpers'; + +describe('Other / Server-only App Bundling /', () => { + describe( + 'Meteor+Rspack Bundler /', + testMeteorRspackBundler({ + appName: 'server-only', + port: 3123, + skipClient: true, + skipTestClient: true, + filePaths: { + server: 'server/main.js', + }, + customAssertions: { + afterRun: async ({ result }) => { + await waitForMeteorOutput( + result.outputLines, + 'server/main.js loaded' + ); + }, + }, + }) + ); +}); diff --git a/tools/modern-tests/skeleton.test.js b/tools/e2e-tests/skeleton.test.js similarity index 91% rename from tools/modern-tests/skeleton.test.js rename to tools/e2e-tests/skeleton.test.js index 3306c1d1aa..1556a00c51 100644 --- a/tools/modern-tests/skeleton.test.js +++ b/tools/e2e-tests/skeleton.test.js @@ -52,6 +52,18 @@ describe('Meteor Skeletons /', () => { }), ); + describe( + "Other / Bare Skeleton /", + testMeteorSkeleton({ + skeletonName: "bare", + port: 3219, + checkAppTitle: false, + checkBodyStyles: false, + skipTestClient: true, + skipBuildCacheCheck: true, + }) + ); + describe( 'Blaze Skeleton /', testMeteorSkeleton({ @@ -93,7 +105,7 @@ describe('Meteor Skeletons /', () => { ); describe( - 'Full Library Skeleton /', + 'Other / Full Skeleton /', testMeteorSkeleton({ skeletonName: 'full', port: 3204, @@ -102,7 +114,7 @@ describe('Meteor Skeletons /', () => { server: 'server/main.js', test: 'imports/api/links/methods.tests.js', }, - }), + }) ); describe( @@ -151,7 +163,7 @@ describe('Meteor Skeletons /', () => { ); describe( - 'Tailwind Library Skeleton /', + 'Other / Tailwind Skeleton /', testMeteorSkeleton({ skeletonName: 'tailwind', port: 3208, @@ -162,19 +174,19 @@ describe('Meteor Skeletons /', () => { }, customAssertions: { afterRun: async () => { - // Verify Tailwind styles for ".bg-gray-100" element + // Verify Tailwind styles for '.bg-gray-100' element await assertStyles('.bg-gray-100', { ['background-color']: 'oklch(0.967 0.003 264.542)', }); }, afterRunProduction: async () => { - // Verify Tailwind styles for ".bg-gray-100" element + // Verify Tailwind styles for '.bg-gray-100' element await assertStyles('.bg-gray-100', { ['background-color']: 'lab(96.1596 -0.0823438 -1.13575)', }); }, }, - }), + }) ); describe( diff --git a/tools/modern-tests/solid.test.js b/tools/e2e-tests/solid.test.js similarity index 100% rename from tools/modern-tests/solid.test.js rename to tools/e2e-tests/solid.test.js diff --git a/tools/modern-tests/svelte.test.js b/tools/e2e-tests/svelte.test.js similarity index 100% rename from tools/modern-tests/svelte.test.js rename to tools/e2e-tests/svelte.test.js diff --git a/tools/modern-tests/test-helpers.js b/tools/e2e-tests/test-helpers.js similarity index 67% rename from tools/modern-tests/test-helpers.js rename to tools/e2e-tests/test-helpers.js index 598112eb42..52c11201f3 100644 --- a/tools/modern-tests/test-helpers.js +++ b/tools/e2e-tests/test-helpers.js @@ -23,6 +23,7 @@ import { assertFileExist, assertMeteorApp, assertMeteorReactApp, + assertPathNotExist, assertRspackScriptTag } from "./assertions"; import fs from "fs-extra"; @@ -31,9 +32,25 @@ import execa from "execa"; import waitOn from "wait-on"; const isCI = process.env.GITHUB_ACTIONS === "true"; +// Link local npm-packages/meteor-rspack so tests run against the latest dev version. +// Set NPM_LINK_RSPACK=false to disable. +const npmLinkLocalRspack = process.env.NPM_LINK_RSPACK !== 'false'; +if (!npmLinkLocalRspack) { + console.warn( + '\x1b[33m⚠ NPM_LINK_RSPACK=false — tests will install @meteorjs/rspack from npm.\n' + + ' If CI fails, ensure the latest @meteorjs/rspack version has been published.\x1b[0m' + ); +} const WAIT_ON = isCI ? 2000 : 500; +const { linkLocalRspack: _linkLocalRspack } = require('./scripts/link-rspack'); + +async function linkLocalRspack(appDir) { + if (!npmLinkLocalRspack) return; + await _linkLocalRspack(appDir); +} + /** * Helper function to set up and run tests for the Meteor Bundler * @param {Object} options - Options for the test @@ -45,7 +62,7 @@ const WAIT_ON = isCI ? 2000 : 500; * @returns {Function} - Jest test function */ export function testMeteorBundler(options) { - const { appName, port, customAssertions, beforeAllBehavior, afterAllBehavior } = options; + const { appName, port, customAssertions, beforeAllBehavior, afterAllBehavior, env = {} } = options; return () => { let meteorProcess; @@ -62,6 +79,9 @@ export function testMeteorBundler(options) { // Setup the Meteor app tempDir = (await setupMeteorApp(appName))?.tempDir; + + // Link local meteor-rspack so the app picks up the latest dev version + await linkLocalRspack(tempDir); }); afterAll(async () => { @@ -81,7 +101,7 @@ export function testMeteorBundler(options) { test(`"meteor run" / should start the app`, async () => { // Run the Meteor app - meteorProcess = (await runMeteorApp(tempDir, port))?.meteorProcess; + meteorProcess = (await runMeteorApp(tempDir, port, { env: env.meteorRun }))?.meteorProcess; // Assert that the Meteor app is running correctly await assertMeteorReactApp(port, { title: appName }); @@ -119,6 +139,8 @@ export function testMeteorBundler(options) { * @param {boolean} options.verbose - Whether to enable verbose output (default: true) * @param {boolean} options.testFullApp - Whether to run tests with the --full-app flag (default: false) * @param {boolean} options.testBundleVisualizer - Whether to run tests with bundle-visualizer in production mode (default: false) + * @param {boolean} options.skipClient - Whether to skip client-specific assertions (default: false) + * @param {boolean} options.skipTestClient - Whether to skip client-side tests (default: false) * @param {string[]} options.checkBundleFilePaths - Array of file paths to check for existence in the bundle * @param {Function} options.beforeAllBehavior - Additional behavior to run in beforeAll * @param {Function} options.afterAllBehavior - Additional behavior to run in afterAll @@ -166,8 +188,21 @@ export function testMeteorRspackBundler(options) { afterAllBehavior, // Build directory (default: '_build') buildDir = '_build', + // Assets context directory (default: 'build-assets') + assetsContext = 'build-assets', + // Chunks context directory (default: 'build-chunks') + chunksContext = 'build-chunks', // Rspack config file (default: 'rspack.config.js') configFile = 'rspack.config.js', + // Custom environment variables + // Per-phase env vars: { meteorRun, meteorRunProduction, meteorTest, meteorTestOnce, meteorBuild } + env = {}, + // Whether to skip client-specific assertions + skipClient = false, + // Whether to skip client-side tests + skipTestClient = false, + // Skip isDevelopment/isProduction/isRun/isTest/isBuild verbose output checks + skipEnvCheck = false, } = options; return () => { @@ -197,7 +232,13 @@ export function testMeteorRspackBundler(options) { // Add Rspack package appDir = isMonorepo ? path.join(tempDir, 'app') : tempDir; - await runMeteorCommand('add', ['rspack'], appDir, { checkExitCode: true }); + + await runMeteorCommand("add", ["rspack"], appDir, { + checkExitCode: true, + }); + + // Link local meteor-rspack so the app picks up the latest dev version + await linkLocalRspack(appDir); // Set meteor.modern.verbose to true if (verbose) { @@ -207,8 +248,9 @@ export function testMeteorRspackBundler(options) { // Run the Meteor app to install Rspack const result = await runMeteorApp(tempDir, port, { - waitForOutput: "=> App running at:", - isMonorepo + waitForOutput: "=> App running at", + isMonorepo, + env: { ...env, ...(env.meteorRun || {}) }, }); meteorProcess = result.meteorProcess; @@ -249,8 +291,10 @@ export function testMeteorRspackBundler(options) { test(`"meteor run" / should run and rebuild the app with Rspack`, async () => { // Run the Meteor app and wait for "restarted at" output const result = await runMeteorApp(tempDir, port, { - waitForOutput: "=> App running at:", - isMonorepo + waitForOutput: "=> App running at", + isMonorepo, + skipWaitOn: skipClient, + env: { ...env, ...(env.meteorRun || {}) }, }); meteorProcess = result.meteorProcess; @@ -258,24 +302,28 @@ export function testMeteorRspackBundler(options) { await wait(WAIT_ON); // Assert that the app files exists - await assertFileExist(appDir, `${buildDir}/main-dev/client-entry.js`); - await assertFileExist(appDir, `${buildDir}/main-dev/client-rspack.js`); - await assertFileExist(appDir, `${buildDir}/main-dev/client-meteor.js`); + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/main-dev/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-dev/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-dev/client-meteor.js`); + } await assertFileExist(appDir, `${buildDir}/main-dev/server-entry.js`); await assertFileExist(appDir, `${buildDir}/main-dev/server-rspack.js`); await assertFileExist(appDir, `${buildDir}/main-dev/server-meteor.js`); - // Assert that the Meteor app is running correctly - await assertMeteorReactApp(port, { title: appName }); + if (!skipClient) { + // Assert that the Meteor app is running correctly + await assertMeteorReactApp(port, { title: appName }); - // Assert that the app is using Rspack - await assertRspackScriptTag(port, true); + // Assert that the app is using Rspack + await assertRspackScriptTag(port, true); - // Assert that the body has the expected CSS styles - await assertBodyStyles({ - 'padding': '10px', - 'font-family': 'sans-serif' - }); + // Assert that the body has the expected CSS styles + await assertBodyStyles({ + 'padding': '10px', + 'font-family': 'sans-serif' + }); + } // Run custom assertions if provided if (customAssertions && customAssertions.afterRun) { @@ -283,20 +331,22 @@ export function testMeteorRspackBundler(options) { } // Update the client code - await appendFileContent(tempDir, filePaths.client, { - content: customUpdates.devClient(customMessages.devClient), - }); - const consoleLogs = await waitForPlaywrightConsole(customMessages.devClient, { returnAllLogs: true }); - - // Run custom assertions if provided - if (customAssertions && customAssertions.afterRunRebuildClient) { - await customAssertions.afterRunRebuildClient({ - tempDir, - port, - meteorProcess, - result, - allConsoleLogs: consoleLogs.allLogs + if (!skipClient) { + await appendFileContent(tempDir, filePaths.client, { + content: customUpdates.devClient(customMessages.devClient), }); + const consoleLogs = await waitForPlaywrightConsole(customMessages.devClient, { returnAllLogs: true }); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunRebuildClient) { + await customAssertions.afterRunRebuildClient({ + tempDir, + port, + meteorProcess, + result, + allConsoleLogs: consoleLogs.allLogs + }); + } } // Update the server code @@ -313,7 +363,7 @@ export function testMeteorRspackBundler(options) { await customAssertions.afterRunRebuildServer({ tempDir, port, meteorProcess, result }); } - if (verbose) { + if (verbose && !skipEnvCheck) { await waitForMeteorOutput( result.outputLines, /.*isDevelopment:.*true.*/ @@ -337,9 +387,11 @@ export function testMeteorRspackBundler(options) { test(`"meteor run --production" / should run and rebuild the app with Rspack in production`, async () => { // Run the Meteor app and wait for "restarted at" output const result = await runMeteorApp(tempDir, port, { - waitForOutput: "=> App running at:", + waitForOutput: "=> App running at", commandOptions: ['--production'], - isMonorepo + isMonorepo, + skipWaitOn: skipClient, + env: { ...env, ...(env.meteorRunProduction || {}) }, }); meteorProcess = result.meteorProcess; @@ -347,27 +399,34 @@ export function testMeteorRspackBundler(options) { await wait(WAIT_ON); // Assert that the app files exists - await assertFileExist(appDir, `${buildDir}/main-prod/client-entry.js`); - await assertFileExist(appDir, `${buildDir}/main-prod/client-rspack.js`); - await assertFileExist(appDir, `${buildDir}/main-prod/client-meteor.js`); + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/main-prod/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/main-prod/client-meteor.js`); + } await assertFileExist(appDir, `${buildDir}/main-prod/server-entry.js`); await assertFileExist(appDir, `${buildDir}/main-prod/server-rspack.js`); await assertFileExist(appDir, `${buildDir}/main-prod/server-meteor.js`); - await assertFileExist(appDir, `${buildDir}/main-prod/index.html`); + + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/main-prod/index.html`); + } await assertFileExist(tempDir, filePaths.server); - // Assert that the Meteor app is running correctly - await assertMeteorReactApp(port, { title: appName }); + if (!skipClient) { + // Assert that the Meteor app is running correctly + await assertMeteorReactApp(port, { title: appName }); - // Assert that the app is using Rspack - await assertRspackScriptTag(port, false); + // Assert that the app is using Rspack + await assertRspackScriptTag(port, false); - // Assert that the body has the expected CSS styles - await assertBodyStyles({ - 'padding': '10px', - 'font-family': 'sans-serif' - }); + // Assert that the body has the expected CSS styles + await assertBodyStyles({ + 'padding': '10px', + 'font-family': 'sans-serif' + }); + } // Run custom assertions if provided if (customAssertions && customAssertions.afterRunProduction) { @@ -375,20 +434,22 @@ export function testMeteorRspackBundler(options) { } // Update the client code - await appendFileContent(tempDir, filePaths.client, { - content: customUpdates.prodClient(customMessages.prodClient), - }); - const consoleLogs = await waitForPlaywrightConsole(customMessages.prodClient, { returnAllLogs: true }); - - // Run custom assertions if provided - if (customAssertions && customAssertions.afterRunProductionRebuildClient) { - await customAssertions.afterRunProductionRebuildClient({ - tempDir, - port, - meteorProcess, - result, - allConsoleLogs: consoleLogs.allLogs + if (!skipClient) { + await appendFileContent(tempDir, filePaths.client, { + content: customUpdates.prodClient(customMessages.prodClient), }); + const consoleLogs = await waitForPlaywrightConsole(customMessages.prodClient, { returnAllLogs: true }); + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterRunProductionRebuildClient) { + await customAssertions.afterRunProductionRebuildClient({ + tempDir, + port, + meteorProcess, + result, + allConsoleLogs: consoleLogs.allLogs + }); + } } // Update the server code @@ -405,7 +466,7 @@ export function testMeteorRspackBundler(options) { await customAssertions.afterRunProductionRebuildServer({ tempDir, port, meteorProcess, result }); } - if (verbose) { + if (verbose && !skipEnvCheck) { await waitForMeteorOutput( result.outputLines, /.*isProduction:.*true.*/ @@ -431,9 +492,10 @@ export function testMeteorRspackBundler(options) { test(`"meteor run --extra-packages bundle-visualizer --production" / should run with bundle-visualizer in production mode`, async () => { // Run the Meteor app with bundle-visualizer in production mode const result = await runMeteorApp(tempDir, port, { - waitForOutput: "=> App running at:", + waitForOutput: "=> App running at", commandOptions: ['--extra-packages', 'bundle-visualizer', '--production'], - isMonorepo + isMonorepo, + env: env.meteorRunProduction }); meteorProcess = result.meteorProcess; @@ -486,10 +548,14 @@ export function testMeteorRspackBundler(options) { test(`"meteor test${testFullApp ? ' --full-app' : ''}" / should run tests with Rspack`, async () => { const result = await runMeteorTests(tempDir, port, { - waitForOutput: "=> App running at:", + waitForOutput: skipTestClient + ? 'TEST_CLIENT=0' + : '=> App running at', commandOptions: testFullApp ? ['--full-app'] : [], checkTestResults: false, - isMonorepo + isMonorepo, + testClient: !skipTestClient, + env: { ...env, ...(env.meteorTest || {}) }, }); meteorProcess = result.meteorProcess; @@ -499,9 +565,11 @@ export function testMeteorRspackBundler(options) { const isTestModule = filePaths.test && !filePaths.testClient && !filePaths.testServer; // Assert that the app files exists - await assertFileExist(appDir, `${buildDir}/test/client-entry.js`); - await assertFileExist(appDir, `${buildDir}/test/client-rspack.js`); - await assertFileExist(appDir, `${buildDir}/test/client-meteor.js`); + if (!skipClient) { + await assertFileExist(appDir, `${buildDir}/test/client-entry.js`); + await assertFileExist(appDir, `${buildDir}/test/client-rspack.js`); + await assertFileExist(appDir, `${buildDir}/test/client-meteor.js`); + } await assertFileExist(appDir, `${buildDir}/test/server-entry.js`); await assertFileExist(appDir, `${buildDir}/test/server-rspack.js`); await assertFileExist(appDir, `${buildDir}/test/server-meteor.js`); @@ -513,29 +581,35 @@ export function testMeteorRspackBundler(options) { // Update the test code if (isTestModule) { - await appendFileContent(tempDir, filePaths.test, { - content: customUpdates.test(customMessages.test), - }); - await waitForMeteorOutput(result.outputLines, customMessages.test); + if (filePaths.test) { + await appendFileContent(tempDir, filePaths.test, { + content: customUpdates.test(customMessages.test), + }); + await waitForMeteorOutput(result.outputLines, customMessages.test); + } } else { - await appendFileContent(tempDir, filePaths.testClient, { - content: customUpdates.test(customMessages.testClient), - }); - await waitForMeteorOutput( - result.outputLines, - customMessages.testClient - ); + if (!skipClient && filePaths.testClient) { + await appendFileContent(tempDir, filePaths.testClient, { + content: customUpdates.test(customMessages.testClient), + }); + await waitForMeteorOutput( + result.outputLines, + customMessages.testClient + ); + } - await appendFileContent(tempDir, filePaths.testServer, { - content: customUpdates.test(customMessages.testServer), - }); - await waitForMeteorOutput( - result.outputLines, - customMessages.testServer - ); + if (filePaths.testServer) { + await appendFileContent(tempDir, filePaths.testServer, { + content: customUpdates.test(customMessages.testServer), + }); + await waitForMeteorOutput( + result.outputLines, + customMessages.testServer + ); + } } - if (verbose) { + if (verbose && !skipEnvCheck) { await waitForMeteorOutput( result.outputLines, /.*isDevelopment:.*true.*/ @@ -561,10 +635,14 @@ export function testMeteorRspackBundler(options) { test(`"meteor test${testFullApp ? ' --full-app' : ''} --once" / should run tests once with Rspack`, async () => { // Test the app with Rspack once const result = await runMeteorTests(tempDir, port, { - waitForOutput: "=> App running at:", + waitForOutput: skipTestClient + ? 'TEST_CLIENT=0' + : '=> App running at', commandOptions: testFullApp ? ['--full-app', '--once'] : ['--once'], checkTestResults: true, - isMonorepo + isMonorepo, + testClient: !skipTestClient, + env: { ...env, ...(env.meteorTestOnce || {}) }, }); // Wait for a margin @@ -578,7 +656,7 @@ export function testMeteorRspackBundler(options) { await assertFileExist(appDir, `${buildDir}/test/server-rspack.js`); await assertFileExist(appDir, `${buildDir}/test/server-meteor.js`); - if (verbose) { + if (verbose && !skipEnvCheck) { await waitForMeteorOutput( result.outputLines, /.*isDevelopment:.*true.*/ @@ -603,13 +681,14 @@ export function testMeteorRspackBundler(options) { const { buildOutputDir, processResult: result } = await buildMeteorApp(tempDir, { commandOptions: ['--directory'], captureOutput: true, - isMonorepo + isMonorepo, + env: env.meteorBuild }); // Wait for a margin await wait(WAIT_ON); - if (verbose) { + if (verbose && !skipEnvCheck) { await waitForMeteorOutput( result.outputLines, /.*isProduction:.*true.*/ @@ -678,6 +757,68 @@ export function testMeteorRspackBundler(options) { await cleanupTempDir(buildOutputDir); } }); + + test(`"meteor reset" / should clear all caches and build artifacts`, async () => { + // Derive METEOR_LOCAL_DIR-aware paths for assertions + const resetEnv = { ...env, ...(env.meteorReset || {}) }; + const meteorLocalDirEnv = resetEnv.METEOR_LOCAL_DIR; + const meteorLocalDirName = meteorLocalDirEnv + ? path.basename(meteorLocalDirEnv.replace(/\\/g, '/')) + : ''; + const localDirSuffix = meteorLocalDirName ? `-${meteorLocalDirName}` : ''; + + // Verify build artifacts exist from previous tests + await assertFileExist(appDir, buildDir); + await assertFileExist(appDir, 'node_modules/.cache/rspack'); + + // Run meteor reset + await runMeteorCommand("reset", [], appDir, { + checkExitCode: true, + env: resetEnv, + }); + + // Verify Rspack build artifacts removed + await assertPathNotExist(appDir, buildDir); + await assertPathNotExist(appDir, 'node_modules/.cache/rspack'); + await assertPathNotExist(appDir, assetsContext); + await assertPathNotExist(appDir, chunksContext); + await assertPathNotExist(appDir, `public/${assetsContext}`); + await assertPathNotExist(appDir, `public/${chunksContext}`); + + // Also verify defaults are cleaned to prevent regressions + await assertPathNotExist(appDir, '_build'); + await assertPathNotExist(appDir, 'public/build-assets'); + await assertPathNotExist(appDir, 'public/build-chunks'); + + // When METEOR_LOCAL_DIR is set, also verify suffixed paths are cleaned + if (localDirSuffix) { + await assertPathNotExist(appDir, `${buildDir}${localDirSuffix}`); + await assertPathNotExist(appDir, `${assetsContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `${chunksContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `public/${assetsContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `public/${chunksContext}${localDirSuffix}`); + await assertPathNotExist(appDir, `_build${localDirSuffix}`); + await assertPathNotExist(appDir, `public/build-assets${localDirSuffix}`); + await assertPathNotExist(appDir, `public/build-chunks${localDirSuffix}`); + } + + // Verify default .meteor/local caches are always cleaned + await assertPathNotExist(appDir, '.meteor/local/build'); + await assertPathNotExist(appDir, '.meteor/local/bundler-cache'); + await assertPathNotExist(appDir, '.meteor/local/plugin-cache'); + + // When METEOR_LOCAL_DIR is set, also verify custom local dir is cleaned + if (meteorLocalDirEnv && meteorLocalDirEnv !== '.meteor/local') { + await assertPathNotExist(appDir, `${meteorLocalDirEnv}/build`); + await assertPathNotExist(appDir, `${meteorLocalDirEnv}/bundler-cache`); + await assertPathNotExist(appDir, `${meteorLocalDirEnv}/plugin-cache`); + } + + // Run custom assertions if provided + if (customAssertions && customAssertions.afterReset) { + await customAssertions.afterReset({ tempDir, appDir }); + } + }); }; } @@ -697,6 +838,10 @@ export function testMeteorRspackBundler(options) { * @param {Function} options.customAssertions.afterRunProduction - Custom assertions to run after running the app in production mode * @param {Function} options.customAssertions.afterTestOnce - Custom assertions to run after running tests once * @param {Function} options.customAssertions.afterBuild - Custom assertions to run after building the app + * @param {boolean} options.checkBodyStyles - Whether to check the body styles (default: true) + * @param {boolean} options.checkAppTitle - Whether to check the Meteor app title (default: true) + * @param {Object} options.bodyStyles - Expected CSS styles for the body + * @param {boolean} options.skipTestClient - Whether to skip client-side tests (default: false) * @param {string[]} options.checkBundleFilePaths - Array of file paths to check for existence in the bundle * @param {Function} options.beforeAllBehavior - Additional behavior to run in beforeAll * @param {Function} options.afterAllBehavior - Additional behavior to run in afterAll @@ -714,10 +859,20 @@ export function testMeteorSkeleton(options) { }, customAssertions = {}, checkBodyStyles = true, + checkAppTitle = true, bodyStyles, + skipTestClient = false, checkBundleFilePaths = [], beforeAllBehavior, afterAllBehavior, + // Per-phase env vars: { meteorRun, meteorRunProduction, meteorTest, meteorBuild } + env = {}, + // Bare skeleton may not create build artifacts (e.g. _build, node_modules/.cache/rspack) + skipBuildCacheCheck = false, + // Assets context directory (default: 'build-assets') + assetsContext = 'build-assets', + // Chunks context directory (default: 'build-chunks') + chunksContext = 'build-chunks', } = options; return () => { @@ -769,6 +924,9 @@ export function testMeteorSkeleton(options) { const packageJsonExists = await fs.pathExists(packageJsonPath); expect(packageJsonExists).toBe(true); + // Link local meteor-rspack so the app picks up the latest dev version + await linkLocalRspack(tempDir); + // Run custom assertions if provided if (customAssertions.afterCreate) { await customAssertions.afterCreate({ tempDir, packageJsonPath }); @@ -778,7 +936,8 @@ export function testMeteorSkeleton(options) { test(`"meteor run" / should run the ${skeletonName} app`, async () => { // Run the newly created app const result = await runMeteorApp(tempDir, port, { - waitForOutput: "=> App running at:" + waitForOutput: "=> App running at", + env: env.meteorRun }); meteorProcess = result.meteorProcess; @@ -786,7 +945,9 @@ export function testMeteorSkeleton(options) { await wait(WAIT_ON); // Assert that the Meteor app is running correctly - await assertMeteorApp(port, { title }); + if (checkAppTitle) { + await assertMeteorApp(port, { title }); + } if (checkBodyStyles) { // Assert that the body has the expected CSS styles @@ -811,8 +972,9 @@ export function testMeteorSkeleton(options) { test(`"meteor run --production" / should run the ${skeletonName} app in production mode`, async () => { // Run the app in production mode const result = await runMeteorApp(tempDir, port, { - waitForOutput: "=> App running at:", - commandOptions: ["--production"] + waitForOutput: "=> App running at", + commandOptions: ["--production"], + env: env.meteorRunProduction }); meteorProcess = result.meteorProcess; @@ -820,7 +982,9 @@ export function testMeteorSkeleton(options) { await wait(WAIT_ON); // Assert that the Meteor app is running correctly - await assertMeteorApp(port, { title }); + if (checkAppTitle) { + await assertMeteorApp(port, { title }); + } if (checkBodyStyles) { // Assert that the body has the expected CSS styles @@ -852,12 +1016,15 @@ export function testMeteorSkeleton(options) { stdio: "inherit", shell: true }); + await linkLocalRspack(tempDir); // Run tests once for the app const result = await runMeteorTests(tempDir, port, { - waitForOutput: "=> App running at:", + waitForOutput: skipTestClient ? "TEST_CLIENT=0" : "=> App running at", commandOptions: ["--once"], - checkTestResults: true + checkTestResults: true, + testClient: !skipTestClient, + env: { ...env, ...(env.meteorTest || {}) }, }); // Wait for a margin @@ -876,7 +1043,8 @@ export function testMeteorSkeleton(options) { // Build the app const { buildOutputDir, processResult: result } = await buildMeteorApp(tempDir, { commandOptions: ["--directory"], - captureOutput: true + captureOutput: true, + env: { ...env, ...(env.meteorBuild || {}) }, }); // Wait for a margin @@ -928,5 +1096,68 @@ export function testMeteorSkeleton(options) { await cleanupTempDir(buildOutputDir); } }); + + test(`"meteor reset" / should clear all caches and build artifacts`, async () => { + // Derive METEOR_LOCAL_DIR-aware paths for assertions + const resetEnv = { ...env, ...(env.meteorReset || {}) }; + const meteorLocalDirEnv = resetEnv.METEOR_LOCAL_DIR; + const meteorLocalDirName = meteorLocalDirEnv + ? path.basename(meteorLocalDirEnv.replace(/\\/g, '/')) + : ''; + const localDirSuffix = meteorLocalDirName ? `-${meteorLocalDirName}` : ''; + + // Verify build artifacts exist from previous tests + if (!skipBuildCacheCheck) { + await assertFileExist(tempDir, "_build"); + await assertFileExist(tempDir, "node_modules/.cache/rspack"); + } + + // Run meteor reset + await runMeteorCommand('reset', [], tempDir, { + checkExitCode: true, + env: resetEnv, + }); + + // Verify Rspack build artifacts removed + await assertPathNotExist(tempDir, '_build'); + await assertPathNotExist(tempDir, 'node_modules/.cache/rspack'); + await assertPathNotExist(tempDir, 'node_modules/.cache/meteor'); + await assertPathNotExist(tempDir, assetsContext); + await assertPathNotExist(tempDir, chunksContext); + await assertPathNotExist(tempDir, `public/${assetsContext}`); + await assertPathNotExist(tempDir, `public/${chunksContext}`); + + // Also verify defaults are cleaned to prevent regressions + await assertPathNotExist(tempDir, 'public/build-assets'); + await assertPathNotExist(tempDir, 'public/build-chunks'); + + // When METEOR_LOCAL_DIR is set, also verify suffixed paths are cleaned + if (localDirSuffix) { + await assertPathNotExist(tempDir, `_build${localDirSuffix}`); + await assertPathNotExist(tempDir, `${assetsContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `${chunksContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/${assetsContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/${chunksContext}${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/build-assets${localDirSuffix}`); + await assertPathNotExist(tempDir, `public/build-chunks${localDirSuffix}`); + } + + // Verify default .meteor/local caches are always cleaned + await assertPathNotExist(tempDir, '.meteor/local/build'); + await assertPathNotExist(tempDir, '.meteor/local/bundler-cache'); + await assertPathNotExist(tempDir, '.meteor/local/plugin-cache'); + + // When METEOR_LOCAL_DIR is set, also verify custom local dir is cleaned + if (meteorLocalDirEnv && meteorLocalDirEnv !== '.meteor/local') { + await assertPathNotExist(tempDir, `${meteorLocalDirEnv}/build`); + await assertPathNotExist(tempDir, `${meteorLocalDirEnv}/bundler-cache`); + await assertPathNotExist(tempDir, `${meteorLocalDirEnv}/plugin-cache`); + } + + // Run custom assertions if provided + if (customAssertions.afterReset) { + await customAssertions.afterReset({ tempDir }); + } + }); }; } diff --git a/tools/modern-tests/typescript.test.js b/tools/e2e-tests/typescript.test.js similarity index 76% rename from tools/modern-tests/typescript.test.js rename to tools/e2e-tests/typescript.test.js index 8814237c39..7be3de5bed 100644 --- a/tools/modern-tests/typescript.test.js +++ b/tools/e2e-tests/typescript.test.js @@ -19,6 +19,8 @@ describe('TypeScript App Bundling /', () => { testServer: 'tests/server.ts', }, buildDir: 'build', + assetsContext: 'assets', + chunksContext: 'chunks', configFile: 'rspack.config.ts', customAssertions: { afterCreate({ tempDir }) { @@ -40,7 +42,18 @@ describe('TypeScript App Bundling /', () => { }); await waitForTypeScriptEnvs(result.outputLines, { isTsxEnabled: true }); await waitForTypeScriptErrorFree(result.outputLines); - await assertFileExist(tempDir, '.meteor/local/types'); + await assertFileExist(tempDir, ".meteor/local/types"); + // Portable build: Meteor.isDevelopment and Meteor.isProduction must not be defined + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/, + { negate: true } + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/, + { negate: true } + ); }, afterRunRebuildClient: async ({ allConsoleLogs }) => { // Check for HMR output as enabled by default @@ -52,6 +65,17 @@ describe('TypeScript App Bundling /', () => { 'white-space': 'break-spaces', }); await waitForTypeScriptEnvs(result.outputLines, { isTsxEnabled: true }); + // Portable build: Meteor.isDevelopment and Meteor.isProduction must not be defined + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/, + { negate: true } + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/, + { negate: true } + ); }, afterRunProductionRebuildClient: async ({ allConsoleLogs }) => { // Check for HMR to not be enabled in production-like mode @@ -65,6 +89,17 @@ describe('TypeScript App Bundling /', () => { }, afterBuild: async ({ result }) => { await waitForTypeScriptEnvs(result.outputLines, { isTsxEnabled: true }); + // Portable build: Meteor.isDevelopment and Meteor.isProduction must not be defined + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isDevelopment[^ ]*: [^ ]*false[^ ]*/, + { negate: true } + ); + await waitForMeteorOutput( + result.outputLines, + /[^ ]*Meteor.isProduction[^ ]*: [^ ]*true[^ ]*/, + { negate: true } + ); }, } })); diff --git a/tools/modern-tests/vue.test.js b/tools/e2e-tests/vue.test.js similarity index 100% rename from tools/modern-tests/vue.test.js rename to tools/e2e-tests/vue.test.js diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 6e2bebc023..38e57d0bc6 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -312,9 +312,13 @@ async function ensureWatchRoot(dirPath: string): Promise { if (/Events were dropped/.test(err.message)) { return; } + if (/RootResolveError/.test(err.message) || /failed to resolve root/.test(err.message)) { + console.warn(`Parcel watcher root resolve error on ${osDirPath}, ignoring: ${err.message}`); + ignoredWatchRoots.add(dirPath); + watchRoots.delete(dirPath); + return; + } console.error(`Parcel watcher error on ${osDirPath}:`, err); - // Only disable native watching for critical errors (like ENOSPC). - // @ts-ignore if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { fallbackToPolling(); } @@ -340,9 +344,11 @@ async function ensureWatchRoot(dirPath: string): Promise { (e.code === "ENOTDIR" || /Not a directory/.test(e.message) || e.code === "EBADF" || - /Bad file descriptor/.test(e.message)) + /Bad file descriptor/.test(e.message) || + /RootResolveError/.test(e.message) || + /failed to resolve root/.test(e.message)) ) { - console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + console.warn(`Skipping watcher for ${osDirPath}: ${e.message || 'not watchable'}`); ignoredWatchRoots.add(dirPath); } else { console.error(`Failed to start watcher for ${osDirPath}:`, e); diff --git a/tools/isobuild/isopack.js b/tools/isobuild/isopack.js index 7bfb88b57e..91f62b9faf 100644 --- a/tools/isobuild/isopack.js +++ b/tools/isobuild/isopack.js @@ -23,6 +23,7 @@ import { requestGarbageCollection } from "../utils/gc.js"; import { Unibuild } from "./unibuild.js"; import rspackHelpers from "../tool-env/rspack"; import { getCurrentNodeBinDir, getDevBundle } from "../fs/files"; +import { runLogInstance } from "../runners/run-log"; var rejectBadPath = function (p) { if (p.match(/\.\./)) { @@ -530,6 +531,9 @@ Object.assign(Isopack.prototype, { // Share the rspackHelpers as part of plugin API rspackHelpers, + // Share the runLogInstance as part of plugin API + runLogInstance, + // 'extension' is a file extension without the separation dot // (eg 'js', 'coffee', 'coffee.md') // diff --git a/tools/project-context.js b/tools/project-context.js index 83633ec6a1..d8e85fe45b 100644 --- a/tools/project-context.js +++ b/tools/project-context.js @@ -1470,8 +1470,7 @@ Object.assign(exports.PlatformList.prototype, { getCordovaPlatforms: function () { var self = this; - return _.difference(self._platforms, - exports.PlatformList.DEFAULT_PLATFORMS); + return _.intersection(self._platforms, ['ios', 'android']); }, usesCordova: function () { diff --git a/tools/runners/run-all.js b/tools/runners/run-all.js index 594e20589c..b2b2567b48 100644 --- a/tools/runners/run-all.js +++ b/tools/runners/run-all.js @@ -218,7 +218,7 @@ class Runner { { arrow: true } ); } else { - runLog.log("App running at: " + self.rootUrl, { arrow: true }); + runLog.log("App running at " + self.rootUrl, { arrow: true }); } if (process.platform === "win32") { diff --git a/tools/runners/run-log.js b/tools/runners/run-log.js index 51efd8a4b3..f533b1787a 100644 --- a/tools/runners/run-log.js +++ b/tools/runners/run-log.js @@ -158,7 +158,7 @@ Object.assign(RunLog.prototype, { self.consecutiveRestartMessages = 1; } - var message = "=> Meteor server restarted at: " + options.rootUrl; + var message = "=> Meteor server restarted at " + options.rootUrl; if (self.consecutiveRestartMessages > 1) { message += " (x" + self.consecutiveRestartMessages + ")"; } @@ -225,3 +225,6 @@ var runLogInstance = new RunLog; function (method) { exports[method] = runLogInstance[method].bind(runLogInstance); }); + +// Export the singleton instance for use in plugins +exports.runLogInstance = runLogInstance; diff --git a/tools/static-assets/skel-angular/package.json b/tools/static-assets/skel-angular/package.json index 757dc6798c..fa3105b024 100644 --- a/tools/static-assets/skel-angular/package.json +++ b/tools/static-assets/skel-angular/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@angular/compiler-cli": "^20.0.0", - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@nx/angular-rspack": "^21.1.0", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", diff --git a/tools/static-assets/skel-apollo/package.json b/tools/static-assets/skel-apollo/package.json index 53df518efa..5fd18f2131 100644 --- a/tools/static-assets/skel-apollo/package.json +++ b/tools/static-assets/skel-apollo/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@graphql-tools/webpack-loader": "^7.0.0", "@rsdoctor/rspack-plugin": "^1.2.3", - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", "@rspack/plugin-react-refresh": "^1.4.3", diff --git a/tools/static-assets/skel-babel/package.json b/tools/static-assets/skel-babel/package.json index 4d7826b580..8c9c45753f 100644 --- a/tools/static-assets/skel-babel/package.json +++ b/tools/static-assets/skel-babel/package.json @@ -17,7 +17,7 @@ "devDependencies": { "@babel/preset-env": "^7.28.3", "@babel/preset-react": "^7.23.3", - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-blaze/package.json b/tools/static-assets/skel-blaze/package.json index 87a7e45e29..6dd838aaf6 100644 --- a/tools/static-assets/skel-blaze/package.json +++ b/tools/static-assets/skel-blaze/package.json @@ -14,7 +14,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-chakra-ui/package.json b/tools/static-assets/skel-chakra-ui/package.json index 63ec45405e..a30ef9f180 100644 --- a/tools/static-assets/skel-chakra-ui/package.json +++ b/tools/static-assets/skel-chakra-ui/package.json @@ -21,7 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-coffeescript/package.json b/tools/static-assets/skel-coffeescript/package.json index adbcf48a9e..6f2ebbce93 100644 --- a/tools/static-assets/skel-coffeescript/package.json +++ b/tools/static-assets/skel-coffeescript/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-full/package.json b/tools/static-assets/skel-full/package.json index 541293d7b5..e7f8bb952e 100644 --- a/tools/static-assets/skel-full/package.json +++ b/tools/static-assets/skel-full/package.json @@ -12,7 +12,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index a54a989670..1091b1727a 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-solid/package.json b/tools/static-assets/skel-solid/package.json index 335c56716e..d14262c517 100644 --- a/tools/static-assets/skel-solid/package.json +++ b/tools/static-assets/skel-solid/package.json @@ -14,7 +14,7 @@ "picocolors": "^1.1.1" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-svelte/package.json b/tools/static-assets/skel-svelte/package.json index 02e36fd2d8..d864cf7c23 100644 --- a/tools/static-assets/skel-svelte/package.json +++ b/tools/static-assets/skel-svelte/package.json @@ -13,7 +13,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-tailwind/package.json b/tools/static-assets/skel-tailwind/package.json index 10589551f4..79935caba4 100644 --- a/tools/static-assets/skel-tailwind/package.json +++ b/tools/static-assets/skel-tailwind/package.json @@ -16,7 +16,7 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json index 0080cdfb3b..59017498e0 100644 --- a/tools/static-assets/skel-typescript/package.json +++ b/tools/static-assets/skel-typescript/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-typescript/tsconfig.json b/tools/static-assets/skel-typescript/tsconfig.json index 4cc0e0d73b..aff98434d9 100644 --- a/tools/static-assets/skel-typescript/tsconfig.json +++ b/tools/static-assets/skel-typescript/tsconfig.json @@ -36,7 +36,8 @@ "resolveJsonModule": true, "types": ["node", "mocha"], "esModuleInterop": true, - "preserveSymlinks": true + "preserveSymlinks": true, + "skipLibCheck": true }, "exclude": [ "./.meteor/**", diff --git a/tools/static-assets/skel-vue/package.json b/tools/static-assets/skel-vue/package.json index 4ef2501d63..a156645d98 100644 --- a/tools/static-assets/skel-vue/package.json +++ b/tools/static-assets/skel-vue/package.json @@ -17,7 +17,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/tests/run.js b/tools/tests/run.js index 0093676d50..e5c7ffeab6 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -463,12 +463,12 @@ selftest.define("'meteor run --port' accepts/rejects proper values", async funct run = s.run("run", "--port", "3500"); run.waitSecs(30); - await run.match('App running at: http://localhost:3500/'); + await run.match('App running at http://localhost:3500/'); await run.stop(); run = s.run("run", "--port", "127.0.0.1:3500"); run.waitSecs(30); - await run.match('App running at: http://127.0.0.1:3500/'); + await run.match('App running at http://127.0.0.1:3500/'); await run.stop(); }); @@ -491,7 +491,7 @@ selftest.define("update package during run", async function () { var runRun = s.run(); runRun.waitSecs(3); - await runRun.match("App running at:"); + await runRun.match("App running at"); var updateRun = s.run("update", "glasser:package-for-selftest"); await updateRun.match( diff --git a/tools/tests/server-restart-port.js b/tools/tests/server-restart-port.js index 47a7e3a49c..7a02aea685 100644 --- a/tools/tests/server-restart-port.js +++ b/tools/tests/server-restart-port.js @@ -23,5 +23,5 @@ async function testHelper(server) { (match, n) => `module.id, ${ ++n }`, )); - await run.match("Meteor server restarted at: http://localhost:21000/"); + await run.match("Meteor server restarted at http://localhost:21000/"); } diff --git a/tools/tests/test-modes.js b/tools/tests/test-modes.js index f06eccebeb..eca226e461 100644 --- a/tools/tests/test-modes.js +++ b/tools/tests/test-modes.js @@ -20,17 +20,17 @@ selftest.define("'meteor test --port' accepts/rejects proper values", async func run = s.run("test", "--port", "3700", "--driver-package", "tmeasday:acceptance-test-driver"); run.waitSecs(30); - await run.match('App running at: http://localhost:3700/'); + await run.match('App running at http://localhost:3700/'); await run.stop(); run = s.run("test", "--port", "127.0.0.1:3700", "--driver-package", "tmeasday:acceptance-test-driver"); run.waitSecs(30); - await run.match('App running at: http://127.0.0.1:3700/'); + await run.match('App running at http://127.0.0.1:3700/'); await run.stop(); run = s.run("test", "--port", "[::]:3700", "--driver-package", "tmeasday:acceptance-test-driver"); run.waitSecs(30); - await run.match('App running at: http://[::]:3700/'); + await run.match('App running at http://[::]:3700/'); await run.stop(); }); diff --git a/tools/tests/typescript.js b/tools/tests/typescript.js index 2828cc6a2b..7933ee5265 100644 --- a/tools/tests/typescript.js +++ b/tools/tests/typescript.js @@ -1,27 +1,28 @@ -// var selftest = require('../tool-testing/selftest.js'); -// var Sandbox = selftest.Sandbox; -// -// selftest.define("typescript template works", function () { -// const s = new Sandbox; -// -// let run = s.run("create", "--typescript", "typescript"); -// -// run.waitSecs(60); -// run.match("Created a new Meteor app in 'typescript'."); -// run.match("To run your new app"); -// -// s.cd("typescript"); -// -// run = s.run("npm", "install"); -// run.expectExit(0); -// -// run = s.run("lint"); -// run.waitSecs(60); -// run.match("[zodern:types] Exiting \"meteor lint\" early"); -// run.expectExit(0); -// -// run = s.run("npx", "tsc"); -// run.waitSecs(60); -// run.expectEnd(); -// run.expectExit(0); -// }); +var selftest = require('../tool-testing/selftest.js'); +var Sandbox = selftest.Sandbox; + +selftest.define("typescript template works", async function () { + const s = new Sandbox(); + await s.init(); + + let run = s.run("create", "--typescript", "typescript"); + + run.waitSecs(60); + await run.match("Created a new Meteor app in 'typescript'."); + await run.match("To run your new app"); + + s.cd("typescript"); + + run = s.run("npm", "install"); + await run.expectExit(0); + + run = s.run("lint"); + run.waitSecs(60); + await run.match('[zodern:types] Exiting "meteor lint" early'); + await run.expectExit(0); + + run = s.run("npx", "tsc"); + run.waitSecs(60); + await run.expectEnd(); + await run.expectExit(0); +}); diff --git a/tools/tool-env/rspack.js b/tools/tool-env/rspack.js index 766ed44ec0..be7794dfba 100644 --- a/tools/tool-env/rspack.js +++ b/tools/tool-env/rspack.js @@ -1,17 +1,25 @@ // Helper functions for Rspack integration const files = require('../fs/files'); +const path = require('path'); const { getMeteorConfig } = require("./meteor-config"); const config = getMeteorConfig(); +// Derive the METEOR_LOCAL_DIR suffix the same way packages/rspack/lib/constants.js does, +// so reset cleans the correct directories when running multiple instances. +const meteorLocalDirName = process.env.METEOR_LOCAL_DIR + ? path.basename(process.env.METEOR_LOCAL_DIR.replace(/\\/g, '/')) + : ''; +const localDirSuffix = meteorLocalDirName ? `-${meteorLocalDirName}` : ''; + // Get the build context from environment variable or use default "_build" -const rspackBuildContext = config?.buildContext || process.env.RSPACK_BUILD_CONTEXT || "_build"; +const rspackBuildContext = config?.buildContext || process.env.RSPACK_BUILD_CONTEXT || `_build${localDirSuffix}`; // Get the assets context from environment variable or use default "build-assets" -const rspackAssetsContext = config?.assetsContext || process.env.RSPACK_ASSETS_CONTEXT || "build-assets"; +const rspackAssetsContext = config?.assetsContext || process.env.RSPACK_ASSETS_CONTEXT || `build-assets${localDirSuffix}`; // Get the bundles context from environment variable or use default "build-chunks" -const rspackChunksContext = config?.chunksContext || process.env.RSPACK_CHUNKS_CONTEXT || "build-chunks"; +const rspackChunksContext = config?.chunksContext || process.env.RSPACK_CHUNKS_CONTEXT || `build-chunks${localDirSuffix}`; // Cache the regex pattern for performance const rspackFilePattern = new RegExp(`^${rspackBuildContext}\\/.*\\/[^\\/]*-rspack\\.js$`); @@ -35,16 +43,51 @@ exports.getRspackResourcesContexts = function() { ]; }; -// Function to get the rspack app contexts +// Function to get the rspack app contexts for cleanup. +// Reads the app's package.json meteor config to resolve custom context names, +// and always includes the default paths to prevent regressions. exports.getRspackAppContexts = function(appDir) { - const rspackResourcesContexts = exports.getRspackResourcesContexts(); - return [ + let appConfig = null; + try { + const pkgPath = files.pathJoin(appDir, 'package.json'); + if (files.exists(pkgPath)) { + const pkg = JSON.parse(files.readFile(pkgPath, 'utf8')); + appConfig = pkg?.meteor || null; + } + } catch (e) { + // Fall back to defaults if package.json can't be read + } + + const appBuildContext = appConfig?.buildContext || process.env.RSPACK_BUILD_CONTEXT || `_build${localDirSuffix}`; + const appAssetsContext = appConfig?.assetsContext || process.env.RSPACK_ASSETS_CONTEXT || `build-assets${localDirSuffix}`; + const appChunksContext = appConfig?.chunksContext || process.env.RSPACK_CHUNKS_CONTEXT || `build-chunks${localDirSuffix}`; + + const contexts = [ files.pathJoin(appDir, "node_modules", ".cache", "rspack"), - files.pathJoin(appDir, rspackBuildContext), - ...rspackResourcesContexts.reduce((arr, context) => [ - ...arr, - files.pathJoin(appDir, `public/${context}`), - files.pathJoin(appDir, `public/${context}`) - ], []) ]; + + // Collect unique context names (configured + defaults to prevent regressions) + const allNames = new Set([ + appBuildContext, '_build', + appAssetsContext, 'build-assets', + appChunksContext, 'build-chunks', + ]); + + for (const name of allNames) { + contexts.push(files.pathJoin(appDir, name)); + contexts.push(files.pathJoin(appDir, `public/${name}`)); + contexts.push(files.pathJoin(appDir, `private/${name}`)); + } + + // When METEOR_LOCAL_DIR is set, also include suffixed paths + if (localDirSuffix) { + for (const name of allNames) { + const suffixed = `${name}${localDirSuffix}`; + contexts.push(files.pathJoin(appDir, suffixed)); + contexts.push(files.pathJoin(appDir, `public/${suffixed}`)); + contexts.push(files.pathJoin(appDir, `private/${suffixed}`)); + } + } + + return contexts; }; diff --git a/tools/unit-tests/jest.config.js b/tools/unit-tests/jest.config.js index a21cc6c896..f78071ebc9 100644 --- a/tools/unit-tests/jest.config.js +++ b/tools/unit-tests/jest.config.js @@ -10,13 +10,13 @@ module.exports = { ], testPathIgnorePatterns: [ "/node_modules/", - "/tools/modern-tests/", + "/tools/e2e-tests/", "/tools/tests/", "/packages/", "/.github/", ], modulePathIgnorePatterns: [ - "/tools/modern-tests/", + "/tools/e2e-tests/", "/tools/tests/", "/tools/static-assets/", "/npm-packages/", diff --git a/tools/utils/utils.js b/tools/utils/utils.js index 54bbfe0b31..81344b1196 100644 --- a/tools/utils/utils.js +++ b/tools/utils/utils.js @@ -34,11 +34,7 @@ exports.parseUrl = function (str, defaults) { } var hasScheme = exports.hasScheme(str); - if (! hasScheme) { - str = "http://" + str; - } - - var parsed = url.parse(str); + const parsed = url.parse(hasScheme ? str : `http://${str}`); // for consistency remove colon at the end of protocol parsed.protocol = parsed.protocol.replace(/\:$/, ''); @@ -59,10 +55,7 @@ exports.parseUrl = function (str, defaults) { exports.formatUrl = function (options) { // For consistency with `Meteor.absoluteUrl`, add a trailing slash to make // this a valid URL - if (!options.pathname) - options.pathname = "/"; - - return url.format(options); + return url.format({ ...options, pathname: options.pathname || "/" }); }; exports.ipAddress = function () { @@ -88,11 +81,6 @@ ${addressEntries.map(entry => entry.address).join(', ')}`); return addressEntries[0].address; }; -exports.hasScheme = function (str) { - return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//); -}; - - exports.hasScheme = function (str) { return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//); }; @@ -105,20 +93,8 @@ exports.isIPv4Address = function (str) { // Prints a package list in a nice format. // Input is an array of objects with keys 'name' and 'description'. exports.printPackageList = function (items, options) { - options = options || {}; - - var rows = _.map(items, function (item) { - var name = item.name; - var description = item.description || 'No description'; - return [name, description]; - }); - - var alphaSort = function (row) { - return row[0]; - }; - rows = _.sortBy(rows, alphaSort); - - var Console = require('../console/console.js').Console; + const rows = _.sortBy(items.map(item => [item.name, item.description || 'No description']), row => row[0]); + const Console = require('../console/console.js').Console; return Console.printTwoColumns(rows, options); }; @@ -773,3 +749,4 @@ export function isEmacs() { emacsDetected = !! (process.env.EMACS === "t" || process.env.INSIDE_EMACS); return emacsDetected; } + diff --git a/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md b/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md index 6c76240f18..6c3d84cb53 100644 --- a/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md +++ b/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md @@ -165,7 +165,9 @@ You can use `.swcrc` config in the root of your project to describe specific [SW You can also configure other options using the `.swcrc` format. For custom SWC configs, see the [SWC configuration API](https://swc.rs/docs/configuration/compilation). -Use `swc.config.js` in your project root for dynamic configuration. Meteor will import and apply the SWC config automatically. This lets you choose a config based on environment variables or other runtime factors. +Use `swc.config.js` in your project root for dynamic configuration. Meteor will import and apply the SWC config automatically. This lets you choose a config based on environment variables or other runtime factors. If you prefer TypeScript, `swc.config.ts` is also supported, Meteor will transpile and load it automatically. + +Meteor checks for config files in this order: `.swcrc` > `swc.config.js` > `swc.config.ts`. Only the first one found is used. You can also review these migration topics that use custom `.swcrc` configs: diff --git a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md index 2ddbe1dc53..f053475cf8 100644 --- a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md +++ b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md @@ -157,21 +157,54 @@ You can use flags to control the final configuration based on the environment. T | `compileWithRspack` | function | Forces given npm deps ([Condition](https://rspack.rs/config/module#condition)[]) to be compiled by Rspack | | `setCache` | function | Enables or disables cache. Accepts true (persistent, default), false, or 'memory' | | `splitVendorChunk` | function | Splits vendor libraries so they are automatically served from a separate chunk | -| `extendSwcConfig` | function | Extends the [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options) to apply only to the app code | +| `extendSwcConfig` | function | Smart-merges custom options into Meteor's default [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options), applying only to app code | +| `replaceSwcConfig` | function | Replaces Meteor's default [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options) entirely with the provided options, applying only to app code | | `extendConfig` | function | Extends the config by applying merged object configs | +| `enablePortableBuild` | function | Omits `Meteor.isDevelopment` and `Meteor.isProduction` from the bundle, making it portable across environments | Some configurations in the Rspack config are reserved for the Meteor-Rspack setup to work, such as Rspack options inside the `entry` and `output` objects. These will trigger warnings if modified. All other settings can be overridden, giving you the flexibility to make any setup compatible with the modern bundler. -If you want to see the final Rspack config applying your overrides, you can enable verbose mode in the modern build stack. +If you want to see the final Rspack config applying your overrides, you can enable [verbose mode](#enable-verbose-mode) in the modern build stack. + +## Logging + +Starting with Meteor 3.4.1, the log output for the default Meteor-Rspack app is simplified to stay as close as possible to Meteor's native experience. By default, logs are less verbose and only show essential information like server restarts and client modifications. + +If there are any compilation warnings or errors, Rspack logs will be shown with their own style and colors. + +### Enable Verbose Mode + +If you need more details about Meteor and Rspack processes, you can enable verbose mode in your `package.json`: ```json -"meteor": { - "modern": { - "verbose": true +{ + "meteor": { + "modern": { + "verbose": true + } } } ``` +### Advanced Rspack Logging + +For even deeper insights into the Rspack compilation process, you can configure [`stats`](https://rspack.rs/config/stats#stats) and [`infrastructureLogging`](https://rspack.rs/config/infrastructure-logging#infrastructurelogging) directly in your `rspack.config.js`. + +- **`stats`**: Controls what bundle information is displayed on each compilation. +- **`infrastructureLogging`**: Controls Rspack infrastructure logs, including HMR verbosity in both the terminal and the browser. To enable detailed logs for updates and serving client code changes, set `infrastructureLogging.level` to `'info'` or higher (it is not enabled by default). + +```javascript +module.exports = defineConfig(Meteor => { + return { + stats: 'detailed', // or other Rspack stats options + infrastructureLogging: { + level: 'info', + }, + // ... rest of your config + }; +}); +``` + ## Migration Topics ### Entry Points @@ -232,15 +265,7 @@ if (condition) { For background, see: [Why nested import](https://github.com/benjamn/reify/blob/main/WHY_NEST_IMPORTS.md). -To use Rspack, migrate your nested imports to a standard form. To identify and fix nested imports in your project, [use verbose mode in Meteor 3.3’s modern transpiler](./meteor-bundler-optimizations.md#optimize-swc-and-handle-fallbacks). Enable it with: - -```json -"meteor": { - "modern": { - "verbose": true - } -} -``` +To use Rspack, migrate your nested imports to a standard form. To identify and fix nested imports in your project, use [verbose mode](#enable-verbose-mode) to see which files are failing. When you run your app, `[Transpiler]` logs will show each file. Focus on `(app)` files that fail with messages like: @@ -604,27 +629,62 @@ module.exports = defineConfig(Meteor => ({ This is a quick configuration for split chunks all within `node_modules` as a `vendor` chunk, if you need more control you can use the [official Rspack split chunks integration guide](https://rspack.rs/guide/optimization/code-splitting#splitchunksplugin). -### Extending SWC config +### Customizing SWC config Rspack uses the SWC configuration to transpile your app code. By default, it inherits any settings from the `.swcrc` file, which also [impacts how Meteor transpiles core and package code](meteor-bundler-optimizations.md#custom-swcrc). -If you want a configuration to apply only to your app code, you can extend the SWC setup using the `Meteor.extendSwcConfig` helper: +If you want a configuration to apply only to your app code (not Meteor packages), two helpers are available: + +#### `Meteor.extendSwcConfig` - smart merge (recommended) + +Merges your custom options on top of Meteor's defaults using a deep merge strategy (the same used by `Meteor.extendConfig`). Only the properties you specify are overridden; everything else (parser settings, React refresh, external helpers, etc) is preserved. ```js const { defineConfig } = require('@meteorjs/rspack'); module.exports = defineConfig(Meteor => ({ - // Extend SWC config + // Add decorator support while keeping all Meteor defaults ...Meteor.extendSwcConfig({ jsc: { parser: { - syntax: 'typescript', + decorators: true, }, }, }), })); ``` +#### `Meteor.replaceSwcConfig` - full replacement + +Discards Meteor's defaults entirely and uses the provided config as-is. Use this when you need complete control over SWC and the smart merge doesn't fit your use case. + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Full SWC config — no Meteor defaults applied + ...Meteor.replaceSwcConfig({ + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: true, + }, + target: 'es2020', + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }), +})); +``` + +:::warning +When using `replaceSwcConfig`, you are responsible for providing all necessary SWC options. Features like React refresh, external helpers and parser defaults that Meteor configures (`Meteor.swcConfigOptions`) will not be applied unless you include them yourself. +::: + ### Interop for Default Imports Meteor originally handled default imports from CommonJS modules automatically. This allowed you to write: @@ -671,7 +731,7 @@ Meteor cache remains active and continues to handle Atmosphere packages and inte This Rspack cache is enabled by default in persistent mode. If you [encounter issues](https://github.com/web-infra-dev/rspack/issues/11804) or prefer to disable it, you can do so in your `rspack.config.js` using the helper: -```json +```javascript const { defineConfig } = require('@meteorjs/rspack'); const { rspack } = require('@rspack/core'); const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); @@ -759,6 +819,40 @@ module.exports = defineConfig(Meteor => ({ })); ``` +### Portable Build + +By default, Meteor-Rspack replaces `Meteor.isDevelopment` and `Meteor.isProduction` with static values at build time. This follows modern bundler conventions where `mode: "production"` enables aggressive dead-code elimination, any code inside `if (Meteor.isDevelopment) { ... }` blocks is stripped entirely from production builds. + +This is the recommended default. It produces smaller, more secure bundles by ensuring development-only code never ships to production. `meteor build` benefits directly from this, as the final output is as lean as possible. + +If you need a single build that works across environments (for example, building once and deploying to both staging and production without rebuilding), you can opt in to portable builds. This omits `Meteor.isDevelopment` and `Meteor.isProduction` from compile-time replacement, keeping them as runtime checks instead. + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + ...Meteor.enablePortableBuild(), +})); +``` + +Note that this trades build optimization for portability — dead-code elimination for development/production branches will no longer apply, resulting in larger bundles. Other flags like `Meteor.isClient`, `Meteor.isServer`, and `Meteor.isTest` are always replaced at build time, since they depend on the build target. + +### Running Multiple Instances + +By default, Meteor and Rspack use fixed directories for their build caches (`.meteor/local` and `_build`). If you try to run multiple instances of the same app simultaneously, they may conflict by attempting to write to the same folders. + +To run multiple instances, you can use the `METEOR_LOCAL_DIR` environment variable to specify a unique local directory for each instance. When this variable is set, the Meteor-Rspack integration automatically extracts the directory name and uses it as a suffix for Rspack's build contexts (`_build`, `build-chunks`, and `build-assets`), ensuring complete isolation between instances. + +```bash +# Instance 1 +PORT=3000 METEOR_LOCAL_DIR=.meteor/local-1 meteor run + +# Instance 2 +PORT=3001 METEOR_LOCAL_DIR=.meteor/local-2 meteor run +``` + +For more details on how this variable affects Rspack, see the [`METEOR_LOCAL_DIR`](../../cli/environment-variables.md#meteor_local_dir) documentation. + ## Benefits Meteor–Rspack integration sends your app code to Rspack to use modern bundler features. Meteor then uses Rspack’s output to handle Meteor-specific tasks (like Atmosphere package compilation) and create the final bundle. @@ -804,9 +898,13 @@ You can compare performance before and after enabling `modern` by running [`mete Large apps are more likely to hit memory limits during Meteor-Rspack builds, but this can also happen on smaller projects depending on the number of dependencies, cache size, and available system memory. If you experience crashes or out-of-memory errors, it's likely that the Rspack child process is running out of heap memory. -A common first reaction is to set [`TOOL_NODE_FLAGS`](../../cli/environment-variables.md#tool-node-flags)` (`TOOL_NODE_FLAGS="--max-old-space-size=8192"`), but this flag is mainly for the Meteor tool's own Node.js process at startup. Rspack runs as a spawned child process and may not inherit it. +Starting from Meteor 3.4.1, you can use [`TOOL_NODE_FLAGS`](../../cli/environment-variables.md#tool-node-flags) to set memory limits that will be automatically inherited by Rspack and other tool processes: -Instead, use the standard `NODE_OPTIONS` environment variable, which Node.js propagates to child processes: +```bash +TOOL_NODE_FLAGS="--max-old-space-size=16384" meteor run +``` + +For Meteor 3.4, you should use the standard `NODE_OPTIONS` environment variable, which Node.js propagates to child processes: ```bash NODE_OPTIONS="--max-old-space-size=16384" meteor run @@ -814,10 +912,6 @@ NODE_OPTIONS="--max-old-space-size=16384" meteor run This raises the heap limit for the Rspack process and should reduce how often memory-related crashes occur. Adjust the value according to your machine's available memory. -:::info -For the Meteor 3.4.x series, as `NODE_OPTIONS` is confirmed to help, one option being considered is to automatically inherit memory settings from `TOOL_NODE_FLAGS` into the spawned Rspack process. -::: - Another approach is to disable Rspack's persistent cache, which is enabled by default and can be memory-intensive. See the [Cache](#cache) migration topic to disable it: ```js @@ -828,7 +922,7 @@ module.exports = defineConfig(Meteor => ({ })); ``` -You can combine both solutions: raise the heap limit with `NODE_OPTIONS` and disable persistent cache to reduce overall memory pressure. +You can combine both solutions: raise the heap limit with `TOOL_NODE_FLAGS` (3.4.1+) or `NODE_OPTIONS` (3.4) and disable persistent cache to reduce overall memory pressure. Rspack itself has reported plans to optimize persistent cache and overall RAM consumption in [Rspack 2.0](https://rspack.rs/misc/planning/roadmap), which should improve memory behavior in future Meteor-Rspack releases. diff --git a/v3-docs/docs/cli/environment-variables.md b/v3-docs/docs/cli/environment-variables.md index b67bc28d91..72bfb8924a 100644 --- a/v3-docs/docs/cli/environment-variables.md +++ b/v3-docs/docs/cli/environment-variables.md @@ -95,6 +95,20 @@ This way each command only processes the files it actually needs, reducing build `METEOR_IGNORE` is automatically set when using the [Rspack bundler integration](../about/modern-build-stack/rspack-bundler-integration.md). Since Rspack handles the client and server app bundling, Meteor's bundler should only worry about what it strictly needs for the Meteor-Rspack integration. By using `METEOR_IGNORE` to exclude folders and dependencies that Rspack already manages or that are irrelevant to Meteor's side of the build, you ensure the most speed is gained from the Rspack delegation. ::: +## METEOR_LOCAL_DIR +(_development_) + +This environment variable allows you to change the location of the `.meteor/local` directory, which Meteor uses to store its build cache and other local state. This is useful for running multiple instances of the same app with different local states or for redirecting the local directory to a different drive or path. + +When using the [Rspack bundler integration](../about/modern-build-stack/rspack-bundler-integration.md), `METEOR_LOCAL_DIR` also influences the Rspack build context. It extracts the name of the folder represented in the path and appends it as a suffix to the following Rspack constants: +- `RSPACK_BUILD_CONTEXT` +- `RSPACK_CHUNKS_CONTEXT` +- `RSPACK_ASSETS_CONTEXT` + +For example, if `METEOR_LOCAL_DIR` is set to `/path/to/.meteor/local-custom`, Rspack will use `_build-local-custom`, `build-chunks-local-custom`, and `build-assets-local-custom` as its context directories, ensuring that build artifacts remain isolated for that specific local environment. + +For more information, see the [Running Multiple Instances](../about/modern-build-stack/rspack-bundler-integration.md#running-multiple-instances) section in the Rspack documentation. + ## METEOR_PROFILE (_development_) @@ -146,9 +160,22 @@ Used to generate URLs to your application by, among others, the accounts package ## TOOL_NODE_FLAGS (_development, production_) -Used to pass flags/variables to Node inside Meteor build. For example you can use this to pass a link to icu data: `TOOL_NODE_FLAGS="--icu-data-dir=node_modules/full-icu"` +Used to pass Node.js flags that Meteor will inherit and spread to other tool processes like Rspack. For example, to increase memory limits for all tools: `TOOL_NODE_FLAGS="--max-old-space-size=4096"` + +By default, these flags are automatically spread to `NODE_OPTIONS` so that tools like Rspack inherit them. This behavior can be controlled using [`TOOL_NODE_FLAGS_INHERIT`](#tool-node-flags-spread). + For full list of available flags see the [Node documentation](https://nodejs.org/dist/latest-v12.x/docs/api/cli.html). +## TOOL_NODE_FLAGS_INHERIT +(_development, production_) + +Controls whether `TOOL_NODE_FLAGS` are prepended to `NODE_OPTIONS`. Enabled by default. + +```bash +# Disable inheritance - Rspack won't inherit the heap limit +TOOL_NODE_FLAGS_INHERIT=0 TOOL_NODE_FLAGS="--max-old-space-size=4096" meteor run +``` + ## UNIX_SOCKET_GROUP (_production_) diff --git a/v3-docs/docs/components/helpers/ParamTable.vue b/v3-docs/docs/components/helpers/ParamTable.vue index 8251d3a7a1..65a6c6d4b9 100644 --- a/v3-docs/docs/components/helpers/ParamTable.vue +++ b/v3-docs/docs/components/helpers/ParamTable.vue @@ -1,14 +1,14 @@ @@ -51,9 +55,7 @@ const sourceCode = `https://github.com/meteor/meteor/blob/devel/packages/${props

    Arguments:

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