From 13ade0e9264f7112de37765d2f54051fc82de473 Mon Sep 17 00:00:00 2001 From: Oleg Date: Thu, 24 Jul 2025 15:55:43 +0300 Subject: [PATCH 01/66] refactor: Update package to modern JS - Replaced array-like objects with Set for better performance and clarity. - Removed duplicate code in `forEachAsync` method. - Fixed a bug in the `clear` method. - Improved clarity by refining some function names. --- packages/callback-hook/hook.js | 108 ++++++++++-------------------- packages/callback-hook/package.js | 2 +- 2 files changed, 37 insertions(+), 73 deletions(-) diff --git a/packages/callback-hook/hook.js b/packages/callback-hook/hook.js index ecc9c2ccfb..a5340666cd 100644 --- a/packages/callback-hook/hook.js +++ b/packages/callback-hook/hook.js @@ -36,23 +36,14 @@ // callback will propagate up to the iterator function, and will // terminate calling the remaining callbacks if not caught. -const hasOwn = Object.prototype.hasOwnProperty; - export class Hook { - constructor(options) { - options = options || {}; - this.nextCallbackId = 0; - this.callbacks = Object.create(null); - // Whether to wrap callbacks with Meteor.bindEnvironment - this.bindEnvironment = true; - if (options.bindEnvironment === false) { - this.bindEnvironment = false; - } + constructor(options = {}) { + this.callbacks = new Set(); - this.wrapAsync = true; - if (options.wrapAsync === false) { - this.wrapAsync = false; - } + // Whether to wrap callbacks with Meteor.bindEnvironment + const { bindEnvironment = true, wrapAsync = true } = options; + this.bindEnvironment = !!bindEnvironment; + this.wrapAsync = !!wrapAsync; if (options.exceptionHandler) { this.exceptionHandler = options.exceptionHandler; @@ -64,6 +55,10 @@ export class Hook { } } + clear() { + this.callbacks.clear(); + } + register(callback) { const exceptionHandler = this.exceptionHandler || function (exception) { // Note: this relies on the undocumented fact that if bindEnvironment's @@ -75,29 +70,23 @@ export class Hook { if (this.bindEnvironment) { callback = Meteor.bindEnvironment(callback, exceptionHandler); } else { - callback = dontBindEnvironment(callback, exceptionHandler); + callback = wrapHookWithErrorHandling(callback, exceptionHandler); } if (this.wrapAsync) { callback = Meteor.wrapFn(callback); } - const id = this.nextCallbackId++; - this.callbacks[id] = callback; + this.callbacks.add(callback); return { callback, stop: () => { - delete this.callbacks[id]; + this.callbacks.delete(callback); } }; } - clear() { - this.nextCallbackId = 0; - this.callbacks = []; - } - /** * For each registered callback, call the passed iterator function with the callback. * @@ -110,31 +99,8 @@ export class Hook { * @param iterator */ forEach(iterator) { - - const ids = Object.keys(this.callbacks); - for (let i = 0; i < ids.length; ++i) { - const id = ids[i]; - // check to see if the callback was removed during iteration - if (hasOwn.call(this.callbacks, id)) { - const callback = this.callbacks[id]; - if (! iterator(callback)) { - break; - } - } - } - } - - async forEachAsync(iterator) { - const ids = Object.keys(this.callbacks); - for (let i = 0; i < ids.length; ++i) { - const id = ids[i]; - // check to see if the callback was removed during iteration - if (hasOwn.call(this.callbacks, id)) { - const callback = this.callbacks[id]; - if (!await iterator(callback)) { - break; - } - } + for (const callback of this.callbacks) { + if (!iterator(callback)) break; } } @@ -147,16 +113,8 @@ export class Hook { * @see forEach */ async forEachAsync(iterator) { - const ids = Object.keys(this.callbacks); - for (let i = 0; i < ids.length; ++i) { - const id = ids[i]; - // check to see if the callback was removed during iteration - if (hasOwn.call(this.callbacks, id)) { - const callback = this.callbacks[id]; - if (!await iterator(callback)) { - break; - } - } + for (const callback of this.callbacks) { + if (!await iterator(callback)) break; } } @@ -170,24 +128,30 @@ export class Hook { } // Copied from Meteor.bindEnvironment and removed all the env stuff. -function dontBindEnvironment(func, onException, _this) { - if (!onException || typeof(onException) === 'string') { - const description = onException || "callback of async function"; - onException = function (error) { - Meteor._debug( - "Exception in " + description, - error - ); - }; - } - - return function (...args) { +function wrapHookWithErrorHandling(func, onException, _this) { + const exceptionHandler = normalizeHookExceptionHandler(onException); + return function executeHookWithErrorHandling(...args) { let ret; try { ret = func.apply(_this, args); } catch (e) { - onException(e); + exceptionHandler(e); } return ret; }; } + +function normalizeHookExceptionHandler(exceptionHandler) { + if (typeof exceptionHandler === 'function') { + return exceptionHandler; + } + + // TODO: The message "callback of async function" is not very useful and needs clarification. + const description = typeof exceptionHandler === 'string' + ? exceptionHandler + : "callback of async function"; + + return function defaultHookExceptionHandler(error) { + Meteor._debug(`Exception in ${description}`, error); + } +} \ No newline at end of file diff --git a/packages/callback-hook/package.js b/packages/callback-hook/package.js index 4b4f756266..82df7371bb 100644 --- a/packages/callback-hook/package.js +++ b/packages/callback-hook/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Register callbacks on a hook", - version: '1.6.0', + version: '1.7.0', }); Package.onUse(function (api) { From 065ef8a4610440b4d3f4e3bb3861ef87897de8da Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 25 Jul 2025 18:19:57 +0300 Subject: [PATCH 02/66] feat: Add .size, ._asArray, and ._fromArray methods --- packages/callback-hook/hook.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/callback-hook/hook.js b/packages/callback-hook/hook.js index a5340666cd..88372251d5 100644 --- a/packages/callback-hook/hook.js +++ b/packages/callback-hook/hook.js @@ -59,6 +59,23 @@ export class Hook { this.callbacks.clear(); } + size() { + return this.callbacks.size; + } + + // WARN: This method is for test compatibility only. + _asArray() { + return Array.from(this.callbacks); + } + + // WARN: This method is for test compatibility only. + _fromArray(arr) { + if (!Array.isArray(arr)) { + throw new Error("Method _fromArray expects an array"); + } + this.callbacks = new Set(arr); + } + register(callback) { const exceptionHandler = this.exceptionHandler || function (exception) { // Note: this relies on the undocumented fact that if bindEnvironment's From c22a174716600945b57ba2078c91a69161036e77 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 25 Jul 2025 18:20:13 +0300 Subject: [PATCH 03/66] chore: bump version to 1.7.1 --- packages/callback-hook/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/callback-hook/package.js b/packages/callback-hook/package.js index 82df7371bb..9d9d33d6c4 100644 --- a/packages/callback-hook/package.js +++ b/packages/callback-hook/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Register callbacks on a hook", - version: '1.7.0', + version: '1.7.1', }); Package.onUse(function (api) { From aaf6eeaf05ee7c712c3101ceba19c93f4f22031d Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 25 Jul 2025 19:25:58 +0300 Subject: [PATCH 04/66] fix: fix account_reconnect_tests.js --- packages/accounts-base/accounts_reconnect_tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/accounts-base/accounts_reconnect_tests.js b/packages/accounts-base/accounts_reconnect_tests.js index 8341cf0a5d..67dcf1199a 100644 --- a/packages/accounts-base/accounts_reconnect_tests.js +++ b/packages/accounts-base/accounts_reconnect_tests.js @@ -112,7 +112,7 @@ if (Meteor.isClient) { loginAsUser1((err) => { test.isUndefined(err, 'Unexpected error logging in as user1'); test.equal( - Object.keys(DDP._reconnectHook.callbacks).length, + DDP._reconnectHook.size(), 1, 'Only one onReconnect callback should be registered' ); @@ -122,7 +122,7 @@ if (Meteor.isClient) { setTimeout(() => { test.isTrue(Meteor.status().connected); test.equal( - Object.keys(DDP._reconnectHook.callbacks).length, + DDP._reconnectHook.size(), 1, 'Only one onReconnect callback should be registered' ); From 9eeb03c95510eeffc43f8a791c24426496c3a5b1 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 25 Jul 2025 19:26:21 +0300 Subject: [PATCH 05/66] fix: fix spiderable_client_tests.js --- .../spiderable/spiderable_client_tests.js | 76 +++++-------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/packages/deprecated/spiderable/spiderable_client_tests.js b/packages/deprecated/spiderable/spiderable_client_tests.js index c46c591ae3..f21a886e6f 100644 --- a/packages/deprecated/spiderable/spiderable_client_tests.js +++ b/packages/deprecated/spiderable/spiderable_client_tests.js @@ -1,8 +1,5 @@ Tinytest.add("spiderable - default hooks registered", function (test, expect) { - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 2 - ); + test.equal(Spiderable._onReadyHook.size(), 2); }); Tinytest.add("spiderable - is not ready while initial subscriptions aren't started", function (test, expect) { @@ -39,21 +36,12 @@ Tinytest.add("spiderable - default hooks can ready", function (test, expect) { }); Tinytest.add("spiderable - is not ready with a custom hook", function (test, expect) { - var callbacks = {} - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 2 - ); + test.equal(Spiderable._onReadyHook.size(), 2); //clear all/default callbacks - _.each(Spiderable._onReadyHook.callbacks, function (value,key,list) { - callbacks[key] = value; - delete list[key]; - }); - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 0 - ); + var callbacks = Spiderable._onReadyHook._asArray() + Spiderable._onReadyHook.clear(); + test.equal(Spiderable._onReadyHook.size(), 0); // actually test not ready @@ -62,41 +50,21 @@ Tinytest.add("spiderable - is not ready with a custom hook", function (test, exp // clear new callback - _.each(Spiderable._onReadyHook.callbacks, function (value,key,list) { - delete list[key]; - }); - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 0 - ); + Spiderable._onReadyHook.clear(); + test.equal(Spiderable._onReadyHook.size(), 0); // restore callbacks - _.each(callbacks, function (value,key,list) { - Spiderable._onReadyHook.callbacks[key] = value; - }); - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 2 - ); + Spiderable._onReadyHook._fromArray(callbacks); + test.equal(Spiderable._onReadyHook.size(), 2); }); Tinytest.add("spiderable - is ready with a custom hook", function (test, expect) { - var callbacks = {} - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 2 - ); + test.equal(Spiderable._onReadyHook.size(), 2); //clear all callbacks - _.each(Spiderable._onReadyHook.callbacks, function (value,key,list) { - callbacks[key] = value; - delete list[key]; - }); - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 0 - ); - + var callbacks = Spiderable._onReadyHook._asArray(); + Spiderable._onReadyHook.clear(); + test.equal(Spiderable._onReadyHook.size(), 0); // actually test ready Spiderable.addReadyCondition(function () { return true; }); @@ -104,20 +72,10 @@ Tinytest.add("spiderable - is ready with a custom hook", function (test, expect) // clear new callback - _.each(Spiderable._onReadyHook.callbacks, function (value,key,list) { - delete list[key]; - }); - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 0 - ); + Spiderable._onReadyHook.clear(); + test.equal(Spiderable._onReadyHook.size(), 0); // restore callbacks - _.each(callbacks, function (value,key,list) { - Spiderable._onReadyHook.callbacks[key] = value; - }); - test.equal( - _.keys(Spiderable._onReadyHook.callbacks).length, - 2 - ); + Spiderable._onReadyHook._fromArray(callbacks); + test.equal(Spiderable._onReadyHook.size(), 2); }); From 83d32d6da24598ffe55d5101bf5e1dd72c7728d1 Mon Sep 17 00:00:00 2001 From: Oleg Date: Sun, 27 Jul 2025 06:16:57 +0300 Subject: [PATCH 06/66] feat: Implement Iterable interface. Adds [Symbol.iterator] method to allow use with for...of loops. --- packages/callback-hook/hook.js | 90 ++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/packages/callback-hook/hook.js b/packages/callback-hook/hook.js index 88372251d5..2b7077fa16 100644 --- a/packages/callback-hook/hook.js +++ b/packages/callback-hook/hook.js @@ -37,28 +37,49 @@ // terminate calling the remaining callbacks if not caught. export class Hook { - constructor(options = {}) { - this.callbacks = new Set(); + /** + * Creates a new Hook instance. + * @param {object} [options={}] - Configuration options for the hook. + * @param {boolean} [options.bindEnvironment=true] - Whether to automatically wrap registered callbacks with `Meteor.bindEnvironment`. + * If `true`, callbacks will run in the Meteor environment of the code that registered them. + * @param {boolean} [options.wrapAsync=true] - Whether to automatically wrap registered callbacks with `Meteor.wrapFn`. + * If `true`, callbacks will be prepared to run asynchronously. + * @param {Function} [options.exceptionHandler] - A custom function to handle exceptions thrown by registered callbacks. + * This function will be called with the exception as its argument. + * If provided, `options.debugPrintExceptions` will be ignored. + * @param {string} [options.debugPrintExceptions] - If an `exceptionHandler` is not provided, and this option is a string, + * exceptions thrown by callbacks will be logged to `Meteor._debug` with this string as a description. + */ + constructor(options = {}) { + this.callbacks = new Set(); - // Whether to wrap callbacks with Meteor.bindEnvironment - const { bindEnvironment = true, wrapAsync = true } = options; - this.bindEnvironment = !!bindEnvironment; - this.wrapAsync = !!wrapAsync; + // Whether to wrap callbacks with Meteor.bindEnvironment + const { bindEnvironment = true, wrapAsync = true } = options; + this.bindEnvironment = !!bindEnvironment; + this.wrapAsync = !!wrapAsync; - if (options.exceptionHandler) { - this.exceptionHandler = options.exceptionHandler; - } else if (options.debugPrintExceptions) { - if (typeof options.debugPrintExceptions !== "string") { - throw new Error("Hook option debugPrintExceptions should be a string"); + if (options.exceptionHandler) { + this.exceptionHandler = options.exceptionHandler; + } else if (options.debugPrintExceptions) { + if (typeof options.debugPrintExceptions !== "string") { + throw new Error("Hook option debugPrintExceptions should be a string"); + } + this.exceptionHandler = options.debugPrintExceptions; } - this.exceptionHandler = options.debugPrintExceptions; } - } + /** + * Clears all registered callbacks from this Hook instance. + * After calling this method, the hook will have no callbacks registered. + */ clear() { this.callbacks.clear(); } + /** + * Returns the number of callbacks currently registered with this Hook instance. + * @returns {number} The number of registered callbacks. + */ size() { return this.callbacks.size; } @@ -76,6 +97,14 @@ export class Hook { this.callbacks = new Set(arr); } + /** + * Registers a new callback with this Hook instance. + * + * @param {Function} callback The function to register. This function will be called when the hook is iterated over. + * @returns {{callback: Function, stop: Function}} An object containing: + * - `callback`: The actual callback function that was added to the hook's internal set (after any wrapping). + * - `stop`: A function that, when called, unregisters this specific callback from the hook. + */ register(callback) { const exceptionHandler = this.exceptionHandler || function (exception) { // Note: this relies on the undocumented fact that if bindEnvironment's @@ -142,9 +171,28 @@ export class Hook { each(iterator) { return this.forEach(iterator); } + + /** + * Makes the Hook instance iterable, allowing it to be used in `for...of` loops. + * It iterates over the registered callbacks. + * @returns {Iterator} An iterator for the registered callbacks. + */ + [Symbol.iterator]() { + return this.callbacks[Symbol.iterator](); + } } -// Copied from Meteor.bindEnvironment and removed all the env stuff. +/** + * Wraps a given function with error handling. If the wrapped function throws an exception, + * it will be caught and passed to the provided exception handler. + * This is similar to `Meteor.bindEnvironment` but without the Meteor environment binding. + * + * @param {Function} func The function to wrap. + * @param {Function|string} onException The exception handler function to call if `func` throws, + * or a string description for default exception logging. + * @param {any} _this The `this` context to bind to `func` when it is called. + * @returns {Function} A new function that executes `func` with error handling. + */ function wrapHookWithErrorHandling(func, onException, _this) { const exceptionHandler = normalizeHookExceptionHandler(onException); return function executeHookWithErrorHandling(...args) { @@ -158,6 +206,16 @@ function wrapHookWithErrorHandling(func, onException, _this) { }; } +/** + * Normalizes an exception handler, ensuring it is a function. + * If a function is provided, it is returned directly. + * If a string is provided, it is used as a description for a default handler that logs exceptions. + * Otherwise, a generic default handler that logs exceptions with a default description is returned. + * + * @param {Function|string} exceptionHandler The exception handler to normalize. Can be a function, + * a string description for logging, or any other value (which defaults to generic logging). + * @returns {Function} A function that handles exceptions. + */ function normalizeHookExceptionHandler(exceptionHandler) { if (typeof exceptionHandler === 'function') { return exceptionHandler; @@ -167,8 +225,8 @@ function normalizeHookExceptionHandler(exceptionHandler) { const description = typeof exceptionHandler === 'string' ? exceptionHandler : "callback of async function"; - + return function defaultHookExceptionHandler(error) { Meteor._debug(`Exception in ${description}`, error); } -} \ No newline at end of file +} From c0b0b29131cf4c44000a6d6c8bd708b62c86bb32 Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 28 Jul 2025 11:07:13 +0300 Subject: [PATCH 07/66] refactor: Rename _asArray to asArray, _fromArray to fromArray --- packages/callback-hook/hook.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/callback-hook/hook.js b/packages/callback-hook/hook.js index 2b7077fa16..2638d3426d 100644 --- a/packages/callback-hook/hook.js +++ b/packages/callback-hook/hook.js @@ -84,15 +84,24 @@ export class Hook { return this.callbacks.size; } - // WARN: This method is for test compatibility only. - _asArray() { + /** + * Returns all registered callbacks as a new Array. + * This provides a snapshot of the current callbacks. + * @returns {Array} An array containing all registered callback functions. + */ + asArray() { return Array.from(this.callbacks); } - // WARN: This method is for test compatibility only. - _fromArray(arr) { + /** + * Replaces the current set of registered callbacks with a new set derived from the given array. + * + * @param {Array} arr An array of callback functions to register with this hook. + * @throws {Error} If the provided argument `arr` is not an array. + */ + fromArray(arr) { if (!Array.isArray(arr)) { - throw new Error("Method _fromArray expects an array"); + throw new Error("Method fromArray expects an array"); } this.callbacks = new Set(arr); } From dc2daa160b7ea493ee80f3c330d8dbea9c8a7465 Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 28 Jul 2025 11:08:31 +0300 Subject: [PATCH 08/66] fix: Fix Spiderable package tests --- packages/deprecated/spiderable/spiderable_client_tests.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/deprecated/spiderable/spiderable_client_tests.js b/packages/deprecated/spiderable/spiderable_client_tests.js index f21a886e6f..bcd2f2db41 100644 --- a/packages/deprecated/spiderable/spiderable_client_tests.js +++ b/packages/deprecated/spiderable/spiderable_client_tests.js @@ -39,7 +39,7 @@ Tinytest.add("spiderable - is not ready with a custom hook", function (test, exp test.equal(Spiderable._onReadyHook.size(), 2); //clear all/default callbacks - var callbacks = Spiderable._onReadyHook._asArray() + var callbacks = Spiderable._onReadyHook.asArray() Spiderable._onReadyHook.clear(); test.equal(Spiderable._onReadyHook.size(), 0); @@ -54,7 +54,7 @@ Tinytest.add("spiderable - is not ready with a custom hook", function (test, exp test.equal(Spiderable._onReadyHook.size(), 0); // restore callbacks - Spiderable._onReadyHook._fromArray(callbacks); + Spiderable._onReadyHook.fromArray(callbacks); test.equal(Spiderable._onReadyHook.size(), 2); }); @@ -62,7 +62,7 @@ Tinytest.add("spiderable - is ready with a custom hook", function (test, expect) test.equal(Spiderable._onReadyHook.size(), 2); //clear all callbacks - var callbacks = Spiderable._onReadyHook._asArray(); + var callbacks = Spiderable._onReadyHook.asArray(); Spiderable._onReadyHook.clear(); test.equal(Spiderable._onReadyHook.size(), 0); @@ -76,6 +76,6 @@ Tinytest.add("spiderable - is ready with a custom hook", function (test, expect) test.equal(Spiderable._onReadyHook.size(), 0); // restore callbacks - Spiderable._onReadyHook._fromArray(callbacks); + Spiderable._onReadyHook.fromArray(callbacks); test.equal(Spiderable._onReadyHook.size(), 2); }); From a77246874d472d781b71910969c1e84b4d4dd937 Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 28 Jul 2025 11:09:19 +0300 Subject: [PATCH 09/66] chore: Set package version to 1.7.0 --- packages/callback-hook/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/callback-hook/package.js b/packages/callback-hook/package.js index 9d9d33d6c4..82df7371bb 100644 --- a/packages/callback-hook/package.js +++ b/packages/callback-hook/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Register callbacks on a hook", - version: '1.7.1', + version: '1.7.0', }); Package.onUse(function (api) { From 5716ea7676110f57f0e17d916a0cac5608082be2 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 30 Jul 2025 18:32:58 +0300 Subject: [PATCH 10/66] chore: Set package version to 1.6.1 --- packages/callback-hook/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/callback-hook/package.js b/packages/callback-hook/package.js index 82df7371bb..6f9e9ac68a 100644 --- a/packages/callback-hook/package.js +++ b/packages/callback-hook/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Register callbacks on a hook", - version: '1.7.0', + version: '1.6.1', }); Package.onUse(function (api) { From 193a0d85d3efaafbebe1e00a1fe24bf509df0d54 Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 12 Aug 2025 16:04:50 +0300 Subject: [PATCH 11/66] chore: Set package version to 1.6.2 --- packages/callback-hook/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/callback-hook/package.js b/packages/callback-hook/package.js index 6f9e9ac68a..c706df472a 100644 --- a/packages/callback-hook/package.js +++ b/packages/callback-hook/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Register callbacks on a hook", - version: '1.6.1', + version: '1.6.2', }); Package.onUse(function (api) { From 5240b244c404237d864d1763c3da83ce816dbd85 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 10 Sep 2025 20:44:42 +0300 Subject: [PATCH 12/66] chore: remove TODO --- packages/callback-hook/hook.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/callback-hook/hook.js b/packages/callback-hook/hook.js index 2638d3426d..8d900c20fc 100644 --- a/packages/callback-hook/hook.js +++ b/packages/callback-hook/hook.js @@ -230,7 +230,6 @@ function normalizeHookExceptionHandler(exceptionHandler) { return exceptionHandler; } - // TODO: The message "callback of async function" is not very useful and needs clarification. const description = typeof exceptionHandler === 'string' ? exceptionHandler : "callback of async function"; From 27aca8486194fc3957a3f8dafefe0c25114c24fe Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 2 Feb 2026 10:44:19 -0300 Subject: [PATCH 13/66] DEV: make descriptions all the same for defer functions --- packages/meteor/meteor.d.ts | 6 +++--- packages/meteor/timers.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/meteor/meteor.d.ts b/packages/meteor/meteor.d.ts index 9edb9cff6d..a8cbda2bb7 100644 --- a/packages/meteor/meteor.d.ts +++ b/packages/meteor/meteor.d.ts @@ -328,7 +328,7 @@ export namespace Meteor { function defer(func: Function): void; /** - * Wrap a function so that it only runs in the specified environments. + * Wrap a function so that it only runs in background in specified environments. * @param func The function to wrap * @param options An object with an `on` property that is an array of environment names: `"development"`, `"production"`, and/or `"test"`. */ @@ -338,13 +338,13 @@ export namespace Meteor { ): T | void; /** - * Wrap a function so that it only runs in development environment. + * Wrap a function to run in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). * @param func The function to wrap */ function deferDev(func: T): T | void; /** - * Wrap a function so that it only runs in production environment. + * Wrap a function to run in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)). * @param func The function to wrap */ function deferProd(func: T): T | void; diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index a6c5d25c0d..eaa4d54d04 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -115,7 +115,7 @@ Meteor.deferrable = function (f, options) { /** * @memberOf Meteor - * @summary Defer execution of a function to run asynchronously in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). + * @summary Wrap a function to run in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). * @locus Anywhere * @param {Function} func The function to run * @param {Object} options The options object @@ -126,7 +126,7 @@ Meteor.deferDev = function (f) { /** * @memberOf Meteor - * @summary Defer execution of a function to run asynchronously in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)). + * @summary Wrap a function to run in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)). * @locus Anywhere * @param {Function} func The function to run * @param {Object} options The options object From 96310abf71177ff2d8903f66fd2f5fe03e601e33 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 2 Feb 2026 11:39:49 -0300 Subject: [PATCH 14/66] dev: updated types --- packages/meteor/meteor.d.ts | 12 ++++++++---- packages/meteor/timers.js | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/meteor/meteor.d.ts b/packages/meteor/meteor.d.ts index a8cbda2bb7..1f8611dc48 100644 --- a/packages/meteor/meteor.d.ts +++ b/packages/meteor/meteor.d.ts @@ -332,8 +332,8 @@ export namespace Meteor { * @param func The function to wrap * @param options An object with an `on` property that is an array of environment names: `"development"`, `"production"`, and/or `"test"`. */ - function deferrable( - func: T, + function deferrable( + func: () => T, options: { on: Array<"development" | "production" | "test"> } ): T | void; @@ -341,13 +341,17 @@ export namespace Meteor { * Wrap a function to run in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). * @param func The function to wrap */ - function deferDev(func: T): T | void; + function deferDev( + func: () => T + ): T | void; /** * Wrap a function to run in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)). * @param func The function to wrap */ - function deferProd(func: T): T | void; + function deferProd( + func: () => T + ): T | void; /** Timeout **/ /** utils **/ diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index eaa4d54d04..dec0f52723 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -86,7 +86,7 @@ Meteor.defer = function (f) { /** * @memberOf Meteor - * @summary Defer execution of a function to run asynchronously in the background based on environment (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). + * @summary Wrap a function so that it only runs in background in specified environments.. * @locus Anywhere * @param {Function} func The function to run * @param {Object} options The options object From 1d738b5ae079ef4ff8e35d7b735ea670b1853cac Mon Sep 17 00:00:00 2001 From: Evan Broder Date: Tue, 10 Feb 2026 22:11:13 -0800 Subject: [PATCH 15/66] Handle deleted PostCSS dependency files gracefully When a file tracked as a PostCSS dependency (e.g. by Tailwind v4's auto-content-detection) is deleted between builds, the CSS minifier would crash with ENOENT instead of treating it as a cache invalidation. Skip optimisticReadFile when optimisticHashOrNull already returned null, and use a sentinel value in the dep hash so the cache key changes and the CSS pipeline re-runs. Fixes #13633 Co-Authored-By: Claude Opus 4.6 --- packages/standard-minifier-css/plugin/postcss.js | 2 +- tools/isobuild/minifier-plugin.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/standard-minifier-css/plugin/postcss.js b/packages/standard-minifier-css/plugin/postcss.js index a34d3fd455..d5ee3d38bb 100644 --- a/packages/standard-minifier-css/plugin/postcss.js +++ b/packages/standard-minifier-css/plugin/postcss.js @@ -114,7 +114,7 @@ export const watchAndHashDeps = Profile( if (dep.type === 'dependency') { fileCount += 1; const fileHash = hashAndWatchFile(dep.file); - hash.update(fileHash).update('\0'); + hash.update(fileHash || 'deleted').update('\0'); } else if (dep.type === 'dir-dependency') { if (dep.dir in globsByDir) { globsByDir[dep.dir].push(dep.glob || '**'); diff --git a/tools/isobuild/minifier-plugin.js b/tools/isobuild/minifier-plugin.js index 10c7a3ec56..44edf7e7f3 100644 --- a/tools/isobuild/minifier-plugin.js +++ b/tools/isobuild/minifier-plugin.js @@ -91,7 +91,7 @@ export class CssFile extends InputFile { const filePath = convertToPosixPath(path); const hash = optimisticHashOrNull(filePath); - const contents = optimisticReadFile(filePath); + const contents = hash !== null ? optimisticReadFile(filePath) : null; this._watchSet.addFile(filePath, hash); return { From 5ead5800bf4bf72cc70a9a1f5e015d4376ddd60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blaisot?= Date: Tue, 17 Feb 2026 10:27:10 +0100 Subject: [PATCH 16/66] fix: invalidate stale dev_bundle cache after git branch switch After switching git branches, .meteor/release changes but .meteor/local/dev_bundle (which is gitignored) keeps pointing to the previous release's dev_bundle. This causes `meteor node` to use the wrong Node.js version until another command like `meteor --version` triggers a full springboard and refreshes the symlink. Store the release string in .meteor/local/dev_bundle_release alongside the dev_bundle symlink. Before trusting the cached symlink, verify it still matches the current .meteor/release content. --- tools/cli/dev-bundle.js | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tools/cli/dev-bundle.js b/tools/cli/dev-bundle.js index 5a60eff3a8..2be9789140 100644 --- a/tools/cli/dev-bundle.js +++ b/tools/cli/dev-bundle.js @@ -37,12 +37,7 @@ async function getDevBundleDir() { } const devBundleLink = path.join(localDir, "dev_bundle"); - const devBundleStat = statOrNull(devBundleLink); - if (devBundleStat) { - return new Promise(function (resolve) { - resolve(links.readLink(devBundleLink)); - }); - } + const devBundleReleaseFile = path.join(localDir, "dev_bundle_release"); const release = fs.readFileSync( releaseFile, "utf8" @@ -52,10 +47,38 @@ async function getDevBundleDir() { return DEFAULT_DEV_BUNDLE_DIR; } + // Check if the cached dev_bundle link still matches the current release. + // After a git branch switch, .meteor/release changes but + // .meteor/local/dev_bundle (which is gitignored) keeps pointing to + // the old release's dev_bundle. + const devBundleStat = statOrNull(devBundleLink); + if (devBundleStat) { + var cachedRelease = null; + try { + cachedRelease = fs.readFileSync( + devBundleReleaseFile, "utf8" + ).replace(/^\s+|\s+$/g, ""); + } catch (e) { + // If the release cache file doesn't exist, invalidate the cache + // so we re-resolve the dev_bundle for the current release. + } + + if (cachedRelease === release) { + return new Promise(function (resolve) { + resolve(links.readLink(devBundleLink)); + }); + } + } + const devBundleDir = await getDevBundleForRelease(release); if (devBundleDir) { links.makeLink(devBundleDir, devBundleLink); + try { + fs.writeFileSync(devBundleReleaseFile, release, "utf8"); + } catch (e) { + // Non-fatal: the link itself was created successfully. + } return devBundleDir; } From 875fe1d51e54028d172d3de1d325f2d6bcd33df5 Mon Sep 17 00:00:00 2001 From: Frederico Maia Date: Wed, 18 Mar 2026 15:17:18 -0300 Subject: [PATCH 17/66] Add TypeScript + Tailwind skeleton support - Introduced a new skeleton type "typescript-tailwind". - Added necessary configuration files and directories for the new skeleton. - Updated documentation to reflect the new skeleton option in the CLI. --- tools/cli/commands.js | 5 +- .../skel-typescript-tailwind/.gitignore | 7 +++ .../.meteor/.gitignore | 1 + .../skel-typescript-tailwind/.meteor/packages | 26 ++++++++++ .../.meteor/platforms | 2 + .../skel-typescript-tailwind/.meteorignore | 2 + .../skel-typescript-tailwind/client/main.css | 6 +++ .../skel-typescript-tailwind/client/main.html | 8 ++++ .../skel-typescript-tailwind/client/main.tsx | 12 +++++ .../imports/api/links.ts | 10 ++++ .../imports/ui/App.tsx | 10 ++++ .../imports/ui/Hello.tsx | 42 +++++++++++++++++ .../imports/ui/Info.tsx | 47 +++++++++++++++++++ .../skel-typescript-tailwind/package.json | 46 ++++++++++++++++++ .../postcss.config.js | 5 ++ .../skel-typescript-tailwind/rspack.config.ts | 31 ++++++++++++ .../skel-typescript-tailwind/server/main.ts | 37 +++++++++++++++ .../skel-typescript-tailwind/tests/main.ts | 21 +++++++++ .../skel-typescript-tailwind/tsconfig.json | 26 ++++++++++ v3-docs/docs/cli/index.md | 2 + 20 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 tools/static-assets/skel-typescript-tailwind/.gitignore create mode 100644 tools/static-assets/skel-typescript-tailwind/.meteor/.gitignore create mode 100644 tools/static-assets/skel-typescript-tailwind/.meteor/packages create mode 100644 tools/static-assets/skel-typescript-tailwind/.meteor/platforms create mode 100644 tools/static-assets/skel-typescript-tailwind/.meteorignore create mode 100644 tools/static-assets/skel-typescript-tailwind/client/main.css create mode 100644 tools/static-assets/skel-typescript-tailwind/client/main.html create mode 100644 tools/static-assets/skel-typescript-tailwind/client/main.tsx create mode 100644 tools/static-assets/skel-typescript-tailwind/imports/api/links.ts create mode 100644 tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx create mode 100644 tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx create mode 100644 tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx create mode 100644 tools/static-assets/skel-typescript-tailwind/package.json create mode 100644 tools/static-assets/skel-typescript-tailwind/postcss.config.js create mode 100644 tools/static-assets/skel-typescript-tailwind/rspack.config.ts create mode 100644 tools/static-assets/skel-typescript-tailwind/server/main.ts create mode 100644 tools/static-assets/skel-typescript-tailwind/tests/main.ts create mode 100644 tools/static-assets/skel-typescript-tailwind/tsconfig.json diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 6c26667dd7..76a6081cb2 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -697,6 +697,7 @@ export const AVAILABLE_SKELETONS = [ "minimal", DEFAULT_SKELETON, "typescript", + "typescript-tailwind", "vue", "svelte", "tailwind", @@ -715,6 +716,7 @@ const SKELETON_INFO = { "minimal": "To create an app with as few Meteor packages as possible", "react": "To create a basic React-based app", "typescript": "To create an app using TypeScript and React", + "typescript-tailwind": "To create an app using TypeScript, React, and Tailwind", "vue": "To create a basic Vue3-based app", "svelte": "To create a basic Svelte app", "tailwind": "To create an app using React and Tailwind", @@ -722,7 +724,7 @@ const SKELETON_INFO = { "solid": "To create a basic Solid app", "coffeescript": "To create a basic CoffeeScript app", "babel": "To create a React app with Babel support", - "angular": "To create a basic Angular app", + "angular": "To create a basic Angular app" }; main.registerCommand({ @@ -741,6 +743,7 @@ main.registerCommand({ react: { type: Boolean }, vue: { type: Boolean }, typescript: { type: Boolean }, + 'typescript-tailwind': { type: Boolean }, apollo: { type: Boolean }, svelte: { type: Boolean }, tailwind: { type: Boolean }, diff --git a/tools/static-assets/skel-typescript-tailwind/.gitignore b/tools/static-assets/skel-typescript-tailwind/.gitignore new file mode 100644 index 0000000000..3e954aa73f --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/.gitignore @@ -0,0 +1,7 @@ +node_modules/ + +# Meteor Modern-Tools build context directories +_build +*/build-assets +*/build-chunks +.rsdoctor diff --git a/tools/static-assets/skel-typescript-tailwind/.meteor/.gitignore b/tools/static-assets/skel-typescript-tailwind/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/static-assets/skel-typescript-tailwind/.meteor/packages b/tools/static-assets/skel-typescript-tailwind/.meteor/packages new file mode 100644 index 0000000000..00d004f864 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/.meteor/packages @@ -0,0 +1,26 @@ +# 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 # Packages every Meteor app needs to have +mobile-experience # Packages for a great mobile UX +mongo # The database Meteor supports right now +reactive-var # Reactive variable for tracker + +standard-minifier-css # CSS minifier run for production mode +standard-minifier-js # JS minifier run for production mode +es5-shim # ECMAScript 5 compatibility for older browsers +ecmascript # Enable ECMAScript2015+ syntax in app code +typescript # Enable TypeScript syntax in .ts and .tsx modules +shell-server # Server-side component of the `meteor shell` command +hot-module-replacement # Update client in development without reloading the page + +~prototype~ +static-html # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data + +rspack # Integrate Rspack into Meteor for client and server app bundling + +zodern:types # Pull in type declarations from other Meteor packages \ No newline at end of file diff --git a/tools/static-assets/skel-typescript-tailwind/.meteor/platforms b/tools/static-assets/skel-typescript-tailwind/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/static-assets/skel-typescript-tailwind/.meteorignore b/tools/static-assets/skel-typescript-tailwind/.meteorignore new file mode 100644 index 0000000000..4568c54a7d --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/.meteorignore @@ -0,0 +1,2 @@ +# Ignore Meteor CSS handling; let Rspack resolve Tailwind styles +client/main.css diff --git a/tools/static-assets/skel-typescript-tailwind/client/main.css b/tools/static-assets/skel-typescript-tailwind/client/main.css new file mode 100644 index 0000000000..6ea2603dca --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/client/main.css @@ -0,0 +1,6 @@ +@import "tailwindcss"; + +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/static-assets/skel-typescript-tailwind/client/main.html b/tools/static-assets/skel-typescript-tailwind/client/main.html new file mode 100644 index 0000000000..c710d3dbd3 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/client/main.html @@ -0,0 +1,8 @@ + + ~name~ + + + + +
+ diff --git a/tools/static-assets/skel-typescript-tailwind/client/main.tsx b/tools/static-assets/skel-typescript-tailwind/client/main.tsx new file mode 100644 index 0000000000..a86c160a73 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/client/main.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Meteor } from 'meteor/meteor'; +import { createRoot } from 'react-dom/client'; +import { App } from '/imports/ui/App'; +import './main.css'; + +Meteor.startup(() => { + const target = document.getElementById('react-target'); + if (target) { + createRoot(target).render(); + } +}); diff --git a/tools/static-assets/skel-typescript-tailwind/imports/api/links.ts b/tools/static-assets/skel-typescript-tailwind/imports/api/links.ts new file mode 100644 index 0000000000..ec0bf4631f --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/imports/api/links.ts @@ -0,0 +1,10 @@ +import { Mongo } from 'meteor/mongo'; + +export interface Link { + _id?: string; + title: string; + url: string; + createdAt: Date; +} + +export const LinksCollection = new Mongo.Collection('links'); diff --git a/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx b/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx new file mode 100644 index 0000000000..90a2b06b9e --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Hello } from './Hello'; +import { Info } from './Info'; + +export const App = () => ( +
+ + +
+); diff --git a/tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx b/tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx new file mode 100644 index 0000000000..9d0e05237c --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+
+
+
+
+

+ Welcome to Meteor! +

+
+ +
+
+
+

+ You've pressed the button {counter} times. +

+
+
+
+ +
+
+
+
+ ) +}; diff --git a/tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx b/tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx new file mode 100644 index 0000000000..bc7a35f123 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { useFind, useSubscribe } from "meteor/react-meteor-data/suspense"; +import { LinksCollection } from "../api/links"; + +export const Info = () => { + useSubscribe("links"); + const data = useFind(LinksCollection, []); + + return ( +
+ {data.map((link) => ( +
+
+ + + + + +
+ + +
+ ))} +
+ ); +}; diff --git a/tools/static-assets/skel-typescript-tailwind/package.json b/tools/static-assets/skel-typescript-tailwind/package.json new file mode 100644 index 0000000000..a75fe7e2a0 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/package.json @@ -0,0 +1,46 @@ +{ + "name": "~name~", + "private": true, + "scripts": { + "start": "meteor lint && meteor run", + "test": "meteor test --once --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "visualize": "meteor --production --extra-packages bundle-visualizer", + "generate-types": "meteor lint" + }, + "dependencies": { + "@babel/runtime": "^7.23.5", + "@swc/helpers": "^0.5.17", + "autoprefixer": "^10.4.4", + "meteor-node-stubs": "^1.2.12", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@meteorjs/rspack": "^1.0.1", + "@rsdoctor/rspack-plugin": "^1.2.3", + "@rspack/cli": "^1.7.1", + "@rspack/core": "^1.7.1", + "@rspack/plugin-react-refresh": "^1.4.3", + "@tailwindcss/postcss": "^4.1.12", + "@types/meteor": "^2.9.9", + "@types/mocha": "^8.2.3", + "@types/node": "^22.10.6", + "@types/react": "^18.2.5", + "@types/react-dom": "^18.2.4", + "postcss": "^8.5.6", + "postcss-loader": "^8.1.1", + "react-refresh": "^0.17.0", + "tailwindcss": "^4.1.12", + "ts-checker-rspack-plugin": "^1.1.5", + "typescript": "^5.9.3" + }, + "meteor": { + "mainModule": { + "client": "client/main.tsx", + "server": "server/main.ts" + }, + "testModule": "tests/main.ts", + "modern": true + } +} diff --git a/tools/static-assets/skel-typescript-tailwind/postcss.config.js b/tools/static-assets/skel-typescript-tailwind/postcss.config.js new file mode 100644 index 0000000000..c2ddf74822 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/tools/static-assets/skel-typescript-tailwind/rspack.config.ts b/tools/static-assets/skel-typescript-tailwind/rspack.config.ts new file mode 100644 index 0000000000..99ea60b81d --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/rspack.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from "@meteorjs/rspack"; +import { TsCheckerRspackPlugin } from "ts-checker-rspack-plugin"; + +/** + * Rspack configuration for Meteor projects. + * + * Provides typed flags on the `Meteor` object, such as: + * - `Meteor.isClient` / `Meteor.isServer` + * - `Meteor.isDevelopment` / `Meteor.isProduction` + * - …and other flags available + * + * Use these flags to adjust your build settings based on environment. + */ +export default defineConfig(Meteor => { + return { + ...Meteor.isClient && { + plugins: [ + ...(!Meteor.isTest && !Meteor.isAppTest ? [new TsCheckerRspackPlugin()] : []), + ], + module: { + rules: [ + { + test: /\.css$/, + use: ["postcss-loader"], + type: "css", + }, + ], + }, + }, + }; +}); diff --git a/tools/static-assets/skel-typescript-tailwind/server/main.ts b/tools/static-assets/skel-typescript-tailwind/server/main.ts new file mode 100644 index 0000000000..a2619f24e8 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/server/main.ts @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; +import { Link, LinksCollection } from '/imports/api/links'; + +async function insertLink({ title, url }: Pick) { + await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); +} + +Meteor.startup(async () => { + // If the Links collection is empty, add some data. + if (await LinksCollection.find().countAsync() === 0) { + await insertLink({ + title: 'Do the Tutorial', + url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + }); + + await insertLink({ + title: 'Follow the Guide', + url: 'http://guide.meteor.com', + }); + + await insertLink({ + title: 'Read the Docs', + url: 'https://docs.meteor.com', + }); + + await insertLink({ + title: 'Discussions', + url: 'https://forums.meteor.com', + }); + } + + // We publish the entire Links collection to all clients. + // In order to be fetched in real-time to the clients + Meteor.publish("links", function () { + return LinksCollection.find(); + }); +}); diff --git a/tools/static-assets/skel-typescript-tailwind/tests/main.ts b/tools/static-assets/skel-typescript-tailwind/tests/main.ts new file mode 100644 index 0000000000..1b098e1aa9 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/tests/main.ts @@ -0,0 +1,21 @@ +import assert from "assert"; +import { Meteor } from "meteor/meteor"; + +describe("~name~", function () { + it("package.json has correct name", async function () { + const { name } = await import("../package.json"); + assert.strictEqual(name, "~name~"); + }); + + if (Meteor.isClient) { + it("client is not server", function () { + assert.strictEqual(Meteor.isServer, false); + }); + } + + if (Meteor.isServer) { + it("server is not client", function () { + assert.strictEqual(Meteor.isClient, false); + }); + } +}); diff --git a/tools/static-assets/skel-typescript-tailwind/tsconfig.json b/tools/static-assets/skel-typescript-tailwind/tsconfig.json new file mode 100644 index 0000000000..21922a37d9 --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "jsx": "react", + "strict": false, + "noEmit": true, + "esModuleInterop": true, + "allowJs": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "/*": ["./*"], + "meteor/react-meteor-data/suspense": [ + ".meteor/local/types/node_modules/package-types/react-meteor-data/package/os/suspense/react-meteor-data.d.ts" + ], + "meteor/*": [ + "node_modules/@types/meteor/*", + ".meteor/local/types/packages.d.ts" + ] + }, + "types": ["meteor", "mocha"] + }, + "exclude": ["node_modules", ".meteor"] +} diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index efb8527bea..20bb00030c 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -246,6 +246,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive Minimal # To create an app with as few Meteor packages as possible React # To create a basic React-based app Typescript # To create an app using TypeScript and React + Typescript-tailwind # To create an app using TypeScript, React, and Tailwind Vue # To create a basic Vue3-based app Svelte # To create a basic Svelte app Tailwind # To create an app using React and Tailwind @@ -278,6 +279,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive | `--apollo` | React + Apollo (GraphQL) | [Meteor 2 with GraphQL](https://react-tutorial.meteor.com/simple-todos-graphql/) | | `--typescript` | React + TypeScript | [TypeScript Guide](/about/build-tool#typescript) | | `--tailwind` | React + Tailwind CSS | - | +| `--typescript-tailwind` | React + TypeScript + Tailwind CSS | - | | `--chakra-ui` | React + Chakra UI | [Simple Tasks Example](https://github.com/fredmaiaarantes/simpletasks) | | `--coffeescript` | CoffeeScript | - | | `--babel` | React with Babel support | - | From 07ccbb2895a5828887d19a2d6b0267a88fb3c6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 24 Mar 2026 17:03:00 +0100 Subject: [PATCH 18/66] enable automatic CSS delegation in Meteor-Rspack integration and improve handling of stylesheet extensions --- npm-packages/meteor-rspack/README.md | 1 + .../plugins/MeteorRspackOutputPlugin.js | 37 +++++++++++++++++- npm-packages/meteor-rspack/rspack.config.js | 7 +++- packages/rspack/lib/compilation.js | 9 ++++- packages/rspack/lib/config.js | 39 +++++++++++++++++++ .../rspack-bundler-integration.md | 4 +- 6 files changed, 92 insertions(+), 5 deletions(-) diff --git a/npm-packages/meteor-rspack/README.md b/npm-packages/meteor-rspack/README.md index e5ea9413a4..b2ea3be3a4 100644 --- a/npm-packages/meteor-rspack/README.md +++ b/npm-packages/meteor-rspack/README.md @@ -14,6 +14,7 @@ When Meteor runs with the Rspack bundler enabled, this package is what generates - **Asset externals and HTML generation** through custom Rspack plugins - **A `defineConfig` helper** that accepts a factory function receiving Meteor environment flags and build utilities - **Customizable config** via `rspack.config.js` in your project root, with safe merging that warns if you try to override reserved settings +- **Automatic CSS delegation** — when rspack is configured with CSS, Less, or SCSS loaders, Meteor automatically detects the handled extensions after the first compilation and stops processing those files itself. No `.meteorignore` entries needed. ## Installation diff --git a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js index a4494ae49e..274e938a7e 100644 --- a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js +++ b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js @@ -6,6 +6,40 @@ const { outputMeteorRspack } = require('../lib/meteorRspackHelpers'); +/** + * Extracts file extensions that rspack is configured to handle + * from the resolved module.rules test patterns. + * Only returns extensions relevant for Meteor delegation (CSS-family). + * @param {import('@rspack/core').Compiler} compiler + * @returns {string[]} Array of extensions like ['.css', '.less', '.scss'] + */ +function extractDelegatedExtensions(compiler) { + const delegatableExtensions = ['.css', '.less', '.scss', '.sass', '.styl']; + const found = new Set(); + + function inspectRules(rules) { + for (const rule of rules) { + if (!rule) continue; + if (rule.test) { + const testStr = rule.test instanceof RegExp + ? rule.test.source + : String(rule.test); + for (const ext of delegatableExtensions) { + const escaped = ext.replace('.', '\\.'); + if (testStr.includes(escaped)) { + found.add(ext); + } + } + } + if (rule.oneOf) inspectRules(rule.oneOf); + if (rule.rules) inspectRules(rule.rules); + } + } + + inspectRules(compiler.options.module?.rules || []); + return Array.from(found); +} + class MeteorRspackOutputPlugin { constructor(options = {}) { this.pluginName = 'MeteorRspackOutputPlugin'; @@ -26,6 +60,7 @@ class MeteorRspackOutputPlugin { ...(this.getData(stats, { compilationCount: this.compilationCount, isRebuild: this.compilationCount > 1, + compiler, }) || {}), }; outputMeteorRspack(data); @@ -33,4 +68,4 @@ class MeteorRspackOutputPlugin { } } -module.exports = { MeteorRspackOutputPlugin }; +module.exports = { MeteorRspackOutputPlugin, extractDelegatedExtensions }; diff --git a/npm-packages/meteor-rspack/rspack.config.js b/npm-packages/meteor-rspack/rspack.config.js index ea1fa3da79..192d4bd0fc 100644 --- a/npm-packages/meteor-rspack/rspack.config.js +++ b/npm-packages/meteor-rspack/rspack.config.js @@ -10,7 +10,7 @@ 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 { MeteorRspackOutputPlugin, extractDelegatedExtensions } = require('./plugins/MeteorRspackOutputPlugin.js'); const { generateEagerTestFile } = require("./lib/test.js"); const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore"); const { @@ -843,7 +843,7 @@ module.exports = async function (inMeteor = {}, argv = {}) { // Add MeteorRspackOutputPlugin as the last plugin to output compilation info const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({ - getData: (stats, { isRebuild, compilationCount }) => ({ + getData: (stats, { isRebuild, compilationCount, compiler }) => ({ name: config.name, mode: config.mode, hasErrors: stats.hasErrors(), @@ -852,6 +852,9 @@ module.exports = async function (inMeteor = {}, argv = {}) { statsOverrided, compilationCount, isRebuild, + ...(!isRebuild && compiler && { + delegatedExtensions: extractDelegatedExtensions(compiler), + }), }), }); config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])]; diff --git a/packages/rspack/lib/compilation.js b/packages/rspack/lib/compilation.js index 1a3de1511e..f0850fb642 100644 --- a/packages/rspack/lib/compilation.js +++ b/packages/rspack/lib/compilation.js @@ -16,6 +16,8 @@ const { setGlobalState } = require('meteor/tools-core/lib/global-state'); +const { applyDelegatedExtensions } = require('./config'); + // Helper function to format milliseconds with comma separators function formatMilliseconds(ms) { return ms.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); @@ -124,10 +126,15 @@ export function setupCompilationTracking() { }; // Define separate onCompile callbacks for client and server - const onCompileClient = (data) => { + const onCompileClient = (data, config) => { // Resolve the promise if it's the first compilation const clientState = getGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientFirstCompile); if (!clientState?.resolved) { + // Apply delegated extensions before resolving (so they're set before Meteor scans) + if (config?.delegatedExtensions?.length > 0) { + applyDelegatedExtensions(config.delegatedExtensions); + } + clientState.resolved = true; clientState.resolve(); setGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientState); diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 5771d25fb2..17b3aa61f8 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -386,3 +386,42 @@ export function configureMeteorForRspack() { } } } + +/** + * Applies delegated extension ignore patterns for entry folder files. + * Called after rspack's first compilation reports which extensions it handles. + * Since Meteor awaits rspack compilation before scanning files, these patterns + * are in place before Meteor processes any application files. + * + * Uses gitignore semantics: a later positive pattern (client/*.css) overrides + * an earlier negation (!client/*.css) that was set in configureMeteorForRspack. + * + * @param {string[]} extensions - Array of extensions like ['.css', '.less'] + */ +export function applyDelegatedExtensions(extensions) { + if (!extensions || extensions.length === 0) return; + + const initialEntrypoints = getInitialEntrypoints(); + const entrypointContexts = [ + initialEntrypoints.mainClient, + initialEntrypoints.mainServer, + ] + .filter(Boolean) + .map(entrypoint => path.dirname(entrypoint)); + + const ignorePatterns = []; + for (const dir of entrypointContexts) { + for (const ext of extensions) { + // ext comes as '.css', glob needs '*.css' + ignorePatterns.push(`${dir}/*${ext}`); + } + } + + if (ignorePatterns.length > 0) { + setMeteorAppIgnore(ignorePatterns.join(' ')); + + if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { + logInfo(`[i] Rspack delegated extensions: ${extensions.join(', ')} (ignored in entry folders)`); + } + } +} 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 46ee9d8fe2..f718a4c5f0 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 @@ -229,7 +229,7 @@ Ensure your app defines these entry files with the correct paths where each modu Defining entry points improves performance even with the Meteor bundler, as Meteor stops scanning and eagerly loading unnecessary files. For Meteor-Rspack integration, this is required, since it does not support automatic code discovery for efficiency. -In Meteor-Rspack integration, all app code is ignored by Meteor and handled by Rspack. By default, Meteor still processes eagerly CSS and HTML files in the entry folder (e.g. `client/*.[html|css]` in most apps). +In Meteor-Rspack integration, all app code is ignored by Meteor and handled by Rspack. By default, Meteor still processes eagerly HTML files in the entry folder (e.g. `client/*.html` in most apps). CSS files in the entry folder are automatically delegated to Rspack when a CSS loader is configured, see [CSS](#css) for details. If no CSS loader is present, Meteor handles them as before. If you need Meteor to handle CSS or HTML files outside the main entry folder, add them to the `modules` field. This field accepts an array of strings, each pointing to a file or folder. @@ -422,6 +422,8 @@ With the Meteor–Rspack integration, `zodern:melte` no longer works. Use the of Meteor-Rspack comes with built-in CSS support. You can import any CSS file into your code, and it will be processed and included in your HTML skeleton automatically. In addition, any CSS file placed in the same folder as your Meteor entry point will be processed and added as global styles without the need for explicit imports. +When Rspack is configured with a CSS rule, whether through `postcss-loader`, `type: "css"`, or any other CSS-handling loader, Meteor automatically detects the handled file extensions after Rspack's first compilation and stops processing those files itself. This means you do not need to manually add CSS files to `.meteorignore` or otherwise tell Meteor to skip them. The same automatic delegation applies to Less and SCSS when their respective loaders are configured. If no CSS rule is present in the rspack configuration, Meteor continues to handle stylesheets as it normally would. + ### CSS Modules [CSS Modules](https://rspack.rs/guide/tech/css#css-modules) are supported out of the box — any file named `*.module.css` is automatically scoped locally. From c39ab099d061681ce6dbae00043178560d142515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 24 Mar 2026 17:12:13 +0100 Subject: [PATCH 19/66] clarify context for automatic CSS delegation in README --- npm-packages/meteor-rspack/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm-packages/meteor-rspack/README.md b/npm-packages/meteor-rspack/README.md index b2ea3be3a4..e7787cfaa1 100644 --- a/npm-packages/meteor-rspack/README.md +++ b/npm-packages/meteor-rspack/README.md @@ -14,7 +14,7 @@ When Meteor runs with the Rspack bundler enabled, this package is what generates - **Asset externals and HTML generation** through custom Rspack plugins - **A `defineConfig` helper** that accepts a factory function receiving Meteor environment flags and build utilities - **Customizable config** via `rspack.config.js` in your project root, with safe merging that warns if you try to override reserved settings -- **Automatic CSS delegation** — when rspack is configured with CSS, Less, or SCSS loaders, Meteor automatically detects the handled extensions after the first compilation and stops processing those files itself. No `.meteorignore` entries needed. +- **Automatic CSS delegation** when rspack is configured with CSS, Less, or SCSS loaders, Meteor automatically detects the handled extensions after the first compilation and stops processing those files itself in the entry folder context. No `.meteorignore` entries needed. ## Installation From 307b6b7fa1d2707b0da57407e25c6d9aa2d31fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:25:50 +0100 Subject: [PATCH 20/66] print all ignores --- packages/rspack/lib/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 17b3aa61f8..70278d75ea 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -421,7 +421,7 @@ export function applyDelegatedExtensions(extensions) { setMeteorAppIgnore(ignorePatterns.join(' ')); if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { - logInfo(`[i] Rspack delegated extensions: ${extensions.join(', ')} (ignored in entry folders)`); + logInfo(`[i] Rspack delegated extensions: ${extensions.join(', ')} (ignored in entry folders)\n ${process.env.METEOR_IGNORE}`); } } } From ee63a89a0285365fd9511e3d7632a984a90d8149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:44:27 +0100 Subject: [PATCH 21/66] add examples module with constants and data validation Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/examples.js | 33 ++++++++++++++++++++++++++++++ tools/tests/examples-tests.js | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tools/cli/examples.js create mode 100644 tools/tests/examples-tests.js diff --git a/tools/cli/examples.js b/tools/cli/examples.js new file mode 100644 index 0000000000..a1ae709df9 --- /dev/null +++ b/tools/cli/examples.js @@ -0,0 +1,33 @@ +var files = require('../fs/files'); +var httpHelpers = require('../utils/http-helpers.js'); +var Console = require('../console/console.js').Console; +const { exec } = require('child_process'); + +const EXAMPLES_REPO = 'https://github.com/meteor/examples'; +const EXAMPLES_BRANCH = 'meteor-3.x'; +const EXAMPLES_JSON_URL = + `https://raw.githubusercontent.com/meteor/examples/${EXAMPLES_BRANCH}/examples.json`; + +function validateExamplesData(data) { + if (!Array.isArray(data)) { + throw new Error('Invalid examples.json format: expected a JSON array.'); + } + return data.filter(entry => { + if (!entry.slug || typeof entry.slug !== 'string') { + Console.warn(`Skipping example entry with missing slug`); + return false; + } + if (!entry.repositoryUrl || typeof entry.repositoryUrl !== 'string') { + Console.warn(`Skipping example '${entry.slug}' with missing repositoryUrl`); + return false; + } + return true; + }); +} + +module.exports = { + validateExamplesData, + EXAMPLES_REPO, + EXAMPLES_BRANCH, + EXAMPLES_JSON_URL +}; diff --git a/tools/tests/examples-tests.js b/tools/tests/examples-tests.js new file mode 100644 index 0000000000..c17f976a67 --- /dev/null +++ b/tools/tests/examples-tests.js @@ -0,0 +1,38 @@ +import { validateExamplesData } from '../cli/examples.js'; +import assert from 'assert'; + +describe('validateExamplesData', () => { + it('returns valid entries unchanged', () => { + const data = [ + { slug: 'foo', repositoryUrl: 'https://github.com/a/b', title: 'Foo', why: 'test', stack: ['Meteor'], meteorVersion: '3.4', isInternal: false, internalPath: null, repository: 'a/b', demo: null, lastUpdatedAt: null } + ]; + const result = validateExamplesData(data); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].slug, 'foo'); + }); + + it('skips entries missing slug', () => { + const data = [ + { repositoryUrl: 'https://github.com/a/b' }, + { slug: 'valid', repositoryUrl: 'https://github.com/c/d' } + ]; + const result = validateExamplesData(data); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].slug, 'valid'); + }); + + it('skips entries missing repositoryUrl', () => { + const data = [ + { slug: 'no-url' }, + { slug: 'valid', repositoryUrl: 'https://github.com/c/d' } + ]; + const result = validateExamplesData(data); + assert.strictEqual(result.length, 1); + }); + + it('throws on non-array input', () => { + assert.throws(() => validateExamplesData('not an array'), /Invalid/); + assert.throws(() => validateExamplesData(null), /Invalid/); + assert.throws(() => validateExamplesData({}), /Invalid/); + }); +}); From 7e61143ae9a1edcf299ad93d242a1d6e852a776f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:45:36 +0100 Subject: [PATCH 22/66] add examples cache read/write Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/examples.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index a1ae709df9..b93b6d333e 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -25,6 +25,26 @@ function validateExamplesData(data) { }); } +function getCachePath() { + return files.pathJoin(files.getHomeDir(), '.meteor', 'examples-cache.json'); +} + +function readCache() { + const cachePath = getCachePath(); + if (!files.exists(cachePath)) return null; + try { + const raw = files.readFile(cachePath, 'utf8'); + return JSON.parse(raw); + } catch (e) { + return null; + } +} + +function writeCache(data) { + const cachePath = getCachePath(); + files.writeFile(cachePath, JSON.stringify(data, null, 2), 'utf8'); +} + module.exports = { validateExamplesData, EXAMPLES_REPO, From 003c109d9bc943140ce5021f08435952769ae69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:45:44 +0100 Subject: [PATCH 23/66] add getExamples orchestrator with cache-first strategy Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/examples.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index b93b6d333e..ba274135e1 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -45,8 +45,51 @@ function writeCache(data) { files.writeFile(cachePath, JSON.stringify(data, null, 2), 'utf8'); } +async function fetchExamplesJson() { + const response = await httpHelpers.request({ + url: EXAMPLES_JSON_URL, + method: 'GET', + }); + if (response.statusCode !== 200) { + throw new Error( + `Failed to fetch examples.json (HTTP ${response.statusCode})` + ); + } + let data; + try { + data = JSON.parse(response.body); + } catch (e) { + throw new Error('Invalid JSON received from examples repository.'); + } + return validateExamplesData(data); +} + +async function getExamples({ refresh = false } = {}) { + if (!refresh) { + const cached = readCache(); + if (cached && cached.examples) { + return cached.examples; + } + } + + const examples = await fetchExamplesJson(); + + if (examples.length === 0) { + throw new Error('No valid examples found in examples.json.'); + } + + writeCache({ + fetchedAt: new Date().toISOString(), + branch: EXAMPLES_BRANCH, + examples, + }); + + return examples; +} + module.exports = { validateExamplesData, + getExamples, EXAMPLES_REPO, EXAMPLES_BRANCH, EXAMPLES_JSON_URL From 4338bcfd67887188298052530a96d9b52807bf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:46:58 +0100 Subject: [PATCH 24/66] add example resolution and clone utilities Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/examples.js | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index ba274135e1..aadf626ff5 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -87,9 +87,96 @@ async function getExamples({ refresh = false } = {}) { return examples; } +function findExample(examples, slug) { + return examples.find(e => e.slug === slug) || null; +} + +async function cloneRepo(url, destPath) { + return new Promise((resolve, reject) => { + exec('git --version', (error) => { + if (error) { + reject(new Error('git is not installed')); + return; + } + + process.env.GIT_TERMINAL_PROMPT = 0; + + const isWindows = process.platform === 'win32'; + const dest = isWindows + ? `"${files.convertToOSPath(destPath)}"` + : destPath; + const command = `git clone --progress ${url} ${dest}`; + + exec(command, { env: process.env }, async (cloneError) => { + if (cloneError) { + // git clone writes progress to stderr, so only reject on real errors + // "Cloning into" on stderr is normal git output, not an error + const msg = cloneError.message || ''; + if (!msg.includes('Cloning into')) { + reject(new Error(`Failed to clone ${url}: ${msg}`)); + return; + } + } + + try { + // Remove .git folder from the cloned repo + await files.rm_recursive_async(files.pathJoin(destPath, '.git')); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + }); +} + +async function cloneSubdirectory(repoUrl, branch, subdir, destPath) { + const tempDir = files.mkdtemp('meteor-example-'); + try { + const branchArg = branch ? `--branch ${branch} ` : ''; + const command = `git clone --progress ${branchArg}${repoUrl} ${tempDir}`; + + process.env.GIT_TERMINAL_PROMPT = 0; + + await new Promise((resolve, reject) => { + exec(command, { env: process.env }, (error) => { + if (error) { + const msg = error.message || ''; + if (!msg.includes('Cloning into')) { + reject(new Error(`Failed to clone ${repoUrl}: ${msg}`)); + return; + } + } + resolve(); + }); + }); + + const subdirPath = files.pathJoin(tempDir, subdir); + if (!files.exists(subdirPath)) { + throw new Error( + `Directory '${subdir}' not found in the repository. The examples list may be outdated — try 'meteor create --list --refresh'.` + ); + } + + await files.cp_r(subdirPath, destPath); + + // Remove .git if it was copied + const destGit = files.pathJoin(destPath, '.git'); + if (files.exists(destGit)) { + await files.rm_recursive_async(destGit); + } + } finally { + // Clean up temp directory + await files.rm_recursive_async(tempDir); + } +} + module.exports = { validateExamplesData, getExamples, + findExample, + cloneRepo, + cloneSubdirectory, EXAMPLES_REPO, EXAMPLES_BRANCH, EXAMPLES_JSON_URL From b5888eaea2f9991a653c13889704758b031dc400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:47:05 +0100 Subject: [PATCH 25/66] add Meteor app validation for create command Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/examples.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index aadf626ff5..8b9636ff90 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -171,12 +171,22 @@ async function cloneSubdirectory(repoUrl, branch, subdir, destPath) { } } +function validateMeteorApp(dirPath) { + const meteorDir = files.pathJoin(dirPath, '.meteor'); + if (!files.exists(meteorDir)) { + throw new Error( + `The directory '${files.convertToOSPath(dirPath)}' is not a Meteor app (no .meteor directory found).` + ); + } +} + module.exports = { validateExamplesData, getExamples, findExample, cloneRepo, cloneSubdirectory, + validateMeteorApp, EXAMPLES_REPO, EXAMPLES_BRANCH, EXAMPLES_JSON_URL From e1085cb8e1a51cfcfcec134e2ee962f64df8a4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:49:52 +0100 Subject: [PATCH 26/66] wire commands.js to new examples module and remove old example code Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/commands.js | 154 ++++++++++++++++-------------- tools/cli/example-repositories.js | 14 --- 2 files changed, 81 insertions(+), 87 deletions(-) delete mode 100644 tools/cli/example-repositories.js diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 88716546f4..04155eb80c 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -124,7 +124,7 @@ import { ensureDevBundleDependencies } from '../cordova/index.js'; import { CordovaRunner } from '../cordova/runner.js'; import { iOSRunTarget, AndroidRunTarget } from '../cordova/run-targets.js'; -import { EXAMPLE_REPOSITORIES } from './example-repositories.js'; +import { getExamples, findExample, cloneRepo, cloneSubdirectory, validateMeteorApp, EXAMPLES_REPO, EXAMPLES_BRANCH } from './examples.js'; // The architecture used by Meteor Software's hosted servers; it's the // architecture used by 'meteor deploy'. @@ -675,17 +675,6 @@ main.registerCommand({ * Resolves into json with * @returns {Promise<[Skeletons, null]> | Promise<[null, Error]>} */ -function getExamplesJSON(){ - return tryRun(async () => { - const response = await httpHelpers.request({ - url: "https://cdn.meteor.com/static/meteor.json", - method: "GET", - useSessionHeader: true, - useAuthHeader: true, - }); - return JSON.parse(response.body); - }); -} const DEFAULT_SKELETON = "react"; export const AVAILABLE_SKELETONS = [ @@ -751,6 +740,8 @@ main.registerCommand({ legacy: { type: Boolean }, prototype: { type: Boolean }, from: { type: String }, + 'from-dir': { type: String }, + refresh: { type: Boolean }, }, pretty: false, catalogRefresh: new catalog.Refresh.Never() @@ -774,6 +765,11 @@ main.registerCommand({ Console.error(); throw new main.ShowUsage(); } + if (options.from || options['from-dir']) { + Console.error("Package creation does not support --from or --from-dir."); + Console.error(); + throw new main.ShowUsage(); + } if (!packageName) { Console.error("Please specify the name of the package."); @@ -895,26 +891,28 @@ main.registerCommand({ } if (options.list) { - Console.info("Available examples:"); - const [json, err] = await getExamplesJSON() - if (err) { - Console.error("Failed to fetch examples:", err.message); - Console.info("Using cached examples.json"); - } - const examples = err ? EXAMPLE_REPOSITORIES : json; - _.each(examples, function (repoInfo, name) { - const branchInfo = repoInfo.branch ? `/tree/${repoInfo.branch}` : ""; + try { + const examples = await getExamples({ refresh: !!options.refresh }); + Console.info('Available examples:'); + Console.info(); + examples.forEach(ex => { + Console.info(Console.command(ex.slug), Console.options({ indent: 2 })); + if (ex.why) { + Console.info(ex.why, Console.options({ indent: 4 })); + } + Console.info(ex.stack.join(', '), Console.options({ indent: 4 })); + const url = ex.repositoryUrl || `${EXAMPLES_REPO}/tree/${EXAMPLES_BRANCH}/${ex.internalPath}`; + Console.info(Console.url(url), Console.options({ indent: 4 })); + Console.info(); + }); Console.info( - Console.command(`${name}: ${repoInfo.repo}${branchInfo}`), - Console.options({ indent: 2 }) + 'To create an example:', + Console.command("'meteor create --example '") ); - }); - - Console.info(); - Console.info( - "To create an example, simply", - Console.command("'meteor create --example '") - ); + } catch (err) { + Console.error(err.message); + return 1; + } return 0; } @@ -1151,57 +1149,67 @@ main.registerCommand({ } - /** - * - * @param {string} url - */ - const setupExampleByURL = async (url) => { - const [ok, err] = await bash`git --version`; - if (err) throw new Error("git is not installed"); - const isWindows = process.platform === "win32"; - - // Set GIT_TERMINAL_PROMPT=0 to disable prompting - process.env.GIT_TERMINAL_PROMPT = 0; - - const gitCommand = isWindows - ? `git clone --progress ${url} "${files.convertToOSPath(appPath)}"` - : `git clone --progress ${url} ${appPath}`; - const [okClone, errClone] = await bash`${gitCommand}`; - const errorMessage = errClone && typeof errClone === "string" ? errClone : errClone?.message; - if (errorMessage && errorMessage.includes("Cloning into")) { - throw new Error("error cloning skeleton"); - } - // remove .git folder from the example - await files.rm_recursive_async(files.pathJoin(appPath, ".git")); - await setupMessages(); - }; - if (options.example) { - const [json, err] = await getExamplesJSON(); + try { + let examples = await getExamples(); + let example = findExample(examples, options.example); - if (err) { - Console.error("Failed to fetch examples:", err.message); - Console.info("Using cached examples.json"); - } + if (!example) { + examples = await getExamples({ refresh: true }); + example = findExample(examples, options.example); + } - const examples = err ? EXAMPLE_REPOSITORIES : json; - const repoInfo = examples[options.example]; - if (!repoInfo) { - Console.error(`${options.example}: no such example.`); - Console.error( - "List available applications with", - Console.command("'meteor create --list'") + "." - ); + if (!example) { + Console.error(`'${options.example}' is not a known example.`); + Console.error('Run', Console.command("'meteor create --list'"), 'to see available examples.'); + return 1; + } + + if (example.isInternal) { + await cloneSubdirectory(EXAMPLES_REPO, EXAMPLES_BRANCH, example.internalPath, appPath); + } else { + await cloneRepo(example.repositoryUrl, appPath); + } + + await setupMessages(); + } catch (err) { + Console.error('Error creating example:', err.message); return 1; } - // repoInfo.repo is the URL of the repo, and repoInfo.branch is the branch - await setupExampleByURL(repoInfo.repo); return 0; } + if (options['from-dir'] && !options.from) { + Console.error('--from-dir requires --from to specify the source repository.'); + return 1; + } if (options.from) { - await setupExampleByURL(options.from); + try { + if (options['from-dir']) { + let repoUrl = options.from; + try { + const examples = await getExamples(); + const example = findExample(examples, options.from); + if (example) { + repoUrl = example.repositoryUrl || EXAMPLES_REPO; + } + } catch (e) { + // If examples fetch fails, treat --from as a URL + } + + await cloneSubdirectory(repoUrl, null, options['from-dir'], appPath); + validateMeteorApp(appPath); + } else { + await cloneRepo(options.from, appPath); + validateMeteorApp(appPath); + } + + await setupMessages(); + } catch (err) { + Console.error(err.message); + return 1; + } return 0; } @@ -1271,8 +1279,8 @@ main.registerCommand({ // using it as it was before 2.x if (release.explicit) throw new Error("Using release option"); - // If local skeleton doesn't exist, use setupExampleByURL - await setupExampleByURL(`https://github.com/meteor/skel-${skeleton}`); + // If local skeleton doesn't exist, clone from GitHub + await cloneRepo(`https://github.com/meteor/skel-${skeleton}`, appPath); } catch (e) { if ( e.message !== "Using prototype option" && diff --git a/tools/cli/example-repositories.js b/tools/cli/example-repositories.js deleted file mode 100644 index f1d67151ef..0000000000 --- a/tools/cli/example-repositories.js +++ /dev/null @@ -1,14 +0,0 @@ -export const EXAMPLE_REPOSITORIES = { - "vue": { "repo": "https://github.com/meteor/skel-vue" }, - "react": { "repo": "https://github.com/meteor/skel-react" }, - "full": { "repo": "https://github.com/meteor/skel-full" }, - "bare": { "repo": "https://github.com/meteor/skel-bare" }, - "blaze": { "repo": "https://github.com/meteor/skel-blaze" }, - "chakra-ui": { "repo": "https://github.com/meteor/skel-chakra-ui" }, - "apollo": { "repo": "https://github.com/meteor/skel-apollo" }, - "minimal": { "repo": "https://github.com/meteor/skel-minimal" }, - "solid": { "repo": "https://github.com/meteor/skel-solid" }, - "svelte": { "repo": "https://github.com/meteor/skel-svelte" }, - "tailwind": { "repo": "https://github.com/meteor/skel-tailwind" }, - "typescript": { "repo": "https://github.com/meteor/skel-typescript" }, -}; From 21f97fa1f7085f0b652f2e9698cede2cca6c0f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 15:51:04 +0100 Subject: [PATCH 27/66] add E2E test for meteor create --list and --example Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/e2e-tests/example.test.js | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tools/e2e-tests/example.test.js diff --git a/tools/e2e-tests/example.test.js b/tools/e2e-tests/example.test.js new file mode 100644 index 0000000000..5946b29959 --- /dev/null +++ b/tools/e2e-tests/example.test.js @@ -0,0 +1,34 @@ +import path from 'path'; +import fs from 'fs'; +import os from 'os'; + +import { runMeteorCommand, cleanupTempDir } from './helpers'; + +describe('Examples /', () => { + it('meteor create --list returns available examples', async () => { + const { processResult } = await runMeteorCommand( + 'create', ['--list'], os.tmpdir(), + { captureOutput: true, checkExitCode: true } + ); + // Should contain at least one example slug in the output + expect(processResult.outputLines.join('\n')).toMatch(/Available examples/); + }); + + it('meteor create --example creates a Meteor app', async () => { + const randomSuffix = Math.random().toString(36).substring(2, 10); + const appName = `meteortest-example-${randomSuffix}`; + const tempDir = path.join(os.tmpdir(), appName); + + try { + await runMeteorCommand( + 'create', ['--example', 'tic-tac-toe', appName], os.tmpdir(), + { checkExitCode: true } + ); + + // Verify the app was created with a .meteor directory + expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true); + } finally { + await cleanupTempDir(tempDir); + } + }); +}); From a0ad99886745710e197f9e1ea958f67002a9cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:16:27 +0100 Subject: [PATCH 28/66] enhance error reporting in CLI examples command and update examples branch reference --- tools/cli/commands.js | 15 ++++++++++----- tools/cli/examples.js | 10 +++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 04155eb80c..3b73ecf140 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -900,14 +900,19 @@ main.registerCommand({ if (ex.why) { Console.info(ex.why, Console.options({ indent: 4 })); } - Console.info(ex.stack.join(', '), Console.options({ indent: 4 })); - const url = ex.repositoryUrl || `${EXAMPLES_REPO}/tree/${EXAMPLES_BRANCH}/${ex.internalPath}`; - Console.info(Console.url(url), Console.options({ indent: 4 })); + if (ex.stack && ex.stack.length) { + Console.info('Tech: ' + ex.stack.join(', '), Console.options({ indent: 4 })); + } + if (ex.demo) { + Console.info('Demo: ' + Console.url(ex.demo), Console.options({ indent: 4 })); + } + const repoUrl = ex.repositoryUrl || `${EXAMPLES_REPO}/tree/${EXAMPLES_BRANCH}/${ex.internalPath}`; + Console.info('Repo: ' + Console.url(repoUrl), Console.options({ indent: 4 })); Console.info(); }); Console.info( - 'To create an example:', - Console.command("'meteor create --example '") + 'Usage:', + Console.command("meteor create --example ") ); } catch (err) { Console.error(err.message); diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 8b9636ff90..6de63774a9 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -4,7 +4,7 @@ var Console = require('../console/console.js').Console; const { exec } = require('child_process'); const EXAMPLES_REPO = 'https://github.com/meteor/examples'; -const EXAMPLES_BRANCH = 'meteor-3.x'; +const EXAMPLES_BRANCH = 'migrate-examples'; const EXAMPLES_JSON_URL = `https://raw.githubusercontent.com/meteor/examples/${EXAMPLES_BRANCH}/examples.json`; @@ -46,18 +46,18 @@ function writeCache(data) { } async function fetchExamplesJson() { - const response = await httpHelpers.request({ + const result = await httpHelpers.request({ url: EXAMPLES_JSON_URL, method: 'GET', }); - if (response.statusCode !== 200) { + if (result.response.statusCode !== 200) { throw new Error( - `Failed to fetch examples.json (HTTP ${response.statusCode})` + `Failed to fetch examples.json (HTTP ${result.response.statusCode})` ); } let data; try { - data = JSON.parse(response.body); + data = JSON.parse(result.body); } catch (e) { throw new Error('Invalid JSON received from examples repository.'); } From c65602e8e28bddd4c724e47454f81e3915659516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:27:18 +0100 Subject: [PATCH 29/66] prioritize network over cache for examples retrieval; add fallback mechanism to cached data. --- tools/cli/examples.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 6de63774a9..002b926b47 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -65,26 +65,31 @@ async function fetchExamplesJson() { } async function getExamples({ refresh = false } = {}) { - if (!refresh) { + // Always try network first, use cache as fallback for offline + try { + const examples = await fetchExamplesJson(); + + if (examples.length === 0) { + throw new Error('No valid examples found in examples.json.'); + } + + writeCache({ + fetchedAt: new Date().toISOString(), + branch: EXAMPLES_BRANCH, + examples, + }); + + return examples; + } catch (fetchError) { + // Network failed — fall back to cache if available const cached = readCache(); if (cached && cached.examples) { return cached.examples; } + + // No cache either — surface the original fetch error + throw fetchError; } - - const examples = await fetchExamplesJson(); - - if (examples.length === 0) { - throw new Error('No valid examples found in examples.json.'); - } - - writeCache({ - fetchedAt: new Date().toISOString(), - branch: EXAMPLES_BRANCH, - examples, - }); - - return examples; } function findExample(examples, slug) { From c27a42ae79ad384535044791ba72f9b0455a99d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:30:03 +0100 Subject: [PATCH 30/66] include Meteor version in CLI examples list --- tools/cli/commands.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 3b73ecf140..89d5676ee0 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -896,7 +896,8 @@ main.registerCommand({ Console.info('Available examples:'); Console.info(); examples.forEach(ex => { - Console.info(Console.command(ex.slug), Console.options({ indent: 2 })); + const version = ex.meteorVersion ? ` (Meteor ${ex.meteorVersion})` : ''; + Console.info(Console.command(ex.slug) + version, Console.options({ indent: 2 })); if (ex.why) { Console.info(ex.why, Console.options({ indent: 4 })); } From edc28739e72878c5d934fa80176f7993237fb1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:31:38 +0100 Subject: [PATCH 31/66] refactor examples tests: replace `assert` with `expect` and switch to CommonJS syntax --- .../examples.test.js} | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) rename tools/{tests/examples-tests.js => cli/examples.test.js} (63%) diff --git a/tools/tests/examples-tests.js b/tools/cli/examples.test.js similarity index 63% rename from tools/tests/examples-tests.js rename to tools/cli/examples.test.js index c17f976a67..981d7e052e 100644 --- a/tools/tests/examples-tests.js +++ b/tools/cli/examples.test.js @@ -1,5 +1,4 @@ -import { validateExamplesData } from '../cli/examples.js'; -import assert from 'assert'; +const { validateExamplesData } = require('./examples.js'); describe('validateExamplesData', () => { it('returns valid entries unchanged', () => { @@ -7,8 +6,8 @@ describe('validateExamplesData', () => { { slug: 'foo', repositoryUrl: 'https://github.com/a/b', title: 'Foo', why: 'test', stack: ['Meteor'], meteorVersion: '3.4', isInternal: false, internalPath: null, repository: 'a/b', demo: null, lastUpdatedAt: null } ]; const result = validateExamplesData(data); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].slug, 'foo'); + expect(result).toHaveLength(1); + expect(result[0].slug).toBe('foo'); }); it('skips entries missing slug', () => { @@ -17,8 +16,8 @@ describe('validateExamplesData', () => { { slug: 'valid', repositoryUrl: 'https://github.com/c/d' } ]; const result = validateExamplesData(data); - assert.strictEqual(result.length, 1); - assert.strictEqual(result[0].slug, 'valid'); + expect(result).toHaveLength(1); + expect(result[0].slug).toBe('valid'); }); it('skips entries missing repositoryUrl', () => { @@ -27,12 +26,12 @@ describe('validateExamplesData', () => { { slug: 'valid', repositoryUrl: 'https://github.com/c/d' } ]; const result = validateExamplesData(data); - assert.strictEqual(result.length, 1); + expect(result).toHaveLength(1); }); it('throws on non-array input', () => { - assert.throws(() => validateExamplesData('not an array'), /Invalid/); - assert.throws(() => validateExamplesData(null), /Invalid/); - assert.throws(() => validateExamplesData({}), /Invalid/); + expect(() => validateExamplesData('not an array')).toThrow(/Invalid/); + expect(() => validateExamplesData(null)).toThrow(/Invalid/); + expect(() => validateExamplesData({})).toThrow(/Invalid/); }); }); From 630900294bac92db933d141baf81bff7fecdbf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:34:37 +0100 Subject: [PATCH 32/66] refactor `validateExamplesData` to support custom warning function via options --- tools/cli/examples.js | 6 +++--- tools/cli/examples.test.js | 37 ------------------------------------- 2 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 tools/cli/examples.test.js diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 002b926b47..6db0eec13a 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -8,17 +8,17 @@ const EXAMPLES_BRANCH = 'migrate-examples'; const EXAMPLES_JSON_URL = `https://raw.githubusercontent.com/meteor/examples/${EXAMPLES_BRANCH}/examples.json`; -function validateExamplesData(data) { +function validateExamplesData(data, { warn = (msg) => Console.warn(msg) } = {}) { if (!Array.isArray(data)) { throw new Error('Invalid examples.json format: expected a JSON array.'); } return data.filter(entry => { if (!entry.slug || typeof entry.slug !== 'string') { - Console.warn(`Skipping example entry with missing slug`); + warn('Skipping example entry with missing slug'); return false; } if (!entry.repositoryUrl || typeof entry.repositoryUrl !== 'string') { - Console.warn(`Skipping example '${entry.slug}' with missing repositoryUrl`); + warn(`Skipping example '${entry.slug}' with missing repositoryUrl`); return false; } return true; diff --git a/tools/cli/examples.test.js b/tools/cli/examples.test.js deleted file mode 100644 index 981d7e052e..0000000000 --- a/tools/cli/examples.test.js +++ /dev/null @@ -1,37 +0,0 @@ -const { validateExamplesData } = require('./examples.js'); - -describe('validateExamplesData', () => { - it('returns valid entries unchanged', () => { - const data = [ - { slug: 'foo', repositoryUrl: 'https://github.com/a/b', title: 'Foo', why: 'test', stack: ['Meteor'], meteorVersion: '3.4', isInternal: false, internalPath: null, repository: 'a/b', demo: null, lastUpdatedAt: null } - ]; - const result = validateExamplesData(data); - expect(result).toHaveLength(1); - expect(result[0].slug).toBe('foo'); - }); - - it('skips entries missing slug', () => { - const data = [ - { repositoryUrl: 'https://github.com/a/b' }, - { slug: 'valid', repositoryUrl: 'https://github.com/c/d' } - ]; - const result = validateExamplesData(data); - expect(result).toHaveLength(1); - expect(result[0].slug).toBe('valid'); - }); - - it('skips entries missing repositoryUrl', () => { - const data = [ - { slug: 'no-url' }, - { slug: 'valid', repositoryUrl: 'https://github.com/c/d' } - ]; - const result = validateExamplesData(data); - expect(result).toHaveLength(1); - }); - - it('throws on non-array input', () => { - expect(() => validateExamplesData('not an array')).toThrow(/Invalid/); - expect(() => validateExamplesData(null)).toThrow(/Invalid/); - expect(() => validateExamplesData({})).toThrow(/Invalid/); - }); -}); From 1e9473f9ea2a000d9f7758890dc6ca613a8846ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:40:00 +0100 Subject: [PATCH 33/66] remove unused `refresh` option from CLI commands --- tools/cli/commands.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 89d5676ee0..479323e714 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -741,7 +741,6 @@ main.registerCommand({ prototype: { type: Boolean }, from: { type: String }, 'from-dir': { type: String }, - refresh: { type: Boolean }, }, pretty: false, catalogRefresh: new catalog.Refresh.Never() @@ -892,7 +891,7 @@ main.registerCommand({ if (options.list) { try { - const examples = await getExamples({ refresh: !!options.refresh }); + const examples = await getExamples(); Console.info('Available examples:'); Console.info(); examples.forEach(ex => { From b29a15b48202b07cd358b21c5f9c697d588c9bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 16:48:17 +0100 Subject: [PATCH 34/66] add support for `--from-branch` option in `meteor create` command --- tools/cli/commands.js | 14 ++++++----- tools/cli/examples.js | 5 ++-- tools/e2e-tests/example.test.js | 43 ++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 479323e714..e737317fea 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -741,6 +741,7 @@ main.registerCommand({ prototype: { type: Boolean }, from: { type: String }, 'from-dir': { type: String }, + 'from-branch': { type: String }, }, pretty: false, catalogRefresh: new catalog.Refresh.Never() @@ -764,8 +765,8 @@ main.registerCommand({ Console.error(); throw new main.ShowUsage(); } - if (options.from || options['from-dir']) { - Console.error("Package creation does not support --from or --from-dir."); + if (options.from || options['from-dir'] || options['from-branch']) { + Console.error("Package creation does not support --from, --from-dir, or --from-branch."); Console.error(); throw new main.ShowUsage(); } @@ -1184,12 +1185,13 @@ main.registerCommand({ return 0; } - if (options['from-dir'] && !options.from) { - Console.error('--from-dir requires --from to specify the source repository.'); + if ((options['from-dir'] || options['from-branch']) && !options.from) { + Console.error('--from-dir and --from-branch require --from to specify the source repository.'); return 1; } if (options.from) { + const branch = options['from-branch'] || null; try { if (options['from-dir']) { let repoUrl = options.from; @@ -1203,10 +1205,10 @@ main.registerCommand({ // If examples fetch fails, treat --from as a URL } - await cloneSubdirectory(repoUrl, null, options['from-dir'], appPath); + await cloneSubdirectory(repoUrl, branch, options['from-dir'], appPath); validateMeteorApp(appPath); } else { - await cloneRepo(options.from, appPath); + await cloneRepo(options.from, appPath, { branch }); validateMeteorApp(appPath); } diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 6db0eec13a..9db981222b 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -96,7 +96,7 @@ function findExample(examples, slug) { return examples.find(e => e.slug === slug) || null; } -async function cloneRepo(url, destPath) { +async function cloneRepo(url, destPath, { branch = null } = {}) { return new Promise((resolve, reject) => { exec('git --version', (error) => { if (error) { @@ -110,7 +110,8 @@ async function cloneRepo(url, destPath) { const dest = isWindows ? `"${files.convertToOSPath(destPath)}"` : destPath; - const command = `git clone --progress ${url} ${dest}`; + const branchArg = branch ? `--branch ${branch} ` : ''; + const command = `git clone --progress ${branchArg}${url} ${dest}`; exec(command, { env: process.env }, async (cloneError) => { if (cloneError) { diff --git a/tools/e2e-tests/example.test.js b/tools/e2e-tests/example.test.js index 5946b29959..8c79ad5599 100644 --- a/tools/e2e-tests/example.test.js +++ b/tools/e2e-tests/example.test.js @@ -4,28 +4,59 @@ import os from 'os'; import { runMeteorCommand, cleanupTempDir } from './helpers'; +function tempApp(prefix) { + const suffix = Math.random().toString(36).substring(2, 10); + const appName = `meteortest-${prefix}-${suffix}`; + return { appName, tempDir: path.join(os.tmpdir(), appName) }; +} + describe('Examples /', () => { it('meteor create --list returns available examples', async () => { const { processResult } = await runMeteorCommand( 'create', ['--list'], os.tmpdir(), { captureOutput: true, checkExitCode: true } ); - // Should contain at least one example slug in the output expect(processResult.outputLines.join('\n')).toMatch(/Available examples/); }); it('meteor create --example creates a Meteor app', async () => { - const randomSuffix = Math.random().toString(36).substring(2, 10); - const appName = `meteortest-example-${randomSuffix}`; - const tempDir = path.join(os.tmpdir(), appName); - + const { appName, tempDir } = tempApp('example'); try { await runMeteorCommand( 'create', ['--example', 'tic-tac-toe', appName], os.tmpdir(), { checkExitCode: true } ); + expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true); + } finally { + await cleanupTempDir(tempDir); + } + }); - // Verify the app was created with a .meteor directory + it('meteor create --from clones an external repo', async () => { + const { appName, tempDir } = tempApp('from'); + try { + await runMeteorCommand( + 'create', ['--from', 'https://github.com/fredmaiaarantes/simpletasks', appName], os.tmpdir(), + { checkExitCode: true } + ); + expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true); + } finally { + await cleanupTempDir(tempDir); + } + }); + + it('meteor create --from with --from-branch and --from-dir extracts a subdirectory', async () => { + const { appName, tempDir } = tempApp('fromdir'); + try { + await runMeteorCommand( + 'create', [ + '--from', 'https://github.com/meteor/examples', + '--from-branch', 'migrate-examples', + '--from-dir', 'parties', + appName + ], os.tmpdir(), + { checkExitCode: true } + ); expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true); } finally { await cleanupTempDir(tempDir); From 9ebb9763bb545334f99d1839639bdfd6bebd43c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 17:07:28 +0100 Subject: [PATCH 35/66] refine `meteor create` documentation: clarify options and add details for `--from-branch` and `--from-dir`. --- tools/cli/help.txt | 53 +++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/tools/cli/help.txt b/tools/cli/help.txt index 811b4ec872..089177d9f4 100644 --- a/tools/cli/help.txt +++ b/tools/cli/help.txt @@ -154,8 +154,8 @@ Options: >>> create Create a new project. Usage: meteor create [--release ] [--bare|--minimal|--full|--react|--vue|--apollo|--svelte|--blaze|--tailwind|--chakra-ui|--solid|--babel|--coffeescript|--angular] - meteor create [--release ] --example [] - meteor create [--release ] --from [] + meteor create [--release ] --example [] + meteor create [--release ] --from [--from-branch ] [--from-dir ] [] meteor create --list meteor create --package [] @@ -176,30 +176,35 @@ package created in an app, will be created using the application's version of meteor and a package created outside a meteor app will use the latest release). You can pass --example to start off with a copy of one of the Meteor -sample applications. Use --list to see the available examples. There are -currently no package examples. +community examples. Use --list to see the available examples. -Options: - --package Create a new meteor package instead of an app. - --example Example template to use. - --from Clones a meteor project from a url. - --list Show list of available examples. - --bare Create an empty app. - --minimal Create an app with as few Meteor packages as possible. - --full Create a fully scaffolded app. - --react Create a basic react-based app, same as default. - --vue Create a basic vue3-based app. - --apollo Create a basic apollo-based app. - --svelte Create a basic svelte-based app. - --typescript Create a basic Typescript React-based app. - --blaze Create a basic blaze-based app. - --tailwind Create a basic react-based app, with tailwind configured. - --chakra-ui Create a basic react-based app, with chakra-ui configured. +Skeleton options: + --bare Create an empty app. + --minimal Create an app with as few Meteor packages as possible. + --full Create a fully scaffolded app. + --react Create a basic react-based app, same as default. + --vue Create a basic vue3-based app. + --apollo Create a basic apollo-based app. + --svelte Create a basic svelte-based app. + --typescript Create a basic Typescript React-based app. + --blaze Create a basic blaze-based app. + --tailwind Create a basic react-based app, with tailwind configured. + --chakra-ui Create a basic react-based app, with chakra-ui configured. --coffeescript Create a basic coffescript app, with react. - --babel Create a React app with Babel support. - --solid Create a basic solid-based app. - --angular Create a basic Angular app. - --prototype Create a prototype app with the insecure & autopublish packages. Can be used along with other app commands + --babel Create a React app with Babel support. + --solid Create a basic solid-based app. + --angular Create a basic Angular app. + --prototype Create a prototype app with the insecure & autopublish packages. + +Example options: + --example Create from a community example (use --list to browse). + --list Show detailed list of available examples. + --from Clone a Meteor project from a Git URL. + --from-branch Branch to clone from (use with --from). + --from-dir Extract only a subdirectory (use with --from). + +Other options: + --package Create a new meteor package instead of an app. >>> update From f0505f6b58c0c532578620f5bca9b8c17d8efc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 17:15:06 +0100 Subject: [PATCH 36/66] add tests for `meteor create` edge cases with `--from` and `--from-dir` options --- tools/e2e-tests/example.test.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tools/e2e-tests/example.test.js b/tools/e2e-tests/example.test.js index 8c79ad5599..eb28b87bc8 100644 --- a/tools/e2e-tests/example.test.js +++ b/tools/e2e-tests/example.test.js @@ -62,4 +62,36 @@ describe('Examples /', () => { await cleanupTempDir(tempDir); } }); + + it('meteor create --from with --from-dir fails for non-existing directory', async () => { + const { appName, tempDir } = tempApp('baddir'); + try { + await expect(runMeteorCommand( + 'create', [ + '--from', 'https://github.com/meteor/examples', + '--from-branch', 'migrate-examples', + '--from-dir', 'this-dir-does-not-exist', + appName + ], os.tmpdir(), + { captureOutput: true, checkExitCode: true } + )).rejects.toThrow(); + } finally { + await cleanupTempDir(tempDir); + } + }); + + it('meteor create --from fails for a non-Meteor repository', async () => { + const { appName, tempDir } = tempApp('nonmeteor'); + try { + await expect(runMeteorCommand( + 'create', [ + '--from', 'https://github.com/meteor/meteor', + appName + ], os.tmpdir(), + { captureOutput: true, checkExitCode: true } + )).rejects.toThrow(); + } finally { + await cleanupTempDir(tempDir); + } + }); }); From b06b4a8f9354def298722b0f9d3a3c071b577ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 25 Mar 2026 17:19:20 +0100 Subject: [PATCH 37/66] update CLI docs to include enhanced example workflows and repository cloning options --- v3-docs/docs/cli/index.md | 46 +++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index efb8527bea..28d50e13eb 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -260,12 +260,50 @@ If you run `meteor create` without arguments, Meteor will launch an interactive | Option | Description | |--------|-------------| -| `--from ` | Clone a Meteor project from a URL | -| `--example ` | Use a specific example template | -| `--list` | Show list of available examples | -| `--release ` | Specify Meteor version (e.g., `--release 2.8`) | +| `--release ` | Specify Meteor version (e.g., `--release 3.4`) | | `--prototype` | Include `autopublish` and `insecure` packages for rapid prototyping (not for production) | +### Examples + +Meteor provides a curated list of community examples that you can use as starting points. The examples are maintained in the [meteor/examples](https://github.com/meteor/examples) repository. + +To browse available examples with descriptions, tech stack, demo links, and repository URLs: + +```bash +meteor create --list +``` + +To create a new app from an example: + +```bash +meteor create my-app --example simple-tasks +``` + +| Option | Description | +|--------|-------------| +| `--example ` | Create from a community example | +| `--list` | Show detailed list of available examples | + +### Cloning from a Git Repository + +You can create a new Meteor app by cloning any Git repository: + +```bash +meteor create my-app --from https://github.com/fredmaiaarantes/simpletasks +``` + +To extract a specific subdirectory from a repository, use `--from-dir`. You can also specify a branch with `--from-branch`: + +```bash +meteor create my-app --from https://github.com/meteor/examples --from-branch migrate-examples --from-dir parties +``` + +| Option | Description | +|--------|-------------| +| `--from ` | Clone a Meteor project from a Git URL | +| `--from-branch ` | Branch to clone from (use with `--from`) | +| `--from-dir ` | Extract only a subdirectory (use with `--from`) | + ### Application Types | Option | Description | Tutorial / Example | From 065eac8650b7462545add1e0d46b28d5f34fe879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 15:49:02 +0100 Subject: [PATCH 38/66] enhance examples listing with improved formatting and additional details. --- tools/cli/commands.js | 38 ++++++++++++++++++++++---------------- tools/console/console.js | 16 ++++++++++++++++ tools/tests/create.js | 2 +- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index e737317fea..72e624bab5 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -16,6 +16,9 @@ var stats = require('../meteor-services/stats.js'); var Console = require('../console/console.js').Console; const { blue, + bold, + cyan, + dim, green, purple, red, @@ -893,28 +896,31 @@ main.registerCommand({ if (options.list) { try { const examples = await getExamples(); - Console.info('Available examples:'); - Console.info(); - examples.forEach(ex => { - const version = ex.meteorVersion ? ` (Meteor ${ex.meteorVersion})` : ''; - Console.info(Console.command(ex.slug) + version, Console.options({ indent: 2 })); + Console.rawInfo(`\n ${bold`Meteor Examples`} ${dim`${examples.length} available`}\n\n`); + + examples.forEach((ex, i) => { + const version = ex.meteorVersion ? dim` v${ex.meteorVersion}` : ''; + Console.rawInfo(` ${cyan`${ex.slug}`}${version}\n`); if (ex.why) { - Console.info(ex.why, Console.options({ indent: 4 })); + Console.rawInfo(` ${ex.why}\n`); } if (ex.stack && ex.stack.length) { - Console.info('Tech: ' + ex.stack.join(', '), Console.options({ indent: 4 })); - } - if (ex.demo) { - Console.info('Demo: ' + Console.url(ex.demo), Console.options({ indent: 4 })); + Console.rawInfo(` ${dim`Tech:`} ${ex.stack.join(' · ')}\n`); } const repoUrl = ex.repositoryUrl || `${EXAMPLES_REPO}/tree/${EXAMPLES_BRANCH}/${ex.internalPath}`; - Console.info('Repo: ' + Console.url(repoUrl), Console.options({ indent: 4 })); - Console.info(); + if (ex.demo) { + Console.rawInfo(` ${dim`Demo:`} ${ex.demo}\n`); + } + if (ex.tutorial) { + Console.rawInfo(` ${dim`Tutorial:`} ${ex.tutorial}\n`); + } + Console.rawInfo(` ${dim`Repo:`} ${repoUrl}\n`); + if (i < examples.length - 1) { + Console.rawInfo('\n'); + } }); - Console.info( - 'Usage:', - Console.command("meteor create --example ") - ); + + Console.rawInfo(`\n ${dim`Usage:`} meteor create ${bold``} --example ${cyan``}\n\n`); } catch (err) { Console.error(err.message); return 1; diff --git a/tools/console/console.js b/tools/console/console.js index a897346483..676d2f664a 100644 --- a/tools/console/console.js +++ b/tools/console/console.js @@ -1335,6 +1335,18 @@ const green = const blue = (text, ...values) => `\x1b[34m${ String.raw({ raw: text }, ...values) }\x1b[0m`; +const cyan = + (text, ...values) => + `\x1b[36m${ String.raw({ raw: text }, ...values) }\x1b[0m`; +const dim = + (text, ...values) => + `\x1b[2m${ String.raw({ raw: text }, ...values) }\x1b[0m`; +const bold = + (text, ...values) => + `\x1b[1m${ String.raw({ raw: text }, ...values) }\x1b[0m`; + +const link = (url, text) => + `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`; const colors = { yellow, @@ -1342,6 +1354,10 @@ const colors = { purple, green, blue, + cyan, + dim, + bold, + link, }; exports.colors = colors; diff --git a/tools/tests/create.js b/tools/tests/create.js index 784c22e03b..e9b91d0947 100644 --- a/tools/tests/create.js +++ b/tools/tests/create.js @@ -45,7 +45,7 @@ selftest.define("create main", async function () { await run.stop(); run = s.run("create", "--list"); - await run.read('Available'); + await run.read('Meteor Examples'); await run.match('react'); await run.expectExit(0); }); From 33f33c98e88d7d43773c8bcd454d83d988ca4d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 15:58:05 +0100 Subject: [PATCH 39/66] update e2e test: adjust match text to "Meteor Examples" for consistency --- tools/e2e-tests/example.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/e2e-tests/example.test.js b/tools/e2e-tests/example.test.js index eb28b87bc8..97fea1d585 100644 --- a/tools/e2e-tests/example.test.js +++ b/tools/e2e-tests/example.test.js @@ -16,7 +16,7 @@ describe('Examples /', () => { 'create', ['--list'], os.tmpdir(), { captureOutput: true, checkExitCode: true } ); - expect(processResult.outputLines.join('\n')).toMatch(/Available examples/); + expect(processResult.outputLines.join('\n')).toMatch(/Meteor Examples/); }); it('meteor create --example creates a Meteor app', async () => { From 2bb34d4e966d001908e1e70a9ba13a06a55917c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 15:58:16 +0100 Subject: [PATCH 40/66] update examples cache: use tropohouse path and handle write failures gracefully --- tools/cli/examples.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 9db981222b..612ac9b457 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -26,7 +26,8 @@ function validateExamplesData(data, { warn = (msg) => Console.warn(msg) } = {}) } function getCachePath() { - return files.pathJoin(files.getHomeDir(), '.meteor', 'examples-cache.json'); + var tropohouse = require('../packaging/tropohouse.js'); + return files.pathJoin(tropohouse.default.root, 'examples-cache.json'); } function readCache() { @@ -41,8 +42,12 @@ function readCache() { } function writeCache(data) { - const cachePath = getCachePath(); - files.writeFile(cachePath, JSON.stringify(data, null, 2), 'utf8'); + try { + const cachePath = getCachePath(); + files.writeFile(cachePath, JSON.stringify(data, null, 2), 'utf8'); + } catch (e) { + // Don't fail the command if it can't write + } } async function fetchExamplesJson() { From 806e2b2a764bf1cfc4f61bf56913000db2704355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 16:39:07 +0100 Subject: [PATCH 41/66] adjust help text matching and refine `--list` option verification --- tools/tests/create.js | 2 +- tools/tests/help.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/tests/create.js b/tools/tests/create.js index e9b91d0947..d726fdb780 100644 --- a/tools/tests/create.js +++ b/tools/tests/create.js @@ -45,7 +45,7 @@ selftest.define("create main", async function () { await run.stop(); run = s.run("create", "--list"); - await run.read('Meteor Examples'); + await run.match('Meteor Examples'); await run.match('react'); await run.expectExit(0); }); diff --git a/tools/tests/help.js b/tools/tests/help.js index e249e1d523..c30313a7dd 100644 --- a/tools/tests/help.js +++ b/tools/tests/help.js @@ -25,8 +25,8 @@ selftest.define("help", async function () { var checkCommandHelp = async function (run) { await run.read("Usage: meteor create"); await run.match("create a new Meteor app"); - await run.match("Options:"); - await run.match(/--list\s*Show list/); + await run.match("Skeleton options:"); + await run.match(/--list\s/); await run.expectExit(0); }; From a26a31574698945806ff284234ec7fa1094bad2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 17:20:28 +0100 Subject: [PATCH 42/66] refactor examples repo cloning: replace `exec` with `execFile` for improved security and environment handling --- tools/cli/examples.js | 50 ++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 612ac9b457..6f50b703a0 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -1,7 +1,7 @@ var files = require('../fs/files'); var httpHelpers = require('../utils/http-helpers.js'); var Console = require('../console/console.js').Console; -const { exec } = require('child_process'); +const { execFile } = require('child_process'); const EXAMPLES_REPO = 'https://github.com/meteor/examples'; const EXAMPLES_BRANCH = 'migrate-examples'; @@ -102,35 +102,28 @@ function findExample(examples, slug) { } async function cloneRepo(url, destPath, { branch = null } = {}) { + const env = Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' }); + return new Promise((resolve, reject) => { - exec('git --version', (error) => { + execFile('git', ['--version'], (error) => { if (error) { reject(new Error('git is not installed')); return; } - process.env.GIT_TERMINAL_PROMPT = 0; + const args = ['clone', '--progress']; + if (branch) { + args.push('--branch', branch); + } + args.push(url, files.convertToOSPath(destPath)); - const isWindows = process.platform === 'win32'; - const dest = isWindows - ? `"${files.convertToOSPath(destPath)}"` - : destPath; - const branchArg = branch ? `--branch ${branch} ` : ''; - const command = `git clone --progress ${branchArg}${url} ${dest}`; - - exec(command, { env: process.env }, async (cloneError) => { + execFile('git', args, { env }, async (cloneError) => { if (cloneError) { - // git clone writes progress to stderr, so only reject on real errors - // "Cloning into" on stderr is normal git output, not an error - const msg = cloneError.message || ''; - if (!msg.includes('Cloning into')) { - reject(new Error(`Failed to clone ${url}: ${msg}`)); - return; - } + reject(new Error(`Failed to clone ${url}: ${cloneError.message}`)); + return; } try { - // Remove .git folder from the cloned repo await files.rm_recursive_async(files.pathJoin(destPath, '.git')); resolve(); } catch (e) { @@ -144,19 +137,18 @@ async function cloneRepo(url, destPath, { branch = null } = {}) { async function cloneSubdirectory(repoUrl, branch, subdir, destPath) { const tempDir = files.mkdtemp('meteor-example-'); try { - const branchArg = branch ? `--branch ${branch} ` : ''; - const command = `git clone --progress ${branchArg}${repoUrl} ${tempDir}`; - - process.env.GIT_TERMINAL_PROMPT = 0; + const env = Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' }); + const args = ['clone', '--progress']; + if (branch) { + args.push('--branch', branch); + } + args.push(repoUrl, tempDir); await new Promise((resolve, reject) => { - exec(command, { env: process.env }, (error) => { + execFile('git', args, { env }, (error) => { if (error) { - const msg = error.message || ''; - if (!msg.includes('Cloning into')) { - reject(new Error(`Failed to clone ${repoUrl}: ${msg}`)); - return; - } + reject(new Error(`Failed to clone ${repoUrl}: ${error.message}`)); + return; } resolve(); }); From 88672b3636a55150ba96a4f27119761a27956879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 17:22:24 +0100 Subject: [PATCH 43/66] handle `--refresh` in examples fetch: avoid using stale cache and update error message for missing directories --- tools/cli/examples.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 6f50b703a0..8c47500d5b 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -86,6 +86,11 @@ async function getExamples({ refresh = false } = {}) { return examples; } catch (fetchError) { + // When refresh is requested, don't fall back to stale cache + if (refresh) { + throw fetchError; + } + // Network failed — fall back to cache if available const cached = readCache(); if (cached && cached.examples) { @@ -157,7 +162,7 @@ async function cloneSubdirectory(repoUrl, branch, subdir, destPath) { const subdirPath = files.pathJoin(tempDir, subdir); if (!files.exists(subdirPath)) { throw new Error( - `Directory '${subdir}' not found in the repository. The examples list may be outdated — try 'meteor create --list --refresh'.` + `Directory '${subdir}' not found in the repository. The examples list may be outdated — try 'meteor create --list' to see current examples.` ); } From 9f61ccbca88a6a4def993a4a63db012a6ed5bed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 26 Mar 2026 17:23:43 +0100 Subject: [PATCH 44/66] validate subdirectory paths in examples fetch to prevent directory traversal attacks --- tools/cli/examples.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/cli/examples.js b/tools/cli/examples.js index 8c47500d5b..d3d4069355 100644 --- a/tools/cli/examples.js +++ b/tools/cli/examples.js @@ -159,7 +159,14 @@ async function cloneSubdirectory(repoUrl, branch, subdir, destPath) { }); }); - const subdirPath = files.pathJoin(tempDir, subdir); + const path = require('path'); + const resolvedTemp = path.resolve(tempDir) + path.sep; + const subdirPath = path.resolve(files.pathJoin(tempDir, subdir)); + if (!subdirPath.startsWith(resolvedTemp)) { + throw new Error( + `Invalid subdirectory '${subdir}': path escapes the repository.` + ); + } if (!files.exists(subdirPath)) { throw new Error( `Directory '${subdir}' not found in the repository. The examples list may be outdated — try 'meteor create --list' to see current examples.` From f3b296efc834ed29b6971255209199f380b63e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 15:31:03 +0100 Subject: [PATCH 45/66] Add custom summary reporter to Jest configuration in e2e tests --- tools/e2e-tests/jest.config.js | 4 ++ tools/e2e-tests/summary-reporter.js | 95 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tools/e2e-tests/summary-reporter.js diff --git a/tools/e2e-tests/jest.config.js b/tools/e2e-tests/jest.config.js index f625e86ac5..3f62f89d54 100644 --- a/tools/e2e-tests/jest.config.js +++ b/tools/e2e-tests/jest.config.js @@ -30,4 +30,8 @@ module.exports = { } }, maxWorkers: 1, + reporters: [ + 'default', + '/summary-reporter.js', + ], }; diff --git a/tools/e2e-tests/summary-reporter.js b/tools/e2e-tests/summary-reporter.js new file mode 100644 index 0000000000..f10612d142 --- /dev/null +++ b/tools/e2e-tests/summary-reporter.js @@ -0,0 +1,95 @@ +/** + * Custom Jest reporter that prints a structured summary of all test results, + * including detailed error logs for failures. + */ +class SummaryReporter { + constructor(globalConfig) { + this._globalConfig = globalConfig; + } + + onRunComplete(_contexts, results) { + const passed = []; + const failed = []; + const skipped = []; + + for (const suite of results.testResults) { + for (const test of suite.testResults) { + const entry = { + name: test.fullName || test.title, + suite: suite.testFilePath.replace(this._globalConfig.rootDir + '/', ''), + duration: test.duration, + status: test.status, + }; + + if (test.status === 'passed') { + passed.push(entry); + } else if (test.status === 'failed') { + entry.errors = test.failureMessages || []; + failed.push(entry); + } else { + skipped.push(entry); + } + } + } + + this._printConsole(passed, failed, skipped); + } + + _printConsole(passed, failed, skipped) { + const divider = '═'.repeat(70); + const thinDivider = '─'.repeat(70); + + console.log('\n' + divider); + console.log(' E2E TEST SUMMARY'); + console.log(divider); + + if (passed.length > 0) { + console.log(`\n PASSED (${passed.length}):`); + console.log(thinDivider); + for (const t of passed) { + const duration = t.duration ? ` (${(t.duration / 1000).toFixed(1)}s)` : ''; + console.log(` [PASS] ${t.name}${duration}`); + } + } + + if (skipped.length > 0) { + console.log(`\n SKIPPED (${skipped.length}):`); + console.log(thinDivider); + for (const t of skipped) { + console.log(` [SKIP] ${t.name}`); + } + } + + if (failed.length > 0) { + console.log(`\n FAILED (${failed.length}):`); + console.log(thinDivider); + for (const t of failed) { + const duration = t.duration ? ` (${(t.duration / 1000).toFixed(1)}s)` : ''; + console.log(`\n [FAIL] ${t.name}${duration}`); + console.log(` Suite: ${t.suite}`); + for (const err of t.errors) { + const indented = err + .split('\n') + .map(line => ` ${line}`) + .join('\n'); + console.log(indented); + } + } + } + + const totalTime = [...passed, ...failed, ...skipped] + .reduce((sum, t) => sum + (t.duration || 0), 0); + + console.log('\n' + divider); + console.log( + ` TOTAL: ${passed.length + failed.length + skipped.length} | ` + + `PASSED: ${passed.length} | ` + + `FAILED: ${failed.length} | ` + + `SKIPPED: ${skipped.length} | ` + + `TIME: ${(totalTime / 1000).toFixed(1)}s` + ); + console.log(divider + '\n'); + } +} + +module.exports = SummaryReporter; From 94308d07d0ff47b2d90bee6316008796b753fcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 15:31:13 +0100 Subject: [PATCH 46/66] remove Jest retry logic for CI environment in e2e test setup --- tools/e2e-tests/jest.setup.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tools/e2e-tests/jest.setup.js b/tools/e2e-tests/jest.setup.js index 8b3f830a1f..3269f839a9 100644 --- a/tools/e2e-tests/jest.setup.js +++ b/tools/e2e-tests/jest.setup.js @@ -1,12 +1,6 @@ // jest.setup.js import chalk from 'chalk'; -const isCI = process.env.GITHUB_ACTIONS === "true"; -if (isCI) { - jest.retryTimes(2); - 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 = ''; From 873bbbcdf08c60caef2decc8d1a35c9c18b774b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 15:46:42 +0100 Subject: [PATCH 47/66] enhance E2E test summary reporter with color-coded output using `chalk` for better readability. --- tools/e2e-tests/summary-reporter.js | 41 ++++++++++++++++------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/tools/e2e-tests/summary-reporter.js b/tools/e2e-tests/summary-reporter.js index f10612d142..ec2593574f 100644 --- a/tools/e2e-tests/summary-reporter.js +++ b/tools/e2e-tests/summary-reporter.js @@ -1,3 +1,5 @@ +const chalk = require('chalk'); + /** * Custom Jest reporter that prints a structured summary of all test results, * including detailed error logs for failures. @@ -36,41 +38,44 @@ class SummaryReporter { } _printConsole(passed, failed, skipped) { - const divider = '═'.repeat(70); - const thinDivider = '─'.repeat(70); + const hasFails = failed.length > 0; + const divider = chalk.dim('═'.repeat(70)); + const thinDivider = chalk.dim('─'.repeat(70)); console.log('\n' + divider); - console.log(' E2E TEST SUMMARY'); + console.log(hasFails + ? chalk.bold.red(' E2E TEST SUMMARY') + : chalk.bold.green(' E2E TEST SUMMARY')); console.log(divider); if (passed.length > 0) { - console.log(`\n PASSED (${passed.length}):`); + console.log(chalk.green(`\n PASSED (${passed.length}):`)); console.log(thinDivider); for (const t of passed) { - const duration = t.duration ? ` (${(t.duration / 1000).toFixed(1)}s)` : ''; - console.log(` [PASS] ${t.name}${duration}`); + const duration = t.duration ? chalk.dim(` (${(t.duration / 1000).toFixed(1)}s)`) : ''; + console.log(` ${chalk.green('✓')} ${t.name}${duration}`); } } if (skipped.length > 0) { - console.log(`\n SKIPPED (${skipped.length}):`); + console.log(chalk.yellow(`\n SKIPPED (${skipped.length}):`)); console.log(thinDivider); for (const t of skipped) { - console.log(` [SKIP] ${t.name}`); + console.log(` ${chalk.yellow('○')} ${chalk.dim(t.name)}`); } } if (failed.length > 0) { - console.log(`\n FAILED (${failed.length}):`); + console.log(chalk.red(`\n FAILED (${failed.length}):`)); console.log(thinDivider); for (const t of failed) { - const duration = t.duration ? ` (${(t.duration / 1000).toFixed(1)}s)` : ''; - console.log(`\n [FAIL] ${t.name}${duration}`); - console.log(` Suite: ${t.suite}`); + const duration = t.duration ? chalk.dim(` (${(t.duration / 1000).toFixed(1)}s)`) : ''; + console.log(`\n ${chalk.red('✕')} ${chalk.bold(t.name)}${duration}`); + console.log(` ${chalk.dim('Suite:')} ${chalk.dim(t.suite)}`); for (const err of t.errors) { const indented = err .split('\n') - .map(line => ` ${line}`) + .map(line => ` ${chalk.red(line)}`) .join('\n'); console.log(indented); } @@ -82,11 +87,11 @@ class SummaryReporter { console.log('\n' + divider); console.log( - ` TOTAL: ${passed.length + failed.length + skipped.length} | ` + - `PASSED: ${passed.length} | ` + - `FAILED: ${failed.length} | ` + - `SKIPPED: ${skipped.length} | ` + - `TIME: ${(totalTime / 1000).toFixed(1)}s` + ` ${chalk.bold('TOTAL:')} ${passed.length + failed.length + skipped.length} ${chalk.dim('|')} ` + + `${chalk.green('PASSED:')} ${chalk.green(passed.length)} ${chalk.dim('|')} ` + + `${chalk.red('FAILED:')} ${chalk.red(failed.length)} ${chalk.dim('|')} ` + + `${chalk.yellow('SKIPPED:')} ${chalk.yellow(skipped.length)} ${chalk.dim('|')} ` + + `${chalk.dim('TIME:')} ${chalk.dim((totalTime / 1000).toFixed(1) + 's')}` ); console.log(divider + '\n'); } From 3ccea457686573d7038460df86c4a86c9a924caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 16:02:51 +0100 Subject: [PATCH 48/66] conditionally display skipped tests in E2E summary based on `E2E_SHOW_SKIPPED` environment variable --- tools/e2e-tests/summary-reporter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/e2e-tests/summary-reporter.js b/tools/e2e-tests/summary-reporter.js index ec2593574f..d0269d0298 100644 --- a/tools/e2e-tests/summary-reporter.js +++ b/tools/e2e-tests/summary-reporter.js @@ -57,7 +57,7 @@ class SummaryReporter { } } - if (skipped.length > 0) { + if (skipped.length > 0 && process.env.E2E_SHOW_SKIPPED) { console.log(chalk.yellow(`\n SKIPPED (${skipped.length}):`)); console.log(thinDivider); for (const t of skipped) { From 681d0df94eee02f3fd75b5ea6681698a2b5d8f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 16:04:15 +0100 Subject: [PATCH 49/66] refactor E2E workflow to replace retry action with custom retry steps for better control over test retries --- .github/workflows/e2e-tests.yml | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ce29d07dfb..268af2df5c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -86,10 +86,25 @@ jobs: run: ./meteor --get-ready - name: Run tests for ${{ matrix.category }} - uses: nick-fields/retry@v3 - with: - max_attempts: 3 - retry_on: error - timeout_minutes: 15 - retry_wait_seconds: 90 - command: npm run test:e2e -- -t="${{ matrix.category }}" + id: test-run + continue-on-error: true + timeout-minutes: 15 + run: npm run test:e2e -- -t="${{ matrix.category }}" + + - name: Retry failed tests for ${{ matrix.category }} (attempt 2) + id: test-retry-1 + if: steps.test-run.outcome == 'failure' + continue-on-error: true + timeout-minutes: 15 + run: | + echo "::warning::First attempt failed, retrying..." + sleep 90 + npm run test:e2e -- -t="${{ matrix.category }}" + + - name: Retry failed tests for ${{ matrix.category }} (attempt 3) + if: steps.test-retry-1.outcome == 'failure' + timeout-minutes: 15 + run: | + echo "::warning::Second attempt failed, retrying..." + sleep 90 + npm run test:e2e -- -t="${{ matrix.category }}" From 65a0a0841be3468a622beee68a748e5414c31052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 16:59:54 +0100 Subject: [PATCH 50/66] update Rspack config to re-append unignore patterns for meteor.modules and add `modules` field to Vue E2E test app --- packages/rspack/lib/config.js | 12 +++++++++++- tools/e2e-tests/apps/vue/package.json | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 70278d75ea..d2675e46e5 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -418,7 +418,17 @@ export function applyDelegatedExtensions(extensions) { } if (ignorePatterns.length > 0) { - setMeteorAppIgnore(ignorePatterns.join(' ')); + // Re-append meteor.modules unignore patterns after the delegation ignores + // so they take precedence (gitignore semantics: last match wins) + const meteorAppConfig = getMeteorAppConfig(); + const unignoredFilesAndFolders = buildUnignorePatterns( + meteorAppConfig?.modules || [], + { skipLevel: 1 }, + ); + + setMeteorAppIgnore( + [...ignorePatterns, ...unignoredFilesAndFolders].join(' ') + ); if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) { logInfo(`[i] Rspack delegated extensions: ${extensions.join(', ')} (ignored in entry folders)\n ${process.env.METEOR_IGNORE}`); diff --git a/tools/e2e-tests/apps/vue/package.json b/tools/e2e-tests/apps/vue/package.json index 31c39838ad..a533902f34 100644 --- a/tools/e2e-tests/apps/vue/package.json +++ b/tools/e2e-tests/apps/vue/package.json @@ -34,6 +34,7 @@ "client": "client/main.js", "server": "server/main.js" }, + "modules": ["client/meteor.css"], "testModule": "tests/main.js" } } From 3642e3faca3ece88ec934d1d753e1e7aef78f962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 17:44:07 +0100 Subject: [PATCH 51/66] refactor `MeteorRspackOutputPlugin` to improve extension extraction logic; differentiate configured and delegated extensions --- .../plugins/MeteorRspackOutputPlugin.js | 35 +++++++++++++++++-- npm-packages/meteor-rspack/rspack.config.js | 2 +- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js index 274e938a7e..45986acb01 100644 --- a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js +++ b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js @@ -9,11 +9,10 @@ const { outputMeteorRspack } = require('../lib/meteorRspackHelpers'); /** * Extracts file extensions that rspack is configured to handle * from the resolved module.rules test patterns. - * Only returns extensions relevant for Meteor delegation (CSS-family). * @param {import('@rspack/core').Compiler} compiler - * @returns {string[]} Array of extensions like ['.css', '.less', '.scss'] + * @returns {Set} Set of extensions like .css, .less, .scss */ -function extractDelegatedExtensions(compiler) { +function extractConfiguredExtensions(compiler) { const delegatableExtensions = ['.css', '.less', '.scss', '.sass', '.styl']; const found = new Set(); @@ -37,6 +36,36 @@ function extractDelegatedExtensions(compiler) { } inspectRules(compiler.options.module?.rules || []); + return found; +} + +/** + * Extracts file extensions that rspack both has rules for AND actually compiled. + * An extension is only delegated if it appears in the config rules and at least + * one file with that extension was part of the compilation dependency graph. + * This prevents Meteor from ignoring files that Rspack is configured for but + * never actually processes. + * @param {import('@rspack/core').Stats} stats + * @param {import('@rspack/core').Compiler} compiler + * @returns {string[]} Array of extensions like ['.css', '.less', '.scss'] + */ +function extractDelegatedExtensions(stats, compiler) { + const configured = extractConfiguredExtensions(compiler); + if (configured.size === 0) return []; + + const found = new Set(); + const path = require('path'); + + for (const module of stats.compilation.modules) { + const resource = module.resource || module.userRequest; + if (!resource) continue; + const ext = path.extname(resource); + if (configured.has(ext)) { + found.add(ext); + if (found.size === configured.size) break; + } + } + return Array.from(found); } diff --git a/npm-packages/meteor-rspack/rspack.config.js b/npm-packages/meteor-rspack/rspack.config.js index c83a9cfd57..e700538991 100644 --- a/npm-packages/meteor-rspack/rspack.config.js +++ b/npm-packages/meteor-rspack/rspack.config.js @@ -854,7 +854,7 @@ module.exports = async function (inMeteor = {}, argv = {}) { compilationCount, isRebuild, ...(!isRebuild && compiler && { - delegatedExtensions: extractDelegatedExtensions(compiler), + delegatedExtensions: extractDelegatedExtensions(stats, compiler), }), }), }); From 80a7c7790dfaeda6a0831c8c9f5e104dda2f8c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 17:55:16 +0100 Subject: [PATCH 52/66] update `MeteorRspackOutputPlugin` to refine extension extraction; prioritize entry folder files for delegation --- .../plugins/MeteorRspackOutputPlugin.js | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js index 45986acb01..548e4183ab 100644 --- a/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js +++ b/npm-packages/meteor-rspack/plugins/MeteorRspackOutputPlugin.js @@ -40,11 +40,11 @@ function extractConfiguredExtensions(compiler) { } /** - * Extracts file extensions that rspack both has rules for AND actually compiled. - * An extension is only delegated if it appears in the config rules and at least - * one file with that extension was part of the compilation dependency graph. - * This prevents Meteor from ignoring files that Rspack is configured for but - * never actually processes. + * Extracts file extensions that rspack both has rules for AND actually compiled + * from files within entry folder paths (e.g. client/, server/). + * An extension is only delegated if Rspack compiled a file with that extension + * from an entry folder. Files in non-entry folders (e.g. imports/) don't count, + * since delegation only ignores entry folder files for Meteor. * @param {import('@rspack/core').Stats} stats * @param {import('@rspack/core').Compiler} compiler * @returns {string[]} Array of extensions like ['.css', '.less', '.scss'] @@ -53,12 +53,39 @@ function extractDelegatedExtensions(stats, compiler) { const configured = extractConfiguredExtensions(compiler); if (configured.size === 0) return []; - const found = new Set(); const path = require('path'); + const fs = require('fs'); + const appRoot = compiler.options.context || process.cwd(); + + // Read entry folders from package.json meteor.mainModule + const entryFolders = new Set(); + try { + const pkgPath = path.join(appRoot, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + const mainModule = pkg?.meteor?.mainModule || {}; + for (const entry of Object.values(mainModule)) { + if (typeof entry === 'string') { + const folder = entry.split('/')[0]; + if (folder) entryFolders.add(folder); + } + } + } catch (e) { + // If we can't read package.json, fall back to config-only + return Array.from(configured); + } + + if (entryFolders.size === 0) return Array.from(configured); + + const found = new Set(); for (const module of stats.compilation.modules) { const resource = module.resource || module.userRequest; if (!resource) continue; + + const relativePath = path.relative(appRoot, resource); + const topFolder = relativePath.split(path.sep)[0]; + if (!entryFolders.has(topFolder)) continue; + const ext = path.extname(resource); if (configured.has(ext)) { found.add(ext); From facf6fa7c533cddf6d8ecc1f7c417a0112e6b661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 27 Mar 2026 18:08:57 +0100 Subject: [PATCH 53/66] document CSS auto-delegation and `meteor.modules` config updates in Rspack E2E coverage --- dev/modern-tools/rspack/E2E_COVERAGE.md | 11 ++++++++--- tools/e2e-tests/apps/vue/.meteorignore | 2 -- tools/static-assets/skel-tailwind/.meteorignore | 2 -- 3 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 tools/e2e-tests/apps/vue/.meteorignore delete mode 100644 tools/static-assets/skel-tailwind/.meteorignore diff --git a/dev/modern-tools/rspack/E2E_COVERAGE.md b/dev/modern-tools/rspack/E2E_COVERAGE.md index 1199c73a50..d656fcd7b2 100644 --- a/dev/modern-tools/rspack/E2E_COVERAGE.md +++ b/dev/modern-tools/rspack/E2E_COVERAGE.md @@ -54,7 +54,7 @@ Full-featured React Router app with custom packages, Less, and advanced rspack c | 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 | +| `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 | @@ -132,12 +132,15 @@ CoffeeScript language support. ### vue -Vue.js framework with Tailwind CSS. +Vue.js framework with Tailwind CSS, CSS auto-delegation, and `meteor.modules` config. | What is covered | Phase | |----------------|-------| | Vue single-file components | All | | Tailwind CSS styles (`.p-8` padding) | Run, Prod | +| CSS auto-delegation (`client/main.css` processed by Rspack, not Meteor) | All | +| `meteor.modules` config preserves `client/meteor.css` for Meteor processing | All | +| Rspack CSS + Meteor CSS coexistence in same entry folder | All | | HMR works in dev, disabled in prod | Run, Prod | ### solid @@ -262,7 +265,7 @@ Where each feature is tested across apps and skeletons. | Static asset bundling | react-router, monorepo | | | Less styles | react-router | | | SCSS styles | typescript | | -| Tailwind CSS | vue | tailwind | +| Tailwind CSS | vue (PostCSS) | tailwind | | Image asset loading | react | | | 404 routing | react-router | | | Meta tags | react-router | | @@ -278,6 +281,8 @@ Where each feature is tested across apps and skeletons. | Custom NODE_ENV compilation | babel | | | Portable build (no isDev/isProd defines) | typescript | | | `Meteor.extendSwcConfig` (path aliases) | typescript | | +| CSS auto-delegation (entry folder filtering) | vue | | +| `meteor.modules` config (preserve files for Meteor) | react-router, vue | | | `meteor reset` cleanup | all apps | all skeletons | | Skeleton creation | | all 14 skeletons | | Body style assertions | | react, tailwind (custom); most others (default) | diff --git a/tools/e2e-tests/apps/vue/.meteorignore b/tools/e2e-tests/apps/vue/.meteorignore deleted file mode 100644 index 21d63e9485..0000000000 --- a/tools/e2e-tests/apps/vue/.meteorignore +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore client/main.css file so that is processed by Rspack import -client/main.css diff --git a/tools/static-assets/skel-tailwind/.meteorignore b/tools/static-assets/skel-tailwind/.meteorignore deleted file mode 100644 index 4568c54a7d..0000000000 --- a/tools/static-assets/skel-tailwind/.meteorignore +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore Meteor CSS handling; let Rspack resolve Tailwind styles -client/main.css From 9c9d400260ff54c500db3735cfe31e4a0444e8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 09:45:30 +0200 Subject: [PATCH 54/66] add `checkout-pr` script and update documentation for testing fork branches locally --- CONTRIBUTING.md | 8 ++ DEVELOPMENT.md | 26 ++++ scripts/checkout-pr.js | 261 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100755 scripts/checkout-pr.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c646fad457..40ac2548f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,14 @@ Current Reviewers: - [@zodern](https://github.com/zodern) - [@radekmie](https://github.com/radekmie) +##### Testing a contributor's branch locally + +To quickly check out a PR branch from a fork for local testing, see the [Testing a fork branch](DEVELOPMENT.md#testing-a-fork-branch) section in `DEVELOPMENT.md`, or run: + +```sh +node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ +``` + #### Core Committer The contributors with commit access to meteor/meteor are employees of Meteor Software LP or community members who have distinguished themselves in other contribution areas or members of partner companies. If you want to become a core committer, please start writing PRs. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 58eda23621..e8c49ab999 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -56,6 +56,32 @@ can run Meteor directly from a Git checkout using these steps: > > Then you can use the chrome debugger inside `chrome://inspect`. +### Testing a fork branch + +When reviewing a pull request or testing changes from a contributor's fork, use the `checkout-pr.js` script to set up a local branch automatically: + +```sh +# From a PR URL (requires gh CLI or falls back to GitHub API via curl) +$ node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ + +# From a user:branch shorthand +$ node scripts/checkout-pr.js : + +# From a full fork repo URL and branch name +$ node scripts/checkout-pr.js +``` + +The script will: + +1. Add the fork as a git remote (named after the fork owner) if not already present +2. Fetch the target branch +3. Create (or update) a local branch named `fork//` +4. Print instructions for switching back to your previous branch + +For upstream PRs (branches on `meteor/meteor` itself), the script detects the existing `origin` remote and checks out the branch directly without the `fork/` prefix. + +If you run the script again for the same fork branch, it will fetch the latest changes and update the local branch. + ### Notes when running from a checkout The following are some distinct differences you must pay attention to when running Meteor from a checkout: diff --git a/scripts/checkout-pr.js b/scripts/checkout-pr.js new file mode 100755 index 0000000000..f904f2a5ac --- /dev/null +++ b/scripts/checkout-pr.js @@ -0,0 +1,261 @@ +#!/usr/bin/env node +// +// checkout-pr.js — prepare a local branch from a fork contribution +// +// Usage: +// node scripts/checkout-pr.js +// node scripts/checkout-pr.js : +// node scripts/checkout-pr.js +// +// Examples: +// node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ +// node scripts/checkout-pr.js : +// node scripts/checkout-pr.js + +'use strict'; + +const { execSync } = require('child_process'); +const https = require('https'); + +// Colors (disabled if stdout is not a TTY) +const isTTY = process.stdout.isTTY; +const c = { + red: isTTY ? '\x1b[0;31m' : '', + green: isTTY ? '\x1b[0;32m' : '', + yellow: isTTY ? '\x1b[0;33m' : '', + cyan: isTTY ? '\x1b[0;36m' : '', + bold: isTTY ? '\x1b[1m' : '', + reset: isTTY ? '\x1b[0m' : '', +}; + +function info(msg) { console.log(`${c.cyan}\u2192${c.reset} ${msg}`); } +function ok(msg) { console.log(`${c.green}\u2713${c.reset} ${msg}`); } +function warn(msg) { console.log(`${c.yellow}\u26A0${c.reset} ${msg}`); } +function err(msg) { console.error(`${c.red}\u2717${c.reset} ${msg}`); } + +function die(msg) { + err(msg); + process.exit(1); +} + +function usage() { + console.log(`Usage: + checkout-pr.js + checkout-pr.js : + checkout-pr.js + +Prepares a local branch from a fork contribution for testing and review. + +Examples: + node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ + node scripts/checkout-pr.js : + node scripts/checkout-pr.js `); + process.exit(1); +} + +function git(cmd, { silent = false } = {}) { + try { + return execSync(`git ${cmd}`, { + encoding: 'utf8', + stdio: silent ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'inherit'], + }).trim(); + } catch (e) { + if (silent) return null; + throw e; + } +} + +function ghCli(args) { + try { + return execSync(`gh ${args}`, { encoding: 'utf8', stdio: 'pipe' }).trim(); + } catch { + return null; + } +} + +function hasCommand(name) { + try { + execSync(process.platform === 'win32' ? `where ${name}` : `command -v ${name}`, { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +function httpsGet(url) { + return new Promise((resolve, reject) => { + https.get(url, { headers: { 'User-Agent': 'meteor-checkout-pr' } }, (res) => { + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject(new Error(`HTTP ${res.statusCode}`)); + } + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => resolve(data)); + }).on('error', reject); + }); +} + +async function extractFromPrUrl(prUrl) { + const match = prUrl.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/); + if (!match) die(`could not parse PR URL: ${prUrl}`); + const [, repoPath, prNumber] = match; + + // Try gh CLI first + if (hasCommand('gh')) { + const result = ghCli(`pr view "${prUrl}" --json headRepositoryOwner,headRefName`); + if (result) { + try { + const data = JSON.parse(result); + const owner = data.headRepositoryOwner?.login; + const branch = data.headRefName; + if (owner && branch) return { owner, branch }; + } catch { /* fall through */ } + } + } + + // Fall back to GitHub REST API + const apiUrl = `https://api.github.com/repos/${repoPath}/pulls/${prNumber}`; + let body; + try { + body = await httpsGet(apiUrl); + } catch (e) { + die(`could not fetch PR data from ${apiUrl} (${e.message})`); + } + + try { + const data = JSON.parse(body); + const owner = data.head?.user?.login; + const branch = data.head?.ref; + if (owner && branch) return { owner, branch }; + } catch { /* fall through */ } + + die(`could not extract fork owner/branch from PR #${prNumber}`); +} + +function extractOwnerFromUrl(url) { + const match = url.match(/github\.com[:/]([^/]+)\//); + return match ? match[1] : null; +} + +function normalizeUrl(url) { + return url + .replace(/\.git$/, '') + .replace(/\/$/, '') + .replace(/^https?:\/\//, '') + .replace(/^git@github\.com:/, 'github.com/'); +} + +async function main() { + const args = process.argv.slice(2); + + // Ensure we're inside a git repo + if (!git('rev-parse --is-inside-work-tree', { silent: true })) { + die('not inside a git repository'); + } + + let forkOwner, forkBranch, forkRepoUrl; + + if (args.length === 0) { + usage(); + } else if (args.length === 1) { + const arg = args[0]; + const prMatch = arg.match(/^https?:\/\/github\.com\/.*\/pull\/\d+/); + const shortMatch = arg.match(/^([^:]+):(.+)$/); + + if (prMatch) { + const result = await extractFromPrUrl(arg); + forkOwner = result.owner; + forkBranch = result.branch; + forkRepoUrl = `https://github.com/${forkOwner}/meteor.git`; + } else if (shortMatch) { + forkOwner = shortMatch[1]; + forkBranch = shortMatch[2]; + forkRepoUrl = `https://github.com/${forkOwner}/meteor.git`; + } else { + err(`unrecognized format: ${arg}`); + console.error(''); + usage(); + } + } else if (args.length === 2) { + forkRepoUrl = args[0]; + forkBranch = args[1]; + forkOwner = extractOwnerFromUrl(forkRepoUrl); + if (!forkOwner) die(`could not extract owner from URL: ${forkRepoUrl}`); + } else { + usage(); + } + + const previousBranch = git('symbolic-ref --short HEAD', { silent: true }) + || git('rev-parse --short HEAD', { silent: true }) + || 'HEAD'; + + // Detect if the PR is from the upstream repo (not a fork) + let remoteName = ''; + let isUpstream = false; + const originUrl = git('remote get-url origin', { silent: true }); + if (originUrl) { + const normFork = normalizeUrl(forkRepoUrl); + const normOrigin = normalizeUrl(originUrl); + if (normFork === normOrigin) { + remoteName = 'origin'; + isUpstream = true; + } + } + + let localBranch; + if (isUpstream) { + localBranch = forkBranch; + } else { + remoteName = forkOwner; + localBranch = `fork/${forkOwner}/${forkBranch}`; + } + + console.log(`${c.bold}--- checkout-pr ---${c.reset}`); + info(`owner: ${c.bold}${forkOwner}${c.reset}`); + info(`branch: ${c.bold}${forkBranch}${c.reset}`); + info(`repo: ${c.bold}${forkRepoUrl}${c.reset}`); + if (isUpstream) { + info(`upstream: yes (using remote '${c.bold}${remoteName}${c.reset}')`); + } + info(`local branch: ${c.bold}${localBranch}${c.reset}`); + console.log(''); + + // Add remote if needed (skip for upstream PRs) + if (!isUpstream) { + const existingUrl = git(`remote get-url "${remoteName}"`, { silent: true }); + if (existingUrl) { + warn(`remote '${remoteName}' already exists, reusing it`); + } else { + info(`adding remote '${remoteName}' \u2192 ${forkRepoUrl}`); + git(`remote add "${remoteName}" "${forkRepoUrl}"`); + ok(`remote '${remoteName}' added`); + } + } + + // Fetch the branch + info(`fetching '${forkBranch}' from '${remoteName}'...`); + const fetchResult = git(`fetch "${remoteName}" "${forkBranch}"`, { silent: true }); + if (fetchResult === null) { + die(`failed to fetch branch '${forkBranch}' from '${remoteName}' \u2014 check that the fork and branch exist`); + } + ok(`fetched latest from '${remoteName}'`); + + // Create or switch to local branch + const branchExists = git(`show-ref --verify "refs/heads/${localBranch}"`, { silent: true }); + if (branchExists) { + warn(`branch '${localBranch}' already exists, switching and updating...`); + git(`checkout "${localBranch}"`); + git(`reset --hard "refs/remotes/${remoteName}/${forkBranch}"`); + } else { + info(`creating branch '${localBranch}'...`); + git(`checkout -b "${localBranch}" "refs/remotes/${remoteName}/${forkBranch}"`); + } + + console.log(''); + ok(`ready on branch: ${c.bold}${localBranch}${c.reset}`); + info(`to switch back: ${c.bold}git checkout ${previousBranch}${c.reset}`); +} + +main().catch((e) => { + die(e.message); +}); From 8f8ed8ccb4c59fd23f6919c2b69ad803a0452106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 14:15:28 +0200 Subject: [PATCH 55/66] support SSH-based fork URLs and add clear error handling for unrecognized repo formats in checkout-pr script --- scripts/checkout-pr.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/checkout-pr.js b/scripts/checkout-pr.js index f904f2a5ac..1b174899b0 100755 --- a/scripts/checkout-pr.js +++ b/scripts/checkout-pr.js @@ -145,6 +145,15 @@ function normalizeUrl(url) { .replace(/^git@github\.com:/, 'github.com/'); } +function buildForkUrl(owner) { + // Match origin's protocol (SSH vs HTTPS) + const originUrl = git('remote get-url origin', { silent: true }) || ''; + if (originUrl.startsWith('git@')) { + return `git@github.com:${owner}/meteor.git`; + } + return `https://github.com/${owner}/meteor.git`; +} + async function main() { const args = process.argv.slice(2); @@ -160,17 +169,22 @@ async function main() { } else if (args.length === 1) { const arg = args[0]; const prMatch = arg.match(/^https?:\/\/github\.com\/.*\/pull\/\d+/); - const shortMatch = arg.match(/^([^:]+):(.+)$/); + const sshMatch = arg.match(/^git@[^:]+:/); + const httpsRepoMatch = arg.match(/^https?:\/\/.*\.git$/); + // user:branch — must not start with git@ (SSH) or contain / before : (URLs) + const shortMatch = !sshMatch && arg.match(/^([^/:]+):(.+)$/); if (prMatch) { const result = await extractFromPrUrl(arg); forkOwner = result.owner; forkBranch = result.branch; - forkRepoUrl = `https://github.com/${forkOwner}/meteor.git`; + forkRepoUrl = buildForkUrl(forkOwner); + } else if (sshMatch || httpsRepoMatch) { + die(`repo URL requires a branch argument: node scripts/checkout-pr.js ${arg} `); } else if (shortMatch) { forkOwner = shortMatch[1]; forkBranch = shortMatch[2]; - forkRepoUrl = `https://github.com/${forkOwner}/meteor.git`; + forkRepoUrl = buildForkUrl(forkOwner); } else { err(`unrecognized format: ${arg}`); console.error(''); From 7ec1dd5948dddcfbb767a5f7ca2026e946fc084a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 14:17:57 +0200 Subject: [PATCH 56/66] add `checkout:pr` npm script and update documentation usage --- CONTRIBUTING.md | 2 +- DEVELOPMENT.md | 8 ++++---- package.json | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40ac2548f1..04902faabf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,7 +58,7 @@ Current Reviewers: To quickly check out a PR branch from a fork for local testing, see the [Testing a fork branch](DEVELOPMENT.md#testing-a-fork-branch) section in `DEVELOPMENT.md`, or run: ```sh -node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ +npm run checkout:pr -- https://github.com/meteor/meteor/pull/ ``` #### Core Committer diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9c5c216561..80f6cbf585 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -70,13 +70,13 @@ When reviewing a pull request or testing changes from a contributor's fork, use ```sh # From a PR URL (requires gh CLI or falls back to GitHub API via curl) -$ node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ +$ npm run checkout:pr -- https://github.com/meteor/meteor/pull/ # From a user:branch shorthand -$ node scripts/checkout-pr.js : +$ npm run checkout:pr -- : -# From a full fork repo URL and branch name -$ node scripts/checkout-pr.js +# From a full fork repo URL and branch name (HTTPS or SSH) +$ npm run checkout:pr -- ``` The script will: diff --git a/package.json b/package.json index 9c096fcd0a..6c4f6cc640 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "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 -- " + "test:e2e": "cd tools/modern-tests && npm test -- ", + "checkout:pr": "node scripts/checkout-pr.js" }, "jshintConfig": { "esversion": 11 From f1a0d2677805aae9ddab034adcb14f22febb58d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 14:21:04 +0200 Subject: [PATCH 57/66] sdd SSH fork URL support to `checkout:pr` script and usage documentation --- DEVELOPMENT.md | 5 ++++- scripts/checkout-pr.js | 16 ++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 80f6cbf585..bf5c84193f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -75,8 +75,11 @@ $ npm run checkout:pr -- https://github.com/meteor/meteor/pull/ # From a user:branch shorthand $ npm run checkout:pr -- : -# From a full fork repo URL and branch name (HTTPS or SSH) +# From a full fork repo URL and branch name (HTTPS) $ npm run checkout:pr -- + +# From a full fork repo URL and branch name (SSH) +$ npm run checkout:pr -- git@github.com:/.git ``` The script will: diff --git a/scripts/checkout-pr.js b/scripts/checkout-pr.js index 1b174899b0..d0c1e32ebd 100755 --- a/scripts/checkout-pr.js +++ b/scripts/checkout-pr.js @@ -6,11 +6,13 @@ // node scripts/checkout-pr.js // node scripts/checkout-pr.js : // node scripts/checkout-pr.js +// node scripts/checkout-pr.js git@github.com:/.git // // Examples: // node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ // node scripts/checkout-pr.js : // node scripts/checkout-pr.js +// node scripts/checkout-pr.js git@github.com:/.git 'use strict'; @@ -40,16 +42,18 @@ function die(msg) { function usage() { console.log(`Usage: - checkout-pr.js - checkout-pr.js : - checkout-pr.js + npm run checkout:pr -- + npm run checkout:pr -- : + npm run checkout:pr -- + npm run checkout:pr -- git@github.com:/.git Prepares a local branch from a fork contribution for testing and review. Examples: - node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ - node scripts/checkout-pr.js : - node scripts/checkout-pr.js `); + npm run checkout:pr -- https://github.com/meteor/meteor/pull/ + npm run checkout:pr -- : + npm run checkout:pr -- + npm run checkout:pr -- git@github.com:/.git `); process.exit(1); } From 88dc95bbd48aee6fc67e937f376742115dacecf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 16:47:54 +0200 Subject: [PATCH 58/66] replace React imports with automatic JSX runtime and add `swc.config.ts` templates for TypeScript and Tailwind skeletons --- .../skel-typescript-tailwind/client/main.tsx | 1 - .../skel-typescript-tailwind/imports/ui/App.tsx | 1 - .../skel-typescript-tailwind/imports/ui/Hello.tsx | 2 +- .../skel-typescript-tailwind/imports/ui/Info.tsx | 1 - .../skel-typescript-tailwind/package.json | 1 + .../skel-typescript-tailwind/swc.config.ts | 13 +++++++++++++ tools/static-assets/skel-typescript/.swcrc | 9 --------- tools/static-assets/skel-typescript/package.json | 1 + tools/static-assets/skel-typescript/swc.config.ts | 13 +++++++++++++ 9 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 tools/static-assets/skel-typescript-tailwind/swc.config.ts delete mode 100644 tools/static-assets/skel-typescript/.swcrc create mode 100644 tools/static-assets/skel-typescript/swc.config.ts diff --git a/tools/static-assets/skel-typescript-tailwind/client/main.tsx b/tools/static-assets/skel-typescript-tailwind/client/main.tsx index a86c160a73..99ef2083ac 100644 --- a/tools/static-assets/skel-typescript-tailwind/client/main.tsx +++ b/tools/static-assets/skel-typescript-tailwind/client/main.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Meteor } from 'meteor/meteor'; import { createRoot } from 'react-dom/client'; import { App } from '/imports/ui/App'; diff --git a/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx b/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx index 90a2b06b9e..a21faaeeec 100644 --- a/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx +++ b/tools/static-assets/skel-typescript-tailwind/imports/ui/App.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Hello } from './Hello'; import { Info } from './Info'; diff --git a/tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx b/tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx index 9d0e05237c..370eeca07c 100644 --- a/tools/static-assets/skel-typescript-tailwind/imports/ui/Hello.tsx +++ b/tools/static-assets/skel-typescript-tailwind/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/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx b/tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx index bc7a35f123..30e8d3355f 100644 --- a/tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx +++ b/tools/static-assets/skel-typescript-tailwind/imports/ui/Info.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useFind, useSubscribe } from "meteor/react-meteor-data/suspense"; import { LinksCollection } from "../api/links"; diff --git a/tools/static-assets/skel-typescript-tailwind/package.json b/tools/static-assets/skel-typescript-tailwind/package.json index a75fe7e2a0..b18a3a0601 100644 --- a/tools/static-assets/skel-typescript-tailwind/package.json +++ b/tools/static-assets/skel-typescript-tailwind/package.json @@ -21,6 +21,7 @@ "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", + "@swc/core": "^1.15.18", "@rspack/plugin-react-refresh": "^1.4.3", "@tailwindcss/postcss": "^4.1.12", "@types/meteor": "^2.9.9", diff --git a/tools/static-assets/skel-typescript-tailwind/swc.config.ts b/tools/static-assets/skel-typescript-tailwind/swc.config.ts new file mode 100644 index 0000000000..699a33a40d --- /dev/null +++ b/tools/static-assets/skel-typescript-tailwind/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/static-assets/skel-typescript/.swcrc b/tools/static-assets/skel-typescript/.swcrc deleted file mode 100644 index bbc8887edb..0000000000 --- a/tools/static-assets/skel-typescript/.swcrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "jsc": { - "transform": { - "react": { - "runtime": "automatic" - } - } - } -} \ No newline at end of file diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json index 59017498e0..58b1a58a59 100644 --- a/tools/static-assets/skel-typescript/package.json +++ b/tools/static-assets/skel-typescript/package.json @@ -19,6 +19,7 @@ "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", + "@swc/core": "^1.15.18", "@rspack/plugin-react-refresh": "^1.4.3", "@types/meteor": "^2.9.9", "@types/mocha": "^8.2.3", diff --git a/tools/static-assets/skel-typescript/swc.config.ts b/tools/static-assets/skel-typescript/swc.config.ts new file mode 100644 index 0000000000..699a33a40d --- /dev/null +++ b/tools/static-assets/skel-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; From 33124cf9f191152cc50ff4678d44ac86cae44ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 16:48:51 +0200 Subject: [PATCH 59/66] add E2E test for TypeScript Tailwind skeleton and disable TsCheckerRspackPlugin in CI --- tools/e2e-tests/skeleton.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tools/e2e-tests/skeleton.test.js b/tools/e2e-tests/skeleton.test.js index 1556a00c51..63aa19e3c2 100644 --- a/tools/e2e-tests/skeleton.test.js +++ b/tools/e2e-tests/skeleton.test.js @@ -216,6 +216,33 @@ describe('Meteor Skeletons /', () => { }), ); + describe( + "Typescript Tailwind Skeleton /", + testMeteorSkeleton({ + skeletonName: "typescript", + port: 3221, + filePaths: { + client: "client/main.tsx", + server: "server/main.ts", + test: "tests/main.ts", + }, + customAssertions: { + afterCreate({ tempDir }) { + if (isCI) { + const rspackConfigPath = path.join(tempDir, "rspack.config.ts"); + // Remove the TsCheckerRspackPlugin plugin as is resource-intense, CI gets exhausted and fails + let configContent = fs.readFileSync(rspackConfigPath, "utf8"); + configContent = configContent.replace( + /\s*new\s+TsCheckerRspackPlugin\(\)/, + "" + ); + fs.writeFileSync(rspackConfigPath, configContent); + } + }, + }, + }) + ); + describe( 'Vue Skeleton /', testMeteorSkeleton({ From f2c0f8136aacb09a6ad24067e6a1a7d5947194aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 17:36:17 +0200 Subject: [PATCH 60/66] add PR number support to `checkout:pr` script and enhance usage documentation --- scripts/checkout-pr.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/checkout-pr.js b/scripts/checkout-pr.js index d0c1e32ebd..7914243b07 100755 --- a/scripts/checkout-pr.js +++ b/scripts/checkout-pr.js @@ -3,12 +3,14 @@ // checkout-pr.js — prepare a local branch from a fork contribution // // Usage: +// node scripts/checkout-pr.js // node scripts/checkout-pr.js // node scripts/checkout-pr.js : // node scripts/checkout-pr.js // node scripts/checkout-pr.js git@github.com:/.git // // Examples: +// node scripts/checkout-pr.js 123 // node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/ // node scripts/checkout-pr.js : // node scripts/checkout-pr.js @@ -42,6 +44,7 @@ function die(msg) { function usage() { console.log(`Usage: + npm run checkout:pr -- npm run checkout:pr -- npm run checkout:pr -- : npm run checkout:pr -- @@ -50,6 +53,7 @@ function usage() { Prepares a local branch from a fork contribution for testing and review. Examples: + npm run checkout:pr -- 123 npm run checkout:pr -- https://github.com/meteor/meteor/pull/ npm run checkout:pr -- : npm run checkout:pr -- @@ -136,6 +140,18 @@ async function extractFromPrUrl(prUrl) { die(`could not extract fork owner/branch from PR #${prNumber}`); } +function getRepoPathFromOrigin() { + const originUrl = git('remote get-url origin', { silent: true }); + if (!originUrl) return null; + // Match HTTPS: https://github.com/owner/repo(.git) + const httpsMatch = originUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/); + if (httpsMatch) return httpsMatch[1]; + // Match SSH: git@github.com:owner/repo(.git) + const sshMatch = originUrl.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/); + if (sshMatch) return sshMatch[1]; + return null; +} + function extractOwnerFromUrl(url) { const match = url.match(/github\.com[:/]([^/]+)\//); return match ? match[1] : null; @@ -172,13 +188,23 @@ async function main() { usage(); } else if (args.length === 1) { const arg = args[0]; + const prNumberMatch = arg.match(/^\d+$/); const prMatch = arg.match(/^https?:\/\/github\.com\/.*\/pull\/\d+/); const sshMatch = arg.match(/^git@[^:]+:/); const httpsRepoMatch = arg.match(/^https?:\/\/.*\.git$/); // user:branch — must not start with git@ (SSH) or contain / before : (URLs) const shortMatch = !sshMatch && arg.match(/^([^/:]+):(.+)$/); - if (prMatch) { + if (prNumberMatch) { + const repoPath = getRepoPathFromOrigin(); + if (!repoPath) die('could not determine repository from origin remote'); + const prUrl = `https://github.com/${repoPath}/pull/${arg}`; + info(`resolved PR #${arg} → ${c.bold}${prUrl}${c.reset}`); + const result = await extractFromPrUrl(prUrl); + forkOwner = result.owner; + forkBranch = result.branch; + forkRepoUrl = buildForkUrl(forkOwner); + } else if (prMatch) { const result = await extractFromPrUrl(arg); forkOwner = result.owner; forkBranch = result.branch; From 7c72c706f77e5b847d44b1b12fa959ce3071c47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 17:38:33 +0200 Subject: [PATCH 61/66] refactor `checkout:pr` script to simplify argument validation and improve error handling --- scripts/checkout-pr.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/checkout-pr.js b/scripts/checkout-pr.js index 7914243b07..1bfdc7cdf2 100755 --- a/scripts/checkout-pr.js +++ b/scripts/checkout-pr.js @@ -184,9 +184,7 @@ async function main() { let forkOwner, forkBranch, forkRepoUrl; - if (args.length === 0) { - usage(); - } else if (args.length === 1) { + if (args.length === 1) { const arg = args[0]; const prNumberMatch = arg.match(/^\d+$/); const prMatch = arg.match(/^https?:\/\/github\.com\/.*\/pull\/\d+/); @@ -218,7 +216,7 @@ async function main() { } else { err(`unrecognized format: ${arg}`); console.error(''); - usage(); + return usage(); } } else if (args.length === 2) { forkRepoUrl = args[0]; @@ -226,7 +224,7 @@ async function main() { forkOwner = extractOwnerFromUrl(forkRepoUrl); if (!forkOwner) die(`could not extract owner from URL: ${forkRepoUrl}`); } else { - usage(); + return usage(); } const previousBranch = git('symbolic-ref --short HEAD', { silent: true }) From 7c35bb0ebe3b6d5f25cc9d605ac2e92ae58c6820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 17:42:09 +0200 Subject: [PATCH 62/66] update test to use "typescript-tailwind" skeleton name in E2E suite --- tools/e2e-tests/skeleton.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/e2e-tests/skeleton.test.js b/tools/e2e-tests/skeleton.test.js index 63aa19e3c2..a747446238 100644 --- a/tools/e2e-tests/skeleton.test.js +++ b/tools/e2e-tests/skeleton.test.js @@ -219,7 +219,7 @@ describe('Meteor Skeletons /', () => { describe( "Typescript Tailwind Skeleton /", testMeteorSkeleton({ - skeletonName: "typescript", + skeletonName: "typescript-tailwind", port: 3221, filePaths: { client: "client/main.tsx", From 186b38f3e7f46e0489b17d9de65dd8a84573801f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 18:36:25 +0200 Subject: [PATCH 63/66] switch JSX setting from "react" to "react-jsx" --- tools/static-assets/skel-typescript-tailwind/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/static-assets/skel-typescript-tailwind/tsconfig.json b/tools/static-assets/skel-typescript-tailwind/tsconfig.json index 21922a37d9..420b46c51a 100644 --- a/tools/static-assets/skel-typescript-tailwind/tsconfig.json +++ b/tools/static-assets/skel-typescript-tailwind/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2020", "module": "ESNext", "moduleResolution": "node", - "jsx": "react", + "jsx": "react-jsx", "strict": false, "noEmit": true, "esModuleInterop": true, From cd04e0e0c4c777b90231c0031ee42550e320c2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 22:22:14 +0200 Subject: [PATCH 64/66] bump @meteorjs/rspack to 1.1.0-beta.33 across all skeletons and constants, add scripts and devDependencies for version management --- npm-packages/meteor-rspack/package-lock.json | 20 +++++++++++++++++-- npm-packages/meteor-rspack/package.json | 9 ++++++++- packages/rspack/lib/constants.js | 2 +- tools/e2e-tests/apps/solid/package.json | 2 +- tools/e2e-tests/apps/svelte/package.json | 2 +- tools/e2e-tests/apps/vue/package.json | 2 +- tools/static-assets/skel-angular/package.json | 2 +- tools/static-assets/skel-apollo/package.json | 2 +- tools/static-assets/skel-babel/package.json | 2 +- tools/static-assets/skel-blaze/package.json | 2 +- .../static-assets/skel-chakra-ui/package.json | 2 +- .../skel-coffeescript/package.json | 2 +- tools/static-assets/skel-full/package.json | 2 +- tools/static-assets/skel-react/package.json | 2 +- tools/static-assets/skel-solid/package.json | 2 +- tools/static-assets/skel-svelte/package.json | 2 +- .../static-assets/skel-tailwind/package.json | 2 +- .../skel-typescript/package.json | 2 +- tools/static-assets/skel-vue/package.json | 2 +- 19 files changed, 43 insertions(+), 20 deletions(-) diff --git a/npm-packages/meteor-rspack/package-lock.json b/npm-packages/meteor-rspack/package-lock.json index 739b4cd71d..4143577f54 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.1.0-beta.31", + "version": "1.1.0-beta.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@meteorjs/rspack", - "version": "1.1.0-beta.31", + "version": "1.1.0-beta.33", "license": "ISC", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -14,6 +14,9 @@ "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", @@ -4470,6 +4473,19 @@ "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 a880bf3f19..8107608514 100644 --- a/npm-packages/meteor-rspack/package.json +++ b/npm-packages/meteor-rspack/package.json @@ -1,17 +1,24 @@ { "name": "@meteorjs/rspack", - "version": "1.1.0-beta.31", + "version": "1.1.0-beta.33", "description": "Configuration logic for using Rspack in Meteor projects", "main": "index.js", "type": "commonjs", "author": "", "license": "ISC", + "scripts": { + "bump": "node ./scripts/bump-version.js", + "publish:beta": "bash ./scripts/publish-beta.sh" + }, "dependencies": { "fast-deep-equal": "^3.1.3", "ignore-loader": "^0.1.2", "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", diff --git a/packages/rspack/lib/constants.js b/packages/rspack/lib/constants.js index 1341f54ce7..89830dd491 100644 --- a/packages/rspack/lib/constants.js +++ b/packages/rspack/lib/constants.js @@ -7,7 +7,7 @@ import path from 'path'; export const DEFAULT_RSPACK_VERSION = '1.7.1'; -export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.31'; +export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.33'; export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3'; diff --git a/tools/e2e-tests/apps/solid/package.json b/tools/e2e-tests/apps/solid/package.json index 5ce93de638..0f055dbf09 100644 --- a/tools/e2e-tests/apps/solid/package.json +++ b/tools/e2e-tests/apps/solid/package.json @@ -22,7 +22,7 @@ "modern": true }, "devDependencies": { - "@meteorjs/rspack": "^1.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "babel-loader": "10.0.0", diff --git a/tools/e2e-tests/apps/svelte/package.json b/tools/e2e-tests/apps/svelte/package.json index cba0d4e7a0..82496ec1a0 100644 --- a/tools/e2e-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": "^1.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "playwright": "1.58.0", diff --git a/tools/e2e-tests/apps/vue/package.json b/tools/e2e-tests/apps/vue/package.json index a533902f34..36b2b717dd 100644 --- a/tools/e2e-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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "@tailwindcss/postcss": "^4.1.12", diff --git a/tools/static-assets/skel-angular/package.json b/tools/static-assets/skel-angular/package.json index fa3105b024..c56f044706 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 5fd18f2131..5ccccde773 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 8c9c45753f..1a86abf697 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 6dd838aaf6..47e5d33755 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 a30ef9f180..d9ca4809d4 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 6f2ebbce93..bde5d75679 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 e7f8bb952e..88393fc5f1 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 1091b1727a..a5d14793d7 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 d14262c517..7a21b3b001 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 d864cf7c23..2f0bfebafe 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 79935caba4..69f05385c7 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@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 58b1a58a59..3877a069a2 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-vue/package.json b/tools/static-assets/skel-vue/package.json index a156645d98..1dda2123b5 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.1.0-beta.31", + "@meteorjs/rspack": "^1.1.0-beta.33", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", From e3e59360c6e2c3df0a5866cdd647425cdbdeed2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 30 Mar 2026 22:24:54 +0200 Subject: [PATCH 65/66] add "Examples" to workflow test matrix in e2e-tests --- .github/workflows/e2e-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ce29d07dfb..6491ba1600 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -33,6 +33,7 @@ jobs: - Babel - Blaze - Coffeescript + - Examples - Monorepo - Other - React From 5004a9efd09b4d2b093e6c716ae5f1f27d50e869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 31 Mar 2026 09:14:43 +0200 Subject: [PATCH 66/66] improve examples documentation --- v3-docs/docs/cli/index.md | 82 +++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index d42635aa56..70f54bb100 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -264,47 +264,6 @@ If you run `meteor create` without arguments, Meteor will launch an interactive | `--release ` | Specify Meteor version (e.g., `--release 3.4`) | | `--prototype` | Include `autopublish` and `insecure` packages for rapid prototyping (not for production) | -### Examples - -Meteor provides a curated list of community examples that you can use as starting points. The examples are maintained in the [meteor/examples](https://github.com/meteor/examples) repository. - -To browse available examples with descriptions, tech stack, demo links, and repository URLs: - -```bash -meteor create --list -``` - -To create a new app from an example: - -```bash -meteor create my-app --example simple-tasks -``` - -| Option | Description | -|--------|-------------| -| `--example ` | Create from a community example | -| `--list` | Show detailed list of available examples | - -### Cloning from a Git Repository - -You can create a new Meteor app by cloning any Git repository: - -```bash -meteor create my-app --from https://github.com/fredmaiaarantes/simpletasks -``` - -To extract a specific subdirectory from a repository, use `--from-dir`. You can also specify a branch with `--from-branch`: - -```bash -meteor create my-app --from https://github.com/meteor/examples --from-branch migrate-examples --from-dir parties -``` - -| Option | Description | -|--------|-------------| -| `--from ` | Clone a Meteor project from a Git URL | -| `--from-branch ` | Branch to clone from (use with `--from`) | -| `--from-dir ` | Extract only a subdirectory (use with `--from`) | - ### Application Types | Option | Description | Tutorial / Example | @@ -401,6 +360,47 @@ The `--prototype` option adds packages that make development faster but shouldn' To learn more about the recommended file structure for Meteor apps, check the [Meteor Guide](/tutorials/application-structure/#javascript-structure). ::: +### Community Examples + +Meteor ships with a collection of example apps that cover specific use cases, great for studying how features work in practice and drawing inspiration from more complete codebases. Official examples live in the [meteor/examples](https://github.com/meteor/examples) repository, while community-contributed ones link to their own repos. + +To browse available examples with descriptions, tech stack, demo links, and repository URLs: + +```bash +meteor create --list +``` + +To create a new app from an example: + +```bash +meteor create my-app --example simple-tasks +``` + +| Option | Description | +|--------|-------------| +| `--example ` | Create from a community example | +| `--list` | Show detailed list of available examples | + +### Create from a Git Repository + +You can create a new Meteor app by cloning any Git repository: + +```bash +meteor create my-app --from https://github.com/fredmaiaarantes/simpletasks +``` + +To extract a specific subdirectory from a repository, use `--from-dir`. You can also specify a branch with `--from-branch`: + +```bash +meteor create my-app --from https://github.com/meteor/examples --from-branch migrate-examples --from-dir parties +``` + +| Option | Description | +|--------|-------------| +| `--from ` | Clone a Meteor project from a Git URL | +| `--from-branch ` | Branch to clone from (use with `--from`) | +| `--from-dir ` | Extract only a subdirectory (use with `--from`) | + ## meteor generate {meteorgenerate} ``meteor generate`` is a command to generate boilerplate for your current project. `meteor generate` receives a name as a parameter, and generates files containing code to create a [Collection](https://docs.meteor.com/api/collections.html) with that name, [Methods](https://docs.meteor.com/api/meteor.html#methods) to perform basic CRUD operations on that Collection, and a [Subscription](https://docs.meteor.com/api/meteor.html#Meteor-publish) to read its data with reactivity from the client.