From f5d1c047bbfcda343c26776418395ca72b4483ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 4 Mar 2026 18:26:42 +0100 Subject: [PATCH] add meteor reset E2E test coverage --- tools/cli/commands.js | 51 +++++++++----- tools/modern-tests/assertions.js | 37 +++++++++++ tools/modern-tests/test-helpers.js | 103 +++++++++++++++++++++++++++++ tools/tool-env/rspack.js | 47 +++++++++---- 4 files changed, 211 insertions(+), 27 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 6c26667dd7..88716546f4 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -1865,6 +1865,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") ); @@ -1879,19 +1888,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, @@ -1901,11 +1914,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/assertions.js b/tools/modern-tests/assertions.js index 4b873f6d9e..437bdec437 100644 --- a/tools/modern-tests/assertions.js +++ b/tools/modern-tests/assertions.js @@ -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/modern-tests/test-helpers.js b/tools/modern-tests/test-helpers.js index d9ea68a623..02984c2008 100644 --- a/tools/modern-tests/test-helpers.js +++ b/tools/modern-tests/test-helpers.js @@ -23,6 +23,7 @@ import { assertFileExist, assertMeteorApp, assertMeteorReactApp, + assertPathNotExist, assertRspackScriptTag } from "./assertions"; import fs from "fs-extra"; @@ -776,6 +777,57 @@ 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 (always check defaults) + await assertPathNotExist(appDir, buildDir); + await assertPathNotExist(appDir, 'node_modules/.cache/rspack'); + 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, `_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 }); + } + }); }; } @@ -1047,5 +1099,56 @@ 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 + 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 (always check defaults) + await assertPathNotExist(tempDir, '_build'); + await assertPathNotExist(tempDir, 'node_modules/.cache/rspack'); + await assertPathNotExist(tempDir, 'node_modules/.cache/meteor'); + 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, `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/tool-env/rspack.js b/tools/tool-env/rspack.js index 766ed44ec0..21749393d8 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,31 @@ exports.getRspackResourcesContexts = function() { ]; }; -// Function to get the rspack app contexts +// Function to get the rspack app contexts for cleanup. +// Always includes the default paths (_build, build-assets, build-chunks) to +// prevent regressions, plus suffixed paths when METEOR_LOCAL_DIR is set. exports.getRspackAppContexts = function(appDir) { - const rspackResourcesContexts = exports.getRspackResourcesContexts(); - return [ + 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}`) - ], []) ]; + + // Always include defaults + const defaults = ['_build', 'build-assets', 'build-chunks']; + for (const name of defaults) { + 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) { + const suffixed = defaults.map(name => `${name}${localDirSuffix}`); + for (const name of suffixed) { + contexts.push(files.pathJoin(appDir, name)); + contexts.push(files.pathJoin(appDir, `public/${name}`)); + contexts.push(files.pathJoin(appDir, `private/${name}`)); + } + } + + return contexts; };