From 76d57698eaed7b706a4d5c0c3a60771314e8bd21 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Wed, 13 Nov 2024 18:44:13 -0300 Subject: [PATCH 001/264] DEV: Add tinytest `filter` option before we relied on the `TINYTEST_FILTER`(and we still do) but now you can use --filter or -f to filter the tests you want to run. --- tools/cli/commands.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index cc79168e02..cf4606fdfd 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -516,7 +516,7 @@ async function doRunCommand(options) { open(`http://localhost:${options.port}`) } } - }, + }, open: options.open, }); } @@ -2047,7 +2047,10 @@ testCommandOptions = { 'extra-packages': { type: String }, - 'exclude-archs': { type: String } + 'exclude-archs': { type: String }, + + // Same as TINYTEST_FILTER + filter: { type: String, short: 'f' }, } }; @@ -2068,6 +2071,9 @@ main.registerCommand(Object.assign( }); async function doTestCommand(options) { + if (options.filter) { + process.env.TINYTEST_FILTER = options.filter; + } // This "metadata" is accessed in a few places. Using a global // variable here was more expedient than navigating the many layers // of abstraction across the build process. From ae80c98d6c870e27cb878e9e5801bb3a3d009298 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Wed, 13 Nov 2024 18:51:42 -0300 Subject: [PATCH 002/264] DOCS: update cli docs to include tinytest filter option --- v3-docs/docs/cli/index.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index 8cdcb64636..2a19726209 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -932,6 +932,21 @@ argument will run tests for all local packages. The results are displayed in an app that runs at `localhost:3000` by default. If you need to, you can pass the `--settings` and `--port` arguments. +If you want to filter the tests by name, you can use `--filter` or `-f` +followed by the name of the test you want to run, it supports regex's. + +```sh +meteor test-packages --filter myTestName +``` + +this command will run only the tests that have `myTestName` in their name. + +Alternatively, you can use the `TINYTEST_FILTER` environment variable to filter: + +```sh +TINYTEST_FILTER=myTestName meteor test-packages +``` +Has the same effect as the previous command. ## meteor admin {meteoradmin} From e558c50de480e036760234eea09f66ac2dcb69e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 19 Mar 2025 13:44:46 +0100 Subject: [PATCH 003/264] adapt babel compile behavior to use SWC by default and fallback to babel --- meteor | 2 +- packages/autoupdate/autoupdate_server.js | 2 +- packages/babel-compiler/babel-compiler.js | 48 ++++++++++++++++++- packages/babel-compiler/package.js | 3 +- packages/boilerplate-generator/generator.js | 8 ++-- .../ddp-client/common/livedata_connection.js | 3 +- packages/ecmascript/runtime-tests.js | 4 +- packages/ejson/ejson.js | 2 +- packages/mongo/collection/methods_index.js | 8 ++-- scripts/dev-bundle-tool-package.js | 4 +- 10 files changed, 65 insertions(+), 19 deletions(-) diff --git a/meteor b/meteor index 745c3affbf..0c7f6125a9 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.4 +BUNDLE_VERSION=22.14.0.5 # OS Check. Put here because here is where we download the precompiled diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 7d20443c27..82af4c65e6 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -25,6 +25,7 @@ // The ID of each document is the client architecture, and the fields of // the document are the versions described above. +import { onMessage } from "meteor/inter-process-messaging"; import { ClientVersions } from "./client_versions.js"; export const Autoupdate = __meteor_runtime_config__.autoupdate = { @@ -152,7 +153,6 @@ function enqueueVersionsRefresh() { const setupListeners = () => { // Listen for messages pertaining to the client-refresh topic. - import { onMessage } from "meteor/inter-process-messaging"; onMessage("client-refresh", enqueueVersionsRefresh); // Another way to tell the process to refresh: send SIGHUP signal diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 11cf63f923..3f2f2e22a7 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -1,5 +1,6 @@ var semver = Npm.require("semver"); var JSON5 = Npm.require("json5"); +var SWC = Npm.require("@swc/core"); /** * A compiler that can be instantiated with features and used inside @@ -145,7 +146,52 @@ BCp.processOneFileForTarget = function (inputFile, source) { try { var result = profile('Babel.compile', function () { - return Babel.compile(source, babelOptions, cacheOptions); + let compilation; + try { + const packagesSkipSwc = []; + const fileSkipSwc = ['webapp_server.js']; // top level await + + // Determine if SWC should be used based on package and file criteria. + const shouldUseSwc = + ['minimongo', 'random'].includes(packageName) || + (!packagesSkipSwc.includes(packageName) && + !fileSkipSwc.includes(inputFilePath)); + + if (shouldUseSwc) { + const isTypescriptSyntax = + inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); + const hasTSXSupport = inputFilePath.endsWith('.tsx'); + const hasJSXSupport = inputFilePath.endsWith('.jsx'); + + const transformed = SWC.transformSync(source, { + jsc: { + target: 'es2015', + parser: { + syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', + jsx: hasJSXSupport, + tsx: hasTSXSupport, + }, + }, + module: { type: 'commonjs' }, + minify: false, + sourceMaps: true, + }); + + compilation = { + code: transformed.code, + map: JSON.parse(transformed.map), + hash: toBeAdded.hash, + sourceType: 'module', + }; + } else { + compilation = Babel.compile(source, babelOptions, cacheOptions); + } + } catch (e) { + // If SWC fails, fall back to Babel + compilation = Babel.compile(source, babelOptions, cacheOptions); + } + + return compilation; }); } catch (e) { if (e.loc) { diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 03822c789b..812b3e96af 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -7,7 +7,8 @@ Package.describe({ Npm.depends({ '@meteorjs/babel': '7.20.1', 'json5': '2.2.3', - 'semver': '7.6.3' + 'semver': '7.6.3', + "@swc/core": "1.11.11", }); Package.onUse(function (api) { diff --git a/packages/boilerplate-generator/generator.js b/packages/boilerplate-generator/generator.js index 0c4d8d426d..6b36f0adf3 100644 --- a/packages/boilerplate-generator/generator.js +++ b/packages/boilerplate-generator/generator.js @@ -1,8 +1,8 @@ import {readFileSync} from 'fs'; import { create as createStream } from "combined-stream2"; -import WebBrowserTemplate from './template-web.browser'; -import WebCordovaTemplate from './template-web.cordova'; +import { headTemplate as modernHeadTemplate, closeTemplate as modernCloseTemplate } from './template-web.browser'; +import { headTemplate as cordovaHeadTemplate, closeTemplate as cordovaCloseTemplate } from './template-web.cordova'; // Copied from webapp_server const readUtf8FileSync = filename => readFileSync(filename, 'utf8'); @@ -151,11 +151,11 @@ function getTemplate(arch) { const prefix = arch.split(".", 2).join("."); if (prefix === "web.browser") { - return WebBrowserTemplate; + return { headTemplate: modernHeadTemplate, closeTemplate: modernCloseTemplate }; } if (prefix === "web.cordova") { - return WebCordovaTemplate; + return { headTemplate: cordovaHeadTemplate, closeTemplate: cordovaCloseTemplate }; } throw new Error("Unsupported arch: " + arch); diff --git a/packages/ddp-client/common/livedata_connection.js b/packages/ddp-client/common/livedata_connection.js index abe1161d21..a568c7ca70 100644 --- a/packages/ddp-client/common/livedata_connection.js +++ b/packages/ddp-client/common/livedata_connection.js @@ -4,6 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { EJSON } from 'meteor/ejson'; import { Random } from 'meteor/random'; import { MongoID } from 'meteor/mongo-id'; +import { ClientStream } from "meteor/socket-stream-client"; import { DDP } from './namespace.js'; import { MethodInvoker } from './method_invoker'; import { @@ -74,8 +75,6 @@ export class Connection { if (typeof url === 'object') { self._stream = url; } else { - import { ClientStream } from "meteor/socket-stream-client"; - self._stream = new ClientStream(url, { retry: options.retry, ConnectionError: DDP.ConnectionError, diff --git a/packages/ecmascript/runtime-tests.js b/packages/ecmascript/runtime-tests.js index a276d7e380..693116b7e0 100644 --- a/packages/ecmascript/runtime-tests.js +++ b/packages/ecmascript/runtime-tests.js @@ -1,3 +1,5 @@ +import { testExport as oyez } from './runtime-tests.js'; + const isNode8OrLater = Meteor.isServer && parseInt(process.versions.node) >= 8; Tinytest.add('ecmascript - runtime - template literals', test => { @@ -169,14 +171,12 @@ Tinytest.add('ecmascript - runtime - classes - properties', test => { static staticProp = 1234; check = self => { - import { testExport as oyez } from './runtime-tests.js'; test.equal(oyez, 'oyez'); test.isTrue(self === this); test.equal(this.property, 'property'); }; method() { - import { testExport as oyez } from './runtime-tests.js'; test.equal(oyez, 'oyez'); } } diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index e83d099279..76ef364651 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -9,6 +9,7 @@ import { isInfOrNaN, handleError, } from './utils'; +import canonicalStringify from './stringify'; /** * @namespace @@ -395,7 +396,6 @@ EJSON.stringify = handleError((item, options) => { let serialized; const json = EJSON.toJSONValue(item); if (options && (options.canonical || options.indent)) { - import canonicalStringify from './stringify'; serialized = canonicalStringify(json, options); } else { serialized = JSON.stringify(json); diff --git a/packages/mongo/collection/methods_index.js b/packages/mongo/collection/methods_index.js index 05d1186054..0731e7a5e1 100644 --- a/packages/mongo/collection/methods_index.js +++ b/packages/mongo/collection/methods_index.js @@ -1,3 +1,5 @@ +import { Log } from 'meteor/logging'; + export const IndexMethods = { // We'll actually design an index API later. For now, we just pass through to // Mongo's, but make it synchronous. @@ -21,8 +23,6 @@ export const IndexMethods = { if (self._collection.createIndexAsync) { await self._collection.createIndexAsync(index, options); } else { - import { Log } from 'meteor/logging'; - Log.debug(`ensureIndexAsync has been deprecated, please use the new 'createIndexAsync' instead${ options?.name ? `, index name: ${ options.name }` : `, index: ${ JSON.stringify(index) }` }`) await self._collection.ensureIndexAsync(index, options); } @@ -54,8 +54,6 @@ export const IndexMethods = { ) && Meteor.settings?.packages?.mongo?.reCreateIndexOnOptionMismatch ) { - import { Log } from 'meteor/logging'; - Log.info(`Re-creating index ${ index } for ${ self._name } due to options mismatch.`); await self._collection.dropIndexAsync(index); await self._collection.createIndexAsync(index, options); @@ -88,4 +86,4 @@ export const IndexMethods = { throw new Error('Can only call dropIndexAsync on server collections'); await self._collection.dropIndexAsync(index); }, -} \ No newline at end of file +} diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 04aaa1029a..d57634cc0d 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -67,7 +67,9 @@ var packageJson = { "anser": "2.1.1", 'xmlbuilder2': '1.8.1', "ws": "7.4.5", - "open":"8.4.2" + "open":"8.4.2", + "@swc/core": "1.11.11", + "swc-to-babel": "4.0.0", } }; From 0a7c05e6ef8e56ec6226b3fdc0352341e3a1363e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 19 Mar 2025 14:41:54 +0100 Subject: [PATCH 004/264] clean --- packages/babel-compiler/babel-compiler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 3f2f2e22a7..322eaaf96c 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -153,8 +153,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = - ['minimongo', 'random'].includes(packageName) || - (!packagesSkipSwc.includes(packageName) && + !packagesSkipSwc.includes(packageName) && !fileSkipSwc.includes(inputFilePath)); if (shouldUseSwc) { From f13d42cd9a33b3f69e1108a481d895bd9660c19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 19 Mar 2025 15:02:23 +0100 Subject: [PATCH 005/264] clean --- packages/babel-compiler/babel-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 322eaaf96c..f8d6217d54 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -154,7 +154,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !packagesSkipSwc.includes(packageName) && - !fileSkipSwc.includes(inputFilePath)); + !fileSkipSwc.includes(inputFilePath); if (shouldUseSwc) { const isTypescriptSyntax = From bdc2df5712d8ee5b9ec2c8ab0e938d7bfc934b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 20 Mar 2025 14:48:29 +0100 Subject: [PATCH 006/264] resolve file.hash to properly resolve caching in some scenarios --- tools/isobuild/import-scanner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index fe6d9639bc..c4e3b04c57 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -977,7 +977,7 @@ export default class ImportScanner { private async findImportedModuleIdentifiers( file: File, ): Promise> { - const fileHash = file.hash; + const fileHash = file.hash instanceof Promise ? await file.hash : file.hash; if (IMPORT_SCANNER_CACHE.has(fileHash)) { return IMPORT_SCANNER_CACHE.get(fileHash) as Record; } From b8472bf257d0b4c29ef3f0e98ca8ef2f6865dd9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 20 Mar 2025 15:04:20 +0100 Subject: [PATCH 007/264] set properly cache on import scanner --- tools/isobuild/import-scanner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index c4e3b04c57..acec76b398 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -988,8 +988,8 @@ export default class ImportScanner { ); // there should always be file.hash, but better safe than sorry - if (file.hash) { - IMPORT_SCANNER_CACHE.set(file.hash, result); + if (fileHash) { + IMPORT_SCANNER_CACHE.set(fileHash, result); } return result; From d34c094bd82dc5ab3ecb30533c022e5f616e6d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Sat, 22 Mar 2025 08:23:27 +0100 Subject: [PATCH 008/264] support acorn as a faster approach to parse code for the analyzer --- scripts/dev-bundle-tool-package.js | 2 +- tools/isobuild/js-analyze.js | 43 +++++++++++++++++++----------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index d57634cc0d..60cc8d69fa 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -69,7 +69,7 @@ var packageJson = { "ws": "7.4.5", "open":"8.4.2", "@swc/core": "1.11.11", - "swc-to-babel": "4.0.0", + "acorn": "8.14.1", } }; diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index 35a929c083..9ed7b5fae3 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -4,6 +4,7 @@ import LRUCache from "lru-cache"; import { Profile } from '../tool-env/profile'; import Visitor from "@meteorjs/reify/lib/visitor.js"; import { findPossibleIndexes } from "@meteorjs/reify/lib/utils.js"; +import acorn from 'acorn'; const hasOwn = Object.prototype.hasOwnProperty; const objToStr = Object.prototype.toString @@ -15,7 +16,7 @@ function isRegExp(value) { var AST_CACHE = new LRUCache({ max: Math.pow(2, 12), length(ast) { - return ast.loc.end.line; + return ast.end; } }); @@ -28,20 +29,32 @@ function tryToParse(source, hash) { let ast; try { Profile.time('jsAnalyze.parse', () => { - ast = parse(source, { - strictMode: false, - sourceType: 'module', - allowImportExportEverywhere: true, - allowReturnOutsideFunction: true, - allowUndeclaredExports: true, - plugins: [ - // Only plugins for stage 3 features are enabled - // Enabling some plugins significantly affects parser performance - 'importAttributes', - 'explicitResourceManagement', - 'decorators' - ] - }); + try { + ast = acorn.parse(source, { + ecmaVersion: 'latest', + sourceType: 'script', + allowAwaitOutsideFunction: true, + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowHashBang: true, + checkPrivateFields: false + }); + } catch (error) { + ast = parse(source, { + strictMode: false, + sourceType: 'module', + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowUndeclaredExports: true, + plugins: [ + // Only plugins for stage 3 features are enabled + // Enabling some plugins significantly affects parser performance + 'importAttributes', + 'explicitResourceManagement', + 'decorators' + ] + }); + } }); } catch (e) { if (typeof e.loc === 'object') { From f257390ccad49fe86078a29a2b9cbe2137ccc2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Sat, 22 Mar 2025 09:14:41 +0100 Subject: [PATCH 009/264] get locations to perserve proper cache --- tools/isobuild/js-analyze.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index 9ed7b5fae3..6e37ded2b1 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -16,7 +16,7 @@ function isRegExp(value) { var AST_CACHE = new LRUCache({ max: Math.pow(2, 12), length(ast) { - return ast.end; + return ast.loc.end.line; } }); @@ -37,7 +37,8 @@ function tryToParse(source, hash) { allowImportExportEverywhere: true, allowReturnOutsideFunction: true, allowHashBang: true, - checkPrivateFields: false + checkPrivateFields: false, + locations: true, }); } catch (error) { ast = parse(source, { From 94e6bd855efb7a46012be030f1bcec6ed1da77d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Sat, 22 Mar 2025 09:28:50 +0100 Subject: [PATCH 010/264] calculate and use lines when is possible to perserve cache length behavior and also keep less RAM usage --- tools/isobuild/js-analyze.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index 6e37ded2b1..e4f1c7f3a2 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -16,7 +16,7 @@ function isRegExp(value) { var AST_CACHE = new LRUCache({ max: Math.pow(2, 12), length(ast) { - return ast.loc.end.line; + return ast?.loc?.end?.line || ast?.lines || ast.end; } }); @@ -32,14 +32,14 @@ function tryToParse(source, hash) { try { ast = acorn.parse(source, { ecmaVersion: 'latest', - sourceType: 'script', + sourceType: 'module', allowAwaitOutsideFunction: true, allowImportExportEverywhere: true, allowReturnOutsideFunction: true, allowHashBang: true, checkPrivateFields: false, - locations: true, }); + Object.assign(ast, { lines: (source?.split('\n')?.length || 0) - 1 }); } catch (error) { ast = parse(source, { strictMode: false, From 51a32122de6c99e12c035c92b97c62c7c1d8b5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Sat, 22 Mar 2025 09:29:42 +0100 Subject: [PATCH 011/264] perserve script sourceType --- tools/isobuild/js-analyze.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index e4f1c7f3a2..0944af885f 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -32,7 +32,7 @@ function tryToParse(source, hash) { try { ast = acorn.parse(source, { ecmaVersion: 'latest', - sourceType: 'module', + sourceType: 'script', allowAwaitOutsideFunction: true, allowImportExportEverywhere: true, allowReturnOutsideFunction: true, From 20930d21b54bbf72e20e7ad6a64bb6be95e4610f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Sat, 22 Mar 2025 09:35:34 +0100 Subject: [PATCH 012/264] better way to count lines --- tools/isobuild/js-analyze.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index 0944af885f..c66f5bb5dd 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -20,6 +20,14 @@ var AST_CACHE = new LRUCache({ } }); +function countLines(str) { + let count = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\n') count++; + } + return count; +} + // Like babel.parse, but annotates any thrown error with $ParseError = true. function tryToParse(source, hash) { if (hash && AST_CACHE.has(hash)) { @@ -39,7 +47,7 @@ function tryToParse(source, hash) { allowHashBang: true, checkPrivateFields: false, }); - Object.assign(ast, { lines: (source?.split('\n')?.length || 0) - 1 }); + Object.assign(ast, { lines: countLines(source) }); } catch (error) { ast = parse(source, { strictMode: false, From bd19fe02f8e5968c0e8ad8a279bb178ff8d5cf92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 25 Mar 2025 14:56:53 +0100 Subject: [PATCH 013/264] better approach to count lines --- tools/isobuild/js-analyze.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index c66f5bb5dd..8f011fff54 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -21,9 +21,11 @@ var AST_CACHE = new LRUCache({ }); function countLines(str) { + let lastIndex = str.indexOf('\n'); let count = 0; - for (let i = 0; i < str.length; i++) { - if (str[i] === '\n') count++; + while (lastIndex > -1) { + count += 1; + lastIndex = str.indexOf('\n', lastIndex + 1); } return count; } From 00780f91f457bbefd4c3f2ab307934fc109b5907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 25 Mar 2025 15:39:18 +0100 Subject: [PATCH 014/264] use an estimation for cache length for better performance --- tools/isobuild/js-analyze.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index 8f011fff54..2de961be63 100644 --- a/tools/isobuild/js-analyze.js +++ b/tools/isobuild/js-analyze.js @@ -16,20 +16,12 @@ function isRegExp(value) { var AST_CACHE = new LRUCache({ max: Math.pow(2, 12), length(ast) { - return ast?.loc?.end?.line || ast?.lines || ast.end; + // Estimate cached lines based on average length per character + const avgCharsPerLine = 40; + return Math.ceil(ast.end / avgCharsPerLine); } }); -function countLines(str) { - let lastIndex = str.indexOf('\n'); - let count = 0; - while (lastIndex > -1) { - count += 1; - lastIndex = str.indexOf('\n', lastIndex + 1); - } - return count; -} - // Like babel.parse, but annotates any thrown error with $ParseError = true. function tryToParse(source, hash) { if (hash && AST_CACHE.has(hash)) { @@ -49,7 +41,6 @@ function tryToParse(source, hash) { allowHashBang: true, checkPrivateFields: false, }); - Object.assign(ast, { lines: countLines(source) }); } catch (error) { ast = parse(source, { strictMode: false, From fbd71e9e6aea22f435198d14ed90bba5aba75440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 25 Mar 2025 18:30:24 +0100 Subject: [PATCH 015/264] add caching as part of compilation --- packages/babel-compiler/babel-compiler.js | 69 ++++++++++++++++------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index f8d6217d54..f9ea38f32f 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -1,6 +1,8 @@ var semver = Npm.require("semver"); var JSON5 = Npm.require("json5"); var SWC = Npm.require("@swc/core"); +var fs = Npm.require('fs'); +var path = Npm.require('path'); /** * A compiler that can be instantiated with features and used inside @@ -51,7 +53,9 @@ BCp.processFilesForTarget = function (inputFiles) { // Returns an object suitable for passing to inputFile.addJavaScript, or // null to indicate there was an error, and nothing should be added. BCp.processOneFileForTarget = function (inputFile, source) { + var self = this; // capture context this._babelrcCache = this._babelrcCache || Object.create(null); + this._transformCache = this._transformCache || Object.create(null); if (typeof source !== "string") { // Other compiler plugins can call processOneFileForTarget with a @@ -122,23 +126,18 @@ BCp.processOneFileForTarget = function (inputFile, source) { }, }; - this.inferTypeScriptConfig( - features, inputFile, cacheOptions.cacheDeps); + this.inferTypeScriptConfig(features, inputFile, cacheOptions.cacheDeps); var babelOptions = Babel.getDefaultOptions(features); babelOptions.caller = { name: "meteor", arch }; - this.inferExtraBabelOptions( - inputFile, - babelOptions, - cacheOptions.cacheDeps - ); + this.inferExtraBabelOptions(inputFile, babelOptions, cacheOptions.cacheDeps); babelOptions.sourceMaps = true; babelOptions.filename = babelOptions.sourceFileName = packageName - ? "packages/" + packageName + "/" + inputFilePath - : inputFilePath; + ? "packages/" + packageName + "/" + inputFilePath + : inputFilePath; if (this.modifyBabelConfig) { this.modifyBabelConfig(babelOptions, inputFile); @@ -146,19 +145,38 @@ BCp.processOneFileForTarget = function (inputFile, source) { try { var result = profile('Babel.compile', function () { - let compilation; + // Determine if SWC should be used based on package and file criteria. + const packagesSkipSwc = []; + const fileSkipSwc = ['webapp_server.js']; // top level await + const shouldUseSwc = !packagesSkipSwc.includes(packageName) && + !fileSkipSwc.includes(inputFilePath); + + // Create a cache key based on the source hash and the compiler used. + const cacheKey = `${toBeAdded.hash}-${shouldUseSwc ? 'swc' : 'babel'}`; + + // Check RAM cache + let compilation = self._transformCache[cacheKey]; + // Check file system cache if enabled + if (!compilation && self.cacheDirectory) { + const cacheFilePath = path.join(self.cacheDirectory, 'compilation-cache', cacheKey + '.json'); + if (fs.existsSync(cacheFilePath)) { + try { + compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8')); + self._transformCache[cacheKey] = compilation; + } catch (e) { + // If reading/parsing the cache fails, ignore and continue. + } + } + } + // Return cached result if found. + if (compilation) { + return compilation; + } + + // Perform compilation try { - const packagesSkipSwc = []; - const fileSkipSwc = ['webapp_server.js']; // top level await - - // Determine if SWC should be used based on package and file criteria. - const shouldUseSwc = - !packagesSkipSwc.includes(packageName) && - !fileSkipSwc.includes(inputFilePath); - if (shouldUseSwc) { - const isTypescriptSyntax = - inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); + const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); const hasTSXSupport = inputFilePath.endsWith('.tsx'); const hasJSXSupport = inputFilePath.endsWith('.jsx'); @@ -190,6 +208,17 @@ BCp.processOneFileForTarget = function (inputFile, source) { compilation = Babel.compile(source, babelOptions, cacheOptions); } + // Save result in caches + self._transformCache[cacheKey] = compilation; + if (self.cacheDirectory) { + const cacheFilePath = path.join(self.cacheDirectory, 'compilation-cache', cacheKey + '.json'); + try { + fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true }); + fs.writeFileSync(cacheFilePath, JSON.stringify(compilation), 'utf8'); + } catch (e) { + // If file caching fails, ignore the error. + } + } return compilation; }); } catch (e) { From 49a639955bd27ad719148a4e1f1a039ac8ef6c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 26 Mar 2025 15:08:58 +0100 Subject: [PATCH 016/264] apply cache only for swc as meteor-babel already describes a cache for babel --- packages/babel-compiler/babel-compiler.js | 73 ++++++++++++----------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index f9ea38f32f..9d928d6bd8 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -55,7 +55,7 @@ BCp.processFilesForTarget = function (inputFiles) { BCp.processOneFileForTarget = function (inputFile, source) { var self = this; // capture context this._babelrcCache = this._babelrcCache || Object.create(null); - this._transformCache = this._transformCache || Object.create(null); + this._swcCache = this._swcCache || Object.create(null); if (typeof source !== "string") { // Other compiler plugins can call processOneFileForTarget with a @@ -151,35 +151,38 @@ BCp.processOneFileForTarget = function (inputFile, source) { const shouldUseSwc = !packagesSkipSwc.includes(packageName) && !fileSkipSwc.includes(inputFilePath); - // Create a cache key based on the source hash and the compiler used. - const cacheKey = `${toBeAdded.hash}-${shouldUseSwc ? 'swc' : 'babel'}`; - // Check RAM cache - let compilation = self._transformCache[cacheKey]; - // Check file system cache if enabled - if (!compilation && self.cacheDirectory) { - const cacheFilePath = path.join(self.cacheDirectory, 'compilation-cache', cacheKey + '.json'); - if (fs.existsSync(cacheFilePath)) { - try { - compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8')); - self._transformCache[cacheKey] = compilation; - } catch (e) { - // If reading/parsing the cache fails, ignore and continue. - } - } - } - // Return cached result if found. - if (compilation) { - return compilation; - } - - // Perform compilation + let compilation; try { if (shouldUseSwc) { + // Create a cache key based on the source hash and the compiler used. + const cacheKey = toBeAdded.hash; + const cacheContext = 'swc-cache'; + + // Check RAM cache + compilation = self._swcCache[cacheKey]; + // Check file system cache if enabled + if (!compilation && self.cacheDirectory) { + const cacheFilePath = path.join(self.cacheDirectory, cacheContext, cacheKey + '.json'); + if (fs.existsSync(cacheFilePath)) { + try { + compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8')); + self._swcCache[cacheKey] = compilation; + } catch (e) { + // If reading/parsing the cache fails, ignore and continue. + } + } + } + // Return cached result if found. + if (compilation) { + return compilation; + } + const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); const hasTSXSupport = inputFilePath.endsWith('.tsx'); const hasJSXSupport = inputFilePath.endsWith('.jsx'); + // Perform compilation const transformed = SWC.transformSync(source, { jsc: { target: 'es2015', @@ -200,6 +203,18 @@ BCp.processOneFileForTarget = function (inputFile, source) { hash: toBeAdded.hash, sourceType: 'module', }; + + // Save result in cache + self._swcCache[cacheKey] = compilation; + if (self.cacheDirectory) { + const cacheFilePath = path.join(self.cacheDirectory, cacheContext, cacheKey + '.json'); + try { + fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true }); + fs.writeFileSync(cacheFilePath, JSON.stringify(compilation), 'utf8'); + } catch (e) { + // If file caching fails, ignore the error. + } + } } else { compilation = Babel.compile(source, babelOptions, cacheOptions); } @@ -207,18 +222,6 @@ BCp.processOneFileForTarget = function (inputFile, source) { // If SWC fails, fall back to Babel compilation = Babel.compile(source, babelOptions, cacheOptions); } - - // Save result in caches - self._transformCache[cacheKey] = compilation; - if (self.cacheDirectory) { - const cacheFilePath = path.join(self.cacheDirectory, 'compilation-cache', cacheKey + '.json'); - try { - fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true }); - fs.writeFileSync(cacheFilePath, JSON.stringify(compilation), 'utf8'); - } catch (e) { - // If file caching fails, ignore the error. - } - } return compilation; }); } catch (e) { From c4e8fd4c4e628d82fae0b8e26b8db089dcde32bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 26 Mar 2025 15:19:50 +0100 Subject: [PATCH 017/264] better cache context --- packages/babel-compiler/babel-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 9d928d6bd8..09d680c19e 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -157,7 +157,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { if (shouldUseSwc) { // Create a cache key based on the source hash and the compiler used. const cacheKey = toBeAdded.hash; - const cacheContext = 'swc-cache'; + const cacheContext = '.swc-cache'; // Check RAM cache compilation = self._swcCache[cacheKey]; From c77611f57c63ea1bde03c618677234def26b48c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 26 Mar 2025 15:51:14 +0100 Subject: [PATCH 018/264] async write the cache --- packages/babel-compiler/babel-compiler.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 09d680c19e..8181009167 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -209,8 +209,12 @@ BCp.processOneFileForTarget = function (inputFile, source) { if (self.cacheDirectory) { const cacheFilePath = path.join(self.cacheDirectory, cacheContext, cacheKey + '.json'); try { - fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true }); - fs.writeFileSync(cacheFilePath, JSON.stringify(compilation), 'utf8'); + const writeFileCache = async () => { + await fs.promises.mkdir(path.dirname(cacheFilePath), { recursive: true }); + await fs.promises.writeFile(cacheFilePath, JSON.stringify(compilation), 'utf8'); + }; + // Asynchronously write the cache without blocking + writeFileCache(); } catch (e) { // If file caching fails, ignore the error. } From 09691e422ca149e91f83dd9173549302ef1190c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 26 Mar 2025 18:02:33 +0100 Subject: [PATCH 019/264] prefer more modern syntax on swc compilation --- packages/babel-compiler/babel-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index f8d6217d54..42aed0dd0f 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -164,7 +164,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { const transformed = SWC.transformSync(source, { jsc: { - target: 'es2015', + target: 'es2022', parser: { syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', jsx: hasJSXSupport, From 5b99337802850a9db319824fc4bb9ca9ed286904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 27 Mar 2025 15:37:37 +0100 Subject: [PATCH 020/264] Revert "prefer more modern syntax on swc compilation" This reverts commit 09691e422ca149e91f83dd9173549302ef1190c6. --- packages/babel-compiler/babel-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 42aed0dd0f..f8d6217d54 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -164,7 +164,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { const transformed = SWC.transformSync(source, { jsc: { - target: 'es2022', + target: 'es2015', parser: { syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', jsx: hasJSXSupport, From e1e44c15cb8a08078e951def3ae9c44de1fa540b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 27 Mar 2025 16:45:11 +0100 Subject: [PATCH 021/264] perserve Meteor's specifics: reify modules, nested imports and top-level await support --- packages/babel-compiler/babel-compiler.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index f8d6217d54..1de5a197d8 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -1,6 +1,8 @@ var semver = Npm.require("semver"); var JSON5 = Npm.require("json5"); var SWC = Npm.require("@swc/core"); +const reifyCompile = Npm.require("@meteorjs/reify/lib/compiler").compile; +const reifyAcornTopLevelParse = Npm.require("@meteorjs/reify/lib/parsers/top-level").parse; /** * A compiler that can be instantiated with features and used inside @@ -149,7 +151,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { let compilation; try { const packagesSkipSwc = []; - const fileSkipSwc = ['webapp_server.js']; // top level await + const fileSkipSwc = []; // top level await // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = @@ -171,13 +173,25 @@ BCp.processOneFileForTarget = function (inputFile, source) { tsx: hasTSXSupport, }, }, - module: { type: 'commonjs' }, + module: { type: 'es6' }, minify: false, sourceMaps: true, }); + let content = transformed.code; + // Perserve Meteor's specifics: reify modules, nested imports and top-level await support. + const result = reifyCompile(content, { + parse: reifyAcornTopLevelParse, + generateLetDeclarations: false, + ast: false, + }); + if (!result.identical) { + identical = false; + content = result.code; + } + compilation = { - code: transformed.code, + code: content, map: JSON.parse(transformed.map), hash: toBeAdded.hash, sourceType: 'module', From 2ddc37e2123cb7dae5e362579d8adfc1a5ee01ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 27 Mar 2025 17:13:08 +0100 Subject: [PATCH 022/264] revert removal of nested imports as they are still compatible --- packages/autoupdate/autoupdate_server.js | 2 +- packages/ddp-client/common/livedata_connection.js | 3 ++- packages/ecmascript/runtime-tests.js | 4 ++-- packages/ejson/ejson.js | 2 +- packages/mongo/collection/methods_index.js | 6 ++++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 82af4c65e6..7d20443c27 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -25,7 +25,6 @@ // The ID of each document is the client architecture, and the fields of // the document are the versions described above. -import { onMessage } from "meteor/inter-process-messaging"; import { ClientVersions } from "./client_versions.js"; export const Autoupdate = __meteor_runtime_config__.autoupdate = { @@ -153,6 +152,7 @@ function enqueueVersionsRefresh() { const setupListeners = () => { // Listen for messages pertaining to the client-refresh topic. + import { onMessage } from "meteor/inter-process-messaging"; onMessage("client-refresh", enqueueVersionsRefresh); // Another way to tell the process to refresh: send SIGHUP signal diff --git a/packages/ddp-client/common/livedata_connection.js b/packages/ddp-client/common/livedata_connection.js index a568c7ca70..abe1161d21 100644 --- a/packages/ddp-client/common/livedata_connection.js +++ b/packages/ddp-client/common/livedata_connection.js @@ -4,7 +4,6 @@ import { Tracker } from 'meteor/tracker'; import { EJSON } from 'meteor/ejson'; import { Random } from 'meteor/random'; import { MongoID } from 'meteor/mongo-id'; -import { ClientStream } from "meteor/socket-stream-client"; import { DDP } from './namespace.js'; import { MethodInvoker } from './method_invoker'; import { @@ -75,6 +74,8 @@ export class Connection { if (typeof url === 'object') { self._stream = url; } else { + import { ClientStream } from "meteor/socket-stream-client"; + self._stream = new ClientStream(url, { retry: options.retry, ConnectionError: DDP.ConnectionError, diff --git a/packages/ecmascript/runtime-tests.js b/packages/ecmascript/runtime-tests.js index 693116b7e0..a276d7e380 100644 --- a/packages/ecmascript/runtime-tests.js +++ b/packages/ecmascript/runtime-tests.js @@ -1,5 +1,3 @@ -import { testExport as oyez } from './runtime-tests.js'; - const isNode8OrLater = Meteor.isServer && parseInt(process.versions.node) >= 8; Tinytest.add('ecmascript - runtime - template literals', test => { @@ -171,12 +169,14 @@ Tinytest.add('ecmascript - runtime - classes - properties', test => { static staticProp = 1234; check = self => { + import { testExport as oyez } from './runtime-tests.js'; test.equal(oyez, 'oyez'); test.isTrue(self === this); test.equal(this.property, 'property'); }; method() { + import { testExport as oyez } from './runtime-tests.js'; test.equal(oyez, 'oyez'); } } diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index 76ef364651..e83d099279 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -9,7 +9,6 @@ import { isInfOrNaN, handleError, } from './utils'; -import canonicalStringify from './stringify'; /** * @namespace @@ -396,6 +395,7 @@ EJSON.stringify = handleError((item, options) => { let serialized; const json = EJSON.toJSONValue(item); if (options && (options.canonical || options.indent)) { + import canonicalStringify from './stringify'; serialized = canonicalStringify(json, options); } else { serialized = JSON.stringify(json); diff --git a/packages/mongo/collection/methods_index.js b/packages/mongo/collection/methods_index.js index 0731e7a5e1..c516d7522b 100644 --- a/packages/mongo/collection/methods_index.js +++ b/packages/mongo/collection/methods_index.js @@ -1,5 +1,3 @@ -import { Log } from 'meteor/logging'; - export const IndexMethods = { // We'll actually design an index API later. For now, we just pass through to // Mongo's, but make it synchronous. @@ -23,6 +21,8 @@ export const IndexMethods = { if (self._collection.createIndexAsync) { await self._collection.createIndexAsync(index, options); } else { + import { Log } from 'meteor/logging'; + Log.debug(`ensureIndexAsync has been deprecated, please use the new 'createIndexAsync' instead${ options?.name ? `, index name: ${ options.name }` : `, index: ${ JSON.stringify(index) }` }`) await self._collection.ensureIndexAsync(index, options); } @@ -54,6 +54,8 @@ export const IndexMethods = { ) && Meteor.settings?.packages?.mongo?.reCreateIndexOnOptionMismatch ) { + import { Log } from 'meteor/logging'; + Log.info(`Re-creating index ${ index } for ${ self._name } due to options mismatch.`); await self._collection.dropIndexAsync(index); await self._collection.createIndexAsync(index, options); From 8e8801e2ef08b7a876d4a93e14bf873a178cddc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 08:55:43 +0100 Subject: [PATCH 023/264] remove unrequired nested imports --- packages/ecmascript/runtime-tests.js | 4 ++-- packages/ejson/ejson.js | 2 +- packages/mongo/collection/methods_index.js | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/ecmascript/runtime-tests.js b/packages/ecmascript/runtime-tests.js index a276d7e380..693116b7e0 100644 --- a/packages/ecmascript/runtime-tests.js +++ b/packages/ecmascript/runtime-tests.js @@ -1,3 +1,5 @@ +import { testExport as oyez } from './runtime-tests.js'; + const isNode8OrLater = Meteor.isServer && parseInt(process.versions.node) >= 8; Tinytest.add('ecmascript - runtime - template literals', test => { @@ -169,14 +171,12 @@ Tinytest.add('ecmascript - runtime - classes - properties', test => { static staticProp = 1234; check = self => { - import { testExport as oyez } from './runtime-tests.js'; test.equal(oyez, 'oyez'); test.isTrue(self === this); test.equal(this.property, 'property'); }; method() { - import { testExport as oyez } from './runtime-tests.js'; test.equal(oyez, 'oyez'); } } diff --git a/packages/ejson/ejson.js b/packages/ejson/ejson.js index e83d099279..76ef364651 100644 --- a/packages/ejson/ejson.js +++ b/packages/ejson/ejson.js @@ -9,6 +9,7 @@ import { isInfOrNaN, handleError, } from './utils'; +import canonicalStringify from './stringify'; /** * @namespace @@ -395,7 +396,6 @@ EJSON.stringify = handleError((item, options) => { let serialized; const json = EJSON.toJSONValue(item); if (options && (options.canonical || options.indent)) { - import canonicalStringify from './stringify'; serialized = canonicalStringify(json, options); } else { serialized = JSON.stringify(json); diff --git a/packages/mongo/collection/methods_index.js b/packages/mongo/collection/methods_index.js index c516d7522b..0731e7a5e1 100644 --- a/packages/mongo/collection/methods_index.js +++ b/packages/mongo/collection/methods_index.js @@ -1,3 +1,5 @@ +import { Log } from 'meteor/logging'; + export const IndexMethods = { // We'll actually design an index API later. For now, we just pass through to // Mongo's, but make it synchronous. @@ -21,8 +23,6 @@ export const IndexMethods = { if (self._collection.createIndexAsync) { await self._collection.createIndexAsync(index, options); } else { - import { Log } from 'meteor/logging'; - Log.debug(`ensureIndexAsync has been deprecated, please use the new 'createIndexAsync' instead${ options?.name ? `, index name: ${ options.name }` : `, index: ${ JSON.stringify(index) }` }`) await self._collection.ensureIndexAsync(index, options); } @@ -54,8 +54,6 @@ export const IndexMethods = { ) && Meteor.settings?.packages?.mongo?.reCreateIndexOnOptionMismatch ) { - import { Log } from 'meteor/logging'; - Log.info(`Re-creating index ${ index } for ${ self._name } due to options mismatch.`); await self._collection.dropIndexAsync(index); await self._collection.createIndexAsync(index, options); From 74bdbd8d00f58995c0f606bdca1f3aa796f0074a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 09:12:02 +0100 Subject: [PATCH 024/264] remove unrequired nested imports --- packages/autoupdate/autoupdate_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 7d20443c27..82af4c65e6 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -25,6 +25,7 @@ // The ID of each document is the client architecture, and the fields of // the document are the versions described above. +import { onMessage } from "meteor/inter-process-messaging"; import { ClientVersions } from "./client_versions.js"; export const Autoupdate = __meteor_runtime_config__.autoupdate = { @@ -152,7 +153,6 @@ function enqueueVersionsRefresh() { const setupListeners = () => { // Listen for messages pertaining to the client-refresh topic. - import { onMessage } from "meteor/inter-process-messaging"; onMessage("client-refresh", enqueueVersionsRefresh); // Another way to tell the process to refresh: send SIGHUP signal From a3b12b2eb611f1de3cedef929fe1d4238158486e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 09:26:53 +0100 Subject: [PATCH 025/264] prefer require than nested import --- packages/ddp-client/common/livedata_connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ddp-client/common/livedata_connection.js b/packages/ddp-client/common/livedata_connection.js index abe1161d21..9755a8012a 100644 --- a/packages/ddp-client/common/livedata_connection.js +++ b/packages/ddp-client/common/livedata_connection.js @@ -74,7 +74,7 @@ export class Connection { if (typeof url === 'object') { self._stream = url; } else { - import { ClientStream } from "meteor/socket-stream-client"; + const { ClientStream } = require("meteor/socket-stream-client"); self._stream = new ClientStream(url, { retry: options.retry, From cf6dc9ca5a34d4f961225294c10391e2e4405dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 09:32:40 +0100 Subject: [PATCH 026/264] ensure incompatible files will directly parse with babel once detected --- packages/babel-compiler/babel-compiler.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 8cf2c4245e..77271959d4 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -58,6 +58,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { var self = this; // capture context this._babelrcCache = this._babelrcCache || Object.create(null); this._swcCache = this._swcCache || Object.create(null); + this._swcIncompatible = this._swcIncompatible || Object.create(null); if (typeof source !== "string") { // Other compiler plugins can call processOneFileForTarget with a @@ -150,8 +151,10 @@ BCp.processOneFileForTarget = function (inputFile, source) { // Determine if SWC should be used based on package and file criteria. const packagesSkipSwc = []; const fileSkipSwc = []; // top level await - const shouldUseSwc = !packagesSkipSwc.includes(packageName) && - !fileSkipSwc.includes(inputFilePath); + const shouldUseSwc = + !packagesSkipSwc.includes(packageName) && + !fileSkipSwc.includes(inputFilePath) && + !self._swcIncompatible[toBeAdded.hash]; // Check RAM cache let compilation; @@ -237,6 +240,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { compilation = Babel.compile(source, babelOptions, cacheOptions); } } catch (e) { + self._swcIncompatible[toBeAdded.hash] = true; // If SWC fails, fall back to Babel compilation = Babel.compile(source, babelOptions, cacheOptions); } From 42b61a4ecc6f50e8d7160314a231967b2fa7394f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 10:35:23 +0100 Subject: [PATCH 027/264] use latest acorn parser and proper reify options --- packages/babel-compiler/babel-compiler.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 77271959d4..d5f0263801 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -2,7 +2,7 @@ var semver = Npm.require("semver"); var JSON5 = Npm.require("json5"); var SWC = Npm.require("@swc/core"); const reifyCompile = Npm.require("@meteorjs/reify/lib/compiler").compile; -const reifyAcornTopLevelParse = Npm.require("@meteorjs/reify/lib/parsers/top-level").parse; +const reifyAcornParse = Npm.require("@meteorjs/reify/lib/parsers/acorn").parse; var fs = Npm.require('fs'); var path = Npm.require('path'); @@ -205,9 +205,21 @@ BCp.processOneFileForTarget = function (inputFile, source) { let content = transformed.code; // Perserve Meteor's specifics: reify modules, nested imports and top-level await support. const result = reifyCompile(content, { - parse: reifyAcornTopLevelParse, + parse: reifyAcornParse, generateLetDeclarations: false, ast: false, + // Enforce reify options for proper compatibility (TODO export getReifyOptions) + // https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-babel/options.js#L19 + avoidModernSyntax: true, + enforceStrictMode: false, + dynamicImport: true, + ...features.topLevelAwait && { topLevelAwait: true }, + ...features.compileForShell && { moduleAlias: 'module' }, + ...(features.modernBrowsers || + features.nodeMajorVersion >= 8) && { + avoidModernSyntax: false, + generateLetDeclarations: true, + }, }); if (!result.identical) { identical = false; From 1d11de08d7afc39cd305f8234809f7d52788ccfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 12:24:21 +0100 Subject: [PATCH 028/264] use reify acorn parser on static-assets server runtime --- tools/static-assets/server/runtime.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/static-assets/server/runtime.js b/tools/static-assets/server/runtime.js index 2b0f800940..9a1c6b15ef 100644 --- a/tools/static-assets/server/runtime.js +++ b/tools/static-assets/server/runtime.js @@ -47,7 +47,7 @@ module.exports = function enable ({ cachePath, createLoader = true } = {}) { }; const reifyVersion = require("@meteorjs/reify/package.json").version; - const reifyBabelParse = require("@meteorjs/reify/lib/parsers/babel").parse; + const reifyAcornParse = require("@meteorjs/reify/lib/parsers/acorn").parse; const reifyCompile = require("@meteorjs/reify/lib/compiler").compile; function compileContent (content) { @@ -55,7 +55,7 @@ module.exports = function enable ({ cachePath, createLoader = true } = {}) { try { const result = reifyCompile(content, { - parse: reifyBabelParse, + parse: reifyAcornParse, generateLetDeclarations: false, ast: false, }); From 94dd07b2d0826e9bf6ab6c3c083b4774641614ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 28 Mar 2025 12:27:39 +0100 Subject: [PATCH 029/264] use reify acorn parser on import scanner --- tools/isobuild/import-scanner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index acec76b398..e05900b2f2 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -45,7 +45,7 @@ import { import { wrap } from "optimism"; const { compile: reifyCompile } = require("@meteorjs/reify/lib/compiler"); -const { parse: reifyBabelParse } = require("@meteorjs/reify/lib/parsers/babel"); +const { parse: reifyAcornParse } = require("@meteorjs/reify/lib/parsers/acorn"); import Resolver, { Resolution } from "./resolver"; import LRUCache from 'lru-cache'; @@ -88,7 +88,7 @@ const reifyCompileWithCache = Profile("reifyCompileWithCache", wrap(function ( const isLegacy = isLegacyArch(bundleArch); let result = reifyCompile(stripHashBang(source), { - parse: reifyBabelParse, + parse: reifyAcornParse, generateLetDeclarations: !isLegacy, avoidModernSyntax: isLegacy, enforceStrictMode: false, From e294d52f8faa11a9cf30665ae02e8aa84827ee68 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 28 Mar 2025 19:26:39 +0200 Subject: [PATCH 030/264] Add replace email utility --- docs/_config.yml | 1 + docs/generators/changelog/versions/3.0.0.md | 1 + docs/source/api/passwords.md | 2 + packages/accounts-base/accounts-base.d.ts | 2 + packages/accounts-password/password_server.js | 46 +++++++++++++++++++ packages/accounts-password/password_tests.js | 26 +++++++++++ v3-docs/docs/api/accounts.md | 2 + .../generators/changelog/versions/3.0.0.md | 1 + 8 files changed, 81 insertions(+) diff --git a/docs/_config.yml b/docs/_config.yml index 6834fe8ced..395aaca44c 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -213,6 +213,7 @@ redirects: /#/full/accounts-setusername: 'api/passwords.html#accounts-setusername' /#/full/accounts-addemail: 'api/passwords.html#accounts-addemail' /#/full/accounts-removeemail: 'api/passwords.html#accounts-removeemail' + /#/full/accounts_replaceemail: 'api/passwords.html#Accounts-replaceEmail' /#/full/accounts_verifyemail: 'api/passwords.html#Accounts-verifyEmail' /#/full/accounts-finduserbyusername: 'api/passwords.html#accounts-finduserbyusername' /#/full/accounts-finduserbyemail: 'api/passwords.html#accounts-finduserbyemail' diff --git a/docs/generators/changelog/versions/3.0.0.md b/docs/generators/changelog/versions/3.0.0.md index c5fa1f1a1e..4c863db6ad 100644 --- a/docs/generators/changelog/versions/3.0.0.md +++ b/docs/generators/changelog/versions/3.0.0.md @@ -55,6 +55,7 @@ - `Accounts.sendVerificationEmail` - `Accounts.addEmail` - `Accounts.removeEmail` + - `Accounts.replaceEmailAsync` - `Accounts.verifyEmail` - `Accounts.createUserVerifyingEmail` - `Accounts.createUser` diff --git a/docs/source/api/passwords.md b/docs/source/api/passwords.md index bb0ddf6b17..908617aa3e 100644 --- a/docs/source/api/passwords.md +++ b/docs/source/api/passwords.md @@ -59,6 +59,8 @@ By default, an email address is added with `{ verified: false }`. Use [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail) to send an email with a link the user can use to verify their email address. +{% apibox "Accounts.replaceEmailAsync" %} + {% apibox "Accounts.removeEmail" %} {% apibox "Accounts.verifyEmail" %} diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 7fbbfde0a6..b11344ce08 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -188,6 +188,8 @@ export namespace Accounts { function removeEmail(userId: string, email: string): Promise; + function replaceEmailAsync(userId: string, oldEmail: string, newEmail: string, verified?: boolean): Promise; + function onCreateUser( func: (options: { profile?: {} | undefined }, user: Meteor.User) => void ): void; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 455c071cc3..6477bdcc3d 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1022,6 +1022,52 @@ Meteor.methods( } }); + +/** + * @summary Asynchronously replace an email address for a user. Use this instead of directly + * updating the database. The operation will fail if there is a different user + * with an email only differing in case. If the specified user has an existing + * email only differing in case however, we replace it. + * @locus Server + * @param {String} userId The ID of the user to update. + * @param {String} oldEmail The email address to replace. + * @param {String} newEmail The new email address to use. + * @param {Boolean} [verified] Optional - whether the new email address should + * be marked as verified. Defaults to false. + * @importFromPackage accounts-base + */ +Accounts.replaceEmailAsync = async (userId, oldEmail, newEmail, verified) => { + check(userId, NonEmptyString); + check(oldEmail, NonEmptyString); + check(newEmail, NonEmptyString); + check(verified, Match.Optional(Boolean)); + + if (verified === void 0) { + verified = false; + } + + const user = await getUserById(userId, { fields: { _id: 1 } }); + if (!user) + throw new Meteor.Error(403, "User not found"); + + // Ensure no user already has this new email + await Accounts._checkForCaseInsensitiveDuplicates( + "emails.address", + "Email", + newEmail, + user._id + ); + + const result = await Meteor.users.updateAsync( + { _id: user._id, 'emails.address': oldEmail }, + { $set: { 'emails.$.address': newEmail, 'emails.$.verified': verified } } + ); + + if (result.modifiedCount === 0) { + throw new Meteor.Error(404, "No user could be found with old email"); + } +}; + /** * @summary Asynchronously add an email address for a user. Use this instead of directly * updating the database. The operation will fail if there is a different user diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index e488c9a89e..08c9b0c4ca 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1789,6 +1789,32 @@ if (Meteor.isServer) (() => { ]); }); + Tinytest.addAsync("passwords, replace email", async test => { + const origEmail = `${ Random.id() }@turing.com`; + const userId = await Accounts.createUser({ + email: origEmail + }); + + const newEmail = `${ Random.id() }@turing.com`; + await Accounts.addEmailAsync(userId, newEmail); + + const thirdEmail = `${ Random.id() }@turing.com`; + await Accounts.addEmailAsync(userId, thirdEmail, true); + const u1 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u1.emails, [ + { address: origEmail, verified: false }, + { address: newEmail, verified: false }, + { address: thirdEmail, verified: true } + ]); + + await Accounts.replaceEmailAsync(userId, newEmail, thirdEmail); + const u2 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u2.emails, [ + { address: origEmail, verified: false }, + { address: thirdEmail, verified: true } + ]); + }) + Tinytest.addAsync("passwords - remove email", async test => { const origEmail = `${ Random.id() }@turing.com`; diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index 2ac9317275..ce5b8699f2 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -888,6 +888,8 @@ email with a link the user can use to verify their email address. + + If the user trying to verify the email has 2FA enabled, this error will be thrown: diff --git a/v3-docs/docs/generators/changelog/versions/3.0.0.md b/v3-docs/docs/generators/changelog/versions/3.0.0.md index 15008c98fe..66fcf78b5b 100644 --- a/v3-docs/docs/generators/changelog/versions/3.0.0.md +++ b/v3-docs/docs/generators/changelog/versions/3.0.0.md @@ -58,6 +58,7 @@ - `Accounts.sendVerificationEmail` - `Accounts.addEmail` - `Accounts.removeEmail` + - `Accounts.replaceEmail` - `Accounts.verifyEmail` - `Accounts.createUserVerifyingEmail` - `Accounts.createUser` From d5e479291eaa5bfe2274a0f362db217ace415256 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 28 Mar 2025 23:03:24 +0200 Subject: [PATCH 031/264] Fix test --- packages/accounts-password/password_tests.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 08c9b0c4ca..8b14a84b9f 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1790,28 +1790,22 @@ if (Meteor.isServer) (() => { }); Tinytest.addAsync("passwords, replace email", async test => { - const origEmail = `${ Random.id() }@turing.com`; + const origEmail = `originalemail@test.com`; const userId = await Accounts.createUser({ email: origEmail }); - const newEmail = `${ Random.id() }@turing.com`; - await Accounts.addEmailAsync(userId, newEmail); + const newEmail = `newemail@test.com`; - const thirdEmail = `${ Random.id() }@turing.com`; - await Accounts.addEmailAsync(userId, thirdEmail, true); const u1 = await Accounts._findUserByQuery({ id: userId }) test.equal(u1.emails, [ - { address: origEmail, verified: false }, - { address: newEmail, verified: false }, - { address: thirdEmail, verified: true } + { address: origEmail, verified: false } ]); - await Accounts.replaceEmailAsync(userId, newEmail, thirdEmail); + await Accounts.replaceEmailAsync(userId, origEmail, newEmail); const u2 = await Accounts._findUserByQuery({ id: userId }) test.equal(u2.emails, [ - { address: origEmail, verified: false }, - { address: thirdEmail, verified: true } + { address: newEmail, verified: false } ]); }) From 9319ca7625730b0dc0f2c0885a1541f73bbf1699 Mon Sep 17 00:00:00 2001 From: 9Morello Date: Sun, 23 Mar 2025 14:27:08 -0300 Subject: [PATCH 032/264] check for environment variable before sending package usage stats --- tools/meteor-services/stats.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/meteor-services/stats.js b/tools/meteor-services/stats.js index f324df7a34..c9c8439d72 100644 --- a/tools/meteor-services/stats.js +++ b/tools/meteor-services/stats.js @@ -22,7 +22,7 @@ var packageList = function (projectContext) { name: name, version: info.version, local: info.kind === 'local', - direct: !! projectContext.projectConstraintsFile.getConstraint(name) + direct: !!projectContext.projectConstraintsFile.getConstraint(name) }); }); return versions; @@ -39,7 +39,8 @@ var recordPackages = async function (options) { // Before doing anything, look at the app's dependencies to see if the // opt-out package is there; if present, we don't record any stats. var packages = packageList(options.projectContext); - if (_.findWhere(packages, { name: OPT_OUT_PACKAGE_NAME })) { + if (_.findWhere(packages, { name: OPT_OUT_PACKAGE_NAME }) || + process.env.DO_NOT_TRACK) { // Print some output for the 'report-stats' self-test. if (process.env.METEOR_PACKAGE_STATS_TEST_OUTPUT) { process.stdout.write("PACKAGE STATS NOT SENT\n"); @@ -86,9 +87,9 @@ var recordPackages = async function (options) { } var result = await conn.call("recordAppPackages", - appIdentifier, - packages, - details); + appIdentifier, + packages, + details); // If the stats server sent us a new session, save it for use on // subsequent requests. @@ -112,7 +113,7 @@ var recordPackages = async function (options) { var logErrorIfInCheckout = function (err) { if ((Console.isInteractive() && files.inCheckout()) - || process.env.METEOR_PACKAGE_STATS_TEST_OUTPUT) { + || process.env.METEOR_PACKAGE_STATS_TEST_OUTPUT) { Console.warn("Failed to record package usage."); Console.warn( "(This error is hidden when you are not running Meteor from a", @@ -145,8 +146,8 @@ var getPackagesForAppIdInTest = function (projectContext) { var connectToPackagesStatsServer = async function () { const sc = new ServiceConnection( - config.getPackageStatsServerUrl(), - {_dontPrintErrors: true} + config.getPackageStatsServerUrl(), + { _dontPrintErrors: true } ); await sc.init(); From 05c789131e5af977bf6b8c07c9a973e17e75c10c Mon Sep 17 00:00:00 2001 From: 9Morello Date: Wed, 26 Mar 2025 11:50:24 -0300 Subject: [PATCH 033/264] add documentation for the `DO_NOT_TRACK` CLI flag --- v3-docs/docs/cli/environment-variables.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/v3-docs/docs/cli/environment-variables.md b/v3-docs/docs/cli/environment-variables.md index 1d46eadbc0..bde20413bb 100644 --- a/v3-docs/docs/cli/environment-variables.md +++ b/v3-docs/docs/cli/environment-variables.md @@ -31,6 +31,15 @@ In the event that your own deployment platform does not support WebSockets, or y Set `DISABLE_SOCKJS=1` if you want to use the native WebSocket implementation instead of SockJS on the client side, for example, if you want to use a custom WebSocket implementation (e.g. [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js/)) on the server side. +## DO_NOT_TRACK +(_development, production_) + +Meteor automatically sends usage statistics about which Meteor packages your app uses by default. This behavior can be disabled by setting `DO_NOT_TRACK` to any truthy value (for example, `DO_NOT_TRACK=1`). + +Having this variable set globally (say, by adding it to your `.bashrc` file) would disable statistics for all Meteor projects in your computer, and do the same for other programs that respect this flag (like [FerretDB](https://www.ferretdb.com/) or [Bun](https://bun.sh/)). + +Alternatively, you can install the `package-stats-opt-out` package by calling `meteor add package-stats-opt-out` inside your Meteor project folder. Having this package installed in your project disables usage statistics being sent to the Meteor project, regardless of whether the `DO_NOT_TRACK` environment variable is set or not. + ## HTTP_FORWARDED_COUNT (_production_) From bf5820feb22170d1be1782e03cde98b8d63b36bc Mon Sep 17 00:00:00 2001 From: 9Morello Date: Sun, 23 Mar 2025 11:51:28 -0300 Subject: [PATCH 034/264] add option to automatically exclude legacy web architectures from builds with a package.json flag --- tools/cli/commands.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 5ee4ead23b..062e7c6bcd 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -261,11 +261,17 @@ export function parseRunTargets(targets) { }); }; -const excludableWebArchs = ['web.browser', 'web.browser.legacy', 'web.cordova']; -function filterWebArchs(webArchs, excludeArchsOption) { - if (excludeArchsOption) { - const excludeArchs = excludeArchsOption.trim().split(/\s*,\s*/) - .filter(arch => excludableWebArchs.includes(arch)); +function isModernArchsOnlyEnabled(appDir) { + const packageJsonPath = files.pathJoin(appDir, 'package.json'); + const packageJsonFile = files.readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonFile); + return packageJson?.meteor?.modernWebArchsOnly === true; +} + +function filterWebArchs(webArchs, excludeArchsOption, appDir) { + const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; + if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { + const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); } return webArchs; @@ -509,7 +515,8 @@ async function doRunCommand(options) { webArchs.push("web.cordova"); } } - webArchs = filterWebArchs(webArchs, options['exclude-archs']); + + webArchs = filterWebArchs(webArchs, options['exclude-archs'], options.appDir); const buildMode = options.production ? 'production' : 'development'; let cordovaRunner; @@ -1391,9 +1398,9 @@ on an OS X system."); // For example, if we want to build only android, there is no need to build // web.browser. let webArchs; + const baseWebArchs = projectContext.platformList.getWebArchs(); if (selectedPlatforms) { - const filteredArchs = projectContext.platformList - .getWebArchs() + const filteredArchs = baseWebArchs .filter(arch => selectedPlatforms.includes(arch)); if ( @@ -1404,6 +1411,8 @@ on an OS X system."); } webArchs = filteredArchs.length ? filteredArchs : undefined; + } else { + webArchs = filterWebArchs(baseWebArchs, options['exclude-archs'], options.appDir); } var buildDir = projectContext.getProjectLocalDirectory('build_tar'); @@ -2465,7 +2474,7 @@ var runTestAppForPackages = async function (projectContext, options) { if (options.cordovaRunner) { webArchs.push("web.cordova"); } - buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs']); + buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs'], projectContext.appDirectory); if (options.deploy) { // Run the constraint solver and build local packages. From 24bc7cb4a9af14c2af32197b9e103a25b02a41f7 Mon Sep 17 00:00:00 2001 From: 9Morello Date: Sun, 23 Mar 2025 11:53:35 -0300 Subject: [PATCH 035/264] add modernWebArchsOnly flag to base package.json files used for new projects --- tools/static-assets/skel-apollo/package.json | 3 ++- tools/static-assets/skel-blaze/package.json | 3 ++- tools/static-assets/skel-chakra-ui/package.json | 3 ++- tools/static-assets/skel-minimal/package.json | 3 ++- tools/static-assets/skel-react/package.json | 3 ++- tools/static-assets/skel-solid/package.json | 3 ++- tools/static-assets/skel-svelte/package.json | 3 ++- tools/static-assets/skel-tailwind/package.json | 3 ++- tools/static-assets/skel-typescript/package.json | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tools/static-assets/skel-apollo/package.json b/tools/static-assets/skel-apollo/package.json index 51ca845233..3bb70f1781 100644 --- a/tools/static-assets/skel-apollo/package.json +++ b/tools/static-assets/skel-apollo/package.json @@ -21,6 +21,7 @@ "client": "client/main.jsx", "server": "server/main.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-blaze/package.json b/tools/static-assets/skel-blaze/package.json index 59e15689c0..21fba0da58 100644 --- a/tools/static-assets/skel-blaze/package.json +++ b/tools/static-assets/skel-blaze/package.json @@ -17,6 +17,7 @@ "client": "client/main.js", "server": "server/main.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-chakra-ui/package.json b/tools/static-assets/skel-chakra-ui/package.json index 690f492c11..92bbdf24ad 100644 --- a/tools/static-assets/skel-chakra-ui/package.json +++ b/tools/static-assets/skel-chakra-ui/package.json @@ -24,6 +24,7 @@ "client": "client/main.jsx", "server": "server/main.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-minimal/package.json b/tools/static-assets/skel-minimal/package.json index 3ade9d3a74..73469db096 100644 --- a/tools/static-assets/skel-minimal/package.json +++ b/tools/static-assets/skel-minimal/package.json @@ -16,6 +16,7 @@ "client": "client/main.js", "server": "server/main.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index 0c0eb6e4a5..08d7e054e3 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -18,6 +18,7 @@ "client": "client/main.jsx", "server": "server/main.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-solid/package.json b/tools/static-assets/skel-solid/package.json index d096a76612..6e8187cbeb 100644 --- a/tools/static-assets/skel-solid/package.json +++ b/tools/static-assets/skel-solid/package.json @@ -18,7 +18,8 @@ "client": "client/entry-meteor.js", "server": "server/entry-meteor.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true }, "devDependencies": { "babel-preset-solid": "^1.8.15", diff --git a/tools/static-assets/skel-svelte/package.json b/tools/static-assets/skel-svelte/package.json index 68e6f37acf..7cf92dfce7 100644 --- a/tools/static-assets/skel-svelte/package.json +++ b/tools/static-assets/skel-svelte/package.json @@ -27,6 +27,7 @@ ] } }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-tailwind/package.json b/tools/static-assets/skel-tailwind/package.json index c02e2d027c..94934d4898 100644 --- a/tools/static-assets/skel-tailwind/package.json +++ b/tools/static-assets/skel-tailwind/package.json @@ -22,6 +22,7 @@ "client": "client/main.jsx", "server": "server/main.js" }, - "testModule": "tests/main.js" + "testModule": "tests/main.js", + "modernWebArchsOnly": true } } diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json index 1bd98a2050..03ff9ce6c7 100644 --- a/tools/static-assets/skel-typescript/package.json +++ b/tools/static-assets/skel-typescript/package.json @@ -25,6 +25,7 @@ "client": "client/main.tsx", "server": "server/main.ts" }, - "testModule": "tests/main.ts" + "testModule": "tests/main.ts", + "modernWebArchsOnly": true } } From de7d9a7a7ed3257625dd9de7121c39f5fc7c0c99 Mon Sep 17 00:00:00 2001 From: 9Morello Date: Sun, 23 Mar 2025 13:51:40 -0300 Subject: [PATCH 036/264] default to false if package.json is not found --- tools/cli/commands.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 062e7c6bcd..26956a589a 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -263,6 +263,9 @@ export function parseRunTargets(targets) { function isModernArchsOnlyEnabled(appDir) { const packageJsonPath = files.pathJoin(appDir, 'package.json'); + if (!files.exists(packageJsonPath)) { + return false; + } const packageJsonFile = files.readFile(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageJsonFile); return packageJson?.meteor?.modernWebArchsOnly === true; From 168afef6c867716885dd4407a038bb15963bafe8 Mon Sep 17 00:00:00 2001 From: 9Morello Date: Wed, 26 Mar 2025 11:08:53 -0300 Subject: [PATCH 037/264] use `require` instead of `JSON.parse` --- tools/cli/commands.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 26956a589a..9d6b91c6c8 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -266,8 +266,7 @@ function isModernArchsOnlyEnabled(appDir) { if (!files.exists(packageJsonPath)) { return false; } - const packageJsonFile = files.readFile(packageJsonPath, 'utf8'); - const packageJson = JSON.parse(packageJsonFile); + const packageJson = require(packageJsonPath); return packageJson?.meteor?.modernWebArchsOnly === true; } @@ -1354,7 +1353,7 @@ var buildCommand = async function (options) { let selectedPlatforms = null; if (options.platforms) { const platformsArray = options.platforms.split(","); - + const excludableWebArchs = ['web.browser', 'web.browser.legacy', 'web.cordova']; platformsArray.forEach(plat => { if (![...excludableWebArchs, 'android', 'ios'].includes(plat)) { throw new Error(`Not allowed platform on '--platforms' flag: ${plat}`) From 3662d344faccbdd95bfc52e88e543d7b11d750b3 Mon Sep 17 00:00:00 2001 From: 9Morello Date: Wed, 26 Mar 2025 12:49:34 -0300 Subject: [PATCH 038/264] change `require` call back to `JSON.parse`, since Windows seems to dislike requiring .js files by their full path --- 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 9d6b91c6c8..40b2cfc34e 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -266,7 +266,8 @@ function isModernArchsOnlyEnabled(appDir) { if (!files.exists(packageJsonPath)) { return false; } - const packageJson = require(packageJsonPath); + const packageJsonFile = files.readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonFile); return packageJson?.meteor?.modernWebArchsOnly === true; } From ee23cb4e575a2765da82f602d47185b55b5f62bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 1 Apr 2025 19:38:58 +0200 Subject: [PATCH 039/264] ensure SWC.compile profile context and address feedback --- packages/babel-compiler/babel-compiler.js | 142 ++++++++++++---------- 1 file changed, 78 insertions(+), 64 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index d5f0263801..d5e1f7437a 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -29,6 +29,67 @@ var isMeteorPre144 = semver.lt(process.version, "4.8.1"); var enableClientTLA = process.env.METEOR_ENABLE_CLIENT_TOP_LEVEL_AWAIT === 'true'; +function compileWithBabel(source, babelOptions, cacheOptions) { + return profile('Babel.compile', function () { + return Babel.compile(source, babelOptions, cacheOptions); + }); +} + +function compileWithSWC(source, { inputFilePath, hash, features, ...swcOptions }) { + return profile('SWC.compile', function () { + // Determine file extension based syntax. + const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); + const hasTSXSupport = inputFilePath.endsWith('.tsx'); + const hasJSXSupport = inputFilePath.endsWith('.jsx'); + + // Perform SWC transformation. + const transformed = SWC.transformSync(source, { + ...swcOptions, + jsc: { + target: 'es2015', + parser: { + syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', + jsx: hasJSXSupport, + tsx: hasTSXSupport, + }, + }, + module: { type: 'es6' }, + minify: false, + sourceMaps: true, + }); + + let content = transformed.code; + + // Preserve Meteor-specific features: reify modules, nested imports, and top-level await support. + const result = reifyCompile(content, { + parse: reifyAcornParse, + generateLetDeclarations: false, + ast: false, + // Enforce reify options for proper compatibility. + avoidModernSyntax: true, + enforceStrictMode: false, + dynamicImport: true, + ...features.topLevelAwait && { topLevelAwait: true }, + ...features.compileForShell && { moduleAlias: 'module' }, + ...(features.modernBrowsers || features.nodeMajorVersion >= 8) && { + avoidModernSyntax: false, + generateLetDeclarations: true, + }, + }); + if (!result.identical) { + content = result.code; + } + + return { + code: content, + map: JSON.parse(transformed.map), + hash, + sourceType: 'module', + }; + }); +} + + BCp.processFilesForTarget = function (inputFiles) { var compiler = this; @@ -147,16 +208,16 @@ BCp.processOneFileForTarget = function (inputFile, source) { } try { - var result = profile('Babel.compile', function () { - // Determine if SWC should be used based on package and file criteria. + var result = (function getTranspilerCompilation() { const packagesSkipSwc = []; const fileSkipSwc = []; // top level await + + // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !packagesSkipSwc.includes(packageName) && !fileSkipSwc.includes(inputFilePath) && - !self._swcIncompatible[toBeAdded.hash]; + !this._swcIncompatible[toBeAdded.hash]; - // Check RAM cache let compilation; try { if (shouldUseSwc) { @@ -165,14 +226,14 @@ BCp.processOneFileForTarget = function (inputFile, source) { const cacheContext = '.swc-cache'; // Check RAM cache - compilation = self._swcCache[cacheKey]; + compilation = this._swcCache[cacheKey]; // Check file system cache if enabled - if (!compilation && self.cacheDirectory) { - const cacheFilePath = path.join(self.cacheDirectory, cacheContext, cacheKey + '.json'); + if (!compilation && this.cacheDirectory) { + const cacheFilePath = path.join(this.cacheDirectory, cacheContext, cacheKey + '.json'); if (fs.existsSync(cacheFilePath)) { try { compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8')); - self._swcCache[cacheKey] = compilation; + this._swcCache[cacheKey] = compilation; } catch (e) { // If reading/parsing the cache fails, ignore and continue. } @@ -183,60 +244,12 @@ BCp.processOneFileForTarget = function (inputFile, source) { return compilation; } - const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); - const hasTSXSupport = inputFilePath.endsWith('.tsx'); - const hasJSXSupport = inputFilePath.endsWith('.jsx'); - - // Perform compilation - const transformed = SWC.transformSync(source, { - jsc: { - target: 'es2015', - parser: { - syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', - jsx: hasJSXSupport, - tsx: hasTSXSupport, - }, - }, - module: { type: 'es6' }, - minify: false, - sourceMaps: true, - }); - - let content = transformed.code; - // Perserve Meteor's specifics: reify modules, nested imports and top-level await support. - const result = reifyCompile(content, { - parse: reifyAcornParse, - generateLetDeclarations: false, - ast: false, - // Enforce reify options for proper compatibility (TODO export getReifyOptions) - // https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-babel/options.js#L19 - avoidModernSyntax: true, - enforceStrictMode: false, - dynamicImport: true, - ...features.topLevelAwait && { topLevelAwait: true }, - ...features.compileForShell && { moduleAlias: 'module' }, - ...(features.modernBrowsers || - features.nodeMajorVersion >= 8) && { - avoidModernSyntax: false, - generateLetDeclarations: true, - }, - }); - if (!result.identical) { - identical = false; - content = result.code; - } - - compilation = { - code: content, - map: JSON.parse(transformed.map), - hash: toBeAdded.hash, - sourceType: 'module', - }; + compilation = compileWithSWC(source, { inputFilePath, hash: toBeAdded.hash, features }); // Save result in cache - self._swcCache[cacheKey] = compilation; - if (self.cacheDirectory) { - const cacheFilePath = path.join(self.cacheDirectory, cacheContext, cacheKey + '.json'); + this._swcCache[cacheKey] = compilation; + if (this.cacheDirectory) { + const cacheFilePath = path.join(this.cacheDirectory, cacheContext, cacheKey + '.json'); try { const writeFileCache = async () => { await fs.promises.mkdir(path.dirname(cacheFilePath), { recursive: true }); @@ -249,15 +262,16 @@ BCp.processOneFileForTarget = function (inputFile, source) { } } } else { - compilation = Babel.compile(source, babelOptions, cacheOptions); + compilation = compileWithBabel(source, babelOptions, cacheOptions); } } catch (e) { - self._swcIncompatible[toBeAdded.hash] = true; + this._swcIncompatible[toBeAdded.hash] = true; // If SWC fails, fall back to Babel - compilation = Babel.compile(source, babelOptions, cacheOptions); + compilation = compileWithBabel(source, babelOptions, cacheOptions); } + return compilation; - }); + }).call(this); } catch (e) { if (e.loc) { // Error is from @babel/parser. From 04c71f1a5b90fa17878c4bc2c5cf19232db29175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 1 Apr 2025 19:40:25 +0200 Subject: [PATCH 040/264] prefer an arrow function --- packages/babel-compiler/babel-compiler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index d5e1f7437a..f4df7343c9 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -208,7 +208,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { } try { - var result = (function getTranspilerCompilation() { + var result = (() => { const packagesSkipSwc = []; const fileSkipSwc = []; // top level await @@ -271,7 +271,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { } return compilation; - }).call(this); + })(); } catch (e) { if (e.loc) { // Error is from @babel/parser. From d137aea93c4815f7b399f20d8a63e050b6bb14a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 1 Apr 2025 19:51:11 +0200 Subject: [PATCH 041/264] refactor cache code --- packages/babel-compiler/babel-compiler.js | 82 +++++++++++++---------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index f4df7343c9..54727ee938 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -35,7 +35,7 @@ function compileWithBabel(source, babelOptions, cacheOptions) { }); } -function compileWithSWC(source, { inputFilePath, hash, features, ...swcOptions }) { +function compileWithSwc(source, swcOptions, { inputFilePath, features }) { return profile('SWC.compile', function () { // Determine file extension based syntax. const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); @@ -83,7 +83,6 @@ function compileWithSWC(source, { inputFilePath, hash, features, ...swcOptions } return { code: content, map: JSON.parse(transformed.map), - hash, sourceType: 'module', }; }); @@ -221,46 +220,17 @@ BCp.processOneFileForTarget = function (inputFile, source) { let compilation; try { if (shouldUseSwc) { - // Create a cache key based on the source hash and the compiler used. + // Create a cache key based on the source hash and the compiler used const cacheKey = toBeAdded.hash; - const cacheContext = '.swc-cache'; - - // Check RAM cache - compilation = this._swcCache[cacheKey]; - // Check file system cache if enabled - if (!compilation && this.cacheDirectory) { - const cacheFilePath = path.join(this.cacheDirectory, cacheContext, cacheKey + '.json'); - if (fs.existsSync(cacheFilePath)) { - try { - compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8')); - this._swcCache[cacheKey] = compilation; - } catch (e) { - // If reading/parsing the cache fails, ignore and continue. - } - } - } + // Check cache + compilation = this.readFromSwcCache({ cacheKey }); // Return cached result if found. if (compilation) { return compilation; } - - compilation = compileWithSWC(source, { inputFilePath, hash: toBeAdded.hash, features }); - + compilation = compileWithSwc(source, {}, { inputFilePath, features }); // Save result in cache - this._swcCache[cacheKey] = compilation; - if (this.cacheDirectory) { - const cacheFilePath = path.join(this.cacheDirectory, cacheContext, cacheKey + '.json'); - try { - const writeFileCache = async () => { - await fs.promises.mkdir(path.dirname(cacheFilePath), { recursive: true }); - await fs.promises.writeFile(cacheFilePath, JSON.stringify(compilation), 'utf8'); - }; - // Asynchronously write the cache without blocking - writeFileCache(); - } catch (e) { - // If file caching fails, ignore the error. - } - } + this.writeToSwcCache({ cacheKey, compilation }); } else { compilation = compileWithBabel(source, babelOptions, cacheOptions); } @@ -689,3 +659,43 @@ function packageNameFromTopLevelModuleId(id) { } return parts[0]; } + +const SwcCacheContext = '.swc-cache'; + +BCp.readFromSwcCache = function({ cacheKey }) { + // Check in-memory cache. + let compilation = this._swcCache[cacheKey]; + // If not found, try file system cache if enabled. + if (!compilation && this.cacheDirectory) { + const cacheFilePath = path.join(this.cacheDirectory, SwcCacheContext, `${cacheKey}.json`); + if (fs.existsSync(cacheFilePath)) { + try { + compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8')); + // Save back to in-memory cache. + this._swcCache[cacheKey] = compilation; + } catch (err) { + // Ignore any errors reading/parsing the cache. + } + } + } + return compilation; +}; + +BCp.writeToSwcCache = function({ cacheKey, compilation }) { + // Save to in-memory cache. + this._swcCache[cacheKey] = compilation; + // If file system caching is enabled, write asynchronously. + if (this.cacheDirectory) { + const cacheFilePath = path.join(this.cacheDirectory, SwcCacheContext, `${cacheKey}.json`); + try { + const writeFileCache = async () => { + await fs.promises.mkdir(path.dirname(cacheFilePath), { recursive: true }); + await fs.promises.writeFile(cacheFilePath, JSON.stringify(compilation), 'utf8'); + }; + // Invoke without blocking the main flow. + writeFileCache(); + } catch (err) { + // If writing fails, ignore the error. + } + } +}; From b512040335ca918e27d611092a79dfaab428fc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 1 Apr 2025 19:52:35 +0200 Subject: [PATCH 042/264] remove unrequired self --- packages/babel-compiler/babel-compiler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 54727ee938..35e3588df7 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -115,7 +115,6 @@ BCp.processFilesForTarget = function (inputFiles) { // Returns an object suitable for passing to inputFile.addJavaScript, or // null to indicate there was an error, and nothing should be added. BCp.processOneFileForTarget = function (inputFile, source) { - var self = this; // capture context this._babelrcCache = this._babelrcCache || Object.create(null); this._swcCache = this._swcCache || Object.create(null); this._swcIncompatible = this._swcIncompatible || Object.create(null); From 91de1f2c867c42c5b0a88adbabac438f05d43e96 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Tue, 1 Apr 2025 20:53:12 -0300 Subject: [PATCH 043/264] DEV: Bump express to 5.1.0 in webapp Also updated express types --- packages/webapp/package.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 6e988ec31c..c57f709f3c 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,12 +1,12 @@ Package.describe({ summary: "Serves a Meteor app over HTTP", - version: "2.0.5", + version: "2.0.6", }); Npm.depends({ "cookie-parser": "1.4.6", - express: "5.0.1", - "@types/express": "5.0.0", + express: "5.1.0", + "@types/express": "5.0.1", compression: "1.7.4", errorhandler: "1.5.1", parseurl: "1.3.3", From 048f81126acf791e0c9f308bc83b94ce6aaa21e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 2 Apr 2025 17:41:31 +0200 Subject: [PATCH 044/264] implement config to enable SWC --- packages/babel-compiler/babel-compiler.js | 64 ++++++++++++++++++++--- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 35e3588df7..7903502e4d 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -88,6 +88,53 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features }) { }); } +function getMeteorAppDir() { + return process.cwd(); +} + +function getMeteorAppPackageJson() { + return JSON.parse( + fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8'), + ); +} + +const _regexCache = new Map(); + +function isRegexLike(str) { + return /[.*+?^${}()|[\]\\]/.test(str); +} + +function isExcludedConfig(name, excludeList = []) { + return excludeList.some(rule => { + if (name === rule) return true; + if (isRegexLike(rule)) { + let regex = _regexCache.get(rule); + if (!regex) { + try { + regex = new RegExp(rule); + _regexCache.set(rule, regex); + } catch (err) { + console.warn(`Invalid regex in exclude list: "${rule}"`); + return false; + } + } + return regex.test(name); + } + + return false; + }); +} + +BCp.initializeMeteorAppConfig = function () { + const currentLastModifiedConfigTime = fs + .statSync(`${getMeteorAppDir()}/package.json`) + ?.mtime?.getTime(); + if (currentLastModifiedConfigTime !== this.lastModifiedConfigTime) { + this.lastModifiedConfigTime = currentLastModifiedConfigTime; + this.lastModifiedConfig = getMeteorAppPackageJson()?.meteor; + } + return this.lastModifiedConfig; +}; BCp.processFilesForTarget = function (inputFiles) { var compiler = this; @@ -95,6 +142,8 @@ BCp.processFilesForTarget = function (inputFiles) { // Reset this cache for each batch processed. this._babelrcCache = null; + this.initializeMeteorAppConfig(); + inputFiles.forEach(function (inputFile) { if (inputFile.supportsLazyCompilation) { inputFile.addJavaScript({ @@ -207,14 +256,17 @@ BCp.processOneFileForTarget = function (inputFile, source) { try { var result = (() => { - const packagesSkipSwc = []; - const fileSkipSwc = []; // top level await - + const isAppCode = packageName == null; + const config = this.lastModifiedConfig?.modernTranspiler; + const hasModernTranspiler = config != null; + const shouldSkipSwc = + !hasModernTranspiler || + (config.excludeApp === true && isAppCode) || + isExcludedConfig(packageName, config.excludePackages || []) || + (isAppCode && isExcludedConfig(inputFilePath, config.excludeFiles || [])); // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = - !packagesSkipSwc.includes(packageName) && - !fileSkipSwc.includes(inputFilePath) && - !this._swcIncompatible[toBeAdded.hash]; + !shouldSkipSwc && !this._swcIncompatible[toBeAdded.hash]; let compilation; try { From cb68023bf2998dc2811eea3c6d692753ba27d86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 2 Apr 2025 17:56:26 +0200 Subject: [PATCH 045/264] better naming --- packages/babel-compiler/babel-compiler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 7903502e4d..9d1c946fdd 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -262,8 +262,8 @@ BCp.processOneFileForTarget = function (inputFile, source) { const shouldSkipSwc = !hasModernTranspiler || (config.excludeApp === true && isAppCode) || - isExcludedConfig(packageName, config.excludePackages || []) || - (isAppCode && isExcludedConfig(inputFilePath, config.excludeFiles || [])); + (isAppCode && isExcludedConfig(inputFilePath, config.excludeAppFiles || [])) || + isExcludedConfig(packageName, config.excludePackages || []); // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !shouldSkipSwc && !this._swcIncompatible[toBeAdded.hash]; From b620ff222d8f0d8d54f24a0d68ec37a8c9b65b78 Mon Sep 17 00:00:00 2001 From: harryadel Date: Wed, 2 Apr 2025 19:00:00 +0200 Subject: [PATCH 046/264] Apply Nacho fixes --- packages/accounts-password/password_tests.js | 37 +++++++++++--------- v3-docs/docs/api/accounts.md | 2 +- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 8b14a84b9f..f34e172dc7 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1789,27 +1789,30 @@ if (Meteor.isServer) (() => { ]); }); - Tinytest.addAsync("passwords, replace email", async test => { - const origEmail = `originalemail@test.com`; - const userId = await Accounts.createUser({ - email: origEmail - }); + - const newEmail = `newemail@test.com`; +Tinytest.addAsync("accounts emails - replace email", async test => { + const origEmail = `originalemail@test.com`; + const userId = await Accounts.createUserAsync({ + email: origEmail, + password: 'password' + }); - const u1 = await Accounts._findUserByQuery({ id: userId }) - test.equal(u1.emails, [ - { address: origEmail, verified: false } - ]); + const newEmail = `newemail@test.com`; - await Accounts.replaceEmailAsync(userId, origEmail, newEmail); - const u2 = await Accounts._findUserByQuery({ id: userId }) - test.equal(u2.emails, [ - { address: newEmail, verified: false } - ]); - }) + const u1 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u1.emails, [ + { address: origEmail, verified: false } + ]); - Tinytest.addAsync("passwords - remove email", + await Accounts.replaceEmailAsync(userId, origEmail, newEmail); + const u2 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u2.emails, [ + { address: newEmail, verified: false } + ]); +}) + + Tinytest.addAsync("passwords - remove email", async test => { const origEmail = `${ Random.id() }@turing.com`; const userId = await Accounts.createUser({ diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index ce5b8699f2..a40c6ef496 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -888,7 +888,7 @@ email with a link the user can use to verify their email address. - + From cc360a3e362d6f6499614dbbdb36cfcd6037eb81 Mon Sep 17 00:00:00 2001 From: harryadel Date: Wed, 2 Apr 2025 19:01:22 +0200 Subject: [PATCH 047/264] Fix doc --- v3-docs/docs/generators/changelog/versions/3.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/generators/changelog/versions/3.0.0.md b/v3-docs/docs/generators/changelog/versions/3.0.0.md index 66fcf78b5b..202729af5d 100644 --- a/v3-docs/docs/generators/changelog/versions/3.0.0.md +++ b/v3-docs/docs/generators/changelog/versions/3.0.0.md @@ -58,7 +58,7 @@ - `Accounts.sendVerificationEmail` - `Accounts.addEmail` - `Accounts.removeEmail` - - `Accounts.replaceEmail` + - `Accounts.replaceEmailAsync` - `Accounts.verifyEmail` - `Accounts.createUserVerifyingEmail` - `Accounts.createUser` From fb2bae51979633adcd80f085f0566cbc30bbd86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 4 Apr 2025 14:14:11 +0200 Subject: [PATCH 048/264] support a configuration to exclude by node_modules and simplify config --- packages/babel-compiler/babel-compiler.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 9d1c946fdd..d388e2b466 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -104,9 +104,11 @@ function isRegexLike(str) { return /[.*+?^${}()|[\]\\]/.test(str); } -function isExcludedConfig(name, excludeList = []) { +function isExcludedConfig(name, excludeList = [], startsWith) { + if (!name) return false; return excludeList.some(rule => { if (name === rule) return true; + if (startsWith && name.startsWith(rule)) return true; if (isRegexLike(rule)) { let regex = _regexCache.get(rule); if (!regex) { @@ -256,13 +258,26 @@ BCp.processOneFileForTarget = function (inputFile, source) { try { var result = (() => { - const isAppCode = packageName == null; + const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/"); + const isAppCode = packageName == null && !isNodeModulesCode; const config = this.lastModifiedConfig?.modernTranspiler; const hasModernTranspiler = config != null; const shouldSkipSwc = !hasModernTranspiler || (config.excludeApp === true && isAppCode) || - (isAppCode && isExcludedConfig(inputFilePath, config.excludeAppFiles || [])) || + (config.excludeNodeModules === true && isNodeModulesCode) || + (isAppCode && + Array.isArray(config.excludeApp) && + isExcludedConfig(inputFilePath, config.excludeApp || [])) || + (isNodeModulesCode && + Array.isArray(config.excludeNodeModules) && + (isExcludedConfig(inputFilePath, config.excludeNodeModules || []) || + isExcludedConfig( + inputFilePath.replace('node_modules/', ''), + config.excludeNodeModules || [], + true, + )) + ) || isExcludedConfig(packageName, config.excludePackages || []); // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = From 85e7f6e7207e2913002cc1ae0d5c8ee323abd5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 4 Apr 2025 14:36:51 +0200 Subject: [PATCH 049/264] support a configuration to exclude by meteor package and its path --- packages/babel-compiler/babel-compiler.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index d388e2b466..e280801181 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -105,7 +105,7 @@ function isRegexLike(str) { } function isExcludedConfig(name, excludeList = [], startsWith) { - if (!name) return false; + if (!name || !excludeList?.length) return false; return excludeList.some(rule => { if (name === rule) return true; if (startsWith && name.startsWith(rule)) return true; @@ -260,12 +260,14 @@ BCp.processOneFileForTarget = function (inputFile, source) { var result = (() => { const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/"); const isAppCode = packageName == null && !isNodeModulesCode; + const isPackageCode = packageName != null; const config = this.lastModifiedConfig?.modernTranspiler; const hasModernTranspiler = config != null; const shouldSkipSwc = !hasModernTranspiler || - (config.excludeApp === true && isAppCode) || - (config.excludeNodeModules === true && isNodeModulesCode) || + (isAppCode && config.excludeApp === true) || + (isNodeModulesCode && config.excludeNodeModules === true) || + (isPackageCode && config.excludePackages === true) || (isAppCode && Array.isArray(config.excludeApp) && isExcludedConfig(inputFilePath, config.excludeApp || [])) || @@ -276,9 +278,14 @@ BCp.processOneFileForTarget = function (inputFile, source) { inputFilePath.replace('node_modules/', ''), config.excludeNodeModules || [], true, - )) - ) || - isExcludedConfig(packageName, config.excludePackages || []); + ))) || + (isPackageCode && + Array.isArray(config.excludePackages) && + (isExcludedConfig(packageName, config.excludePackages || []) || + isExcludedConfig( + `${packageName}/${inputFilePath}`, + config.excludePackages || [], + ))); // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !shouldSkipSwc && !this._swcIncompatible[toBeAdded.hash]; From cc1f6a4943a42606421bb29130e78e574ba51c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 7 Apr 2025 15:31:34 +0200 Subject: [PATCH 050/264] add a verbose config to get insights of transpilation process --- packages/babel-compiler/babel-compiler.js | 99 ++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index e280801181..0b3d9cc4a9 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -104,6 +104,62 @@ function isRegexLike(str) { return /[.*+?^${}()|[\]\\]/.test(str); } +function color(text, code) { + return `\x1b[${code}m${text}\x1b[0m`; +} + +function logTranspilation({ + packageName, + inputFilePath, + usedSwc, + cacheHit, + isNodeModulesCode, + errorMessage = '', + tip = '', +}) { + const transpiler = usedSwc ? 'SWC' : 'Babel'; + const transpilerColor = usedSwc ? 32 : 33; + const label = color('[Transpiler]', 36); + const transpilerPart = `${label} Used ${color( + transpiler, + transpilerColor, + )} for`; + const filePathPadded = `${ + packageName ? `${packageName}/` : '' + }${inputFilePath}`.padEnd(50); + let rawOrigin = ''; + if (packageName) { + rawOrigin = `(package)`; + } else { + rawOrigin = isNodeModulesCode ? '(node_modules)' : '(app)'; + } + const originPaddedRaw = rawOrigin.padEnd(35); + const originPaddedColored = packageName + ? originPaddedRaw + : isNodeModulesCode + ? color(originPaddedRaw, 90) + : color(originPaddedRaw, 35); + const cacheStatus = errorMessage + ? color('⚠️ Fallback', 33) + : usedSwc + ? cacheHit + ? color('🟢 Cache hit', 32) + : color('🔴 Cache miss', 31) + : ''; + console.log( + `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}`, + ); + if (errorMessage) { + console.log(); + console.log(` ↳ ${color('Error:', 31)} ${errorMessage}`); + if (tip) { + console.log(); + console.log(` ${color('💡 Tip:', 33)} ${tip}`); + } + console.log(); + } +} + function isExcludedConfig(name, excludeList = [], startsWith) { if (!name || !excludeList?.length) return false; return excludeList.some(rule => { @@ -292,6 +348,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { let compilation; try { + let usedSwc = false; if (shouldUseSwc) { // Create a cache key based on the source hash and the compiler used const cacheKey = toBeAdded.hash; @@ -299,18 +356,58 @@ BCp.processOneFileForTarget = function (inputFile, source) { compilation = this.readFromSwcCache({ cacheKey }); // Return cached result if found. if (compilation) { + if (config?.verbose) { + logTranspilation({ + usedSwc: true, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: true, + }); + } return compilation; } - compilation = compileWithSwc(source, {}, { inputFilePath, features }); + compilation = compileWithSwc( + source, + {}, + { inputFilePath, features }, + ); // Save result in cache this.writeToSwcCache({ cacheKey, compilation }); + usedSwc = true; } else { compilation = compileWithBabel(source, babelOptions, cacheOptions); + usedSwc = false; + } + + if (config?.verbose) { + logTranspilation({ + usedSwc, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: false, + }); } } catch (e) { this._swcIncompatible[toBeAdded.hash] = true; // If SWC fails, fall back to Babel compilation = compileWithBabel(source, babelOptions, cacheOptions); + if (config?.verbose) { + logTranspilation({ + usedSwc: false, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: false, + errorMessage: e?.message, + ...(e?.message?.includes( + 'cannot be used outside of module code', + ) && { + tip: 'Remove nested import to support SWC transpilation and improve speed', + }), + }); + } } return compilation; From 14202008ac648e135328715a235e7551f40d5796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 7 Apr 2025 16:22:36 +0200 Subject: [PATCH 051/264] better tip on nested imports --- packages/babel-compiler/babel-compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 0b3d9cc4a9..6402f4dc99 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -404,7 +404,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { ...(e?.message?.includes( 'cannot be used outside of module code', ) && { - tip: 'Remove nested import to support SWC transpilation and improve speed', + tip: 'Remove nested imports or replace them with require to support SWC and improve speed.', }), }); } From 681f59a20606b85b388b735547a6f71c5b7dc8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 7 Apr 2025 17:29:57 +0200 Subject: [PATCH 052/264] support swcConfig and plugin support --- packages/babel-compiler/babel-compiler.js | 278 +++++++++++++--------- 1 file changed, 169 insertions(+), 109 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 6402f4dc99..526b1cb215 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -42,9 +42,7 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features }) { const hasTSXSupport = inputFilePath.endsWith('.tsx'); const hasJSXSupport = inputFilePath.endsWith('.jsx'); - // Perform SWC transformation. - const transformed = SWC.transformSync(source, { - ...swcOptions, + const baseSwcConfig = { jsc: { target: 'es2015', parser: { @@ -56,7 +54,19 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features }) { module: { type: 'es6' }, minify: false, sourceMaps: true, - }); + }; + const nextSwcConfig = + Object.keys(swcOptions)?.length > 0 + ? deepMerge(baseSwcConfig, swcOptions, [ + 'module.type', + 'jsc.parser.syntax', + 'jsc.parser.jsx', + 'jsc.parser.tsx', + ]) + : baseSwcConfig; + + // Perform SWC transformation. + const transformed = SWC.transformSync(source, nextSwcConfig); let content = transformed.code; @@ -88,110 +98,31 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features }) { }); } -function getMeteorAppDir() { - return process.cwd(); -} - -function getMeteorAppPackageJson() { - return JSON.parse( - fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8'), - ); -} - -const _regexCache = new Map(); - -function isRegexLike(str) { - return /[.*+?^${}()|[\]\\]/.test(str); -} - -function color(text, code) { - return `\x1b[${code}m${text}\x1b[0m`; -} - -function logTranspilation({ - packageName, - inputFilePath, - usedSwc, - cacheHit, - isNodeModulesCode, - errorMessage = '', - tip = '', -}) { - const transpiler = usedSwc ? 'SWC' : 'Babel'; - const transpilerColor = usedSwc ? 32 : 33; - const label = color('[Transpiler]', 36); - const transpilerPart = `${label} Used ${color( - transpiler, - transpilerColor, - )} for`; - const filePathPadded = `${ - packageName ? `${packageName}/` : '' - }${inputFilePath}`.padEnd(50); - let rawOrigin = ''; - if (packageName) { - rawOrigin = `(package)`; - } else { - rawOrigin = isNodeModulesCode ? '(node_modules)' : '(app)'; - } - const originPaddedRaw = rawOrigin.padEnd(35); - const originPaddedColored = packageName - ? originPaddedRaw - : isNodeModulesCode - ? color(originPaddedRaw, 90) - : color(originPaddedRaw, 35); - const cacheStatus = errorMessage - ? color('⚠️ Fallback', 33) - : usedSwc - ? cacheHit - ? color('🟢 Cache hit', 32) - : color('🔴 Cache miss', 31) - : ''; - console.log( - `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}`, - ); - if (errorMessage) { - console.log(); - console.log(` ↳ ${color('Error:', 31)} ${errorMessage}`); - if (tip) { - console.log(); - console.log(` ${color('💡 Tip:', 33)} ${tip}`); - } - console.log(); - } -} - -function isExcludedConfig(name, excludeList = [], startsWith) { - if (!name || !excludeList?.length) return false; - return excludeList.some(rule => { - if (name === rule) return true; - if (startsWith && name.startsWith(rule)) return true; - if (isRegexLike(rule)) { - let regex = _regexCache.get(rule); - if (!regex) { - try { - regex = new RegExp(rule); - _regexCache.set(rule, regex); - } catch (err) { - console.warn(`Invalid regex in exclude list: "${rule}"`); - return false; - } - } - return regex.test(name); - } - - return false; - }); -} - BCp.initializeMeteorAppConfig = function () { const currentLastModifiedConfigTime = fs .statSync(`${getMeteorAppDir()}/package.json`) ?.mtime?.getTime(); - if (currentLastModifiedConfigTime !== this.lastModifiedConfigTime) { - this.lastModifiedConfigTime = currentLastModifiedConfigTime; - this.lastModifiedConfig = getMeteorAppPackageJson()?.meteor; + if (currentLastModifiedConfigTime !== this.lastModifiedMeteorConfigTime) { + this.lastModifiedMeteorConfigTime = currentLastModifiedConfigTime; + this.lastModifiedMeteorConfig = getMeteorAppPackageJson()?.meteor; } - return this.lastModifiedConfig; + return this.lastModifiedMeteorConfig; +}; + +BCp.initializeMeteorAppSwcrc = function () { + if (!fs.existsSync(`${getMeteorAppDir()}/.swcrc`)) { + this.lastModifiedSwcConfig = {}; + return; + } + + const currentLastModifiedConfigTime = fs + .statSync(`${getMeteorAppDir()}/.swcrc`) + ?.mtime?.getTime(); + if (currentLastModifiedConfigTime !== this.lastModifiedSwcConfigTime) { + this.lastModifiedSwcConfigTime = currentLastModifiedConfigTime; + this.lastModifiedSwcConfig = getMeteorAppSwcrc(); + } + return this.lastModifiedSwcConfig; }; BCp.processFilesForTarget = function (inputFiles) { @@ -201,6 +132,7 @@ BCp.processFilesForTarget = function (inputFiles) { this._babelrcCache = null; this.initializeMeteorAppConfig(); + this.initializeMeteorAppSwcrc(); inputFiles.forEach(function (inputFile) { if (inputFile.supportsLazyCompilation) { @@ -317,7 +249,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/"); const isAppCode = packageName == null && !isNodeModulesCode; const isPackageCode = packageName != null; - const config = this.lastModifiedConfig?.modernTranspiler; + const config = this.lastModifiedMeteorConfig?.modernTranspiler; const hasModernTranspiler = config != null; const shouldSkipSwc = !hasModernTranspiler || @@ -342,16 +274,16 @@ BCp.processOneFileForTarget = function (inputFile, source) { `${packageName}/${inputFilePath}`, config.excludePackages || [], ))); + + const cacheKey = `${toBeAdded.hash}${this.lastModifiedSwcConfigTime ||''}`; // Determine if SWC should be used based on package and file criteria. - const shouldUseSwc = - !shouldSkipSwc && !this._swcIncompatible[toBeAdded.hash]; + const shouldUseSwc = !shouldSkipSwc && !this._swcIncompatible[cacheKey]; let compilation; try { let usedSwc = false; if (shouldUseSwc) { // Create a cache key based on the source hash and the compiler used - const cacheKey = toBeAdded.hash; // Check cache compilation = this.readFromSwcCache({ cacheKey }); // Return cached result if found. @@ -369,7 +301,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { } compilation = compileWithSwc( source, - {}, + this.lastModifiedSwcConfig, { inputFilePath, features }, ); // Save result in cache @@ -390,7 +322,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { }); } } catch (e) { - this._swcIncompatible[toBeAdded.hash] = true; + this._swcIncompatible[cacheKey] = true; // If SWC fails, fall back to Babel compilation = compileWithBabel(source, babelOptions, cacheOptions); if (config?.verbose) { @@ -869,3 +801,131 @@ BCp.writeToSwcCache = function({ cacheKey, compilation }) { } } }; + +function getMeteorAppDir() { + return process.cwd(); +} + +function getMeteorAppPackageJson() { + return JSON.parse( + fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8'), + ); +} + +function getMeteorAppSwcrc() { + try { + return JSON.parse(fs.readFileSync(`${getMeteorAppDir()}/.swcrc`, 'utf-8')); + } catch (e) { + console.error('Error parsing .swcrc file', e); + } +} + +const _regexCache = new Map(); + +function isRegexLike(str) { + return /[.*+?^${}()|[\]\\]/.test(str); +} + +function isExcludedConfig(name, excludeList = [], startsWith) { + if (!name || !excludeList?.length) return false; + return excludeList.some(rule => { + if (name === rule) return true; + if (startsWith && name.startsWith(rule)) return true; + if (isRegexLike(rule)) { + let regex = _regexCache.get(rule); + if (!regex) { + try { + regex = new RegExp(rule); + _regexCache.set(rule, regex); + } catch (err) { + console.warn(`Invalid regex in exclude list: "${rule}"`); + return false; + } + } + return regex.test(name); + } + + return false; + }); +} + +function color(text, code) { + return `\x1b[${code}m${text}\x1b[0m`; +} + +function logTranspilation({ + packageName, + inputFilePath, + usedSwc, + cacheHit, + isNodeModulesCode, + errorMessage = '', + tip = '', +}) { + const transpiler = usedSwc ? 'SWC' : 'Babel'; + const transpilerColor = usedSwc ? 32 : 33; + const label = color('[Transpiler]', 36); + const transpilerPart = `${label} Used ${color( + transpiler, + transpilerColor, + )} for`; + const filePathPadded = `${ + packageName ? `${packageName}/` : '' + }${inputFilePath}`.padEnd(50); + let rawOrigin = ''; + if (packageName) { + rawOrigin = `(package)`; + } else { + rawOrigin = isNodeModulesCode ? '(node_modules)' : '(app)'; + } + const originPaddedRaw = rawOrigin.padEnd(35); + const originPaddedColored = packageName + ? originPaddedRaw + : isNodeModulesCode + ? color(originPaddedRaw, 90) + : color(originPaddedRaw, 35); + const cacheStatus = errorMessage + ? color('⚠️ Fallback', 33) + : usedSwc + ? cacheHit + ? color('🟢 Cache hit', 32) + : color('🔴 Cache miss', 31) + : ''; + console.log( + `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}`, + ); + if (errorMessage) { + console.log(); + console.log(` ↳ ${color('Error:', 31)} ${errorMessage}`); + if (tip) { + console.log(); + console.log(` ${color('💡 Tip:', 33)} ${tip}`); + } + console.log(); + } +} + +function deepMerge(target, source, preservePaths, inPath = '') { + for (const key in source) { + const fullPath = inPath ? `${inPath}.${key}` : key; + + // Skip preserved paths + if (preservePaths.includes(fullPath)) continue; + + if ( + typeof source[key] === 'object' && + source[key] !== null && + !Array.isArray(source[key]) + ) { + target[key] = deepMerge( + target[key] || {}, + source[key], + preservePaths, + fullPath, + ); + } else { + target[key] = source[key]; + } + } + return target; +} From 630fb0ef233b1e658ffc7afbd114013e6ef5fb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 7 Apr 2025 17:59:24 +0200 Subject: [PATCH 053/264] print configs in verbose modes --- packages/babel-compiler/babel-compiler.js | 52 +++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 526b1cb215..0d88ec35dd 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -98,31 +98,42 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features }) { }); } +let lastModifiedMeteorConfig; +let lastModifiedMeteorConfigTime; BCp.initializeMeteorAppConfig = function () { const currentLastModifiedConfigTime = fs .statSync(`${getMeteorAppDir()}/package.json`) ?.mtime?.getTime(); - if (currentLastModifiedConfigTime !== this.lastModifiedMeteorConfigTime) { - this.lastModifiedMeteorConfigTime = currentLastModifiedConfigTime; - this.lastModifiedMeteorConfig = getMeteorAppPackageJson()?.meteor; + if (currentLastModifiedConfigTime !== lastModifiedMeteorConfigTime) { + lastModifiedMeteorConfigTime = currentLastModifiedConfigTime; + lastModifiedMeteorConfig = getMeteorAppPackageJson()?.meteor; + + if (lastModifiedMeteorConfig?.modernTranspiler?.verbose) { + logConfigBlock('Meteor Config', lastModifiedMeteorConfig); + } } - return this.lastModifiedMeteorConfig; + return lastModifiedMeteorConfig; }; +let lastModifiedSwcConfig; +let lastModifiedSwcConfigTime; BCp.initializeMeteorAppSwcrc = function () { if (!fs.existsSync(`${getMeteorAppDir()}/.swcrc`)) { - this.lastModifiedSwcConfig = {}; + lastModifiedSwcConfig = {}; return; } - const currentLastModifiedConfigTime = fs .statSync(`${getMeteorAppDir()}/.swcrc`) ?.mtime?.getTime(); - if (currentLastModifiedConfigTime !== this.lastModifiedSwcConfigTime) { - this.lastModifiedSwcConfigTime = currentLastModifiedConfigTime; - this.lastModifiedSwcConfig = getMeteorAppSwcrc(); + if (currentLastModifiedConfigTime !== lastModifiedSwcConfigTime) { + lastModifiedSwcConfigTime = currentLastModifiedConfigTime; + lastModifiedSwcConfig = getMeteorAppSwcrc(); + + if (lastModifiedMeteorConfig?.modernTranspiler?.verbose) { + logConfigBlock('SWC Config', lastModifiedSwcConfig); + } } - return this.lastModifiedSwcConfig; + return lastModifiedSwcConfig; }; BCp.processFilesForTarget = function (inputFiles) { @@ -249,7 +260,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/"); const isAppCode = packageName == null && !isNodeModulesCode; const isPackageCode = packageName != null; - const config = this.lastModifiedMeteorConfig?.modernTranspiler; + const config = lastModifiedMeteorConfig?.modernTranspiler; const hasModernTranspiler = config != null; const shouldSkipSwc = !hasModernTranspiler || @@ -275,7 +286,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { config.excludePackages || [], ))); - const cacheKey = `${toBeAdded.hash}${this.lastModifiedSwcConfigTime ||''}`; + const cacheKey = `${toBeAdded.hash}${lastModifiedSwcConfigTime ||''}`; // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !shouldSkipSwc && !this._swcIncompatible[cacheKey]; @@ -301,7 +312,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { } compilation = compileWithSwc( source, - this.lastModifiedSwcConfig, + lastModifiedSwcConfig, { inputFilePath, features }, ); // Save result in cache @@ -905,6 +916,21 @@ function logTranspilation({ } } +function logConfigBlock(description, configObject) { + const label = color('[Config]', 36); + const descriptionColor = color(description, 90); + + console.log(`${label} ${descriptionColor}`); + + const configLines = JSON.stringify(configObject, null, 2) + .replace(/"([^"]+)":/g, '$1:') + .split('\n') + .map(line => ' ' + line); + + configLines.forEach(line => console.log(line)); + console.log(); +} + function deepMerge(target, source, preservePaths, inPath = '') { for (const key in source) { const fullPath = inPath ? `${inPath}.${key}` : key; From 9370fdfd9c705b85b1b2871ab1fe5a3d33aca84f Mon Sep 17 00:00:00 2001 From: Welkin Wong Date: Tue, 8 Apr 2025 15:32:06 +0800 Subject: [PATCH 054/264] DEV: Enhance type definitions for Meteor methods --- packages/meteor/meteor.d.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/meteor/meteor.d.ts b/packages/meteor/meteor.d.ts index 94f4a9753f..d14ec0d1ef 100644 --- a/packages/meteor/meteor.d.ts +++ b/packages/meteor/meteor.d.ts @@ -162,14 +162,26 @@ export namespace Meteor { * @param name Name of method to invoke * @param args Optional method arguments */ - function call(name: string, ...args: any[]): any; + function call< + Result extends + | EJSONable + | EJSONable[] + | EJSONableProperty + | EJSONableProperty[] + >(name: string, ...args: any[]): Result; /** * Invokes a method with an async stub, passing any number of arguments. * @param name Name of method to invoke * @param args Optional method arguments */ - function callAsync(name: string, ...args: any[]): Promise; + function callAsync< + Result extends + | EJSONable + | EJSONable[] + | EJSONableProperty + | EJSONableProperty[] + >(name: string, ...args: any[]): Promise & { stubPromise: Promise, serverPromise: Promise }; interface MethodApplyOptions< Result extends @@ -226,7 +238,7 @@ export namespace Meteor { error: global_Error | Meteor.Error | undefined, result?: Result ) => void - ): any; + ): Result; /** * Invokes a method with an async stub, passing any number of arguments. @@ -249,7 +261,7 @@ export namespace Meteor { error: global_Error | Meteor.Error | undefined, result?: Result ) => void - ): Promise; + ): Promise & { stubPromise: Promise, serverPromise: Promise }; /** Method **/ /** Url **/ From 51dbad059b9fe06dc5f22b2a122c9a1b7c0ffbe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 14:56:36 +0200 Subject: [PATCH 055/264] support SWC for legacy browsers --- packages/babel-compiler/babel-compiler.js | 53 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 0d88ec35dd..3694b56c26 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -35,16 +35,17 @@ function compileWithBabel(source, babelOptions, cacheOptions) { }); } -function compileWithSwc(source, swcOptions, { inputFilePath, features }) { +function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { return profile('SWC.compile', function () { // Determine file extension based syntax. const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); const hasTSXSupport = inputFilePath.endsWith('.tsx'); const hasJSXSupport = inputFilePath.endsWith('.jsx'); + const isLegacyWebArch = arch.includes('legacy'); const baseSwcConfig = { jsc: { - target: 'es2015', + ...!isLegacyWebArch && { target: 'es2015' }, parser: { syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', jsx: hasJSXSupport, @@ -54,10 +55,14 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features }) { module: { type: 'es6' }, minify: false, sourceMaps: true, + ...isLegacyWebArch && { + env: { targets: lastModifiedSwcLegacyConfig || {} }, + }, }; const nextSwcConfig = Object.keys(swcOptions)?.length > 0 ? deepMerge(baseSwcConfig, swcOptions, [ + 'env.targets', 'module.type', 'jsc.parser.syntax', 'jsc.parser.jsx', @@ -136,6 +141,16 @@ BCp.initializeMeteorAppSwcrc = function () { return lastModifiedSwcConfig; }; +let lastModifiedSwcLegacyConfig; +BCp.initializeMeteorAppLegacyConfig = function () { + const swcLegacyConfig = convertBabelTargetsForSwc(Babel.getMinimumModernBrowserVersions()); + if (lastModifiedMeteorConfig?.modernTranspiler?.verbose && !lastModifiedSwcLegacyConfig) { + logConfigBlock('SWC Legacy Config', swcLegacyConfig); + } + lastModifiedSwcLegacyConfig = swcLegacyConfig; + return lastModifiedSwcConfig; +}; + BCp.processFilesForTarget = function (inputFiles) { var compiler = this; @@ -144,6 +159,7 @@ BCp.processFilesForTarget = function (inputFiles) { this.initializeMeteorAppConfig(); this.initializeMeteorAppSwcrc(); + this.initializeMeteorAppLegacyConfig(); inputFiles.forEach(function (inputFile) { if (inputFile.supportsLazyCompilation) { @@ -260,6 +276,8 @@ BCp.processOneFileForTarget = function (inputFile, source) { const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/"); const isAppCode = packageName == null && !isNodeModulesCode; const isPackageCode = packageName != null; + const isLegacyWebArch = arch.includes('legacy'); + const config = lastModifiedMeteorConfig?.modernTranspiler; const hasModernTranspiler = config != null; const shouldSkipSwc = @@ -267,6 +285,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { (isAppCode && config.excludeApp === true) || (isNodeModulesCode && config.excludeNodeModules === true) || (isPackageCode && config.excludePackages === true) || + (isLegacyWebArch && config.excludeLegacy === true) || (isAppCode && Array.isArray(config.excludeApp) && isExcludedConfig(inputFilePath, config.excludeApp || [])) || @@ -286,7 +305,9 @@ BCp.processOneFileForTarget = function (inputFile, source) { config.excludePackages || [], ))); - const cacheKey = `${toBeAdded.hash}${lastModifiedSwcConfigTime ||''}`; + const cacheKey = `${toBeAdded.hash}${lastModifiedSwcConfigTime || ''}${ + isLegacyWebArch ? 'legacy' : '' + }`; // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !shouldSkipSwc && !this._swcIncompatible[cacheKey]; @@ -306,6 +327,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { packageName, isNodeModulesCode, cacheHit: true, + arch, }); } return compilation; @@ -313,7 +335,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { compilation = compileWithSwc( source, lastModifiedSwcConfig, - { inputFilePath, features }, + { inputFilePath, features, arch }, ); // Save result in cache this.writeToSwcCache({ cacheKey, compilation }); @@ -330,6 +352,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { packageName, isNodeModulesCode, cacheHit: false, + arch, }); } } catch (e) { @@ -343,6 +366,7 @@ BCp.processOneFileForTarget = function (inputFile, source) { packageName, isNodeModulesCode, cacheHit: false, + arch, errorMessage: e?.message, ...(e?.message?.includes( 'cannot be used outside of module code', @@ -870,6 +894,7 @@ function logTranspilation({ usedSwc, cacheHit, isNodeModulesCode, + arch, errorMessage = '', tip = '', }) { @@ -902,8 +927,9 @@ function logTranspilation({ ? color('🟢 Cache hit', 32) : color('🔴 Cache miss', 31) : ''; + const archPart = arch ? color(` (${arch})`, 90) : ''; console.log( - `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}`, + `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}${archPart}`, ); if (errorMessage) { console.log(); @@ -955,3 +981,20 @@ function deepMerge(target, source, preservePaths, inPath = '') { } return target; } + +function convertBabelTargetsForSwc(babelTargets) { + const allowedEnvs = new Set([ + 'chrome', 'opera', 'edge', 'firefox', 'safari', + 'ie', 'ios', 'android', 'node', 'electron' + ]); + + const filteredTargets = {}; + for (const [env, version] of Object.entries(babelTargets)) { + if (allowedEnvs.has(env)) { + // Convert an array version (e.g., [10, 3]) into "10.3", otherwise convert to string. + filteredTargets[env] = Array.isArray(version) ? version.join('.') : version.toString(); + } + } + + return filteredTargets; +} From 40a3cedce9a66d37a2cb3f530b41c5adae6534fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 16:01:45 +0200 Subject: [PATCH 056/264] clean --- packages/babel-compiler/babel-compiler.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 3694b56c26..03176da2dd 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -305,9 +305,13 @@ BCp.processOneFileForTarget = function (inputFile, source) { config.excludePackages || [], ))); - const cacheKey = `${toBeAdded.hash}${lastModifiedSwcConfigTime || ''}${ - isLegacyWebArch ? 'legacy' : '' - }`; + const cacheKey = [ + toBeAdded.hash, + lastModifiedSwcConfigTime, + isLegacyWebArch ? 'legacy' : '', + ] + .filter(Boolean) + .join('-'); // Determine if SWC should be used based on package and file criteria. const shouldUseSwc = !shouldSkipSwc && !this._swcIncompatible[cacheKey]; From 9e9c1be76fd7505a7f4a111f80adce8dbe8889c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 16:21:40 +0200 Subject: [PATCH 057/264] use a meteor-specific version for SWC --- packages/babel-compiler/babel-compiler.js | 2 +- packages/babel-compiler/package.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 03176da2dd..18bd05f828 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -1,6 +1,6 @@ var semver = Npm.require("semver"); var JSON5 = Npm.require("json5"); -var SWC = Npm.require("@swc/core"); +var SWC = Npm.require("@meteorjs/swc-core"); const reifyCompile = Npm.require("@meteorjs/reify/lib/compiler").compile; const reifyAcornParse = Npm.require("@meteorjs/reify/lib/parsers/acorn").parse; var fs = Npm.require('fs'); diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 812b3e96af..0e9c3c420d 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -8,7 +8,7 @@ Npm.depends({ '@meteorjs/babel': '7.20.1', 'json5': '2.2.3', 'semver': '7.6.3', - "@swc/core": "1.11.11", + "@meteorjs/swc-core": "1.1.2", }); Package.onUse(function (api) { From 252303441446260f4971ca0836f24064e7bf7b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 16:31:32 +0200 Subject: [PATCH 058/264] fix syntax --- packages/babel-compiler/babel-compiler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 18bd05f828..73a39d28b5 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -45,7 +45,7 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { const isLegacyWebArch = arch.includes('legacy'); const baseSwcConfig = { jsc: { - ...!isLegacyWebArch && { target: 'es2015' }, + ...(!isLegacyWebArch && { target: 'es2015' }), parser: { syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', jsx: hasJSXSupport, @@ -55,9 +55,9 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { module: { type: 'es6' }, minify: false, sourceMaps: true, - ...isLegacyWebArch && { + ...(isLegacyWebArch && { env: { targets: lastModifiedSwcLegacyConfig || {} }, - }, + }), }; const nextSwcConfig = Object.keys(swcOptions)?.length > 0 From c69e757bb7686f7b1b8df5ceaf6d37f9deaff4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 16:32:57 +0200 Subject: [PATCH 059/264] remove un-required @swc/core from the meteor bundler --- scripts/dev-bundle-tool-package.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 60cc8d69fa..2b92b9e5f6 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -68,7 +68,6 @@ var packageJson = { 'xmlbuilder2': '1.8.1', "ws": "7.4.5", "open":"8.4.2", - "@swc/core": "1.11.11", "acorn": "8.14.1", } }; From 9d9de24b322cca2845f41219062f889a2c27b354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 16:39:07 +0200 Subject: [PATCH 060/264] ignore server babel-compiler file from the check legacy syntax as node 22 is modern enough --- packages/babel-compiler/babel-compiler.js | 8 ++++---- scripts/admin/check-legacy-syntax/check-syntax.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 73a39d28b5..c7e4354207 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -84,12 +84,12 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { avoidModernSyntax: true, enforceStrictMode: false, dynamicImport: true, - ...features.topLevelAwait && { topLevelAwait: true }, - ...features.compileForShell && { moduleAlias: 'module' }, - ...(features.modernBrowsers || features.nodeMajorVersion >= 8) && { + ...(features.topLevelAwait && { topLevelAwait: true }), + ...(features.compileForShell && { moduleAlias: 'module' }), + ...((features.modernBrowsers || features.nodeMajorVersion >= 8) && { avoidModernSyntax: false, generateLetDeclarations: true, - }, + }), }); if (!result.identical) { content = result.code; diff --git a/scripts/admin/check-legacy-syntax/check-syntax.js b/scripts/admin/check-legacy-syntax/check-syntax.js index d5a1db490f..58b1ce1284 100644 --- a/scripts/admin/check-legacy-syntax/check-syntax.js +++ b/scripts/admin/check-legacy-syntax/check-syntax.js @@ -42,6 +42,7 @@ const packages = { autopublish: {}, "babel-compiler": { serverFiles: ["babel.js", "babel-compiler.js"], + ignoredFiles: ["babel-compiler.js"], }, "babel-runtime": {}, "browser-policy": {}, From 7bae9b5c91bca0b36b9de69a5edc94a07521508a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 17:05:47 +0200 Subject: [PATCH 061/264] bump meteor bundle version --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index 0c7f6125a9..70ccbf127d 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.5 +BUNDLE_VERSION=22.14.0.6 # OS Check. Put here because here is where we download the precompiled From 5239dc5da9ba1894b6509f77eab3b70fd8abc7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 19:45:10 +0200 Subject: [PATCH 062/264] bump bundle version --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index 70ccbf127d..07a1b72746 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.6 +BUNDLE_VERSION=22.14.0.7 # OS Check. Put here because here is where we download the precompiled From f23c7dac5967a60652d64b929c85dd690c759371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 8 Apr 2025 19:50:23 +0200 Subject: [PATCH 063/264] re-run checks From b8b59b0b7daec466762d37817d504993e74d1a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 9 Apr 2025 10:39:38 +0200 Subject: [PATCH 064/264] re-run checks From 3a039e56f4d9c57501f4f4a4a2079605e4ec8926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 9 Apr 2025 14:26:54 +0200 Subject: [PATCH 065/264] precheck on meteor config --- packages/babel-compiler/babel-compiler.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index c7e4354207..6f668817bd 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -106,6 +106,9 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { let lastModifiedMeteorConfig; let lastModifiedMeteorConfigTime; BCp.initializeMeteorAppConfig = function () { + if (!fs.existsSync(`${getMeteorAppDir()}/package.json`)) { + return; + } const currentLastModifiedConfigTime = fs .statSync(`${getMeteorAppDir()}/package.json`) ?.mtime?.getTime(); From a5022137db1bb9cf7835e483f5ec5c548da965a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 9 Apr 2025 14:28:06 +0200 Subject: [PATCH 066/264] precheck on meteor config --- packages/babel-compiler/babel-compiler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 6f668817bd..6bf6c85fea 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -106,7 +106,7 @@ function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { let lastModifiedMeteorConfig; let lastModifiedMeteorConfigTime; BCp.initializeMeteorAppConfig = function () { - if (!fs.existsSync(`${getMeteorAppDir()}/package.json`)) { + if (!lastModifiedMeteorConfig && !fs.existsSync(`${getMeteorAppDir()}/package.json`)) { return; } const currentLastModifiedConfigTime = fs @@ -126,7 +126,7 @@ BCp.initializeMeteorAppConfig = function () { let lastModifiedSwcConfig; let lastModifiedSwcConfigTime; BCp.initializeMeteorAppSwcrc = function () { - if (!fs.existsSync(`${getMeteorAppDir()}/.swcrc`)) { + if (!lastModifiedSwcConfig && !fs.existsSync(`${getMeteorAppDir()}/.swcrc`)) { lastModifiedSwcConfig = {}; return; } From 3a583d50503863f7ac39005ccad044429e556fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 9 Apr 2025 15:04:22 +0200 Subject: [PATCH 067/264] fix ci --- packages/babel-compiler/babel-compiler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 6bf6c85fea..d323554201 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -35,7 +35,7 @@ function compileWithBabel(source, babelOptions, cacheOptions) { }); } -function compileWithSwc(source, swcOptions, { inputFilePath, features, arch }) { +function compileWithSwc(source, swcOptions = {}, { inputFilePath, features, arch }) { return profile('SWC.compile', function () { // Determine file extension based syntax. const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); @@ -127,7 +127,6 @@ let lastModifiedSwcConfig; let lastModifiedSwcConfigTime; BCp.initializeMeteorAppSwcrc = function () { if (!lastModifiedSwcConfig && !fs.existsSync(`${getMeteorAppDir()}/.swcrc`)) { - lastModifiedSwcConfig = {}; return; } const currentLastModifiedConfigTime = fs From f1684b42fef2a53b14ebe140b9c518d3261829d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 11:00:48 +0200 Subject: [PATCH 068/264] ensure web.cordova arch is forced when cordova commands --- tools/cli/commands.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 40b2cfc34e..6b3c2168ff 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -271,11 +271,15 @@ function isModernArchsOnlyEnabled(appDir) { return packageJson?.meteor?.modernWebArchsOnly === true; } -function filterWebArchs(webArchs, excludeArchsOption, appDir) { - const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; - if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { - const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; - webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); +function filterWebArchs(webArchs, excludeArchsOption, appDir, options) { + const isCordovaEnabled = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg)); + if (!isCordovaEnabled) { + const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; + if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { + const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; + webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); + } + return webArchs; } return webArchs; } @@ -519,7 +523,7 @@ async function doRunCommand(options) { } } - webArchs = filterWebArchs(webArchs, options['exclude-archs'], options.appDir); + webArchs = filterWebArchs(webArchs, options['exclude-archs'], options.appDir, options); const buildMode = options.production ? 'production' : 'development'; let cordovaRunner; @@ -1415,7 +1419,7 @@ on an OS X system."); webArchs = filteredArchs.length ? filteredArchs : undefined; } else { - webArchs = filterWebArchs(baseWebArchs, options['exclude-archs'], options.appDir); + webArchs = filterWebArchs(baseWebArchs, options['exclude-archs'], options.appDir, options); } var buildDir = projectContext.getProjectLocalDirectory('build_tar'); @@ -2477,7 +2481,7 @@ var runTestAppForPackages = async function (projectContext, options) { if (options.cordovaRunner) { webArchs.push("web.cordova"); } - buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs'], projectContext.appDirectory); + buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs'], projectContext.appDirectory, options); if (options.deploy) { // Run the constraint solver and build local packages. From fb37a8ee2d2b114ecd39ffabada78fd6626ddab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 12:00:02 +0200 Subject: [PATCH 069/264] ensure web.browser.legacy arch is forced when build commands except if configured modern-only --- tools/cli/commands.js | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 6b3c2168ff..178f9aaf64 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -272,14 +272,29 @@ function isModernArchsOnlyEnabled(appDir) { } function filterWebArchs(webArchs, excludeArchsOption, appDir, options) { - const isCordovaEnabled = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg)); - if (!isCordovaEnabled) { - const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; - if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { - const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; - webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); + const platforms = (options.platforms || []); + const isBuildMode = platforms?.length > 0; + if (isBuildMode) { + // Build Mode + const isModernOnlyPlatform = platforms.includes('modern') && !platforms.includes('legacy'); + if (isModernOnlyPlatform) { + webArchs = webArchs.filter(arch => arch !== 'web.browser.legacy'); + } + const hasCordovaPlatforms = platforms.includes('android') || platforms.includes('ios'); + if (!hasCordovaPlatforms) { + webArchs = webArchs.filter(arch => arch !== 'web.cordova'); + } + } else { + // Dev & Test Mode + const isCordovaEnabled = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg)); + if (!isCordovaEnabled) { + const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; + if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { + const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; + webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); + } + return webArchs; } - return webArchs; } return webArchs; } @@ -1419,7 +1434,10 @@ on an OS X system."); webArchs = filteredArchs.length ? filteredArchs : undefined; } else { - webArchs = filterWebArchs(baseWebArchs, options['exclude-archs'], options.appDir, options); + webArchs = filterWebArchs(baseWebArchs, options['exclude-archs'], options.appDir, { + ...options, + platforms: projectContext.platformList.getPlatforms(), + }); } var buildDir = projectContext.getProjectLocalDirectory('build_tar'); From cf0c412ea61c995d0d336cb33a051ac62d3a8ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 12:06:37 +0200 Subject: [PATCH 070/264] clean --- tools/cli/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 178f9aaf64..6f152f371a 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -286,8 +286,8 @@ function filterWebArchs(webArchs, excludeArchsOption, appDir, options) { } } else { // Dev & Test Mode - const isCordovaEnabled = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg)); - if (!isCordovaEnabled) { + const isCordovaDev = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg)); + if (!isCordovaDev) { const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; From f628ed9745351355a0a72f0a9144ef9c0c61e58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 13:53:36 +0200 Subject: [PATCH 071/264] support build option to profile builds --- tools/cli/commands.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 40b2cfc34e..55c5fe08c4 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -3362,7 +3362,7 @@ const setupBenchmarkSuite = async (profilingPath) => { process.env.GIT_TERMINAL_PROMPT = 0; const repoUrl = "https://github.com/meteor/performance"; - const branch = "v3.2.0"; + const branch = "v3.3.0"; const gitCommand = [ `mkdir -p ${profilingPath}`, `git clone --no-checkout --depth 1 --filter=tree:0 --sparse --progress --branch ${branch} --single-branch ${repoUrl} ${profilingPath}`, @@ -3401,9 +3401,10 @@ async function doBenchmarkCommand(options) { const meteorSizeEnvs = [ !!options['size-only'] && 'METEOR_BUNDLE_SIZE_ONLY=true', - !!options['size'] && 'METEOR_BUNDLE_SIZE=true' + !!options['size'] && 'METEOR_BUNDLE_SIZE=true', + !!options['build'] && 'METEOR_BUNDLE_BUILD=true', ].filter(Boolean); - const meteorOptions = args.filter(arg => !['--size-only', '--size'].includes(arg)); + const meteorOptions = args.filter(arg => !['--size-only', '--size', '--build'].includes(arg)); const profilingCommand = [ `${meteorSizeEnvs.join(' ')} ${profilingPath}/scripts/monitor-bundler.sh ${projectContext.projectDir} ${new Date().getTime()} ${meteorOptions.join(' ')}`.trim(), @@ -3420,8 +3421,9 @@ main.registerCommand( maxArgs: Infinity, options: { ...runCommandOptions.options || {}, - 'size': { type: Boolean }, - 'size-only': { type: Boolean }, + 'size': { type: Boolean }, + 'size-only': { type: Boolean }, + 'build': { type: Boolean }, }, catalogRefresh: new catalog.Refresh.Never(), }, doBenchmarkCommand); From fcb317a31f6e6b9d68a69f6bc9253b60f60d1d70 Mon Sep 17 00:00:00 2001 From: Welkin Wong Date: Thu, 10 Apr 2025 19:57:00 +0800 Subject: [PATCH 072/264] fix(webapp): prevent truncated agent string in lookupUserAgent (#13686) * fix(webapp): prevent truncated agent string in lookupUserAgent In useragent-ng's lookup function implementation, there are security validations and truncation checks for useragent length. For certain useragent use cases where the length exceeds 150 characters, premature truncation before being passed to the lookup function results in incorrect agent identification. * test(webapp): add tests for agent identification including special user agent --- packages/webapp/webapp_server.js | 2 +- packages/webapp/webapp_tests.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 552c3c8587..124e32c9d6 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -132,7 +132,7 @@ var identifyBrowser = function(userAgentString) { patch: 0 }; } - var userAgent = lookupUserAgent(userAgentString.substring(0, 150)); + var userAgent = lookupUserAgent(userAgentString); return { name: camelCase(userAgent.family), major: +userAgent.major, diff --git a/packages/webapp/webapp_tests.js b/packages/webapp/webapp_tests.js index d4d40394f3..60e6ba8c34 100644 --- a/packages/webapp/webapp_tests.js +++ b/packages/webapp/webapp_tests.js @@ -153,6 +153,25 @@ Tinytest.addAsync("webapp - modern/legacy static files", test => { return Promise.all(promises); }); +const specialUserAgent = + "Mozilla/5.0 (Linux; Android 5.1.1; MI NOTE Pro Build/LMY47V; wv) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/48.0.2564.116 " + + "Mobile Safari/537.36 baidubrowser/7.7.13.0 (Baidu; P1 5.1.1)" + +Tinytest.addAsync("webapp - agent identification", async function (test) { + const modernBrowser = WebAppInternals.identifyBrowser(modernUserAgent); + test.equal(modernBrowser.name, "chrome"); + test.equal(modernBrowser.major, 68); + test.equal(modernBrowser.minor, 0); + test.equal(modernBrowser.patch, 3440); + + const specialBrowser = WebAppInternals.identifyBrowser(specialUserAgent); + test.equal(specialBrowser.name, "baiduBrowser"); + test.equal(specialBrowser.major, 7); + test.equal(specialBrowser.minor, 7); + test.equal(specialBrowser.patch, 13); +}) + Tinytest.addAsync( "webapp - additional static javascript", async function (test) { From 7ced1c795e9409d0b7cb44b532d9c0982a82a219 Mon Sep 17 00:00:00 2001 From: ericm546 Date: Thu, 10 Apr 2025 13:57:30 +0200 Subject: [PATCH 073/264] Fix timing issue with oplog include/exclude collections (#13687) * Fix timing issue with oplog include/exclude collections * Add a test --------- Co-authored-by: Jan Dvorak --- packages/mongo/oplog_tailing.ts | 129 +++++++++++++++++----------- packages/mongo/tests/oplog_tests.js | 62 +++++++++++++ 2 files changed, 141 insertions(+), 50 deletions(-) diff --git a/packages/mongo/oplog_tailing.ts b/packages/mongo/oplog_tailing.ts index d56bf8e8eb..2df4bf7aaa 100644 --- a/packages/mongo/oplog_tailing.ts +++ b/packages/mongo/oplog_tailing.ts @@ -37,13 +37,15 @@ export class OplogHandle { public _dbName: string; private _oplogLastEntryConnection: MongoConnection | null; private _oplogTailConnection: MongoConnection | null; - private _oplogOptions: { excludeCollections?: string[]; includeCollections?: string[] } | null; + private _oplogOptions: { + excludeCollections?: string[]; + includeCollections?: string[]; + }; private _stopped: boolean; private _tailHandle: any; private _readyPromiseResolver: (() => void) | null; private _readyPromise: Promise; public _crossbar: any; - private _baseOplogSelector: any; private _catchingUpResolvers: CatchingUpResolver[]; private _lastProcessedTS: any; private _onSkippedEntriesHook: any; @@ -61,29 +63,24 @@ export class OplogHandle { this._resolveTimeout = null; this._oplogLastEntryConnection = null; this._oplogTailConnection = null; - this._oplogOptions = null; this._stopped = false; this._tailHandle = null; this._readyPromiseResolver = null; - this._readyPromise = new Promise(r => this._readyPromiseResolver = r); + this._readyPromise = new Promise(r => this._readyPromiseResolver = r); this._crossbar = new DDPServer._Crossbar({ factPackage: "mongo-livedata", factName: "oplog-watchers" }); - this._baseOplogSelector = { - ns: new RegExp("^(?:" + [ - // @ts-ignore - Meteor._escapeRegExp(this._dbName + "."), - // @ts-ignore - Meteor._escapeRegExp("admin.$cmd"), - ].join("|") + ")"), - $or: [ - { op: { $in: ['i', 'u', 'd'] } }, - { op: 'c', 'o.drop': { $exists: true } }, - { op: 'c', 'o.dropDatabase': 1 }, - { op: 'c', 'o.applyOps': { $exists: true } }, - ] - }; + const includeCollections = + Meteor.settings?.packages?.mongo?.oplogIncludeCollections; + const excludeCollections = + Meteor.settings?.packages?.mongo?.oplogExcludeCollections; + if (includeCollections?.length && excludeCollections?.length) { + throw new Error( + "Can't use both mongo oplog settings oplogIncludeCollections and oplogExcludeCollections at the same time." + ); + } + this._oplogOptions = { includeCollections, excludeCollections }; this._catchingUpResolvers = []; this._lastProcessedTS = null; @@ -95,6 +92,67 @@ export class OplogHandle { this._startTrailingPromise = this._startTailing(); } + private _getOplogSelector(lastProcessedTS?: any): any { + const oplogCriteria: any = [ + { + $or: [ + { op: { $in: ["i", "u", "d"] } }, + { op: "c", "o.drop": { $exists: true } }, + { op: "c", "o.dropDatabase": 1 }, + { op: "c", "o.applyOps": { $exists: true } }, + ], + }, + ]; + + const nsRegex = new RegExp( + "^(?:" + + [ + // @ts-ignore + Meteor._escapeRegExp(this._dbName + "."), + // @ts-ignore + Meteor._escapeRegExp("admin.$cmd"), + ].join("|") + + ")" + ); + + if (this._oplogOptions.excludeCollections?.length) { + oplogCriteria.push({ + ns: { + $regex: nsRegex, + $nin: this._oplogOptions.excludeCollections.map( + (collName: string) => `${this._dbName}.${collName}` + ), + }, + }); + } else if (this._oplogOptions.includeCollections?.length) { + oplogCriteria.push({ + $or: [ + { ns: /^admin\.\$cmd/ }, + { + ns: { + $in: this._oplogOptions.includeCollections.map( + (collName: string) => `${this._dbName}.${collName}` + ), + }, + }, + ], + }); + } else { + oplogCriteria.push({ + ns: nsRegex, + }); + } + if(lastProcessedTS) { + oplogCriteria.push({ + ts: { $gt: lastProcessedTS }, + }); + } + + return { + $and: oplogCriteria, + }; + } + async stop(): Promise { if (this._stopped) return; this._stopped = true; @@ -156,10 +214,11 @@ export class OplogHandle { let lastEntry: OplogEntry | null = null; while (!this._stopped) { + const oplogSelector = this._getOplogSelector(); try { lastEntry = await this._oplogLastEntryConnection.findOneAsync( OPLOG_COLLECTION, - this._baseOplogSelector, + oplogSelector, { projection: { ts: 1 }, sort: { $natural: -1 } } ); break; @@ -238,41 +297,11 @@ export class OplogHandle { { sort: { $natural: -1 }, projection: { ts: 1 } } ); - let oplogSelector: any = { ...this._baseOplogSelector }; + const oplogSelector = this._getOplogSelector(lastOplogEntry?.ts); if (lastOplogEntry) { - oplogSelector.ts = { $gt: lastOplogEntry.ts }; this._lastProcessedTS = lastOplogEntry.ts; } - const includeCollections = Meteor.settings?.packages?.mongo?.oplogIncludeCollections; - const excludeCollections = Meteor.settings?.packages?.mongo?.oplogExcludeCollections; - - if (includeCollections?.length && excludeCollections?.length) { - throw new Error("Can't use both mongo oplog settings oplogIncludeCollections and oplogExcludeCollections at the same time."); - } - - if (excludeCollections?.length) { - oplogSelector.ns = { - $regex: oplogSelector.ns, - $nin: excludeCollections.map((collName: string) => `${this._dbName}.${collName}`) - }; - this._oplogOptions = { excludeCollections }; - } else if (includeCollections?.length) { - oplogSelector = { - $and: [ - { - $or: [ - { ns: /^admin\.\$cmd/ }, - { ns: { $in: includeCollections.map((collName: string) => `${this._dbName}.${collName}`) } } - ] - }, - { $or: oplogSelector.$or }, - { ts: oplogSelector.ts } - ] - }; - this._oplogOptions = { includeCollections }; - } - const cursorDescription = new CursorDescription( OPLOG_COLLECTION, oplogSelector, diff --git a/packages/mongo/tests/oplog_tests.js b/packages/mongo/tests/oplog_tests.js index 201a09e904..eb6f5eb3df 100644 --- a/packages/mongo/tests/oplog_tests.js +++ b/packages/mongo/tests/oplog_tests.js @@ -281,6 +281,68 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync( } ); +process.env.MONGO_OPLOG_URL && + Tinytest.addAsync( + "mongo-livedata - oplog - oplogSettings - oplog doesn't get stuck on waitUntilCaughtUp", + async (test) => { + try { + const includeCollectionName = "oplog-a-" + Random.id(); + const excludeCollectionName = "oplog-b-" + Random.id(); + const mongoPackageSettings = { + oplogIncludeCollections: [includeCollectionName], + }; + + previousMongoPackageSettings = { + ...(Meteor.settings?.packages?.mongo || {}), + }; + if (!Meteor.settings.packages) Meteor.settings.packages = {}; + Meteor.settings.packages.mongo = mongoPackageSettings; + + const myOplogHandle = new MongoInternals.OplogHandle( + process.env.MONGO_OPLOG_URL, + "meteor" + ); + await myOplogHandle._startTrailingPromise; + MongoInternals.defaultRemoteCollectionDriver().mongo._setOplogHandle( + myOplogHandle + ); + + const IncludeCollection = new Mongo.Collection(includeCollectionName); + const ExcludeCollection = new Mongo.Collection(excludeCollectionName); + await IncludeCollection.rawCollection().insertOne({ + include: "yes", + foo: "bar", + }); + + // Previously, when the last document inserted in the oplog was excluded from the oplog tailing, + // waitUntilCaughtUp would hang until an oplog-tracked document was inserted. + // This was preventing the observeChange callbacks from being called. + await ExcludeCollection.rawCollection().insertOne({ + include: "no", + foo: "bar", + }); + const shouldBeTracked = Promise.race([ + new Promise((resolve) => { + IncludeCollection.find({ include: "yes" }).observeChanges({ + added() { + resolve(true); + }, + }); + }), + new Promise((resolve) => setTimeout(() => resolve(false), 2000)), + ]); + + test.equal(await shouldBeTracked, true); + } finally { + // Reset: + Meteor.settings.packages.mongo = { ...previousMongoPackageSettings }; + MongoInternals.defaultRemoteCollectionDriver().mongo._setOplogHandle( + defaultOplogHandle + ); + } + } + ); + // TODO this is commented for now, but we need to find out the cause // PR: https://github.com/meteor/meteor/pull/12057 // Meteor.isServer && Tinytest.addAsync( From 89a0b645890a251a67b6d90e236eebcd0a770190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 14:01:53 +0200 Subject: [PATCH 074/264] document new build option --- tools/cli/help.txt | 1 + v3-docs/docs/cli/index.md | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/cli/help.txt b/tools/cli/help.txt index 4694b83b50..d0c16928be 100644 --- a/tools/cli/help.txt +++ b/tools/cli/help.txt @@ -1173,6 +1173,7 @@ Use METEOR_LOG_DIR= to set a custom log directory. Options: --size monitor both bundle runtime and size --size-only monitor only the bundle size + --build monitor build time The rest of options for this command are the same as those for the meteor run command. You can pass typical runtime options (such as --settings, --exclude-archs, etc.) diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index e42054b913..08a114cfd9 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -175,13 +175,15 @@ This command monitors the bundler process and tracks key performance metrics to ### Options -| Option | Description | -|--------|-------------| -| `--size` | Monitor both bundle runtime and size | -| `--size-only` | Monitor only the bundle size | +| Option | Description | +|---------------|--------------------------------------| +| `--size` | Monitor both bundle runtime and size | +| `--size-only` | Monitor only the bundle size | +| `--build` | Monitor build time | ::: info All other options from `meteor run` are also supported (e.g., `--settings`, `--exclude-archs`). +If you use the --build option, it also accepts meteor build flags (e.g. `--mobile-settings`, `--architecture`). ::: ### Environment Variables @@ -211,6 +213,9 @@ METEOR_IDLE_TIMEOUT=120 meteor profile --settings settings.json # Profile with custom entrypoints METEOR_CLIENT_ENTRYPOINT=client/main.js METEOR_SERVER_ENTRYPOINT=server/main.js meteor profile + +# Monitor build time +meteor profile --build ``` ::: details Customizing the Profiling Process From 86c106c66e29929c23c7b60fc1aa83546b4f90d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 14:02:49 +0200 Subject: [PATCH 075/264] document new build option --- v3-docs/docs/cli/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index 08a114cfd9..05ef6d3a7b 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -208,14 +208,14 @@ meteor profile # Monitor bundle size only meteor profile --size-only +# Monitor build time +meteor profile --build + # Profile with custom settings and timeout METEOR_IDLE_TIMEOUT=120 meteor profile --settings settings.json # Profile with custom entrypoints METEOR_CLIENT_ENTRYPOINT=client/main.js METEOR_SERVER_ENTRYPOINT=server/main.js meteor profile - -# Monitor build time -meteor profile --build ``` ::: details Customizing the Profiling Process From d170fa1857308394cf7c3c047e3d0a2bdba48a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 10 Apr 2025 14:04:23 +0200 Subject: [PATCH 076/264] document new build option --- v3-docs/docs/cli/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index 05ef6d3a7b..f5bdf21f66 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -219,7 +219,7 @@ METEOR_CLIENT_ENTRYPOINT=client/main.js METEOR_SERVER_ENTRYPOINT=server/main.js ``` ::: details Customizing the Profiling Process -You can pass any option that works with `meteor run` to customize the profiling process. This allows you to profile your application under specific conditions that match your deployment environment. +You can pass any option that works with `meteor run` to customize the profiling process. This allows you to profile your application under specific conditions that match your deployment environment. The same applies to the `--build` option, which matches `meteor build` options. ::: ## meteor create _app-name_ {#meteorcreate} From 5f7580c3c93fcad9b5656230c7b4b001c62ac2e1 Mon Sep 17 00:00:00 2001 From: 9Morello Date: Thu, 10 Apr 2025 18:07:08 -0300 Subject: [PATCH 077/264] fix type definition of ServerSink (#13689) --- packages/server-render/server-render.d.ts | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/server-render/server-render.d.ts b/packages/server-render/server-render.d.ts index 8f55c0fe07..a920e2514f 100644 --- a/packages/server-render/server-render.d.ts +++ b/packages/server-render/server-render.d.ts @@ -19,9 +19,34 @@ export interface ClientSink { getCookies(): { [key: string]: string }; } +/** + * Meteor parses the user agent string in an attempt to identify the browser. + * This is used, for example, to determine whether to serve the modern + * or the legacy bundle, in case your app uses both.. + */ +type IdentifiedBrowser = { + name: string; + major: number; + minor: number; + patch: number; +} + +/** + * A categorized request is an IncomingMessage with a pre-parsed URL, + * and additional properties added by Meteor. + */ +export type CategorizedRequest = Omit & { + browser: IdentifiedBrowser; + dynamicHead: string | undefined; + dynamicBody: string | undefined; + modern: boolean; + path: string; + url: URL; +} + export interface ServerSink extends ClientSink { // Server-only: - request: http.IncomingMessage; + request: CategorizedRequest; arch: string; head: string; body: string; From badcdeea1a30cadc969e9d0c086eea7b20f34345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 14:55:33 +0200 Subject: [PATCH 078/264] re-run checks From c7c7b7bdcfd602354fa90e90592729c1d2bbcda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 15:51:59 +0200 Subject: [PATCH 079/264] re-run checks From 132c8a676d1ebd35b9f3178adf859d9dcaade0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 16:08:01 +0200 Subject: [PATCH 080/264] bump bundle version --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index 745c3affbf..f131742641 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.4 +BUNDLE_VERSION=22.14.0.8 # OS Check. Put here because here is where we download the precompiled From 8a61d9faa29ad2fdafe7f63746dad385f485fa77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 16:24:47 +0200 Subject: [PATCH 081/264] re-run checks From 678064515a7762bbb30d04220087d4f653b8ec6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 18:07:49 +0200 Subject: [PATCH 082/264] ensure exclude-archs options override the modern arch package.json file --- tools/cli/commands.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 436e0ce778..2acc99eac6 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -288,9 +288,11 @@ function filterWebArchs(webArchs, excludeArchsOption, appDir, options) { // Dev & Test Mode const isCordovaDev = (options.args || []).some(arg => ['ios', 'ios-device', 'android', 'android-device'].includes(arg)); if (!isCordovaDev) { - const automaticallyIgnoredLegacyArchs = (appDir && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; - if (excludeArchsOption || automaticallyIgnoredLegacyArchs.length) { - const excludeArchs = [...(excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []), ...automaticallyIgnoredLegacyArchs]; + const excludeArchsOptions = excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []; + const hasExcludeArchsOptions = (excludeArchsOptions?.length || 0) > 0; + const automaticallyIgnoredLegacyArchs = (appDir && !hasExcludeArchsOptions && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; + if (hasExcludeArchsOptions || automaticallyIgnoredLegacyArchs.length) { + const excludeArchs = [...excludeArchsOptions, ...automaticallyIgnoredLegacyArchs]; webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); } return webArchs; From 083a1ad32cdf607ef84bd0dcb050f298275f609f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 18:15:16 +0200 Subject: [PATCH 083/264] add a warning on use modernWebArchsOnly and exclude-archs --- tools/cli/commands.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 2acc99eac6..c44a026cd7 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -290,7 +290,11 @@ function filterWebArchs(webArchs, excludeArchsOption, appDir, options) { if (!isCordovaDev) { const excludeArchsOptions = excludeArchsOption ? excludeArchsOption.trim().split(/\s*,\s*/) : []; const hasExcludeArchsOptions = (excludeArchsOptions?.length || 0) > 0; - const automaticallyIgnoredLegacyArchs = (appDir && !hasExcludeArchsOptions && isModernArchsOnlyEnabled(appDir)) ? ['web.browser.legacy', 'web.cordova'] : []; + const hasModernArchsOnlyEnabled = appDir && isModernArchsOnlyEnabled(appDir); + if (hasExcludeArchsOptions && hasModernArchsOnlyEnabled) { + console.warn('modernWebArchsOnly and --exclude-archs are both active. If both are set, --exclude-archs takes priority.'); + } + const automaticallyIgnoredLegacyArchs = (!hasExcludeArchsOptions && hasModernArchsOnlyEnabled) ? ['web.browser.legacy', 'web.cordova'] : []; if (hasExcludeArchsOptions || automaticallyIgnoredLegacyArchs.length) { const excludeArchs = [...excludeArchsOptions, ...automaticallyIgnoredLegacyArchs]; webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); From c38491519e909a421abb83878104386ed0da9b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 18:30:41 +0200 Subject: [PATCH 084/264] add docs for SWC adoption --- v3-docs/docs/.vitepress/config.mts | 14 +++ v3-docs/docs/about/modern-build-stack.md | 13 ++ .../modern-build-stack/modern-bundler.md | 19 +++ .../modern-transpiler-swc.md | 113 ++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 v3-docs/docs/about/modern-build-stack.md create mode 100644 v3-docs/docs/about/modern-build-stack/modern-bundler.md create mode 100644 v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 83add47b6a..5e37b0ab0a 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -168,6 +168,20 @@ export default defineConfig({ text: "Cordova", link: "/about/cordova", }, + { + text: "Modern Build Stack", + link: "/about/modern-build-stack.md", + items: [ + { + text: "Modern Transpiler: SWC", + link: "/about/modern-build-stack/modern-transpiler-swc.md", + }, + { + text: "Modern Bundler", + link: "/about/modern-build-stack/modern-bundler.md", + }, + ] + }, ], collapsed: true, }, diff --git a/v3-docs/docs/about/modern-build-stack.md b/v3-docs/docs/about/modern-build-stack.md new file mode 100644 index 0000000000..6facbd687d --- /dev/null +++ b/v3-docs/docs/about/modern-build-stack.md @@ -0,0 +1,13 @@ +# Modern Build Stack + +The Meteor bundler is made up of several key components that enhance your experience both during development and when deploying to production. These include: + +- **Transpiler**: Responsible for converting each file into a syntax compatible across different browsers and runtime environments. +- **Bundler**: Handles discovering your app’s files and dependencies, including Meteor packages and core modules, then links them into production-ready bundles. It also applies optimizations to produce lighter builds and faster processes. +- **Dev Server**: During development, it watches for file changes, and supports fast feedback via HMR, bundle visualizers, debug tools, and more. At runtime, it provides a full-featured server environment with support for SSR and modern APIs powered by Express. + +To improve the development and deployment experience for all Meteor projects, we’re revamping each of these components with a focus on better performance, smarter tooling, and leaner bundle sizes: + +- **Modern Transpiler**: Meteor is adopting **SWC** as a faster alternative to Babel. +- **Modern Bundler**: A new bundler will handle only your app’s code, supporting tree-shaking, popular plugins, and better features for both development and production. Meanwhile, Meteor’s core bundler will continue handling Meteor-specific tasks, such as compiling Atmosphere packages, with optimized workflows. +- **Dev Server Enhancements**: The dev server remains a core part of Meteor, now with ongoing improvements in performance and developer features. diff --git a/v3-docs/docs/about/modern-build-stack/modern-bundler.md b/v3-docs/docs/about/modern-build-stack/modern-bundler.md new file mode 100644 index 0000000000..1c72489185 --- /dev/null +++ b/v3-docs/docs/about/modern-build-stack/modern-bundler.md @@ -0,0 +1,19 @@ +# Modern bundler + +Meteor handles watching and linking all project files into the final bundle. While we'd like to offload more of this to modern bundlers, we're still focused on keeping what's left in the Meteor context as fast as possible. + +Integration with a modern bundler is in progress for Meteor 3.4. Meanwhile, we've optimized existing processes for better performance. + +## Modern builds + +Starting with Meteor 3.3, new apps skip `web.browser.legacy` and `web.cordova` by default in development mode (unless developing for native). + +For existing apps, enable this by adding to `package.json`: + +```json +"meteor": { + "modernWebArchsOnly": true +} +``` + +This works like using `--exclude-archs web.browser.legacy,web.cordova` with `meteor run`. diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md new file mode 100644 index 0000000000..006a5c15b3 --- /dev/null +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -0,0 +1,113 @@ +# Modern transpiler: SWC + +Meteor has long used Babel, a mature and still widely adopted transpiler. However, it lags behind newer tools like SWC in terms of speed. SWC and others are not only faster but are growing in use and features, often surpassing Babel. + +Since transpilation is one of the slowest steps in development, Meteor now gives you the option to use SWC for your apps. + +## Enable SWC + +Add this to your app's `package.json`: + +```json +"meteor": { + "modernTranspiler": true +} +``` + +When starting your app for web or native, SWC will handle all files: your app, npm packages, and Atmosphere packages. This also applies to production builds. + +## Verbose transpilation process + +To analyze and improve transpilation, you can enable verbose output. Add this to `package.json`: + +```json +"meteor": { + "modernTranspiler": { + "verbose": true + } +} +``` + +This shows each file being processed, its context, cache usage, and whether it fell back to Babel due to incompatibilities. + +## Adapt your code to benefit from SWC + +If all your code uses SWC, you're good and can turn off verbosity. But if you see logs like: + +``` shell +[Transpiler] Used Babel for () Fallback + +``` + +There are a few things you can do. + +First, check the fallback details. It may show why SWC couldn’t handle the file. A common reason is nested imports, `import` statements inside a function. Moving them to the top level may fix it. These nested imports work via a Babel plugin specific to Meteor, which SWC doesn’t support. + +Other reasons might involve features tied to Babel plugins. If so, you’ll need to find a similar plugin for SWC. See the [SWC plugin list](https://plugins.swc.rs/versions/range/271). + +Second, you might choose to ignore the fallback if those files are fine with Babel. Even with SWC enabled, Meteor will continue using Babel for those files on future rebuilds. + +Third, you can exclude files or contexts from SWC. For example, if you're using `babel-plugin-react-compiler`, you can exclude your app code adding this to `package.json`: + +```json +"meteor": { + "modernTranspiler": { + "excludeApp": true + } +} +``` + +Or exclude only specific files like `.jsx`: + +```json +"meteor": { + "modernTranspiler": { + "excludeApp": ["\\.jsx"] + } +} +``` + +You can also use `excludePackages`, `excludeNodeModules`, and `excludeLegacy` for finer control. See the `modernTranspiler` config docs for more. + +When no alternatives exist, these settings let you still get most of SWC’s speed benefits by limiting fallback use. + +We expect most apps will benefit just by enabling `modernTranspiler: true`. Most Meteor packages should work right away, except ones using nested imports. Node modules will mostly work too, since they follow common standards. Most app code should also work unless it depends on Babel-specific behavior. + +> Remember to turn off verbosity when you're done with optimizations. + +## User custom .swcrc + +You can use .swcrc config in the root of your project to describe specific [SWC plugins](https://github.com/swc-project/plugins) there, that will be applied to compile the entire files of your project. + +## Config API + +- `modernTranspiler: [true|false]` + Enables or disables the use of the modern transpiler (SWC). If disabled, Babel will be used directly instead. + +- `modernTranspiler.excludeApp: [true|false] or [string[]]` + If true, the app’s own code (outside of Meteor core and packages) will continue using Babel. + Otherwise, a list of file paths or regex-like patterns within the app to exclude from SWC transpilation. + +- `modernTranspiler.excludeNodeModules: [true|false] or [string[]]` + If true, the app’s node_modules will continue using Babel. + Otherwise, a list of NPM packages names, file paths or regex-like patterns within the node_modules folder to exclude from SWC transpilation. + +- `modernTranspiler.excludePackages: [true|false] or [string[]]` + If true, the Meteor’s packages will continue using Babel. + Otherwise, a list of package names, file paths or regex-like patterns within the package to exclude from SWC transpilation. + +- `modernTranspiler.excludeLegacy: [true|false]` + If true, the Meteor’s bundle for legacy browsers will continue using Babel. + +- `modernTranspiler.verbose: [true|false]` + If true, the transpilation process for files is shown when running the app. This helps understand which transpiler is used for each file, what fallbacks are applied, and gives a chance to either exclude files to always use Babel or migrate fully to SWC. + +## Troubleshotting + +If you run into issues, try `meteor reset` or delete the `.meteor` folder. + +For help or to report issues, post on [GitHub](https://github.com/meteor/meteor/issues) or the [Meteor forums](https://forums.meteor.com). We’re focused on making Meteor faster and your feedback helps. + +You can compare performance before and after enabling `modernTranspiler` by running [`meteor profile`](../../cli/index.md#meteorprofile). Share your results to show progress to others. + +> **[Check out modern bundler options](./modern-bundler.md) to improve performance and access newer build features.** From a5fa1f8208f41cff5dfaca592ec28cfbe3dd28ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 18:41:59 +0200 Subject: [PATCH 085/264] fix docs --- v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 006a5c15b3..64aff0fe24 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -104,7 +104,7 @@ You can use .swcrc config in the root of your project to describe specific [SWC ## Troubleshotting -If you run into issues, try `meteor reset` or delete the `.meteor` folder. +If you run into issues, try `meteor reset` or delete the `.meteor/local` folder in the project root. For help or to report issues, post on [GitHub](https://github.com/meteor/meteor/issues) or the [Meteor forums](https://forums.meteor.com). We’re focused on making Meteor faster and your feedback helps. From cc30ec881e2d38c72f024ac156a2e611bd7ca1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 19:02:40 +0200 Subject: [PATCH 086/264] fix docs --- .../docs/about/modern-build-stack/modern-transpiler-swc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 64aff0fe24..66f7964a9d 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -67,7 +67,7 @@ Or exclude only specific files like `.jsx`: } ``` -You can also use `excludePackages`, `excludeNodeModules`, and `excludeLegacy` for finer control. See the `modernTranspiler` config docs for more. +You can also use `excludePackages`, `excludeNodeModules`, and `excludeLegacy` for finer control. See the [`modernTranspiler` config docs](#config-api) for more. When no alternatives exist, these settings let you still get most of SWC’s speed benefits by limiting fallback use. @@ -75,7 +75,7 @@ We expect most apps will benefit just by enabling `modernTranspiler: true`. Most > Remember to turn off verbosity when you're done with optimizations. -## User custom .swcrc +## Custom .swcrc You can use .swcrc config in the root of your project to describe specific [SWC plugins](https://github.com/swc-project/plugins) there, that will be applied to compile the entire files of your project. From c26bc46b1591647caf508b9c3db009919304a2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 19:05:24 +0200 Subject: [PATCH 087/264] fix docs --- v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 66f7964a9d..3a8930089a 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -1,6 +1,6 @@ # Modern transpiler: SWC -Meteor has long used Babel, a mature and still widely adopted transpiler. However, it lags behind newer tools like SWC in terms of speed. SWC and others are not only faster but are growing in use and features, often surpassing Babel. +Meteor has long used Babel, a mature and still widely adopted transpiler. However, it lags behind newer tools like SWC in terms of speed. SWC and others are not only faster but are growing in use and features, reaching parity with Babel. Since transpilation is one of the slowest steps in development, Meteor now gives you the option to use SWC for your apps. From fdc5a5b64b8f9f2c37d83bc36505468bad0b74a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 14 Apr 2025 21:04:49 +0200 Subject: [PATCH 088/264] fix docs --- v3-docs/docs/about/modern-build-stack.md | 13 +++++++++++++ .../docs/about/modern-build-stack/modern-bundler.md | 2 +- .../modern-build-stack/modern-transpiler-swc.md | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/v3-docs/docs/about/modern-build-stack.md b/v3-docs/docs/about/modern-build-stack.md index 6facbd687d..433782a81d 100644 --- a/v3-docs/docs/about/modern-build-stack.md +++ b/v3-docs/docs/about/modern-build-stack.md @@ -11,3 +11,16 @@ To improve the development and deployment experience for all Meteor projects, we - **Modern Transpiler**: Meteor is adopting **SWC** as a faster alternative to Babel. - **Modern Bundler**: A new bundler will handle only your app’s code, supporting tree-shaking, popular plugins, and better features for both development and production. Meanwhile, Meteor’s core bundler will continue handling Meteor-specific tasks, such as compiling Atmosphere packages, with optimized workflows. - **Dev Server Enhancements**: The dev server remains a core part of Meteor, now with ongoing improvements in performance and developer features. + +## Quick start + +To start using the new build stack, add the following to your `package.json`: + +```json +"meteor": { + "modernWebArchsOnly": true, + "modernTranspiler": true +} +``` + +Learn more about these settings in the [Modern Transpiler](modern-build-stack/modern-transpiler.md) and [Modern Bundler](modern-build-stack/modern-bundler.md) guides. diff --git a/v3-docs/docs/about/modern-build-stack/modern-bundler.md b/v3-docs/docs/about/modern-build-stack/modern-bundler.md index 1c72489185..d5ebb5b55e 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-bundler.md +++ b/v3-docs/docs/about/modern-build-stack/modern-bundler.md @@ -1,4 +1,4 @@ -# Modern bundler +# Modern Bundler Meteor handles watching and linking all project files into the final bundle. While we'd like to offload more of this to modern bundlers, we're still focused on keeping what's left in the Meteor context as fast as possible. diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 3a8930089a..278da99499 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -1,4 +1,4 @@ -# Modern transpiler: SWC +# Modern Transpiler: SWC Meteor has long used Babel, a mature and still widely adopted transpiler. However, it lags behind newer tools like SWC in terms of speed. SWC and others are not only faster but are growing in use and features, reaching parity with Babel. @@ -47,7 +47,7 @@ Other reasons might involve features tied to Babel plugins. If so, you’ll need Second, you might choose to ignore the fallback if those files are fine with Babel. Even with SWC enabled, Meteor will continue using Babel for those files on future rebuilds. -Third, you can exclude files or contexts from SWC. For example, if you're using `babel-plugin-react-compiler`, you can exclude your app code adding this to `package.json`: +Third, you can exclude files or contexts from SWC. For example, if you're using `babel-plugin-react-compiler`, which SWC doesn't support yet, you can exclude your app code adding this to `package.json`: ```json "meteor": { From 39b8f65c3b6acb72c9de4c23e564e77ccc5bd89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 08:25:21 +0200 Subject: [PATCH 089/264] fix docs --- v3-docs/docs/about/modern-build-stack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/about/modern-build-stack.md b/v3-docs/docs/about/modern-build-stack.md index 433782a81d..f76892371d 100644 --- a/v3-docs/docs/about/modern-build-stack.md +++ b/v3-docs/docs/about/modern-build-stack.md @@ -23,4 +23,4 @@ To start using the new build stack, add the following to your `package.json`: } ``` -Learn more about these settings in the [Modern Transpiler](modern-build-stack/modern-transpiler.md) and [Modern Bundler](modern-build-stack/modern-bundler.md) guides. +Learn more about these settings in the [Modern Transpiler](modern-build-stack/modern-transpiler-swc.md) and [Modern Bundler](modern-build-stack/modern-bundler.md) guides. From 041f194a84d6501ce8147c9eaa562698cc3ad3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 10:00:23 +0200 Subject: [PATCH 090/264] re-run checks From 90eb76d64d952f27ce1221e681b1d2351b60527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 10:01:49 +0200 Subject: [PATCH 091/264] re-run checks From 3de4c410072ee1fbd22d588e79da80e8e96abea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 11:57:06 +0200 Subject: [PATCH 092/264] re-run checks From 9a229cd665e9e621a75eefedb428d8480b74c00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 14:35:43 +0200 Subject: [PATCH 093/264] ensure new meteor apps have modernTranspiler enabled --- tools/static-assets/skel-apollo/package.json | 3 ++- tools/static-assets/skel-blaze/package.json | 3 ++- tools/static-assets/skel-chakra-ui/package.json | 3 ++- tools/static-assets/skel-minimal/package.json | 3 ++- tools/static-assets/skel-react/package.json | 3 ++- tools/static-assets/skel-solid/package.json | 3 ++- tools/static-assets/skel-svelte/package.json | 3 ++- tools/static-assets/skel-tailwind/package.json | 3 ++- tools/static-assets/skel-typescript/package.json | 3 ++- v3-docs/docs/about/modern-build-stack.md | 2 +- 10 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tools/static-assets/skel-apollo/package.json b/tools/static-assets/skel-apollo/package.json index 3bb70f1781..5ecaafb90b 100644 --- a/tools/static-assets/skel-apollo/package.json +++ b/tools/static-assets/skel-apollo/package.json @@ -22,6 +22,7 @@ "server": "server/main.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-blaze/package.json b/tools/static-assets/skel-blaze/package.json index 21fba0da58..241f4001e8 100644 --- a/tools/static-assets/skel-blaze/package.json +++ b/tools/static-assets/skel-blaze/package.json @@ -18,6 +18,7 @@ "server": "server/main.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-chakra-ui/package.json b/tools/static-assets/skel-chakra-ui/package.json index 92bbdf24ad..561d051ac7 100644 --- a/tools/static-assets/skel-chakra-ui/package.json +++ b/tools/static-assets/skel-chakra-ui/package.json @@ -25,6 +25,7 @@ "server": "server/main.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-minimal/package.json b/tools/static-assets/skel-minimal/package.json index 73469db096..f0038698c7 100644 --- a/tools/static-assets/skel-minimal/package.json +++ b/tools/static-assets/skel-minimal/package.json @@ -17,6 +17,7 @@ "server": "server/main.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index 08d7e054e3..59d8f0c986 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -19,6 +19,7 @@ "server": "server/main.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-solid/package.json b/tools/static-assets/skel-solid/package.json index 6e8187cbeb..b24dd0ead4 100644 --- a/tools/static-assets/skel-solid/package.json +++ b/tools/static-assets/skel-solid/package.json @@ -19,7 +19,8 @@ "server": "server/entry-meteor.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true }, "devDependencies": { "babel-preset-solid": "^1.8.15", diff --git a/tools/static-assets/skel-svelte/package.json b/tools/static-assets/skel-svelte/package.json index 7cf92dfce7..07c594351b 100644 --- a/tools/static-assets/skel-svelte/package.json +++ b/tools/static-assets/skel-svelte/package.json @@ -28,6 +28,7 @@ } }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-tailwind/package.json b/tools/static-assets/skel-tailwind/package.json index 94934d4898..3ad26935c0 100644 --- a/tools/static-assets/skel-tailwind/package.json +++ b/tools/static-assets/skel-tailwind/package.json @@ -23,6 +23,7 @@ "server": "server/main.js" }, "testModule": "tests/main.js", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json index 03ff9ce6c7..a5a9a1e979 100644 --- a/tools/static-assets/skel-typescript/package.json +++ b/tools/static-assets/skel-typescript/package.json @@ -26,6 +26,7 @@ "server": "server/main.ts" }, "testModule": "tests/main.ts", - "modernWebArchsOnly": true + "modernWebArchsOnly": true, + "modernTranspiler": true } } diff --git a/v3-docs/docs/about/modern-build-stack.md b/v3-docs/docs/about/modern-build-stack.md index f76892371d..e16e814e24 100644 --- a/v3-docs/docs/about/modern-build-stack.md +++ b/v3-docs/docs/about/modern-build-stack.md @@ -14,7 +14,7 @@ To improve the development and deployment experience for all Meteor projects, we ## Quick start -To start using the new build stack, add the following to your `package.json`: +Start using the new build stack by creating a Meteor app, or add this to your `package.json` in an existing one: ```json "meteor": { From 6d4251edb612059061579987d82fe577315584bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 15:20:48 +0200 Subject: [PATCH 094/264] bump meteor bundle --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index 21b45c3580..b743dccaa9 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.9 +BUNDLE_VERSION=22.14.0.10 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. From e26204770be7f5fa12e3b8398fe4f40c162feed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 15:30:53 +0200 Subject: [PATCH 095/264] re-run checks From 2f7bb724a947b9e0d39d2a5241cb3a78d3ef5072 Mon Sep 17 00:00:00 2001 From: italo jose Date: Tue, 15 Apr 2025 12:13:32 -0300 Subject: [PATCH 096/264] Meteor version to 3.3-beta.0 :comet: --- packages/accounts-base/package.js | 2 +- packages/accounts-password/package.js | 2 +- packages/autoupdate/package.js | 2 +- packages/babel-compiler/package.js | 2 +- packages/boilerplate-generator/package.js | 2 +- packages/ddp-client/package.js | 2 +- packages/ecmascript/package.js | 2 +- packages/ejson/package.js | 2 +- packages/meteor-tool/package.js | 2 +- packages/meteor/package.js | 2 +- packages/mongo/package.js | 2 +- packages/server-render/package.js | 2 +- packages/webapp/package.js | 2 +- .../admin/meteor-release-experimental.json | 2 +- .../generators/changelog/versions/3.3.0.md | 73 +++++++++++++++++++ 15 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 v3-docs/docs/generators/changelog/versions/3.3.0.md diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 6af3172a4e..ecc46de7ad 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "A user account system", - version: "3.1.0", + version: "3.1.1-beta330.0", }); Package.onUse((api) => { diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index 31786541ba..2950560337 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -5,7 +5,7 @@ Package.describe({ // 2.2.x in the future. The version was also bumped to 2.0.0 temporarily // during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2 // through -beta.5 and -rc.0 have already been published. - version: "3.1.0", + version: "3.2.0-beta330.0", }); Npm.depends({ diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index 4769c9e31b..459cba05a9 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Update the client when new client code is available', - version: '2.0.0', + version: '2.0.1-beta330.0', }); Package.onUse(function(api) { diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 0e9c3c420d..4fdefa740f 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "babel-compiler", summary: "Parser/transpiler for ECMAScript 2015+ syntax", - version: '7.11.3', + version: '7.12.0-beta330.0', }); Npm.depends({ diff --git a/packages/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index 032ed11c8d..df24da34cd 100644 --- a/packages/boilerplate-generator/package.js +++ b/packages/boilerplate-generator/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Generates the boilerplate html from program's manifest", - version: '2.0.0', + version: '2.0.1-beta330.0', }); Npm.depends({ diff --git a/packages/ddp-client/package.js b/packages/ddp-client/package.js index 1b0e48c929..b5e9a2a938 100644 --- a/packages/ddp-client/package.js +++ b/packages/ddp-client/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Meteor's latency-compensated distributed data client", - version: "3.1.0", + version: "3.1.1-beta330.0", documentation: null, }); diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index e74159738d..087abbf932 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.16.10', + version: '0.16.11-beta330.0', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/ejson/package.js b/packages/ejson/package.js index 3ff1a14262..e492be2e5b 100644 --- a/packages/ejson/package.js +++ b/packages/ejson/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Extended and Extensible JSON library', - version: '1.1.4', + version: '1.1.5-beta330.0', }); Package.onUse(function onUse(api) { diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index fac4bece2d..bfbbde48e6 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "The Meteor command-line tool", - version: "3.2.0", + version: "3.3.0-beta.0", }); Package.includeTool(); diff --git a/packages/meteor/package.js b/packages/meteor/package.js index a9ee2425dc..0d0b0b31ee 100644 --- a/packages/meteor/package.js +++ b/packages/meteor/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Core Meteor environment", - version: '2.1.0', + version: '2.1.1-beta330.0', }); Package.registerBuildPlugin({ diff --git a/packages/mongo/package.js b/packages/mongo/package.js index 0013a658e3..e3b80827ee 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -9,7 +9,7 @@ Package.describe({ summary: "Adaptor for using MongoDB and Minimongo over DDP", - version: "2.1.1", + version: "2.1.2-beta330.0", }); Npm.depends({ diff --git a/packages/server-render/package.js b/packages/server-render/package.js index fabcf15208..21b7727831 100644 --- a/packages/server-render/package.js +++ b/packages/server-render/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "server-render", - version: '0.4.2', + version: '0.4.3-beta330.0', summary: "Generic support for server-side rendering in Meteor apps", documentation: "README.md" }); diff --git a/packages/webapp/package.js b/packages/webapp/package.js index c57f709f3c..e54945bae0 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Serves a Meteor app over HTTP", - version: "2.0.6", + version: "2.0.7-beta330.0", }); Npm.depends({ diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index da73ff6743..c0e35f1d70 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "3.2-beta.2", + "version": "3.3-beta.0", "recommended": false, "official": false, "description": "Meteor experimental release" diff --git a/v3-docs/docs/generators/changelog/versions/3.3.0.md b/v3-docs/docs/generators/changelog/versions/3.3.0.md new file mode 100644 index 0000000000..0e63e671fd --- /dev/null +++ b/v3-docs/docs/generators/changelog/versions/3.3.0.md @@ -0,0 +1,73 @@ +## v3.3.0, 2025-04-15 + +### Highlights + +- Support SWC transpiler for faster dev and builds [PR#13657](https://github.com/meteor/meteor/pull/13657) +- Default to modern architecture in new apps [PR#13665](https://github.com/meteor/meteor/pull/13665) +- Support CPU profiling in Meteor 3 bundler [PR#13650](https://github.com/meteor/meteor/pull/13650) +- Improve `meteor profile`@show rebuild steps, support `--build` [PR#13694](https://github.com/meteor/meteor/pull/13694) +- Improve `useFind` and `useSubscribe` React hooks +- Add `replaceEmailAsync` helper to Accounts [PR#13677](https://github.com/meteor/meteor/pull/13677) +- Fix user agent detection and oplog collection filtering +- Refine type definitions for Meteor methods and SSR's ServerSink +- Update Express to 5.1.0 + +All merged PRs@[GitHub PRs 3.3](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3) + +React packages PRs@[PR#419](https://github.com/meteor/react-packages/pull/419), [PR#429](https://github.com/meteor/react-packages/pull/429) and [PR#430](https://github.com/meteor/react-packages/pull/430) + + +#### Breaking Changes + +N/A + +#### Internal API changes + +N/A + +#### Migration Steps + +Please run the following command to update your project: + +```bash + +meteor update --release 3.3-beta.0 + +``` + +#### Bumped Meteor Packages + +- accounts-base@3.1.1 +- accounts-password@3.2.0 +- autoupdate@2.0.1 +- babel-compiler@7.12.0 +- boilerplate-generator@2.0.1 +- ddp-client@3.1.1 +- ecmascript@0.16.11 +- ejson@1.1.5 +- meteor@2.1.1 +- mongo@2.1.2 +- server-render@0.4.3 +- webapp@2.0.7 +- meteor-tool@3.3.0 + +#### Bumped NPM Packages + +- meteor-node-stubs@1.2.17 + +#### Special thanks to + +✨✨✨ + +- [@nachocodoner](https://github.com/nachocodoner) +- [@italojs](https://github.com/italojs) +- [@Grubba27](https://github.com/Grubba27) +- [@zodern](https://github.com/zodern) +- [@9Morello](https://github.com/9Morello) +- [@welkinwong](https://github.com/welkinwong) +- [@Poyoman39](https://github.com/Poyoman39) +- [@PedroMarianoAlmeida](https://github.com/PedroMarianoAlmeida) +- [@harryadel](https://github.com/harryadel) +- [@ericm546](https://github.com/ericm546) + +✨✨✨ From a244031b5b251bfe8087551f83e1716b87acb85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 15 Apr 2025 19:03:57 +0200 Subject: [PATCH 097/264] exclude legacy arch compilation in dev mode --- tools/cli/commands.js | 3 +++ tools/isobuild/compiler.js | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index c44a026cd7..ad46ad573d 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -545,6 +545,8 @@ async function doRunCommand(options) { } webArchs = filterWebArchs(webArchs, options['exclude-archs'], options.appDir, options); + global.webArchs = webArchs; + const buildMode = options.production ? 'production' : 'development'; let cordovaRunner; @@ -2512,6 +2514,7 @@ var runTestAppForPackages = async function (projectContext, options) { webArchs.push("web.cordova"); } buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs'], projectContext.appDirectory, options); + global.webArchs = buildOptions.webArchs; if (options.deploy) { // Run the constraint solver and build local packages. diff --git a/tools/isobuild/compiler.js b/tools/isobuild/compiler.js index 66cb49e2b0..6fa2793186 100644 --- a/tools/isobuild/compiler.js +++ b/tools/isobuild/compiler.js @@ -150,6 +150,7 @@ compiler.compile = Profile(function (packageSource, options) { // Isopack#initFromPath). var isobuildFeatures = []; packageSource.architectures.forEach((sourceArch) => { + if (global.webArchs != null && ![...global.webArchs, 'os'].includes(sourceArch.arch)) return; sourceArch.uses.forEach((use) => { if (!use.weak && isIsobuildFeaturePackage(use.package) && isobuildFeatures.indexOf(use.package) === -1) { @@ -181,6 +182,7 @@ compiler.compile = Profile(function (packageSource, options) { if (architecture.arch === 'web.cordova' && ! includeCordovaUnibuild) { continue; } + if (global.webArchs != null && ![...global.webArchs, 'os'].includes(architecture.arch)) continue; // TODO -> Maybe this withCache will bring some problems in other commands. await files.withCache(async () => { @@ -226,6 +228,7 @@ compiler.lint = Profile(function (packageSource, options) { && architecture.arch === 'web.cordova') { continue; } + if (global.webArchs != null && ![...global.webArchs, 'os'].includes(architecture.arch)) continue; const unibuildWarnings = await lintUnibuild({ isopack: options.isopack, @@ -246,6 +249,8 @@ compiler.getMinifiers = async function (packageSource, options) { var minifiers = []; for (const architecture of packageSource.architectures) { + if (global.webArchs != null && ![...global.webArchs, 'os'].includes(architecture.arch)) continue; + var activePluginPackages = await getActivePluginPackages(options.isopack, { isopackCache: options.isopackCache, uses: architecture.uses From a38bcaee9a377e5ad54b6642edd924029a544408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 16 Apr 2025 10:41:36 +0200 Subject: [PATCH 098/264] support @parcel/watcher as the watcher alternative cross-os perserving polling fallback --- scripts/dev-bundle-tool-package.js | 3 +- tools/fs/safe-watcher.ts | 770 ++++++++++++----------------- 2 files changed, 330 insertions(+), 443 deletions(-) diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 2b92b9e5f6..8d9292efb0 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -57,8 +57,7 @@ var packageJson = { escope: "3.6.0", split2: "3.2.2", multipipe: "2.0.1", - pathwatcher: "8.1.2", - "vscode-nsfw": "2.1.8", + "@parcel/watcher": "2.5.1", // The @wry/context package version must be compatible with the // version constraint imposed by optimism/package.json. optimism: "0.16.1", diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index cea3d74d7b..5f521fb024 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -1,476 +1,364 @@ -import { FSWatcher, Stats, BigIntStats } from "fs"; +import { Stats } from 'fs'; +import { dirname } from "path"; +import ParcelWatcher from "@parcel/watcher"; + import { Profile } from "../tool-env/profile"; -import { - statOrNull, - convertToOSPath, - watchFile, - unwatchFile, - toPosixPath, - pathRelative -} from "./files"; -import { - join as nativeJoin -} from 'path'; -import nsfw from 'vscode-nsfw'; +import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; -const pathwatcher = require('pathwatcher'); +export type SafeWatcher = { close: () => void; }; + +type ChangeCallback = (event: string) => void; + +interface Entry extends SafeWatcher { + callbacks: Set; + _fire(event: string): void; +} + +// Registry mapping normalized absolute paths to their watcher entry. +const entries: Record = Object.create(null); + +// Watch roots are directories for which we have an active ParcelWatcher subscription. +const watchRoots = new Set(); +// For each watch root, store its active subscription. +const dirSubscriptions = new Map(); +// A set of roots that are known to be unwatchable. +const ignoredWatchRoots = new Set(); + +// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to +// force the use of files.watchFile instead of ParcelWatcher. +let watcherEnabled = !JSON.parse(process.env.METEOR_WATCH_FORCE_POLLING || "false"); + +/** + * Polling fallback globals. + * The legacy Meteor strategy used polling for cases where native watchers failed. + * We keep track of files that changed, so that we can poll them faster. + */ +var DEFAULT_POLLING_INTERVAL = + +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000); + +var NO_WATCHER_POLLING_INTERVAL = + +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500); -// Default to prioritizing changed files, but disable that behavior (and -// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED -// is explicitly set to a string that parses to a falsy value. var PRIORITIZE_CHANGED = true; if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED && ! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) { PRIORITIZE_CHANGED = false; } -var DEFAULT_POLLING_INTERVAL = - +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000); - -var NO_WATCHER_POLLING_INTERVAL = - +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500); - -// This may seems like a long time to wait before actually closing the -// file watchers, but it's to our advantage if they survive restarts. -const WATCHER_CLEANUP_DELAY_MS = 30000; - -// Since linux doesn't have recursive file watching, nsfw has to walk the -// watched folder and create a separate watcher for each subfolder. Until it has a -// way for us to filter which folders it walks we will continue to use -// pathwatcher to avoid having too many watchers. -let watcherLibrary = process.env.METEOR_WATCHER_LIBRARY || - (process.platform === 'linux' ? 'pathwatcher' : 'nsfw'); - -// Pathwatcher complains (using console.error, ugh) if you try to watch -// two files with the same stat.ino number but different paths on linux, so we have -// to deduplicate files by ino. -const DEDUPLICATE_BY_INO = watcherLibrary === 'pathwatcher'; -// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to -// force the use of files.watchFile instead of watchLibrary.watch. -let watcherEnabled = ! JSON.parse( - process.env.METEOR_WATCH_FORCE_POLLING || "false" -); - -const entriesByIno = new Map; - -export type SafeWatcher = { - close: () => void; -} - -type EntryCallback = (event: string) => void; - -interface Entry extends SafeWatcher { - callbacks: Set; - rewatch: () => void; - release: (callback: EntryCallback) => void; - _fire: (event: string) => void; -} - -const entries: Record = Object.create(null); - -// Folders that are watched recursively -let watchRoots = new Set(); - // Set of paths for which a change event has been fired, watched with // watchLibrary.watch if available. This could be an LRU cache, but in // practice it should never grow large enough for that to matter. const changedPaths = new Set; +function shouldIgnorePath(absPath: string): boolean { + const posixPath = toPosixPath(absPath); + const parts = posixPath.split('/'); + + // Check for .meteor: allow the .meteor directory itself, + // but ignore its "local" subdirectory (or any immediate child folder that indicates cache). + const meteorIndex = parts.indexOf(".meteor"); + if (meteorIndex !== -1) { + const nextPart = parts[meteorIndex + 1]; + if (nextPart && nextPart === "local") { + // Ignore anything inside .meteor/local + return true; + } + // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). + } + + // For node_modules: ignore contents unless the package directory is a symlink (which means it’s under active development). + const nmIndex = parts.indexOf("node_modules"); + if (nmIndex !== -1) { + try { + const stat = lstat(absPath); + if (!stat?.isSymbolicLink()) { + return true; + } + // If the package folder is symlinked, we want to watch it. + } catch (e) { + return true; + } + } + + return false; +} + +/** + * Ensure that the given directory is being watched by @parcel/watcher. + * If it is not a directory or is unwatchable, it is immediately added to an ignore set. + */ +async function ensureWatchRoot(dirPath: string): Promise { + if (!watcherEnabled || watchRoots.has(dirPath) || ignoredWatchRoots.has(dirPath)) { + return; + } + + // If an ancestor is already watched, skip this one. + for (const root of watchRoots) { + const rel = pathRelative(root, dirPath); + if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) { + return; + } + } + + // Remove any existing roots that are now encompassed by the new one. + for (const root of Array.from(watchRoots)) { + const rel = pathRelative(dirPath, root); + if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) { + const sub = dirSubscriptions.get(root); + if (sub) { + try { + await sub.unsubscribe(); + } catch (_) { + /* ignore errors */ + } + } + dirSubscriptions.delete(root); + watchRoots.delete(root); + } + } + + const osDirPath = convertToOSPath(dirPath); + // Check that osDirPath is indeed a directory. + try { + const stats = statOrNull(osDirPath); + if (!stats?.isDirectory()) { + console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + ignoredWatchRoots.add(dirPath); + return; + } + } catch (e) { + console.error(`Failed to stat ${osDirPath}:`, e); + ignoredWatchRoots.add(dirPath); + return; + } + + // Set up ignore patterns to skip deep node_modules and .meteor/local cache + const ignorePatterns = ["**/node_modules/**", "**/.meteor/local/**"]; + try { + const subscription = await ParcelWatcher.subscribe( + osDirPath, + (err, events) => { + if (err) { + console.error(`Parcel watcher error on ${osDirPath}:`, err); + // Only disable native watching for critical errors (like ENOSPC). + // @ts-ignore + if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { + fallbackToPolling(); + } + return; + } + // Dispatch each event to any registered entries. + for (const event of events) { + const changedPath = toPosixPath(event.path); + const entry = entries[changedPath]; + if (!entry) continue; + // In Meteor's safe-watcher API, both create/update trigger "change" events. + const evtType = event.type === "delete" ? "delete" : "change"; + entry._fire(evtType); + } + }, + { ignore: ignorePatterns } + ); + watchRoots.add(dirPath); + dirSubscriptions.set(dirPath, subscription); + } catch (e: any) { + if ( + e && + (e.code === "ENOTDIR" || + /Not a directory/.test(e.message) || + e.code === "EBADF" || + /Bad file descriptor/.test(e.message)) + ) { + console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + ignoredWatchRoots.add(dirPath); + } else { + console.error(`Failed to start watcher for ${osDirPath}:`, e); + if (e.code === "ENOSPC" || e.errno === require("constants").ENOSPC) { + fallbackToPolling(); + } + } + } +} + +/** + * Creates a new watch entry for a specific file (or directory) and + * holds its registered callbacks. + */ +function startNewEntry(absPath: string): Entry { + const callbacks = new Set(); + let closed = false; + const entry: Entry = { + callbacks, + close() { + if (closed) return; + closed = true; + delete entries[absPath]; + }, + _fire(event: string) { + callbacks.forEach(cb => { + try { + cb(event); + } catch (e) { + // Ignore callback errors. + } + }); + } + }; + return entry; +} + +/** + * The primary API function to watch a file or directory. + * This registers the callback on the internally managed entry and + * ensures that a Parcel watcher is subscribed to a covering directory. + */ +export const watch = Profile( + "safeWatcher.watch", + ( + absPath: string, + callback: ChangeCallback + ): SafeWatcher => { + absPath = toPosixPath(absPath); + // If the path should be ignored, immediately return a noop SafeWatcher. + if (shouldIgnorePath(absPath)) { + return { close() {} }; + } + // If native watching is disabled, use the polling strategy. + if (!watcherEnabled) { + return startPolling(absPath, callback); + } + // Try to reuse an existing entry if one was created before. + let entry = entries[absPath]; + if (!entry) { + entry = startNewEntry(absPath); + entries[absPath] = entry; + // Determine the directory that should be watched. + let watchTarget: string; + try { + const st = statOrNull(convertToOSPath(absPath)); + watchTarget = st?.isDirectory() ? absPath : toPosixPath(dirname(convertToOSPath(absPath))); + } catch (e) { + watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + } + // Set up a watcher on the parent directory (or the directory itself) if not already active. + ensureWatchRoot(watchTarget); + } + // Register the callback for this file. + entry.callbacks.add(callback); + return { + close() { + if (entries[absPath]) { + entries[absPath]!.callbacks.delete(callback); + if (entries[absPath]!.callbacks.size === 0) { + entries[absPath]!.close(); + } + } + } + }; +}); + +/** + * Externally force a directory to be watched. + * If the provided path is a file, its parent directory is used. + */ +export function addWatchRoot(absPath: string) { + absPath = toPosixPath(absPath); + let watchTarget = absPath; + try { + const st = statOrNull(convertToOSPath(absPath)); + if (!st?.isDirectory()) { + watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + } + } catch (e) { + watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + } + ensureWatchRoot(watchTarget); +} + +async function safeUnsubscribeSub(root: string) { + const sub = dirSubscriptions.get(root); + if (!sub) return; // Already unsubscribed. + // Remove from our maps immediately to prevent further unsubscribe calls. + dirSubscriptions.delete(root); + watchRoots.delete(root); + try { + await sub.unsubscribe(); + } catch (e) { + console.error(`Error during unsubscribe for ${root}:`, e); + } +} + +export async function closeAllWatchers() { + for (const root of Array.from(watchRoots)) { + await safeUnsubscribeSub(root); + } +} + function hasPriority(absPath: string) { // If we're not prioritizing changed files, then all files have // priority, which means they should be watched with native file // watchers if the platform supports them. If we are prioritizing // changed files, then only changed files get priority. return PRIORITIZE_CHANGED - ? changedPaths.has(absPath) - : true; + ? changedPaths.has(absPath) + : true; } -function acquireWatcher(absPath: string, callback: EntryCallback) { - const entry = entries[absPath] || ( - entries[absPath] = startNewWatcher(absPath)); - - // Watches successfully established in the past may have become invalid - // because the watched file was deleted or renamed, so we need to make - // sure we're still watching every time we call safeWatcher.watch. - entry.rewatch(); - - // The size of the entry.callbacks Set also serves as a reference count - // for this watcher. - entry.callbacks.add(callback); - - return entry; -} - -function startNewWatcher(absPath: string): Entry { - let stat: Stats | BigIntStats | null | undefined = null; - - if (DEDUPLICATE_BY_INO) { - stat = statOrNull(absPath); - if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) { - const entry = entriesByIno.get(stat.ino); - if (entries[absPath] === entry) { - return entry; - } - } - } else { - let entry = entries[absPath]; - if (entry) { - return entry; - } - } - - function safeUnwatch() { - if (watcher) { - watcher.close(); - watcher = null; - if (stat && stat.ino > 0) { - entriesByIno.delete(stat.ino); - } - } - } - - let lastWatcherEventTime = Date.now(); - const callbacks = new Set(); - let watcherCleanupTimer: ReturnType | null = null; - let watcher: FSWatcher | null = null; - - // Determines the polling interval to be used for the fs.watchFile-based - // safety net that works on all platforms and file systems. - function getPollingInterval() { - if (hasPriority(absPath)) { - // Regardless of whether we have a native file watcher and it works - // correctly on this file system, poll prioritized files (that is, - // files that have been changed at least once) at a higher frequency - // (every 500ms by default). - return NO_WATCHER_POLLING_INTERVAL; - } - - if (watcherEnabled || PRIORITIZE_CHANGED) { - // As long as native file watching is enabled (even if it doesn't - // work correctly) and the developer hasn't explicitly opted out of - // the file watching priority system, poll unchanged files at a - // lower frequency (every 5000ms by default). - return DEFAULT_POLLING_INTERVAL; - } - - // If native file watching is disabled and the developer has - // explicitly opted out of the priority system, poll everything at the - // higher frequency (every 500ms by default). Note that this leads to - // higher idle CPU usage, so the developer may want to adjust the - // METEOR_WATCH_POLLING_INTERVAL_MS environment variable. +// Determines the polling interval to be used for the fs.watchFile-based +// safety net that works on all platforms and file systems. +function getPollingInterval(absPath: string): number { + if (hasPriority(absPath)) { + // Regardless of whether we have a native file watcher and it works + // correctly on this file system, poll prioritized files (that is, + // files that have been changed at least once) at a higher frequency + // (every 500ms by default). return NO_WATCHER_POLLING_INTERVAL; } - function fire(event: string) { - if (event !== "change") { - // When we receive a "delete" or "rename" event, the watcher is - // probably not going to generate any more notifications for this - // file, so we close and nullify the watcher to ensure that - // entry.rewatch() will attempt to reestablish the watcher the next - // time we call safeWatcher.watch. - safeUnwatch(); + if (watcherEnabled || PRIORITIZE_CHANGED) { + // As long as native file watching is enabled (even if it doesn't + // work correctly) and the developer hasn't explicitly opted out of + // the file watching priority system, poll unchanged files at a + // lower frequency (every 5000ms by default). + return DEFAULT_POLLING_INTERVAL; + } - // Make sure we don't throttle the watchFile callback after a - // "delete" or "rename" event, since it is now our only reliable - // source of file change notifications. - lastWatcherEventTime = 0; + // If native file watching is disabled and the developer has + // explicitly opted out of the priority system, poll everything at the + // higher frequency (every 500ms by default). Note that this leads to + // higher idle CPU usage, so the developer may want to adjust the + // METEOR_WATCH_POLLING_INTERVAL_MS environment variable. + return NO_WATCHER_POLLING_INTERVAL; +} - } else { +function startPolling(absPath: string, callback: ChangeCallback): SafeWatcher { + const osPath = convertToOSPath(absPath); + // Initial polling interval. + let interval = getPollingInterval(absPath); + const pollCallback = (curr: Stats, prev: Stats) => { + // Compare modification times to detect a change. + if (+curr.mtime !== +prev.mtime) { changedPaths.add(absPath); - rewatch(); + callback("change"); } - - callbacks.forEach(cb => cb(event)); - } - - function watchWrapper(event: string) { - lastWatcherEventTime = Date.now(); - fire(event); - - // It's tempting to call unwatchFile(absPath, watchFileWrapper) here, - // but previous watcher success is no guarantee of future watcher - // reliability. For example, watchLibrary.watch works just fine when file - // changes originate from within a Vagrant VM, but changes to shared - // files made outside the VM are invisible to watcher, so our only - // hope of catching them is to continue polling. - } - - function rewatch() { - if (hasPriority(absPath)) { - if (watcher) { - // Already watching; nothing to do. - return; - } - watcher = watchLibraryWatch(absPath, watchWrapper); - } else if (watcher) { - safeUnwatch(); - } - - // Since we're about to restart the stat-based file watcher, we don't - // want to miss any of its events because of the lastWatcherEventTime - // throttling that it attempts to do. - lastWatcherEventTime = 0; - - // We use files.watchFile in addition to watcher.watch as a fail-safe - // to detect file changes even on network file systems. However - // (unless the user disabled watcher or this watcher call failed), we - // use a relatively long default polling interval of 5000ms to save - // CPU cycles. - statWatch(absPath, getPollingInterval(), watchFileWrapper); - } - - function watchFileWrapper(newStat: Stats, oldStat: Stats) { - if (newStat.ino === 0 && - oldStat.ino === 0 && - +newStat.mtime === +oldStat.mtime) { - // Node calls the watchFile listener once with bogus identical stat - // objects, which should not trigger a file change event. - return; - } - - // If a watcher event fired in the last polling interval, ignore - // this event. - if (Date.now() - lastWatcherEventTime > getPollingInterval()) { - fire("change"); - } - } - - const entry = { - callbacks, - rewatch, - - release(callback: EntryCallback) { - if (! entries[absPath]) { - return; - } - - callbacks.delete(callback); - if (callbacks.size > 0) { - return; - } - - // Once there are no more callbacks in the Set, close both watchers - // and nullify the shared data. - if (watcherCleanupTimer) { - clearTimeout(watcherCleanupTimer); - } - - watcherCleanupTimer = setTimeout(() => { - if (callbacks.size > 0) { - // If another callback was added while the timer was pending, we - // can avoid tearing anything down. - return; - } - entry.close(); - }, WATCHER_CLEANUP_DELAY_MS); - }, - - close() { - if (entries[absPath] !== entry) return; - entries[absPath] = null; - - if (watcherCleanupTimer) { - clearTimeout(watcherCleanupTimer); - watcherCleanupTimer = null; - } - - safeUnwatch(); - - unwatchFile(absPath, watchFileWrapper); - }, - _fire: fire }; - - if (stat && stat.ino > 0) { - entriesByIno.set(stat.ino, entry); - } - - return entry; -} - -export function closeAllWatchers() { - Object.keys(entries).forEach(absPath => { - const entry = entries[absPath]; - if (entry) { - entry.close(); + watchFile(osPath, { interval }, pollCallback); + return { + close() { + unwatchFile(osPath, pollCallback); + changedPaths.delete(absPath); } - }); + }; } -const statWatchers = Object.create(null); - -function statWatch( - absPath: string, - interval: number, - callback: (current: Stats, previous: Stats) => void, -) { - let statWatcher = statWatchers[absPath]; - - if (!statWatcher) { - statWatcher = { - interval, - changeListeners: [], - stat: null - }; - statWatchers[absPath] = statWatcher; - } - - // If the interval needs to be changed, replace the watcher. - // Node will only recreate the watcher with the new interval if all old - // watchers are stopped (which unwatchFile does when not passed a - // specific listener) - if (statWatcher.interval !== interval && statWatcher.stat) { - // This stops all stat watchers for the file, not just those created by - // statWatch - unwatchFile(absPath); - statWatcher.stat = null; - statWatcher.interval = interval; - } - - if (!statWatcher.changeListeners.includes(callback)) { - statWatcher.changeListeners.push(callback); - } - - if (!statWatcher.stat) { - const newStat = watchFile(absPath, { - persistent: false, // never persistent - interval, - }, (newStat, oldStat) => { - statWatcher.changeListeners.forEach(( - listener: (newStat: Stats, oldStat: Stats) => void - ) => { - listener(newStat, oldStat); - }); - }); - - newStat.on("stop", () => { - if (statWatchers[absPath] === statWatch) { - delete statWatchers[absPath]; - } - }); - - statWatcher.stat = newStat; - } - - return statWatcher; -} - -function watchLibraryWatch(absPath: string, callback: EntryCallback) { - if (watcherEnabled && watcherLibrary === 'pathwatcher') { - try { - return pathwatcher.watch(convertToOSPath(absPath), callback); - } catch (e: any) { - maybeSuggestRaisingWatchLimit(e); - // ... ignore the error. We'll still have watchFile, which is good - // enough. - } - } - - return null; -} - -let suggestedRaisingWatchLimit = false; - -function maybeSuggestRaisingWatchLimit(error: Error & { errno: number }) { - var constants = require('constants'); - var archinfo = require('../utils/archinfo'); - if (! suggestedRaisingWatchLimit && - // Note: the not-super-documented require('constants') maps from - // strings to SYSTEM errno values. System errno values aren't the same - // as the numbers used internally by libuv! Once we're upgraded - // to Node 0.12, we'll have the system errno as a string (on 'code'), - // but the support for that wasn't in Node 0.10's uv. - // See our PR https://github.com/atom/node-pathwatcher/pull/53 - // (and make sure to read the final commit message, not the original - // proposed PR, which had a slightly different interface). - error.errno === constants.ENOSPC && - // The only suggestion we currently have is for Linux. - archinfo.matches(archinfo.host(), 'os.linux')) { - - // Check suggestedRaisingWatchLimit again because archinfo.host() may - // have yielded. - if (suggestedRaisingWatchLimit) return; - suggestedRaisingWatchLimit = true; - - var Console = require('../console/console.js').Console; - if (! Console.isHeadless()) { - Console.arrowWarn( - "It looks like a simple tweak to your system's configuration will " + - "make many tools (including this Meteor command) more efficient. " + - "To learn more, see " + - Console.url("https://github.com/meteor/docs/blob/master/long-form/file-change-watcher-efficiency.md")); - } +/** + * Fall back to polling. If a critical error occurs, + * we disable native watching and close all existing native watchers. + */ +function fallbackToPolling() { + if (watcherEnabled) { + console.error("Critical native watcher error encountered. Falling back to polling for all entries."); + watcherEnabled = false; + closeAllWatchers(); } } - -export const watch = Profile( - "safeWatcher.watch", - (absPath: string, callback: EntryCallback) => { - const entry = acquireWatcher(absPath, callback); - return { - close() { - entry.release(callback); - } - } as SafeWatcher; - } -); - -const fireNames = { - [nsfw.actions.CREATED]: 'change', - [nsfw.actions.MODIFIED]: 'change', - [nsfw.actions.DELETED]: 'delete' -} - -export function addWatchRoot(absPath: string) { - if (watchRoots.has(absPath) || watcherLibrary !== 'nsfw' || !watcherEnabled) { - return; - } - - watchRoots.add(absPath); - - // If there already is a watcher for a parent directory, there is no need - // to create this watcher. - for (const path of watchRoots) { - let relativePath = pathRelative(path, absPath); - if ( - path !== absPath && - !relativePath.startsWith('..') && - !relativePath.startsWith('/') - ) { - return; - } - } - - // TODO: check if there are any existing watchers that are children of this - // watcher and stop them - - nsfw( - convertToOSPath(absPath), - (events) => { - events.forEach(event => { - if(event.action === nsfw.actions.RENAMED) { - let oldPath = nativeJoin(event.directory, event.oldFile); - let oldEntry = entries[toPosixPath(oldPath)]; - if (oldEntry) { - oldEntry._fire('rename'); - } - - let path = nativeJoin(event.newDirectory, event.newFile); - let newEntry = entries[toPosixPath(path)]; - if (newEntry) { - newEntry._fire('change'); - } - } else { - let path = nativeJoin(event.directory, event.file); - let entry = entries[toPosixPath(path)]; - if (entry) { - entry._fire(fireNames[event.action]); - } - } - }) - } - ).then(watcher => { - watcher.start() - }); -} From 6de4d9dd14f589a393c398e279b65e20c0c30341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 16 Apr 2025 10:55:58 +0200 Subject: [PATCH 099/264] Revert "support @parcel/watcher as the watcher alternative cross-os perserving polling fallback" This reverts commit a38bcaee9a377e5ad54b6642edd924029a544408. --- scripts/dev-bundle-tool-package.js | 3 +- tools/fs/safe-watcher.ts | 770 +++++++++++++++++------------ 2 files changed, 443 insertions(+), 330 deletions(-) diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 8d9292efb0..2b92b9e5f6 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -57,7 +57,8 @@ var packageJson = { escope: "3.6.0", split2: "3.2.2", multipipe: "2.0.1", - "@parcel/watcher": "2.5.1", + pathwatcher: "8.1.2", + "vscode-nsfw": "2.1.8", // The @wry/context package version must be compatible with the // version constraint imposed by optimism/package.json. optimism: "0.16.1", diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 5f521fb024..cea3d74d7b 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -1,364 +1,476 @@ -import { Stats } from 'fs'; -import { dirname } from "path"; -import ParcelWatcher from "@parcel/watcher"; - +import { FSWatcher, Stats, BigIntStats } from "fs"; import { Profile } from "../tool-env/profile"; -import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; +import { + statOrNull, + convertToOSPath, + watchFile, + unwatchFile, + toPosixPath, + pathRelative +} from "./files"; +import { + join as nativeJoin +} from 'path'; +import nsfw from 'vscode-nsfw'; -export type SafeWatcher = { close: () => void; }; - -type ChangeCallback = (event: string) => void; - -interface Entry extends SafeWatcher { - callbacks: Set; - _fire(event: string): void; -} - -// Registry mapping normalized absolute paths to their watcher entry. -const entries: Record = Object.create(null); - -// Watch roots are directories for which we have an active ParcelWatcher subscription. -const watchRoots = new Set(); -// For each watch root, store its active subscription. -const dirSubscriptions = new Map(); -// A set of roots that are known to be unwatchable. -const ignoredWatchRoots = new Set(); - -// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to -// force the use of files.watchFile instead of ParcelWatcher. -let watcherEnabled = !JSON.parse(process.env.METEOR_WATCH_FORCE_POLLING || "false"); - -/** - * Polling fallback globals. - * The legacy Meteor strategy used polling for cases where native watchers failed. - * We keep track of files that changed, so that we can poll them faster. - */ -var DEFAULT_POLLING_INTERVAL = - +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000); - -var NO_WATCHER_POLLING_INTERVAL = - +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500); +const pathwatcher = require('pathwatcher'); +// Default to prioritizing changed files, but disable that behavior (and +// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED +// is explicitly set to a string that parses to a falsy value. var PRIORITIZE_CHANGED = true; if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED && ! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) { PRIORITIZE_CHANGED = false; } +var DEFAULT_POLLING_INTERVAL = + +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000); + +var NO_WATCHER_POLLING_INTERVAL = + +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500); + +// This may seems like a long time to wait before actually closing the +// file watchers, but it's to our advantage if they survive restarts. +const WATCHER_CLEANUP_DELAY_MS = 30000; + +// Since linux doesn't have recursive file watching, nsfw has to walk the +// watched folder and create a separate watcher for each subfolder. Until it has a +// way for us to filter which folders it walks we will continue to use +// pathwatcher to avoid having too many watchers. +let watcherLibrary = process.env.METEOR_WATCHER_LIBRARY || + (process.platform === 'linux' ? 'pathwatcher' : 'nsfw'); + +// Pathwatcher complains (using console.error, ugh) if you try to watch +// two files with the same stat.ino number but different paths on linux, so we have +// to deduplicate files by ino. +const DEDUPLICATE_BY_INO = watcherLibrary === 'pathwatcher'; +// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to +// force the use of files.watchFile instead of watchLibrary.watch. +let watcherEnabled = ! JSON.parse( + process.env.METEOR_WATCH_FORCE_POLLING || "false" +); + +const entriesByIno = new Map; + +export type SafeWatcher = { + close: () => void; +} + +type EntryCallback = (event: string) => void; + +interface Entry extends SafeWatcher { + callbacks: Set; + rewatch: () => void; + release: (callback: EntryCallback) => void; + _fire: (event: string) => void; +} + +const entries: Record = Object.create(null); + +// Folders that are watched recursively +let watchRoots = new Set(); + // Set of paths for which a change event has been fired, watched with // watchLibrary.watch if available. This could be an LRU cache, but in // practice it should never grow large enough for that to matter. const changedPaths = new Set; -function shouldIgnorePath(absPath: string): boolean { - const posixPath = toPosixPath(absPath); - const parts = posixPath.split('/'); - - // Check for .meteor: allow the .meteor directory itself, - // but ignore its "local" subdirectory (or any immediate child folder that indicates cache). - const meteorIndex = parts.indexOf(".meteor"); - if (meteorIndex !== -1) { - const nextPart = parts[meteorIndex + 1]; - if (nextPart && nextPart === "local") { - // Ignore anything inside .meteor/local - return true; - } - // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). - } - - // For node_modules: ignore contents unless the package directory is a symlink (which means it’s under active development). - const nmIndex = parts.indexOf("node_modules"); - if (nmIndex !== -1) { - try { - const stat = lstat(absPath); - if (!stat?.isSymbolicLink()) { - return true; - } - // If the package folder is symlinked, we want to watch it. - } catch (e) { - return true; - } - } - - return false; -} - -/** - * Ensure that the given directory is being watched by @parcel/watcher. - * If it is not a directory or is unwatchable, it is immediately added to an ignore set. - */ -async function ensureWatchRoot(dirPath: string): Promise { - if (!watcherEnabled || watchRoots.has(dirPath) || ignoredWatchRoots.has(dirPath)) { - return; - } - - // If an ancestor is already watched, skip this one. - for (const root of watchRoots) { - const rel = pathRelative(root, dirPath); - if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) { - return; - } - } - - // Remove any existing roots that are now encompassed by the new one. - for (const root of Array.from(watchRoots)) { - const rel = pathRelative(dirPath, root); - if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) { - const sub = dirSubscriptions.get(root); - if (sub) { - try { - await sub.unsubscribe(); - } catch (_) { - /* ignore errors */ - } - } - dirSubscriptions.delete(root); - watchRoots.delete(root); - } - } - - const osDirPath = convertToOSPath(dirPath); - // Check that osDirPath is indeed a directory. - try { - const stats = statOrNull(osDirPath); - if (!stats?.isDirectory()) { - console.warn(`Skipping watcher for ${osDirPath}: not a directory`); - ignoredWatchRoots.add(dirPath); - return; - } - } catch (e) { - console.error(`Failed to stat ${osDirPath}:`, e); - ignoredWatchRoots.add(dirPath); - return; - } - - // Set up ignore patterns to skip deep node_modules and .meteor/local cache - const ignorePatterns = ["**/node_modules/**", "**/.meteor/local/**"]; - try { - const subscription = await ParcelWatcher.subscribe( - osDirPath, - (err, events) => { - if (err) { - console.error(`Parcel watcher error on ${osDirPath}:`, err); - // Only disable native watching for critical errors (like ENOSPC). - // @ts-ignore - if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { - fallbackToPolling(); - } - return; - } - // Dispatch each event to any registered entries. - for (const event of events) { - const changedPath = toPosixPath(event.path); - const entry = entries[changedPath]; - if (!entry) continue; - // In Meteor's safe-watcher API, both create/update trigger "change" events. - const evtType = event.type === "delete" ? "delete" : "change"; - entry._fire(evtType); - } - }, - { ignore: ignorePatterns } - ); - watchRoots.add(dirPath); - dirSubscriptions.set(dirPath, subscription); - } catch (e: any) { - if ( - e && - (e.code === "ENOTDIR" || - /Not a directory/.test(e.message) || - e.code === "EBADF" || - /Bad file descriptor/.test(e.message)) - ) { - console.warn(`Skipping watcher for ${osDirPath}: not a directory`); - ignoredWatchRoots.add(dirPath); - } else { - console.error(`Failed to start watcher for ${osDirPath}:`, e); - if (e.code === "ENOSPC" || e.errno === require("constants").ENOSPC) { - fallbackToPolling(); - } - } - } -} - -/** - * Creates a new watch entry for a specific file (or directory) and - * holds its registered callbacks. - */ -function startNewEntry(absPath: string): Entry { - const callbacks = new Set(); - let closed = false; - const entry: Entry = { - callbacks, - close() { - if (closed) return; - closed = true; - delete entries[absPath]; - }, - _fire(event: string) { - callbacks.forEach(cb => { - try { - cb(event); - } catch (e) { - // Ignore callback errors. - } - }); - } - }; - return entry; -} - -/** - * The primary API function to watch a file or directory. - * This registers the callback on the internally managed entry and - * ensures that a Parcel watcher is subscribed to a covering directory. - */ -export const watch = Profile( - "safeWatcher.watch", - ( - absPath: string, - callback: ChangeCallback - ): SafeWatcher => { - absPath = toPosixPath(absPath); - // If the path should be ignored, immediately return a noop SafeWatcher. - if (shouldIgnorePath(absPath)) { - return { close() {} }; - } - // If native watching is disabled, use the polling strategy. - if (!watcherEnabled) { - return startPolling(absPath, callback); - } - // Try to reuse an existing entry if one was created before. - let entry = entries[absPath]; - if (!entry) { - entry = startNewEntry(absPath); - entries[absPath] = entry; - // Determine the directory that should be watched. - let watchTarget: string; - try { - const st = statOrNull(convertToOSPath(absPath)); - watchTarget = st?.isDirectory() ? absPath : toPosixPath(dirname(convertToOSPath(absPath))); - } catch (e) { - watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); - } - // Set up a watcher on the parent directory (or the directory itself) if not already active. - ensureWatchRoot(watchTarget); - } - // Register the callback for this file. - entry.callbacks.add(callback); - return { - close() { - if (entries[absPath]) { - entries[absPath]!.callbacks.delete(callback); - if (entries[absPath]!.callbacks.size === 0) { - entries[absPath]!.close(); - } - } - } - }; -}); - -/** - * Externally force a directory to be watched. - * If the provided path is a file, its parent directory is used. - */ -export function addWatchRoot(absPath: string) { - absPath = toPosixPath(absPath); - let watchTarget = absPath; - try { - const st = statOrNull(convertToOSPath(absPath)); - if (!st?.isDirectory()) { - watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); - } - } catch (e) { - watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); - } - ensureWatchRoot(watchTarget); -} - -async function safeUnsubscribeSub(root: string) { - const sub = dirSubscriptions.get(root); - if (!sub) return; // Already unsubscribed. - // Remove from our maps immediately to prevent further unsubscribe calls. - dirSubscriptions.delete(root); - watchRoots.delete(root); - try { - await sub.unsubscribe(); - } catch (e) { - console.error(`Error during unsubscribe for ${root}:`, e); - } -} - -export async function closeAllWatchers() { - for (const root of Array.from(watchRoots)) { - await safeUnsubscribeSub(root); - } -} - function hasPriority(absPath: string) { // If we're not prioritizing changed files, then all files have // priority, which means they should be watched with native file // watchers if the platform supports them. If we are prioritizing // changed files, then only changed files get priority. return PRIORITIZE_CHANGED - ? changedPaths.has(absPath) - : true; + ? changedPaths.has(absPath) + : true; } -// Determines the polling interval to be used for the fs.watchFile-based -// safety net that works on all platforms and file systems. -function getPollingInterval(absPath: string): number { - if (hasPriority(absPath)) { - // Regardless of whether we have a native file watcher and it works - // correctly on this file system, poll prioritized files (that is, - // files that have been changed at least once) at a higher frequency - // (every 500ms by default). +function acquireWatcher(absPath: string, callback: EntryCallback) { + const entry = entries[absPath] || ( + entries[absPath] = startNewWatcher(absPath)); + + // Watches successfully established in the past may have become invalid + // because the watched file was deleted or renamed, so we need to make + // sure we're still watching every time we call safeWatcher.watch. + entry.rewatch(); + + // The size of the entry.callbacks Set also serves as a reference count + // for this watcher. + entry.callbacks.add(callback); + + return entry; +} + +function startNewWatcher(absPath: string): Entry { + let stat: Stats | BigIntStats | null | undefined = null; + + if (DEDUPLICATE_BY_INO) { + stat = statOrNull(absPath); + if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) { + const entry = entriesByIno.get(stat.ino); + if (entries[absPath] === entry) { + return entry; + } + } + } else { + let entry = entries[absPath]; + if (entry) { + return entry; + } + } + + function safeUnwatch() { + if (watcher) { + watcher.close(); + watcher = null; + if (stat && stat.ino > 0) { + entriesByIno.delete(stat.ino); + } + } + } + + let lastWatcherEventTime = Date.now(); + const callbacks = new Set(); + let watcherCleanupTimer: ReturnType | null = null; + let watcher: FSWatcher | null = null; + + // Determines the polling interval to be used for the fs.watchFile-based + // safety net that works on all platforms and file systems. + function getPollingInterval() { + if (hasPriority(absPath)) { + // Regardless of whether we have a native file watcher and it works + // correctly on this file system, poll prioritized files (that is, + // files that have been changed at least once) at a higher frequency + // (every 500ms by default). + return NO_WATCHER_POLLING_INTERVAL; + } + + if (watcherEnabled || PRIORITIZE_CHANGED) { + // As long as native file watching is enabled (even if it doesn't + // work correctly) and the developer hasn't explicitly opted out of + // the file watching priority system, poll unchanged files at a + // lower frequency (every 5000ms by default). + return DEFAULT_POLLING_INTERVAL; + } + + // If native file watching is disabled and the developer has + // explicitly opted out of the priority system, poll everything at the + // higher frequency (every 500ms by default). Note that this leads to + // higher idle CPU usage, so the developer may want to adjust the + // METEOR_WATCH_POLLING_INTERVAL_MS environment variable. return NO_WATCHER_POLLING_INTERVAL; } - if (watcherEnabled || PRIORITIZE_CHANGED) { - // As long as native file watching is enabled (even if it doesn't - // work correctly) and the developer hasn't explicitly opted out of - // the file watching priority system, poll unchanged files at a - // lower frequency (every 5000ms by default). - return DEFAULT_POLLING_INTERVAL; - } + function fire(event: string) { + if (event !== "change") { + // When we receive a "delete" or "rename" event, the watcher is + // probably not going to generate any more notifications for this + // file, so we close and nullify the watcher to ensure that + // entry.rewatch() will attempt to reestablish the watcher the next + // time we call safeWatcher.watch. + safeUnwatch(); - // If native file watching is disabled and the developer has - // explicitly opted out of the priority system, poll everything at the - // higher frequency (every 500ms by default). Note that this leads to - // higher idle CPU usage, so the developer may want to adjust the - // METEOR_WATCH_POLLING_INTERVAL_MS environment variable. - return NO_WATCHER_POLLING_INTERVAL; -} + // Make sure we don't throttle the watchFile callback after a + // "delete" or "rename" event, since it is now our only reliable + // source of file change notifications. + lastWatcherEventTime = 0; -function startPolling(absPath: string, callback: ChangeCallback): SafeWatcher { - const osPath = convertToOSPath(absPath); - // Initial polling interval. - let interval = getPollingInterval(absPath); - const pollCallback = (curr: Stats, prev: Stats) => { - // Compare modification times to detect a change. - if (+curr.mtime !== +prev.mtime) { + } else { changedPaths.add(absPath); - callback("change"); + rewatch(); } - }; - watchFile(osPath, { interval }, pollCallback); - return { + + callbacks.forEach(cb => cb(event)); + } + + function watchWrapper(event: string) { + lastWatcherEventTime = Date.now(); + fire(event); + + // It's tempting to call unwatchFile(absPath, watchFileWrapper) here, + // but previous watcher success is no guarantee of future watcher + // reliability. For example, watchLibrary.watch works just fine when file + // changes originate from within a Vagrant VM, but changes to shared + // files made outside the VM are invisible to watcher, so our only + // hope of catching them is to continue polling. + } + + function rewatch() { + if (hasPriority(absPath)) { + if (watcher) { + // Already watching; nothing to do. + return; + } + watcher = watchLibraryWatch(absPath, watchWrapper); + } else if (watcher) { + safeUnwatch(); + } + + // Since we're about to restart the stat-based file watcher, we don't + // want to miss any of its events because of the lastWatcherEventTime + // throttling that it attempts to do. + lastWatcherEventTime = 0; + + // We use files.watchFile in addition to watcher.watch as a fail-safe + // to detect file changes even on network file systems. However + // (unless the user disabled watcher or this watcher call failed), we + // use a relatively long default polling interval of 5000ms to save + // CPU cycles. + statWatch(absPath, getPollingInterval(), watchFileWrapper); + } + + function watchFileWrapper(newStat: Stats, oldStat: Stats) { + if (newStat.ino === 0 && + oldStat.ino === 0 && + +newStat.mtime === +oldStat.mtime) { + // Node calls the watchFile listener once with bogus identical stat + // objects, which should not trigger a file change event. + return; + } + + // If a watcher event fired in the last polling interval, ignore + // this event. + if (Date.now() - lastWatcherEventTime > getPollingInterval()) { + fire("change"); + } + } + + const entry = { + callbacks, + rewatch, + + release(callback: EntryCallback) { + if (! entries[absPath]) { + return; + } + + callbacks.delete(callback); + if (callbacks.size > 0) { + return; + } + + // Once there are no more callbacks in the Set, close both watchers + // and nullify the shared data. + if (watcherCleanupTimer) { + clearTimeout(watcherCleanupTimer); + } + + watcherCleanupTimer = setTimeout(() => { + if (callbacks.size > 0) { + // If another callback was added while the timer was pending, we + // can avoid tearing anything down. + return; + } + entry.close(); + }, WATCHER_CLEANUP_DELAY_MS); + }, + close() { - unwatchFile(osPath, pollCallback); - changedPaths.delete(absPath); - } + if (entries[absPath] !== entry) return; + entries[absPath] = null; + + if (watcherCleanupTimer) { + clearTimeout(watcherCleanupTimer); + watcherCleanupTimer = null; + } + + safeUnwatch(); + + unwatchFile(absPath, watchFileWrapper); + }, + _fire: fire }; + + if (stat && stat.ino > 0) { + entriesByIno.set(stat.ino, entry); + } + + return entry; } -/** - * Fall back to polling. If a critical error occurs, - * we disable native watching and close all existing native watchers. - */ -function fallbackToPolling() { - if (watcherEnabled) { - console.error("Critical native watcher error encountered. Falling back to polling for all entries."); - watcherEnabled = false; - closeAllWatchers(); +export function closeAllWatchers() { + Object.keys(entries).forEach(absPath => { + const entry = entries[absPath]; + if (entry) { + entry.close(); + } + }); +} + +const statWatchers = Object.create(null); + +function statWatch( + absPath: string, + interval: number, + callback: (current: Stats, previous: Stats) => void, +) { + let statWatcher = statWatchers[absPath]; + + if (!statWatcher) { + statWatcher = { + interval, + changeListeners: [], + stat: null + }; + statWatchers[absPath] = statWatcher; + } + + // If the interval needs to be changed, replace the watcher. + // Node will only recreate the watcher with the new interval if all old + // watchers are stopped (which unwatchFile does when not passed a + // specific listener) + if (statWatcher.interval !== interval && statWatcher.stat) { + // This stops all stat watchers for the file, not just those created by + // statWatch + unwatchFile(absPath); + statWatcher.stat = null; + statWatcher.interval = interval; + } + + if (!statWatcher.changeListeners.includes(callback)) { + statWatcher.changeListeners.push(callback); + } + + if (!statWatcher.stat) { + const newStat = watchFile(absPath, { + persistent: false, // never persistent + interval, + }, (newStat, oldStat) => { + statWatcher.changeListeners.forEach(( + listener: (newStat: Stats, oldStat: Stats) => void + ) => { + listener(newStat, oldStat); + }); + }); + + newStat.on("stop", () => { + if (statWatchers[absPath] === statWatch) { + delete statWatchers[absPath]; + } + }); + + statWatcher.stat = newStat; + } + + return statWatcher; +} + +function watchLibraryWatch(absPath: string, callback: EntryCallback) { + if (watcherEnabled && watcherLibrary === 'pathwatcher') { + try { + return pathwatcher.watch(convertToOSPath(absPath), callback); + } catch (e: any) { + maybeSuggestRaisingWatchLimit(e); + // ... ignore the error. We'll still have watchFile, which is good + // enough. + } + } + + return null; +} + +let suggestedRaisingWatchLimit = false; + +function maybeSuggestRaisingWatchLimit(error: Error & { errno: number }) { + var constants = require('constants'); + var archinfo = require('../utils/archinfo'); + if (! suggestedRaisingWatchLimit && + // Note: the not-super-documented require('constants') maps from + // strings to SYSTEM errno values. System errno values aren't the same + // as the numbers used internally by libuv! Once we're upgraded + // to Node 0.12, we'll have the system errno as a string (on 'code'), + // but the support for that wasn't in Node 0.10's uv. + // See our PR https://github.com/atom/node-pathwatcher/pull/53 + // (and make sure to read the final commit message, not the original + // proposed PR, which had a slightly different interface). + error.errno === constants.ENOSPC && + // The only suggestion we currently have is for Linux. + archinfo.matches(archinfo.host(), 'os.linux')) { + + // Check suggestedRaisingWatchLimit again because archinfo.host() may + // have yielded. + if (suggestedRaisingWatchLimit) return; + suggestedRaisingWatchLimit = true; + + var Console = require('../console/console.js').Console; + if (! Console.isHeadless()) { + Console.arrowWarn( + "It looks like a simple tweak to your system's configuration will " + + "make many tools (including this Meteor command) more efficient. " + + "To learn more, see " + + Console.url("https://github.com/meteor/docs/blob/master/long-form/file-change-watcher-efficiency.md")); + } } } + +export const watch = Profile( + "safeWatcher.watch", + (absPath: string, callback: EntryCallback) => { + const entry = acquireWatcher(absPath, callback); + return { + close() { + entry.release(callback); + } + } as SafeWatcher; + } +); + +const fireNames = { + [nsfw.actions.CREATED]: 'change', + [nsfw.actions.MODIFIED]: 'change', + [nsfw.actions.DELETED]: 'delete' +} + +export function addWatchRoot(absPath: string) { + if (watchRoots.has(absPath) || watcherLibrary !== 'nsfw' || !watcherEnabled) { + return; + } + + watchRoots.add(absPath); + + // If there already is a watcher for a parent directory, there is no need + // to create this watcher. + for (const path of watchRoots) { + let relativePath = pathRelative(path, absPath); + if ( + path !== absPath && + !relativePath.startsWith('..') && + !relativePath.startsWith('/') + ) { + return; + } + } + + // TODO: check if there are any existing watchers that are children of this + // watcher and stop them + + nsfw( + convertToOSPath(absPath), + (events) => { + events.forEach(event => { + if(event.action === nsfw.actions.RENAMED) { + let oldPath = nativeJoin(event.directory, event.oldFile); + let oldEntry = entries[toPosixPath(oldPath)]; + if (oldEntry) { + oldEntry._fire('rename'); + } + + let path = nativeJoin(event.newDirectory, event.newFile); + let newEntry = entries[toPosixPath(path)]; + if (newEntry) { + newEntry._fire('change'); + } + } else { + let path = nativeJoin(event.directory, event.file); + let entry = entries[toPosixPath(path)]; + if (entry) { + entry._fire(fireNames[event.action]); + } + } + }) + } + ).then(watcher => { + watcher.start() + }); +} From 31d2b7337857fd47cb6bfe14295038997d1855d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 16 Apr 2025 10:56:27 +0200 Subject: [PATCH 100/264] Revert "Revert "support @parcel/watcher as the watcher alternative cross-os perserving polling fallback"" This reverts commit 6de4d9dd14f589a393c398e279b65e20c0c30341. --- scripts/dev-bundle-tool-package.js | 3 +- tools/fs/safe-watcher.ts | 770 ++++++++++++----------------- 2 files changed, 330 insertions(+), 443 deletions(-) diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 2b92b9e5f6..8d9292efb0 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -57,8 +57,7 @@ var packageJson = { escope: "3.6.0", split2: "3.2.2", multipipe: "2.0.1", - pathwatcher: "8.1.2", - "vscode-nsfw": "2.1.8", + "@parcel/watcher": "2.5.1", // The @wry/context package version must be compatible with the // version constraint imposed by optimism/package.json. optimism: "0.16.1", diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index cea3d74d7b..5f521fb024 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -1,476 +1,364 @@ -import { FSWatcher, Stats, BigIntStats } from "fs"; +import { Stats } from 'fs'; +import { dirname } from "path"; +import ParcelWatcher from "@parcel/watcher"; + import { Profile } from "../tool-env/profile"; -import { - statOrNull, - convertToOSPath, - watchFile, - unwatchFile, - toPosixPath, - pathRelative -} from "./files"; -import { - join as nativeJoin -} from 'path'; -import nsfw from 'vscode-nsfw'; +import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; -const pathwatcher = require('pathwatcher'); +export type SafeWatcher = { close: () => void; }; + +type ChangeCallback = (event: string) => void; + +interface Entry extends SafeWatcher { + callbacks: Set; + _fire(event: string): void; +} + +// Registry mapping normalized absolute paths to their watcher entry. +const entries: Record = Object.create(null); + +// Watch roots are directories for which we have an active ParcelWatcher subscription. +const watchRoots = new Set(); +// For each watch root, store its active subscription. +const dirSubscriptions = new Map(); +// A set of roots that are known to be unwatchable. +const ignoredWatchRoots = new Set(); + +// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to +// force the use of files.watchFile instead of ParcelWatcher. +let watcherEnabled = !JSON.parse(process.env.METEOR_WATCH_FORCE_POLLING || "false"); + +/** + * Polling fallback globals. + * The legacy Meteor strategy used polling for cases where native watchers failed. + * We keep track of files that changed, so that we can poll them faster. + */ +var DEFAULT_POLLING_INTERVAL = + +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000); + +var NO_WATCHER_POLLING_INTERVAL = + +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500); -// Default to prioritizing changed files, but disable that behavior (and -// thus prioritize all files equally) if METEOR_WATCH_PRIORITIZE_CHANGED -// is explicitly set to a string that parses to a falsy value. var PRIORITIZE_CHANGED = true; if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED && ! JSON.parse(process.env.METEOR_WATCH_PRIORITIZE_CHANGED)) { PRIORITIZE_CHANGED = false; } -var DEFAULT_POLLING_INTERVAL = - +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 5000); - -var NO_WATCHER_POLLING_INTERVAL = - +(process.env.METEOR_WATCH_POLLING_INTERVAL_MS || 500); - -// This may seems like a long time to wait before actually closing the -// file watchers, but it's to our advantage if they survive restarts. -const WATCHER_CLEANUP_DELAY_MS = 30000; - -// Since linux doesn't have recursive file watching, nsfw has to walk the -// watched folder and create a separate watcher for each subfolder. Until it has a -// way for us to filter which folders it walks we will continue to use -// pathwatcher to avoid having too many watchers. -let watcherLibrary = process.env.METEOR_WATCHER_LIBRARY || - (process.platform === 'linux' ? 'pathwatcher' : 'nsfw'); - -// Pathwatcher complains (using console.error, ugh) if you try to watch -// two files with the same stat.ino number but different paths on linux, so we have -// to deduplicate files by ino. -const DEDUPLICATE_BY_INO = watcherLibrary === 'pathwatcher'; -// Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to -// force the use of files.watchFile instead of watchLibrary.watch. -let watcherEnabled = ! JSON.parse( - process.env.METEOR_WATCH_FORCE_POLLING || "false" -); - -const entriesByIno = new Map; - -export type SafeWatcher = { - close: () => void; -} - -type EntryCallback = (event: string) => void; - -interface Entry extends SafeWatcher { - callbacks: Set; - rewatch: () => void; - release: (callback: EntryCallback) => void; - _fire: (event: string) => void; -} - -const entries: Record = Object.create(null); - -// Folders that are watched recursively -let watchRoots = new Set(); - // Set of paths for which a change event has been fired, watched with // watchLibrary.watch if available. This could be an LRU cache, but in // practice it should never grow large enough for that to matter. const changedPaths = new Set; +function shouldIgnorePath(absPath: string): boolean { + const posixPath = toPosixPath(absPath); + const parts = posixPath.split('/'); + + // Check for .meteor: allow the .meteor directory itself, + // but ignore its "local" subdirectory (or any immediate child folder that indicates cache). + const meteorIndex = parts.indexOf(".meteor"); + if (meteorIndex !== -1) { + const nextPart = parts[meteorIndex + 1]; + if (nextPart && nextPart === "local") { + // Ignore anything inside .meteor/local + return true; + } + // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). + } + + // For node_modules: ignore contents unless the package directory is a symlink (which means it’s under active development). + const nmIndex = parts.indexOf("node_modules"); + if (nmIndex !== -1) { + try { + const stat = lstat(absPath); + if (!stat?.isSymbolicLink()) { + return true; + } + // If the package folder is symlinked, we want to watch it. + } catch (e) { + return true; + } + } + + return false; +} + +/** + * Ensure that the given directory is being watched by @parcel/watcher. + * If it is not a directory or is unwatchable, it is immediately added to an ignore set. + */ +async function ensureWatchRoot(dirPath: string): Promise { + if (!watcherEnabled || watchRoots.has(dirPath) || ignoredWatchRoots.has(dirPath)) { + return; + } + + // If an ancestor is already watched, skip this one. + for (const root of watchRoots) { + const rel = pathRelative(root, dirPath); + if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) { + return; + } + } + + // Remove any existing roots that are now encompassed by the new one. + for (const root of Array.from(watchRoots)) { + const rel = pathRelative(dirPath, root); + if (root !== dirPath && !rel.startsWith("..") && !rel.startsWith("/")) { + const sub = dirSubscriptions.get(root); + if (sub) { + try { + await sub.unsubscribe(); + } catch (_) { + /* ignore errors */ + } + } + dirSubscriptions.delete(root); + watchRoots.delete(root); + } + } + + const osDirPath = convertToOSPath(dirPath); + // Check that osDirPath is indeed a directory. + try { + const stats = statOrNull(osDirPath); + if (!stats?.isDirectory()) { + console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + ignoredWatchRoots.add(dirPath); + return; + } + } catch (e) { + console.error(`Failed to stat ${osDirPath}:`, e); + ignoredWatchRoots.add(dirPath); + return; + } + + // Set up ignore patterns to skip deep node_modules and .meteor/local cache + const ignorePatterns = ["**/node_modules/**", "**/.meteor/local/**"]; + try { + const subscription = await ParcelWatcher.subscribe( + osDirPath, + (err, events) => { + if (err) { + console.error(`Parcel watcher error on ${osDirPath}:`, err); + // Only disable native watching for critical errors (like ENOSPC). + // @ts-ignore + if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { + fallbackToPolling(); + } + return; + } + // Dispatch each event to any registered entries. + for (const event of events) { + const changedPath = toPosixPath(event.path); + const entry = entries[changedPath]; + if (!entry) continue; + // In Meteor's safe-watcher API, both create/update trigger "change" events. + const evtType = event.type === "delete" ? "delete" : "change"; + entry._fire(evtType); + } + }, + { ignore: ignorePatterns } + ); + watchRoots.add(dirPath); + dirSubscriptions.set(dirPath, subscription); + } catch (e: any) { + if ( + e && + (e.code === "ENOTDIR" || + /Not a directory/.test(e.message) || + e.code === "EBADF" || + /Bad file descriptor/.test(e.message)) + ) { + console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + ignoredWatchRoots.add(dirPath); + } else { + console.error(`Failed to start watcher for ${osDirPath}:`, e); + if (e.code === "ENOSPC" || e.errno === require("constants").ENOSPC) { + fallbackToPolling(); + } + } + } +} + +/** + * Creates a new watch entry for a specific file (or directory) and + * holds its registered callbacks. + */ +function startNewEntry(absPath: string): Entry { + const callbacks = new Set(); + let closed = false; + const entry: Entry = { + callbacks, + close() { + if (closed) return; + closed = true; + delete entries[absPath]; + }, + _fire(event: string) { + callbacks.forEach(cb => { + try { + cb(event); + } catch (e) { + // Ignore callback errors. + } + }); + } + }; + return entry; +} + +/** + * The primary API function to watch a file or directory. + * This registers the callback on the internally managed entry and + * ensures that a Parcel watcher is subscribed to a covering directory. + */ +export const watch = Profile( + "safeWatcher.watch", + ( + absPath: string, + callback: ChangeCallback + ): SafeWatcher => { + absPath = toPosixPath(absPath); + // If the path should be ignored, immediately return a noop SafeWatcher. + if (shouldIgnorePath(absPath)) { + return { close() {} }; + } + // If native watching is disabled, use the polling strategy. + if (!watcherEnabled) { + return startPolling(absPath, callback); + } + // Try to reuse an existing entry if one was created before. + let entry = entries[absPath]; + if (!entry) { + entry = startNewEntry(absPath); + entries[absPath] = entry; + // Determine the directory that should be watched. + let watchTarget: string; + try { + const st = statOrNull(convertToOSPath(absPath)); + watchTarget = st?.isDirectory() ? absPath : toPosixPath(dirname(convertToOSPath(absPath))); + } catch (e) { + watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + } + // Set up a watcher on the parent directory (or the directory itself) if not already active. + ensureWatchRoot(watchTarget); + } + // Register the callback for this file. + entry.callbacks.add(callback); + return { + close() { + if (entries[absPath]) { + entries[absPath]!.callbacks.delete(callback); + if (entries[absPath]!.callbacks.size === 0) { + entries[absPath]!.close(); + } + } + } + }; +}); + +/** + * Externally force a directory to be watched. + * If the provided path is a file, its parent directory is used. + */ +export function addWatchRoot(absPath: string) { + absPath = toPosixPath(absPath); + let watchTarget = absPath; + try { + const st = statOrNull(convertToOSPath(absPath)); + if (!st?.isDirectory()) { + watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + } + } catch (e) { + watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + } + ensureWatchRoot(watchTarget); +} + +async function safeUnsubscribeSub(root: string) { + const sub = dirSubscriptions.get(root); + if (!sub) return; // Already unsubscribed. + // Remove from our maps immediately to prevent further unsubscribe calls. + dirSubscriptions.delete(root); + watchRoots.delete(root); + try { + await sub.unsubscribe(); + } catch (e) { + console.error(`Error during unsubscribe for ${root}:`, e); + } +} + +export async function closeAllWatchers() { + for (const root of Array.from(watchRoots)) { + await safeUnsubscribeSub(root); + } +} + function hasPriority(absPath: string) { // If we're not prioritizing changed files, then all files have // priority, which means they should be watched with native file // watchers if the platform supports them. If we are prioritizing // changed files, then only changed files get priority. return PRIORITIZE_CHANGED - ? changedPaths.has(absPath) - : true; + ? changedPaths.has(absPath) + : true; } -function acquireWatcher(absPath: string, callback: EntryCallback) { - const entry = entries[absPath] || ( - entries[absPath] = startNewWatcher(absPath)); - - // Watches successfully established in the past may have become invalid - // because the watched file was deleted or renamed, so we need to make - // sure we're still watching every time we call safeWatcher.watch. - entry.rewatch(); - - // The size of the entry.callbacks Set also serves as a reference count - // for this watcher. - entry.callbacks.add(callback); - - return entry; -} - -function startNewWatcher(absPath: string): Entry { - let stat: Stats | BigIntStats | null | undefined = null; - - if (DEDUPLICATE_BY_INO) { - stat = statOrNull(absPath); - if (stat && stat.ino > 0 && entriesByIno.has(stat.ino)) { - const entry = entriesByIno.get(stat.ino); - if (entries[absPath] === entry) { - return entry; - } - } - } else { - let entry = entries[absPath]; - if (entry) { - return entry; - } - } - - function safeUnwatch() { - if (watcher) { - watcher.close(); - watcher = null; - if (stat && stat.ino > 0) { - entriesByIno.delete(stat.ino); - } - } - } - - let lastWatcherEventTime = Date.now(); - const callbacks = new Set(); - let watcherCleanupTimer: ReturnType | null = null; - let watcher: FSWatcher | null = null; - - // Determines the polling interval to be used for the fs.watchFile-based - // safety net that works on all platforms and file systems. - function getPollingInterval() { - if (hasPriority(absPath)) { - // Regardless of whether we have a native file watcher and it works - // correctly on this file system, poll prioritized files (that is, - // files that have been changed at least once) at a higher frequency - // (every 500ms by default). - return NO_WATCHER_POLLING_INTERVAL; - } - - if (watcherEnabled || PRIORITIZE_CHANGED) { - // As long as native file watching is enabled (even if it doesn't - // work correctly) and the developer hasn't explicitly opted out of - // the file watching priority system, poll unchanged files at a - // lower frequency (every 5000ms by default). - return DEFAULT_POLLING_INTERVAL; - } - - // If native file watching is disabled and the developer has - // explicitly opted out of the priority system, poll everything at the - // higher frequency (every 500ms by default). Note that this leads to - // higher idle CPU usage, so the developer may want to adjust the - // METEOR_WATCH_POLLING_INTERVAL_MS environment variable. +// Determines the polling interval to be used for the fs.watchFile-based +// safety net that works on all platforms and file systems. +function getPollingInterval(absPath: string): number { + if (hasPriority(absPath)) { + // Regardless of whether we have a native file watcher and it works + // correctly on this file system, poll prioritized files (that is, + // files that have been changed at least once) at a higher frequency + // (every 500ms by default). return NO_WATCHER_POLLING_INTERVAL; } - function fire(event: string) { - if (event !== "change") { - // When we receive a "delete" or "rename" event, the watcher is - // probably not going to generate any more notifications for this - // file, so we close and nullify the watcher to ensure that - // entry.rewatch() will attempt to reestablish the watcher the next - // time we call safeWatcher.watch. - safeUnwatch(); + if (watcherEnabled || PRIORITIZE_CHANGED) { + // As long as native file watching is enabled (even if it doesn't + // work correctly) and the developer hasn't explicitly opted out of + // the file watching priority system, poll unchanged files at a + // lower frequency (every 5000ms by default). + return DEFAULT_POLLING_INTERVAL; + } - // Make sure we don't throttle the watchFile callback after a - // "delete" or "rename" event, since it is now our only reliable - // source of file change notifications. - lastWatcherEventTime = 0; + // If native file watching is disabled and the developer has + // explicitly opted out of the priority system, poll everything at the + // higher frequency (every 500ms by default). Note that this leads to + // higher idle CPU usage, so the developer may want to adjust the + // METEOR_WATCH_POLLING_INTERVAL_MS environment variable. + return NO_WATCHER_POLLING_INTERVAL; +} - } else { +function startPolling(absPath: string, callback: ChangeCallback): SafeWatcher { + const osPath = convertToOSPath(absPath); + // Initial polling interval. + let interval = getPollingInterval(absPath); + const pollCallback = (curr: Stats, prev: Stats) => { + // Compare modification times to detect a change. + if (+curr.mtime !== +prev.mtime) { changedPaths.add(absPath); - rewatch(); + callback("change"); } - - callbacks.forEach(cb => cb(event)); - } - - function watchWrapper(event: string) { - lastWatcherEventTime = Date.now(); - fire(event); - - // It's tempting to call unwatchFile(absPath, watchFileWrapper) here, - // but previous watcher success is no guarantee of future watcher - // reliability. For example, watchLibrary.watch works just fine when file - // changes originate from within a Vagrant VM, but changes to shared - // files made outside the VM are invisible to watcher, so our only - // hope of catching them is to continue polling. - } - - function rewatch() { - if (hasPriority(absPath)) { - if (watcher) { - // Already watching; nothing to do. - return; - } - watcher = watchLibraryWatch(absPath, watchWrapper); - } else if (watcher) { - safeUnwatch(); - } - - // Since we're about to restart the stat-based file watcher, we don't - // want to miss any of its events because of the lastWatcherEventTime - // throttling that it attempts to do. - lastWatcherEventTime = 0; - - // We use files.watchFile in addition to watcher.watch as a fail-safe - // to detect file changes even on network file systems. However - // (unless the user disabled watcher or this watcher call failed), we - // use a relatively long default polling interval of 5000ms to save - // CPU cycles. - statWatch(absPath, getPollingInterval(), watchFileWrapper); - } - - function watchFileWrapper(newStat: Stats, oldStat: Stats) { - if (newStat.ino === 0 && - oldStat.ino === 0 && - +newStat.mtime === +oldStat.mtime) { - // Node calls the watchFile listener once with bogus identical stat - // objects, which should not trigger a file change event. - return; - } - - // If a watcher event fired in the last polling interval, ignore - // this event. - if (Date.now() - lastWatcherEventTime > getPollingInterval()) { - fire("change"); - } - } - - const entry = { - callbacks, - rewatch, - - release(callback: EntryCallback) { - if (! entries[absPath]) { - return; - } - - callbacks.delete(callback); - if (callbacks.size > 0) { - return; - } - - // Once there are no more callbacks in the Set, close both watchers - // and nullify the shared data. - if (watcherCleanupTimer) { - clearTimeout(watcherCleanupTimer); - } - - watcherCleanupTimer = setTimeout(() => { - if (callbacks.size > 0) { - // If another callback was added while the timer was pending, we - // can avoid tearing anything down. - return; - } - entry.close(); - }, WATCHER_CLEANUP_DELAY_MS); - }, - - close() { - if (entries[absPath] !== entry) return; - entries[absPath] = null; - - if (watcherCleanupTimer) { - clearTimeout(watcherCleanupTimer); - watcherCleanupTimer = null; - } - - safeUnwatch(); - - unwatchFile(absPath, watchFileWrapper); - }, - _fire: fire }; - - if (stat && stat.ino > 0) { - entriesByIno.set(stat.ino, entry); - } - - return entry; -} - -export function closeAllWatchers() { - Object.keys(entries).forEach(absPath => { - const entry = entries[absPath]; - if (entry) { - entry.close(); + watchFile(osPath, { interval }, pollCallback); + return { + close() { + unwatchFile(osPath, pollCallback); + changedPaths.delete(absPath); } - }); + }; } -const statWatchers = Object.create(null); - -function statWatch( - absPath: string, - interval: number, - callback: (current: Stats, previous: Stats) => void, -) { - let statWatcher = statWatchers[absPath]; - - if (!statWatcher) { - statWatcher = { - interval, - changeListeners: [], - stat: null - }; - statWatchers[absPath] = statWatcher; - } - - // If the interval needs to be changed, replace the watcher. - // Node will only recreate the watcher with the new interval if all old - // watchers are stopped (which unwatchFile does when not passed a - // specific listener) - if (statWatcher.interval !== interval && statWatcher.stat) { - // This stops all stat watchers for the file, not just those created by - // statWatch - unwatchFile(absPath); - statWatcher.stat = null; - statWatcher.interval = interval; - } - - if (!statWatcher.changeListeners.includes(callback)) { - statWatcher.changeListeners.push(callback); - } - - if (!statWatcher.stat) { - const newStat = watchFile(absPath, { - persistent: false, // never persistent - interval, - }, (newStat, oldStat) => { - statWatcher.changeListeners.forEach(( - listener: (newStat: Stats, oldStat: Stats) => void - ) => { - listener(newStat, oldStat); - }); - }); - - newStat.on("stop", () => { - if (statWatchers[absPath] === statWatch) { - delete statWatchers[absPath]; - } - }); - - statWatcher.stat = newStat; - } - - return statWatcher; -} - -function watchLibraryWatch(absPath: string, callback: EntryCallback) { - if (watcherEnabled && watcherLibrary === 'pathwatcher') { - try { - return pathwatcher.watch(convertToOSPath(absPath), callback); - } catch (e: any) { - maybeSuggestRaisingWatchLimit(e); - // ... ignore the error. We'll still have watchFile, which is good - // enough. - } - } - - return null; -} - -let suggestedRaisingWatchLimit = false; - -function maybeSuggestRaisingWatchLimit(error: Error & { errno: number }) { - var constants = require('constants'); - var archinfo = require('../utils/archinfo'); - if (! suggestedRaisingWatchLimit && - // Note: the not-super-documented require('constants') maps from - // strings to SYSTEM errno values. System errno values aren't the same - // as the numbers used internally by libuv! Once we're upgraded - // to Node 0.12, we'll have the system errno as a string (on 'code'), - // but the support for that wasn't in Node 0.10's uv. - // See our PR https://github.com/atom/node-pathwatcher/pull/53 - // (and make sure to read the final commit message, not the original - // proposed PR, which had a slightly different interface). - error.errno === constants.ENOSPC && - // The only suggestion we currently have is for Linux. - archinfo.matches(archinfo.host(), 'os.linux')) { - - // Check suggestedRaisingWatchLimit again because archinfo.host() may - // have yielded. - if (suggestedRaisingWatchLimit) return; - suggestedRaisingWatchLimit = true; - - var Console = require('../console/console.js').Console; - if (! Console.isHeadless()) { - Console.arrowWarn( - "It looks like a simple tweak to your system's configuration will " + - "make many tools (including this Meteor command) more efficient. " + - "To learn more, see " + - Console.url("https://github.com/meteor/docs/blob/master/long-form/file-change-watcher-efficiency.md")); - } +/** + * Fall back to polling. If a critical error occurs, + * we disable native watching and close all existing native watchers. + */ +function fallbackToPolling() { + if (watcherEnabled) { + console.error("Critical native watcher error encountered. Falling back to polling for all entries."); + watcherEnabled = false; + closeAllWatchers(); } } - -export const watch = Profile( - "safeWatcher.watch", - (absPath: string, callback: EntryCallback) => { - const entry = acquireWatcher(absPath, callback); - return { - close() { - entry.release(callback); - } - } as SafeWatcher; - } -); - -const fireNames = { - [nsfw.actions.CREATED]: 'change', - [nsfw.actions.MODIFIED]: 'change', - [nsfw.actions.DELETED]: 'delete' -} - -export function addWatchRoot(absPath: string) { - if (watchRoots.has(absPath) || watcherLibrary !== 'nsfw' || !watcherEnabled) { - return; - } - - watchRoots.add(absPath); - - // If there already is a watcher for a parent directory, there is no need - // to create this watcher. - for (const path of watchRoots) { - let relativePath = pathRelative(path, absPath); - if ( - path !== absPath && - !relativePath.startsWith('..') && - !relativePath.startsWith('/') - ) { - return; - } - } - - // TODO: check if there are any existing watchers that are children of this - // watcher and stop them - - nsfw( - convertToOSPath(absPath), - (events) => { - events.forEach(event => { - if(event.action === nsfw.actions.RENAMED) { - let oldPath = nativeJoin(event.directory, event.oldFile); - let oldEntry = entries[toPosixPath(oldPath)]; - if (oldEntry) { - oldEntry._fire('rename'); - } - - let path = nativeJoin(event.newDirectory, event.newFile); - let newEntry = entries[toPosixPath(path)]; - if (newEntry) { - newEntry._fire('change'); - } - } else { - let path = nativeJoin(event.directory, event.file); - let entry = entries[toPosixPath(path)]; - if (entry) { - entry._fire(fireNames[event.action]); - } - } - }) - } - ).then(watcher => { - watcher.start() - }); -} From 95c59bed286caafe98d147bf76e8cf614bfaa3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 16 Apr 2025 11:54:47 +0200 Subject: [PATCH 101/264] bump bundle version --- meteor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor b/meteor index b743dccaa9..b86a040689 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.10 +BUNDLE_VERSION=22.14.0.11 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. From 04477d52dc742bc1c54d564bf8107174401742b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 16 Apr 2025 12:04:12 +0200 Subject: [PATCH 102/264] re-run checks From 3763bea0e99130fcb7e3e1f7e28487515921f3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 14:00:35 +0200 Subject: [PATCH 103/264] don't use transactions for sqlite read queries --- tools/cli/commands.js | 1 - tools/packaging/catalog/catalog-remote.js | 70 ++++++++++++++--------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index c44a026cd7..130b51df25 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -473,7 +473,6 @@ main.registerCommand(Object.assign( async function doRunCommand(options) { Console.setVerbose(!!options.verbose); - // Additional args are interpreted as run targets const runTargets = parseRunTargets(options.args); diff --git a/tools/packaging/catalog/catalog-remote.js b/tools/packaging/catalog/catalog-remote.js index e40be5086c..643dedc7f5 100644 --- a/tools/packaging/catalog/catalog-remote.js +++ b/tools/packaging/catalog/catalog-remote.js @@ -441,9 +441,9 @@ Object.assign(Table.prototype, { return "(" + _.times(n, function () { return "?" }).join(",") + ")"; }, - find: async function (txn, id) { + find: async function (id) { var self = this; - var rows = await txn.query(self._selectQuery, [ id ]); + var rows = await self.db._query(self._selectQuery, [ id ]); if (rows.length !== 0) { if (rows.length !== 1) { throw new Error("Corrupt database (PK violation)"); @@ -812,24 +812,44 @@ Object.assign(RemoteCatalog.prototype, { // Given a release track, returns all recommended version *records* for this // track, sorted by their orderKey. Returns the empty array if the release // track does not exist or does not have any recommended versions. + // getSortedRecommendedReleaseRecords: async function (track, laterThanOrderKey) { + // var self = this; + // // XXX releaseVersions content objects are kinda big; if we put + // // 'recommended' and 'orderKey' in their own columns this could be faster + // var result = await self._contentQuery( + // "SELECT content FROM releaseVersions WHERE track=?", track); + // + // var recommended = _.filter(result, function (v) { + // if (!v.recommended) + // return false; + // return !laterThanOrderKey || v.orderKey > laterThanOrderKey; + // }); + // + // var recSort = _.sortBy(recommended, function (rec) { + // return rec.orderKey; + // }); + // recSort.reverse(); + // return recSort; + // }, + getSortedRecommendedReleaseRecords: async function (track, laterThanOrderKey) { - var self = this; - // XXX releaseVersions content objects are kinda big; if we put - // 'recommended' and 'orderKey' in their own columns this could be faster - var result = await self._contentQuery( - "SELECT content FROM releaseVersions WHERE track=?", track); + const hasMinKey = laterThanOrderKey != null; - var recommended = _.filter(result, function (v) { - if (!v.recommended) - return false; - return !laterThanOrderKey || v.orderKey > laterThanOrderKey; - }); + // Always use JSON1 to filter & sort directly in SQL + const sql = ` + SELECT content + FROM releaseVersions + WHERE track = ? + AND json_extract(content, '$.recommended') = 1 + ${hasMinKey ? "AND json_extract(content, '$.orderKey') > ?" : ""} + ORDER BY json_extract(content, '$.orderKey') DESC + `; + const params = hasMinKey + ? [track, laterThanOrderKey] + : [track]; - var recSort = _.sortBy(recommended, function (rec) { - return rec.orderKey; - }); - recSort.reverse(); - return recSort; + // _contentQuery will JSON.parse(content) for you + return this._contentQuery(sql, params); }, // Given a release track, returns all version records for this track. @@ -881,9 +901,9 @@ Object.assign(RemoteCatalog.prototype, { }, // Executes a query, returning an array of each content column parsed as JSON - _contentQuery: async function (query, params) { + _contentQuery: async function (query, params, transaction = false) { var self = this; - var rows = await self._columnsQuery(query, params); + var rows = await self._columnsQuery(query, params, transaction); return _.map(rows, function(entity) { return JSON.parse(entity.content); }); @@ -893,9 +913,7 @@ Object.assign(RemoteCatalog.prototype, { // No JSON parsing is performed. _columnsQuery: async function (query, params) { var self = this; - var rows = await self.db.runInTransaction(function (txn) { - return txn.query(query, params); - }); + var rows = await self.db._query(query, params); return rows; }, @@ -946,9 +964,7 @@ Object.assign(RemoteCatalog.prototype, { getMetadata: async function(key) { var self = this; - var row = await self.db.runInTransaction(function (txn) { - return self.tableMetadata.find(txn, key); - }); + var row = await self.tableMetadata.find(key); if (row) { return JSON.parse(row['content']); } @@ -970,9 +986,7 @@ Object.assign(RemoteCatalog.prototype, { shouldShowBanner: async function (releaseName, bannerDate) { var self = this; - var row = await self.db.runInTransaction(function (txn) { - return self.tableBannersShown.find(txn, releaseName); - }); + var row = await self.tableBannersShown.find(releaseName); // We've never printed a banner for this release. if (! row) return true; From 2ca305cd63a6cb20d3d7054b9dda89e52c7b7ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 14:03:17 +0200 Subject: [PATCH 104/264] clean --- tools/packaging/catalog/catalog-remote.js | 24 ++--------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/tools/packaging/catalog/catalog-remote.js b/tools/packaging/catalog/catalog-remote.js index 643dedc7f5..ca78109f1a 100644 --- a/tools/packaging/catalog/catalog-remote.js +++ b/tools/packaging/catalog/catalog-remote.js @@ -812,26 +812,6 @@ Object.assign(RemoteCatalog.prototype, { // Given a release track, returns all recommended version *records* for this // track, sorted by their orderKey. Returns the empty array if the release // track does not exist or does not have any recommended versions. - // getSortedRecommendedReleaseRecords: async function (track, laterThanOrderKey) { - // var self = this; - // // XXX releaseVersions content objects are kinda big; if we put - // // 'recommended' and 'orderKey' in their own columns this could be faster - // var result = await self._contentQuery( - // "SELECT content FROM releaseVersions WHERE track=?", track); - // - // var recommended = _.filter(result, function (v) { - // if (!v.recommended) - // return false; - // return !laterThanOrderKey || v.orderKey > laterThanOrderKey; - // }); - // - // var recSort = _.sortBy(recommended, function (rec) { - // return rec.orderKey; - // }); - // recSort.reverse(); - // return recSort; - // }, - getSortedRecommendedReleaseRecords: async function (track, laterThanOrderKey) { const hasMinKey = laterThanOrderKey != null; @@ -901,9 +881,9 @@ Object.assign(RemoteCatalog.prototype, { }, // Executes a query, returning an array of each content column parsed as JSON - _contentQuery: async function (query, params, transaction = false) { + _contentQuery: async function (query, params) { var self = this; - var rows = await self._columnsQuery(query, params, transaction); + var rows = await self._columnsQuery(query, params); return _.map(rows, function(entity) { return JSON.parse(entity.content); }); From 3df0e26258ee10b11fbf96db142b50cbaa4efe1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 15:05:05 +0200 Subject: [PATCH 105/264] clean --- tools/cli/commands.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 130b51df25..c44a026cd7 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -473,6 +473,7 @@ main.registerCommand(Object.assign( async function doRunCommand(options) { Console.setVerbose(!!options.verbose); + // Additional args are interpreted as run targets const runTargets = parseRunTargets(options.args); From a18fadef80e42d7af848259bbf67011ed7fad66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 16:28:13 +0200 Subject: [PATCH 106/264] fix issue with find operation on table, make sure to pass the db --- tools/packaging/catalog/catalog-remote.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/packaging/catalog/catalog-remote.js b/tools/packaging/catalog/catalog-remote.js index ca78109f1a..85b0fefc12 100644 --- a/tools/packaging/catalog/catalog-remote.js +++ b/tools/packaging/catalog/catalog-remote.js @@ -441,9 +441,9 @@ Object.assign(Table.prototype, { return "(" + _.times(n, function () { return "?" }).join(",") + ")"; }, - find: async function (id) { + find: async function (db, id) { var self = this; - var rows = await self.db._query(self._selectQuery, [ id ]); + var rows = await db._query(self._selectQuery, [ id ]); if (rows.length !== 0) { if (rows.length !== 1) { throw new Error("Corrupt database (PK violation)"); @@ -944,7 +944,7 @@ Object.assign(RemoteCatalog.prototype, { getMetadata: async function(key) { var self = this; - var row = await self.tableMetadata.find(key); + var row = await self.tableMetadata.find(self.db, key); if (row) { return JSON.parse(row['content']); } @@ -966,7 +966,7 @@ Object.assign(RemoteCatalog.prototype, { shouldShowBanner: async function (releaseName, bannerDate) { var self = this; - var row = await self.tableBannersShown.find(releaseName); + var row = await self.tableBannersShown.find(self.db, releaseName); // We've never printed a banner for this release. if (! row) return true; From 2b2300639c7ae884f5c00bdb4a52659ff72ffbcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 17:52:41 +0200 Subject: [PATCH 107/264] adapt code to support lru and intent to solve tests breaking --- tools/fs/safe-watcher.ts | 51 +++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 5f521fb024..b0e4165bef 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -1,6 +1,7 @@ import { Stats } from 'fs'; import { dirname } from "path"; import ParcelWatcher from "@parcel/watcher"; +import LRUCache from 'lru-cache'; import { Profile } from "../tool-env/profile"; import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; @@ -14,8 +15,39 @@ interface Entry extends SafeWatcher { _fire(event: string): void; } +// Set METEOR_WATCH_USE_LRU environment variable to a truthy value to +// enable LRU caching for watcher entries to reduce memory usage. +const useLRU = Boolean(JSON.parse(process.env.METEOR_WATCH_USE_LRU || "true")); + // Registry mapping normalized absolute paths to their watcher entry. -const entries: Record = Object.create(null); +// If LRU caching is enabled, this will be an LRUCache instance. +// Otherwise, we use a Map object for entries. +const entries = useLRU + ? new LRUCache({ + max: Math.pow(2, 20), // 1MB max size + length: (entry, key) => { + return key.length + (entry ? 100 : 10); + }, + dispose: (key, entry) => { + return entry.close() + }, + }) + : new Map(); + +function getEntry(path: string): Entry | null | undefined { + return entries.get(path); +} + +function setEntry(path: string, entry: Entry | null): void { + entries.set(path, entry); +} + +function deleteEntry(path: string): void { + if (useLRU) { + return entries.del(path); + } + return entries.delete(path); +} // Watch roots are directories for which we have an active ParcelWatcher subscription. const watchRoots = new Set(); @@ -150,7 +182,7 @@ async function ensureWatchRoot(dirPath: string): Promise { // Dispatch each event to any registered entries. for (const event of events) { const changedPath = toPosixPath(event.path); - const entry = entries[changedPath]; + const entry = getEntry(changedPath); if (!entry) continue; // In Meteor's safe-watcher API, both create/update trigger "change" events. const evtType = event.type === "delete" ? "delete" : "change"; @@ -192,7 +224,7 @@ function startNewEntry(absPath: string): Entry { close() { if (closed) return; closed = true; - delete entries[absPath]; + deleteEntry(absPath); }, _fire(event: string) { callbacks.forEach(cb => { @@ -228,10 +260,10 @@ export const watch = Profile( return startPolling(absPath, callback); } // Try to reuse an existing entry if one was created before. - let entry = entries[absPath]; + let entry = getEntry(absPath); if (!entry) { entry = startNewEntry(absPath); - entries[absPath] = entry; + setEntry(absPath, entry); // Determine the directory that should be watched. let watchTarget: string; try { @@ -247,10 +279,11 @@ export const watch = Profile( entry.callbacks.add(callback); return { close() { - if (entries[absPath]) { - entries[absPath]!.callbacks.delete(callback); - if (entries[absPath]!.callbacks.size === 0) { - entries[absPath]!.close(); + const entry = getEntry(absPath); + if (entry) { + entry.callbacks.delete(callback); + if (entry.callbacks.size === 0) { + entry.close(); } } } From d6c1ecacb7dd2c641efb1089ebdf5a3e27535381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 18:21:08 +0200 Subject: [PATCH 108/264] dont activate lru by default --- tools/fs/safe-watcher.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index b0e4165bef..659e3fc54c 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -17,19 +17,22 @@ interface Entry extends SafeWatcher { // Set METEOR_WATCH_USE_LRU environment variable to a truthy value to // enable LRU caching for watcher entries to reduce memory usage. -const useLRU = Boolean(JSON.parse(process.env.METEOR_WATCH_USE_LRU || "true")); +const useLRU = Boolean(JSON.parse(process.env.METEOR_WATCH_USE_LRU || "false")); + +// enable to tweak maximum entries size (default 500MB). +const sizeMaxLRU = JSON.parse(process.env.METEOR_WATCH_USE_LRU_SIZE || Math.pow(2, 20) * 500); // Registry mapping normalized absolute paths to their watcher entry. // If LRU caching is enabled, this will be an LRUCache instance. // Otherwise, we use a Map object for entries. const entries = useLRU ? new LRUCache({ - max: Math.pow(2, 20), // 1MB max size + max: sizeMaxLRU, // 1MB max size length: (entry, key) => { return key.length + (entry ? 100 : 10); }, dispose: (key, entry) => { - return entry.close() + return entry.close(); }, }) : new Map(); From cca65e3751f77e17c73719aeacd1e0b2b99f3a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 18:29:20 +0200 Subject: [PATCH 109/264] fix precheck --- tools/fs/safe-watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 659e3fc54c..f7a2c86296 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -32,7 +32,7 @@ const entries = useLRU return key.length + (entry ? 100 : 10); }, dispose: (key, entry) => { - return entry.close(); + return entry?.close(); }, }) : new Map(); From d6f52c3e77df065cb924970cd7c8a8688f6f2095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 19:16:28 +0200 Subject: [PATCH 110/264] use polling on CI as using a linux env with inotify backend --- .circleci/config.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index be2fe2cae4..65b5920634 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,6 +115,16 @@ build_machine_environment: NUM_GROUPS: 12 RUNNING_AVG_LENGTH: 6 + # Force Meteor to use polling to avoid errors like: + # "double free or corruption (out) IOT instruction" + # Happens on Linux when using the inotify backend. + # Doesn't affect watching files with @parcel/watch, + # but causes Meteor commands to exit errored. + # This impacts CI pipelines. + # Using Watchman can fix the issue as well. + METEOR_WATCH_FORCE_POLLING: true + METEOR_WATCH_POLLING_INTERVAL_MS: 1000 + jobs: Get Ready: <<: *build_machine_environment From fe36f5ed7088d07eaf19e78ac3a1767830557eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Mon, 21 Apr 2025 19:21:48 +0200 Subject: [PATCH 111/264] fix --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 65b5920634..7dace8fe26 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -117,7 +117,7 @@ build_machine_environment: # Force Meteor to use polling to avoid errors like: # "double free or corruption (out) IOT instruction" - # Happens on Linux when using the inotify backend. + # Happens on Linux when using the fts (brute force) backend. # Doesn't affect watching files with @parcel/watch, # but causes Meteor commands to exit errored. # This impacts CI pipelines. From ab067c7e7800a634b687857783426c0e1cdc8825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 22 Apr 2025 16:39:53 +0200 Subject: [PATCH 112/264] fix issue with memory by ensure unsubscribing all watchers --- .circleci/config.yml | 10 ---------- tools/fs/safe-watcher.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7dace8fe26..be2fe2cae4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,16 +115,6 @@ build_machine_environment: NUM_GROUPS: 12 RUNNING_AVG_LENGTH: 6 - # Force Meteor to use polling to avoid errors like: - # "double free or corruption (out) IOT instruction" - # Happens on Linux when using the fts (brute force) backend. - # Doesn't affect watching files with @parcel/watch, - # but causes Meteor commands to exit errored. - # This impacts CI pipelines. - # Using Watchman can fix the issue as well. - METEOR_WATCH_FORCE_POLLING: true - METEOR_WATCH_POLLING_INTERVAL_MS: 1000 - jobs: Get Ready: <<: *build_machine_environment diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index f7a2c86296..6834975f03 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -6,6 +6,43 @@ import LRUCache from 'lru-cache'; import { Profile } from "../tool-env/profile"; import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; +// Register process exit handlers to ensure subscriptions are properly cleaned up +const registerExitHandlers = () => { + + // For SIGINT and SIGTERM, we need to handle the async cleanup before the process exits + const cleanupAndExit = (signal: string) => { + // Clear the timeout if cleanup completes successfully + closeAllWatchers().then(() => { + process.exit(0); + }).catch(err => { + console.error(`Error closing watchers on ${signal}:`, err); + process.exit(1); + }); + }; + + // Handle SIGINT (Ctrl+C) + process.on('SIGINT', () => cleanupAndExit('SIGINT')); + + // Handle SIGTERM + process.on('SIGTERM', () => cleanupAndExit('SIGTERM')); + + // Handle 'exit' event + process.on('exit', () => { + try { + for (const root of Array.from(watchRoots)) { + const sub = dirSubscriptions.get(root); + if (sub) { + sub.unsubscribe(); + dirSubscriptions.delete(root); + watchRoots.delete(root); + } + } + } catch (err) { + console.error('Error during synchronous cleanup on exit:', err); + } + }); +}; + export type SafeWatcher = { close: () => void; }; type ChangeCallback = (event: string) => void; @@ -398,3 +435,6 @@ function fallbackToPolling() { closeAllWatchers(); } } + +// Register exit handlers to ensure proper cleanup of subscriptions +registerExitHandlers(); From efe346f3fa869fd3df6e69286eb002fd790ac0a8 Mon Sep 17 00:00:00 2001 From: italo jose Date: Sat, 29 Mar 2025 07:36:37 -0300 Subject: [PATCH 113/264] builder: removing files async --- tools/isobuild/builder.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js index 563d47bf01..997872d60b 100644 --- a/tools/isobuild/builder.js +++ b/tools/isobuild/builder.js @@ -126,7 +126,21 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` async init() { // Build the output from scratch if (this.resetBuildPath) { - await files.rm_recursive(this.buildPath); + // Generate a temp path name for the old build directory + const oldBuildPath = this.buildPath + '.old-' + Math.floor(Math.random() * 999999); + + // If the original buildPath exists, rename it first + if (files.exists(this.buildPath)) { + await files.rename(this.buildPath, oldBuildPath); + + // Start deletion of old directory asynchronously without awaiting + files.rm_recursive(oldBuildPath).catch(e => { + // Log error but don't fail the build + console.error(`Error removing old build directory ${oldBuildPath}:`, e); + }); + } + + // Create the new build directory immediately without waiting for deletion await files.mkdir_p(this.buildPath, 0o755); } this.watchSet = new WatchSet(); From a53781d8526ba65d010be9f75647313a514f765d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 22 Apr 2025 17:32:59 +0200 Subject: [PATCH 114/264] apply broadly the rm_recursive_deferred strategy --- tools/cli/commands.js | 2 +- tools/fs/files.ts | 14 ++++++++++++++ tools/isobuild/builder.js | 25 +++++++------------------ tools/isobuild/compiler-plugin.js | 2 +- tools/isobuild/isopack-cache.js | 10 +++++----- tools/isobuild/meteor-npm.js | 14 +++++++------- tools/project-context.js | 2 +- tools/runners/run-mongo.js | 2 +- 8 files changed, 37 insertions(+), 34 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index c44a026cd7..9436a44c97 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -1601,7 +1601,7 @@ https://guide.meteor.com/cordova.html#submitting-android }); } - await files.rm_recursive(buildDir); + await files.rm_recursive_deferred(buildDir); const npmShrinkwrapFilePath = files.pathJoin(bundlePath, 'programs/server/npm-shrinkwrap.json'); if (files.exists(npmShrinkwrapFilePath)) { diff --git a/tools/fs/files.ts b/tools/fs/files.ts index cc9e0c8ee7..1babe78416 100644 --- a/tools/fs/files.ts +++ b/tools/fs/files.ts @@ -358,6 +358,20 @@ export const rm_recursive = Profile("files.rm_recursive", async (path: string) = } }); +export const rm_recursive_deferred = Profile("files.rm_recursive_deferred", async (path: string) => { + // Generate a temp path name for the old build directory + const oldBuildPath = path + '.old-' + Math.floor(Math.random() * 999999); + // If the original buildPath exists, rename it first + if (exists(path)) { + await rename(path, oldBuildPath); + // Start deletion of old directory asynchronously without awaiting + rm_recursive(oldBuildPath).catch(e => { + // Log error but don't fail the build + console.error(`Error removing old build directory ${oldBuildPath}:`, e); + }); + } +}); + // Returns the base64 SHA256 of the given file. export function fileHash(filename: string) { const crypto = require('crypto'); diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js index 997872d60b..edf1174049 100644 --- a/tools/isobuild/builder.js +++ b/tools/isobuild/builder.js @@ -1,7 +1,7 @@ import assert from "assert"; import {WatchSet, readAndWatchFile, sha1} from '../fs/watch'; import files, { - symlinkWithOverwrite, realpath, + symlinkWithOverwrite, realpath, rm_recursive_deferred, } from '../fs/files'; import NpmDiscards from './npm-discards'; import {Profile} from '../tool-env/profile'; @@ -126,20 +126,9 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` async init() { // Build the output from scratch if (this.resetBuildPath) { - // Generate a temp path name for the old build directory - const oldBuildPath = this.buildPath + '.old-' + Math.floor(Math.random() * 999999); - - // If the original buildPath exists, rename it first - if (files.exists(this.buildPath)) { - await files.rename(this.buildPath, oldBuildPath); - - // Start deletion of old directory asynchronously without awaiting - files.rm_recursive(oldBuildPath).catch(e => { - // Log error but don't fail the build - console.error(`Error removing old build directory ${oldBuildPath}:`, e); - }); - } - + console.time("--> (builder.js-Line: 130)\n await files.rm_recursive_deferred: "); + await files.rm_recursive_deferred(this.buildPath); + console.timeEnd("--> (builder.js-Line: 130)\n await files.rm_recursive_deferred: "); // Create the new build directory immediately without waiting for deletion await files.mkdir_p(this.buildPath, 0o755); } @@ -895,7 +884,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` removed[path] = true; } else { // directory - await files.rm_recursive(absPath); + await files.rm_recursive_deferred(absPath); // mark all sub-paths as removed, too paths.forEach((anotherPath) => { @@ -918,7 +907,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` // Delete the partially-completed bundle. Do not disturb outputPath. abort() { - return files.rm_recursive(this.buildPath); + return files.rm_recursive_deferred(this.buildPath); } // Returns a WatchSet representing all files that were read from disk by the @@ -950,7 +939,7 @@ async function atomicallyRewriteFile(path, data, options) { // replacing a directory with a file; this is rare (so it can // be a slow path) but can legitimately happen if e.g. a developer // puts a file where there used to be a directory in their app. - await files.rm_recursive(path); + await files.rm_recursive_deferred(path); files.rename(rpath, path); } else { throw e; diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js index f2a52e6373..6eaa2f332c 100644 --- a/tools/isobuild/compiler-plugin.js +++ b/tools/isobuild/compiler-plugin.js @@ -1832,7 +1832,7 @@ export class PackageSourceBatch { if (cacheFilename) { // Write asynchronously. try { - await files.rm_recursive(wildcardCacheFilename); + await files.rm_recursive_deferred(wildcardCacheFilename); } finally { await files.writeFileAtomically(cacheFilename, retAsJSON); } diff --git a/tools/isobuild/isopack-cache.js b/tools/isobuild/isopack-cache.js index 3ae3104dab..850db04166 100644 --- a/tools/isobuild/isopack-cache.js +++ b/tools/isobuild/isopack-cache.js @@ -83,19 +83,19 @@ export class IsopackCache { // Wipe specific packages. for (const packageName of packages) { if (self.cacheDir) { - await files.rm_recursive(self._isopackDir(packageName)); + await files.rm_recursive_deferred(self._isopackDir(packageName)); } if (self._pluginCacheDirRoot) { - await files.rm_recursive(self._pluginCacheDirForPackage(packageName)); + await files.rm_recursive_deferred(self._pluginCacheDirForPackage(packageName)); } } } else { // Wipe all packages. if (self.cacheDir) { - await files.rm_recursive(self.cacheDir); + await files.rm_recursive_deferred(self.cacheDir); } if (self._pluginCacheDirRoot) { - await files.rm_recursive(self._pluginCacheDirRoot); + await files.rm_recursive_deferred(self._pluginCacheDirRoot); } } } @@ -351,7 +351,7 @@ export class IsopackCache { } else { // Nope! Compile it again. Give it a fresh plugin cache. if (pluginCacheDir) { - await files.rm_recursive(pluginCacheDir); + await files.rm_recursive_deferred(pluginCacheDir); files.mkdir_p(pluginCacheDir); } diff --git a/tools/isobuild/meteor-npm.js b/tools/isobuild/meteor-npm.js index f051abc634..d1ebe42341 100644 --- a/tools/isobuild/meteor-npm.js +++ b/tools/isobuild/meteor-npm.js @@ -87,7 +87,7 @@ meteorNpm.updateDependencies = async function (packageName, // It didn't exist, which is exactly what we wanted. return false; } - await files.rm_recursive(newPackageNpmDir); + await files.rm_recursive_deferred(newPackageNpmDir); return false; } @@ -102,7 +102,7 @@ meteorNpm.updateDependencies = async function (packageName, // proceed. if (files.exists(packageNpmDir) && ! files.exists(files.pathJoin(packageNpmDir, 'npm-shrinkwrap.json'))) { - await files.rm_recursive(packageNpmDir); + await files.rm_recursive_deferred(packageNpmDir); } // with the changes on npm 8, where there were changes to how the packages metadata is given @@ -114,7 +114,7 @@ meteorNpm.updateDependencies = async function (packageName, files.pathJoin(packageNpmDir, 'npm-shrinkwrap.json') )); if (shrinkwrap.lockfileVersion !== LOCK_FILE_VERSION) { - await files.rm_recursive(packageNpmDir); + await files.rm_recursive_deferred(packageNpmDir); } } catch (e) {} } @@ -143,7 +143,7 @@ meteorNpm.updateDependencies = async function (packageName, throw e; } finally { if (files.exists(newPackageNpmDir)) { - await files.rm_recursive(newPackageNpmDir); + await files.rm_recursive_deferred(newPackageNpmDir); } tmpDirs = _.without(tmpDirs, newPackageNpmDir); } @@ -384,7 +384,7 @@ Profile("meteorNpm.rebuildIfNonPortable", async function (nodeModulesDir) { const rebuildResult = await runNpmCommand(getRebuildArgs(), tempDir); if (! rebuildResult.success) { buildmessage.error(rebuildResult.error); - await files.rm_recursive(tempDir); + await files.rm_recursive_deferred(tempDir); return false; } @@ -420,7 +420,7 @@ Profile("meteorNpm.rebuildIfNonPortable", async function (nodeModulesDir) { await files.renameDirAlmostAtomically(tempPkgDirs[pkgPath], pkgPath); } - await files.rm_recursive(tempDir); + await files.rm_recursive_deferred(tempDir); return true; }); @@ -644,7 +644,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir, } if (oldNodeVersion !== currentNodeCompatibilityVersion()) { - await files.rm_recursive(nodeModulesDir); + await files.rm_recursive_deferred(nodeModulesDir); } } diff --git a/tools/project-context.js b/tools/project-context.js index 192b3558db..0dce0164eb 100644 --- a/tools/project-context.js +++ b/tools/project-context.js @@ -1664,7 +1664,7 @@ Object.assign(exports.ReleaseFile.prototype, { if (this.isCheckout()) { // Only create .meteor/local/dev_bundle if .meteor/release refers to // an actual release, and remove it otherwise. - await files.rm_recursive(devBundleLink); + await files.rm_recursive_deferred(devBundleLink); return; } diff --git a/tools/runners/run-mongo.js b/tools/runners/run-mongo.js index 3b8b8bf603..c9a7de7334 100644 --- a/tools/runners/run-mongo.js +++ b/tools/runners/run-mongo.js @@ -474,7 +474,7 @@ var launchMongo = async function(options) { if (options.multiple) { // This is only for testing, so we're OK with incurring the replset // setup on each startup. - await files.rm_recursive(dbPath); + await files.rm_recursive_deferred(dbPath); files.mkdir_p(dbPath, 0o755); } else if (portFile) { var portFileExists = false; From 79a91702e24d963b582cb4868355a68dcc7903df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 22 Apr 2025 17:34:02 +0200 Subject: [PATCH 115/264] clean logs --- tools/isobuild/builder.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js index edf1174049..8472f15f16 100644 --- a/tools/isobuild/builder.js +++ b/tools/isobuild/builder.js @@ -126,9 +126,7 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` async init() { // Build the output from scratch if (this.resetBuildPath) { - console.time("--> (builder.js-Line: 130)\n await files.rm_recursive_deferred: "); await files.rm_recursive_deferred(this.buildPath); - console.timeEnd("--> (builder.js-Line: 130)\n await files.rm_recursive_deferred: "); // Create the new build directory immediately without waiting for deletion await files.mkdir_p(this.buildPath, 0o755); } From 90cf947d524cf839ff79688e194371195cba4089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Tue, 22 Apr 2025 18:04:39 +0200 Subject: [PATCH 116/264] improve global includedWebArchs for later compilation --- tools/cli/commands.js | 6 ++++-- tools/isobuild/compiler.js | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index ad46ad573d..78d90cc834 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -545,7 +545,8 @@ async function doRunCommand(options) { } webArchs = filterWebArchs(webArchs, options['exclude-archs'], options.appDir, options); - global.webArchs = webArchs; + // Set the webArchs to include for compilation later + global.includedWebArchs = webArchs; const buildMode = options.production ? 'production' : 'development'; @@ -2514,7 +2515,8 @@ var runTestAppForPackages = async function (projectContext, options) { webArchs.push("web.cordova"); } buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs'], projectContext.appDirectory, options); - global.webArchs = buildOptions.webArchs; + // Set the webArchs to include for compilation later + global.includedWebArchs = buildOptions.webArchs; if (options.deploy) { // Run the constraint solver and build local packages. diff --git a/tools/isobuild/compiler.js b/tools/isobuild/compiler.js index 6fa2793186..1f3f2b15bd 100644 --- a/tools/isobuild/compiler.js +++ b/tools/isobuild/compiler.js @@ -150,7 +150,7 @@ compiler.compile = Profile(function (packageSource, options) { // Isopack#initFromPath). var isobuildFeatures = []; packageSource.architectures.forEach((sourceArch) => { - if (global.webArchs != null && ![...global.webArchs, 'os'].includes(sourceArch.arch)) return; + if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(sourceArch.arch)) return; sourceArch.uses.forEach((use) => { if (!use.weak && isIsobuildFeaturePackage(use.package) && isobuildFeatures.indexOf(use.package) === -1) { @@ -182,7 +182,7 @@ compiler.compile = Profile(function (packageSource, options) { if (architecture.arch === 'web.cordova' && ! includeCordovaUnibuild) { continue; } - if (global.webArchs != null && ![...global.webArchs, 'os'].includes(architecture.arch)) continue; + if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(architecture.arch)) continue; // TODO -> Maybe this withCache will bring some problems in other commands. await files.withCache(async () => { @@ -228,7 +228,7 @@ compiler.lint = Profile(function (packageSource, options) { && architecture.arch === 'web.cordova') { continue; } - if (global.webArchs != null && ![...global.webArchs, 'os'].includes(architecture.arch)) continue; + if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(architecture.arch)) continue; const unibuildWarnings = await lintUnibuild({ isopack: options.isopack, @@ -249,7 +249,7 @@ compiler.getMinifiers = async function (packageSource, options) { var minifiers = []; for (const architecture of packageSource.architectures) { - if (global.webArchs != null && ![...global.webArchs, 'os'].includes(architecture.arch)) continue; + if (global.includedWebArchs != null && ![...global.includedWebArchs, 'os'].includes(architecture.arch)) continue; var activePluginPackages = await getActivePluginPackages(options.isopack, { isopackCache: options.isopackCache, From e4a057cf7f27a41d49d2d5e3dba9c1c1819d9524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 08:58:02 +0200 Subject: [PATCH 117/264] ensure to mark a path as being subscribed beforehand to avoid parallel repeated subscriptions to the same folder context --- tools/fs/safe-watcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 6834975f03..f70c7aef3d 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -207,6 +207,7 @@ async function ensureWatchRoot(dirPath: string): Promise { // Set up ignore patterns to skip deep node_modules and .meteor/local cache const ignorePatterns = ["**/node_modules/**", "**/.meteor/local/**"]; try { + watchRoots.add(dirPath); const subscription = await ParcelWatcher.subscribe( osDirPath, (err, events) => { @@ -217,6 +218,7 @@ async function ensureWatchRoot(dirPath: string): Promise { if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { fallbackToPolling(); } + watchRoots.delete(dirPath); return; } // Dispatch each event to any registered entries. @@ -231,7 +233,6 @@ async function ensureWatchRoot(dirPath: string): Promise { }, { ignore: ignorePatterns } ); - watchRoots.add(dirPath); dirSubscriptions.set(dirPath, subscription); } catch (e: any) { if ( @@ -249,6 +250,7 @@ async function ensureWatchRoot(dirPath: string): Promise { fallbackToPolling(); } } + watchRoots.delete(dirPath); } } From a50f07b7802ab6ca08a057a91f980895784be0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 09:05:21 +0200 Subject: [PATCH 118/264] remove LRU on file replacement since finally was not needed to fix an issue --- tools/fs/safe-watcher.ts | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index f70c7aef3d..984470028c 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -1,7 +1,6 @@ import { Stats } from 'fs'; import { dirname } from "path"; import ParcelWatcher from "@parcel/watcher"; -import LRUCache from 'lru-cache'; import { Profile } from "../tool-env/profile"; import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; @@ -52,27 +51,8 @@ interface Entry extends SafeWatcher { _fire(event: string): void; } -// Set METEOR_WATCH_USE_LRU environment variable to a truthy value to -// enable LRU caching for watcher entries to reduce memory usage. -const useLRU = Boolean(JSON.parse(process.env.METEOR_WATCH_USE_LRU || "false")); - -// enable to tweak maximum entries size (default 500MB). -const sizeMaxLRU = JSON.parse(process.env.METEOR_WATCH_USE_LRU_SIZE || Math.pow(2, 20) * 500); - // Registry mapping normalized absolute paths to their watcher entry. -// If LRU caching is enabled, this will be an LRUCache instance. -// Otherwise, we use a Map object for entries. -const entries = useLRU - ? new LRUCache({ - max: sizeMaxLRU, // 1MB max size - length: (entry, key) => { - return key.length + (entry ? 100 : 10); - }, - dispose: (key, entry) => { - return entry?.close(); - }, - }) - : new Map(); +const entries = new Map(); function getEntry(path: string): Entry | null | undefined { return entries.get(path); @@ -83,10 +63,7 @@ function setEntry(path: string, entry: Entry | null): void { } function deleteEntry(path: string): void { - if (useLRU) { - return entries.del(path); - } - return entries.delete(path); + entries.delete(path); } // Watch roots are directories for which we have an active ParcelWatcher subscription. @@ -118,8 +95,7 @@ if (process.env.METEOR_WATCH_PRIORITIZE_CHANGED && } // Set of paths for which a change event has been fired, watched with -// watchLibrary.watch if available. This could be an LRU cache, but in -// practice it should never grow large enough for that to matter. +// watchLibrary.watch if available. const changedPaths = new Set; function shouldIgnorePath(absPath: string): boolean { From 2338850138108833ff9748d1f2dec6e67a6bc198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 10:04:24 +0200 Subject: [PATCH 119/264] watch properly add/delete events --- tools/fs/safe-watcher.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 984470028c..4da8a8fe2b 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -1,9 +1,8 @@ import { Stats } from 'fs'; -import { dirname } from "path"; import ParcelWatcher from "@parcel/watcher"; import { Profile } from "../tool-env/profile"; -import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile } from "./files"; +import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname } from "./files"; // Register process exit handlers to ensure subscriptions are properly cleaned up const registerExitHandlers = () => { @@ -66,6 +65,27 @@ function deleteEntry(path: string): void { entries.delete(path); } +function findNearestEntry(startPath: string): Entry | null { + let currentPath = pathResolve(startPath); + + while (true) { + const entry = getEntry(currentPath); + if (entry) { + return entry; // Found it! + } + + const parentPath = pathDirname(currentPath); + if (parentPath === currentPath) { + // Reached root + break; + } + + currentPath = parentPath; + } + + return null; +} + // Watch roots are directories for which we have an active ParcelWatcher subscription. const watchRoots = new Set(); // For each watch root, store its active subscription. @@ -187,6 +207,7 @@ async function ensureWatchRoot(dirPath: string): Promise { const subscription = await ParcelWatcher.subscribe( osDirPath, (err, events) => { + console.log("--> (safe-watcher.ts-Line: 212) events: ", events); if (err) { console.error(`Parcel watcher error on ${osDirPath}:`, err); // Only disable native watching for critical errors (like ENOSPC). @@ -200,7 +221,7 @@ async function ensureWatchRoot(dirPath: string): Promise { // Dispatch each event to any registered entries. for (const event of events) { const changedPath = toPosixPath(event.path); - const entry = getEntry(changedPath); + const entry = findNearestEntry(changedPath); if (!entry) continue; // In Meteor's safe-watcher API, both create/update trigger "change" events. const evtType = event.type === "delete" ? "delete" : "change"; @@ -286,9 +307,9 @@ export const watch = Profile( let watchTarget: string; try { const st = statOrNull(convertToOSPath(absPath)); - watchTarget = st?.isDirectory() ? absPath : toPosixPath(dirname(convertToOSPath(absPath))); + watchTarget = st?.isDirectory() ? absPath : toPosixPath(pathDirname(convertToOSPath(absPath))); } catch (e) { - watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + watchTarget = toPosixPath(pathDirname(convertToOSPath(absPath))); } // Set up a watcher on the parent directory (or the directory itself) if not already active. ensureWatchRoot(watchTarget); @@ -318,10 +339,10 @@ export function addWatchRoot(absPath: string) { try { const st = statOrNull(convertToOSPath(absPath)); if (!st?.isDirectory()) { - watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + watchTarget = toPosixPath(pathDirname(convertToOSPath(absPath))); } } catch (e) { - watchTarget = toPosixPath(dirname(convertToOSPath(absPath))); + watchTarget = toPosixPath(pathDirname(convertToOSPath(absPath))); } ensureWatchRoot(watchTarget); } From 2a4728fe6cec8effe3844b0976804436d0a1f06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 10:07:21 +0200 Subject: [PATCH 120/264] clean logs --- tools/fs/safe-watcher.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 4da8a8fe2b..2a003e5c79 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -207,7 +207,6 @@ async function ensureWatchRoot(dirPath: string): Promise { const subscription = await ParcelWatcher.subscribe( osDirPath, (err, events) => { - console.log("--> (safe-watcher.ts-Line: 212) events: ", events); if (err) { console.error(`Parcel watcher error on ${osDirPath}:`, err); // Only disable native watching for critical errors (like ENOSPC). From 677ed084fa96ffcde1982d2aafb66625f4c1ab9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 10:24:16 +0200 Subject: [PATCH 121/264] upgrade Node to latest 22.15.0 --- .travis.yml | 2 +- meteor | 2 +- scripts/build-dev-bundle-common.sh | 2 +- v3-docs/v3-migration-docs/index.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5d5a1e7852..1eb4d5be93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ dist: jammy sudo: required services: xvfb node_js: - - "22.14.0" + - "22.15.0" cache: directories: - ".meteor" diff --git a/meteor b/meteor index b743dccaa9..f8b6467b82 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.10 +BUNDLE_VERSION=22.15.0.0 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/scripts/build-dev-bundle-common.sh b/scripts/build-dev-bundle-common.sh index f60e62fbee..cbb17bf018 100644 --- a/scripts/build-dev-bundle-common.sh +++ b/scripts/build-dev-bundle-common.sh @@ -5,7 +5,7 @@ set -u UNAME=$(uname) ARCH=$(uname -m) -NODE_VERSION=22.14.0 +NODE_VERSION=22.15.0 MONGO_VERSION_64BIT=7.0.16 MONGO_VERSION_32BIT=3.2.22 NPM_VERSION=10.9.2 diff --git a/v3-docs/v3-migration-docs/index.md b/v3-docs/v3-migration-docs/index.md index 4d55017747..15f96a16ae 100644 --- a/v3-docs/v3-migration-docs/index.md +++ b/v3-docs/v3-migration-docs/index.md @@ -1,6 +1,6 @@ --- meteor_version: 3.1.0 -node_version: 22.14.0 +node_version: 22.15.0 npm_version: 10.9.2 --- # Meteor 3.0 Migration Guide From 1e39167d93193f96da0ef3113fc780c67a88e025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 10:33:36 +0200 Subject: [PATCH 122/264] re-run checks From 5933b9ac8490e7897b38b40f86f53244749660c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 11:17:45 +0200 Subject: [PATCH 123/264] watch node_modules outside npm context to fix a test and avoid regression (keep performance gains) --- tools/fs/safe-watcher.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 2a003e5c79..719638429e 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -2,7 +2,7 @@ import { Stats } from 'fs'; import ParcelWatcher from "@parcel/watcher"; import { Profile } from "../tool-env/profile"; -import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname } from "./files"; +import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname, exists } from "./files"; // Register process exit handlers to ensure subscriptions are properly cleaned up const registerExitHandlers = () => { @@ -134,6 +134,12 @@ function shouldIgnorePath(absPath: string): boolean { // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). } + const cwd = toPosixPath(process.cwd()); + const isWithinCwd = absPath.startsWith(cwd); + if (isWithinCwd) { + return absPath.includes(`${cwd}/node_modules`); + } + // For node_modules: ignore contents unless the package directory is a symlink (which means it’s under active development). const nmIndex = parts.indexOf("node_modules"); if (nmIndex !== -1) { @@ -200,8 +206,11 @@ async function ensureWatchRoot(dirPath: string): Promise { return; } - // Set up ignore patterns to skip deep node_modules and .meteor/local cache - const ignorePatterns = ["**/node_modules/**", "**/.meteor/local/**"]; + // Set up ignore patterns to skip node_modules and .meteor/local cache + const cwd = toPosixPath(process.cwd()); + const isWithinCwd = dirPath.startsWith(cwd); + const ignPrefix = isWithinCwd ? "" : "**/"; + const ignorePatterns = [`${ignPrefix}node_modules/**`, `${ignPrefix}.meteor/local/**`]; try { watchRoots.add(dirPath); const subscription = await ParcelWatcher.subscribe( From 76428cfdb2bb70ff3a81e0971454eeff7f938ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 11:25:08 +0200 Subject: [PATCH 124/264] early exit on .meteor/local context --- tools/fs/safe-watcher.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 719638429e..4c793979d8 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -122,6 +122,13 @@ function shouldIgnorePath(absPath: string): boolean { const posixPath = toPosixPath(absPath); const parts = posixPath.split('/'); + const cwd = toPosixPath(process.cwd()); + const isWithinCwd = absPath.startsWith(cwd); + + if (isWithinCwd && absPath.includes(`${cwd}/.meteor/local`)) { + return true; + } + // Check for .meteor: allow the .meteor directory itself, // but ignore its "local" subdirectory (or any immediate child folder that indicates cache). const meteorIndex = parts.indexOf(".meteor"); @@ -134,8 +141,6 @@ function shouldIgnorePath(absPath: string): boolean { // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). } - const cwd = toPosixPath(process.cwd()); - const isWithinCwd = absPath.startsWith(cwd); if (isWithinCwd) { return absPath.includes(`${cwd}/node_modules`); } From 69e6e55ba3ec548154717af1b28a71ed534e73f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 12:06:35 +0200 Subject: [PATCH 125/264] disable symbolic link node_module watching since was not originally supported and covered by tests --- tools/fs/safe-watcher.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 4c793979d8..e99a139400 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -2,7 +2,7 @@ import { Stats } from 'fs'; import ParcelWatcher from "@parcel/watcher"; import { Profile } from "../tool-env/profile"; -import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname, exists } from "./files"; +import { statOrNull, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname } from "./files"; // Register process exit handlers to ensure subscriptions are properly cleaned up const registerExitHandlers = () => { @@ -141,22 +141,15 @@ function shouldIgnorePath(absPath: string): boolean { // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). } + // For project node_modules: ignore npm node_modules, rest are valid if (isWithinCwd) { return absPath.includes(`${cwd}/node_modules`); } - // For node_modules: ignore contents unless the package directory is a symlink (which means it’s under active development). + // For external node_modules: ignore contents const nmIndex = parts.indexOf("node_modules"); if (nmIndex !== -1) { - try { - const stat = lstat(absPath); - if (!stat?.isSymbolicLink()) { - return true; - } - // If the package folder is symlinked, we want to watch it. - } catch (e) { - return true; - } + return true; } return false; From dc17c9003f2f576751ec7a6fa010ef2d842efb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 12:39:16 +0200 Subject: [PATCH 126/264] ensure old tests can exit properly on using the new closeAllWatchers --- tools/tests/old/test-bundler-assets.js | 9 ++++++++- tools/tests/old/test-bundler-options.js | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tools/tests/old/test-bundler-assets.js b/tools/tests/old/test-bundler-assets.js index c0c1278434..6f7479f7bc 100644 --- a/tools/tests/old/test-bundler-assets.js +++ b/tools/tests/old/test-bundler-assets.js @@ -172,6 +172,13 @@ makeGlobalAsyncLocalStorage().run( // Allow the process to exit normally, since optimistic file watchers // may be keeping the event loop busy. - safeWatcher.closeAllWatchers(); + safeWatcher.closeAllWatchers() + .then(() => { + process.exit(0); + }) + .catch(err => { + console.error(`Error closing watchers:`, err); + process.exit(1); + }); } ); diff --git a/tools/tests/old/test-bundler-options.js b/tools/tests/old/test-bundler-options.js index 2bf8d00aa1..871146afb6 100644 --- a/tools/tests/old/test-bundler-options.js +++ b/tools/tests/old/test-bundler-options.js @@ -200,6 +200,13 @@ makeGlobalAsyncLocalStorage().run( // Allow the process to exit normally, since optimistic file watchers // may be keeping the event loop busy. - safeWatcher.closeAllWatchers(); + safeWatcher.closeAllWatchers() + .then(() => { + process.exit(0); + }) + .catch(err => { + console.error(`Error closing watchers:`, err); + process.exit(1); + }); } ); From c2dcae2d3953b5eb51c237c5dba3116daa423859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 12:44:07 +0200 Subject: [PATCH 127/264] ensure old tests can exit properly on using the new closeAllWatchers --- tools/tests/old/test-bundler-assets.js | 10 ++-------- tools/tests/old/test-bundler-options.js | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/tools/tests/old/test-bundler-assets.js b/tools/tests/old/test-bundler-assets.js index 6f7479f7bc..d15af06f27 100644 --- a/tools/tests/old/test-bundler-assets.js +++ b/tools/tests/old/test-bundler-assets.js @@ -172,13 +172,7 @@ makeGlobalAsyncLocalStorage().run( // Allow the process to exit normally, since optimistic file watchers // may be keeping the event loop busy. - safeWatcher.closeAllWatchers() - .then(() => { - process.exit(0); - }) - .catch(err => { - console.error(`Error closing watchers:`, err); - process.exit(1); - }); + safeWatcher.closeAllWatchers(); + process.exit(0); } ); diff --git a/tools/tests/old/test-bundler-options.js b/tools/tests/old/test-bundler-options.js index 871146afb6..9d2b75a299 100644 --- a/tools/tests/old/test-bundler-options.js +++ b/tools/tests/old/test-bundler-options.js @@ -200,13 +200,7 @@ makeGlobalAsyncLocalStorage().run( // Allow the process to exit normally, since optimistic file watchers // may be keeping the event loop busy. - safeWatcher.closeAllWatchers() - .then(() => { - process.exit(0); - }) - .catch(err => { - console.error(`Error closing watchers:`, err); - process.exit(1); - }); + safeWatcher.closeAllWatchers(); + process.exit(0); } ); From 9a2e39fce98a9d599be04256fc0ea87a68492553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 23 Apr 2025 14:14:09 +0200 Subject: [PATCH 128/264] re-run checks From ad7fb255f532ac28ae9fd8541c41419a705871da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 08:51:50 +0200 Subject: [PATCH 129/264] support symlinks with polling --- tools/fs/safe-watcher.ts | 58 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index e99a139400..969d20a54e 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -2,7 +2,7 @@ import { Stats } from 'fs'; import ParcelWatcher from "@parcel/watcher"; import { Profile } from "../tool-env/profile"; -import { statOrNull, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname } from "./files"; +import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname, realpathOrNull, readdir, pathJoin } from "./files"; // Register process exit handlers to ensure subscriptions are properly cleaned up const registerExitHandlers = () => { @@ -93,6 +93,9 @@ const dirSubscriptions = new Map(); // A set of roots that are known to be unwatchable. const ignoredWatchRoots = new Set(); +// A set of roots that are known to be symbolic links. +const symlinkRoots = new Set(); + // Set METEOR_WATCH_FORCE_POLLING environment variable to a truthy value to // force the use of files.watchFile instead of ParcelWatcher. let watcherEnabled = !JSON.parse(process.env.METEOR_WATCH_FORCE_POLLING || "false"); @@ -155,6 +158,49 @@ function shouldIgnorePath(absPath: string): boolean { return false; } +/** + * Check if a path is a symbolic link. + * + * Symbolic links are not supported natively in some operating systems, + * so we need to use polling for them to ensure they are properly watched. + * This function is used to determine if a path is a symbolic link, + * so we can use polling instead of native watching for it. + * + * If a path is a symbolic link, its root is added to the symlinkRoots set. + */ +function isSymbolicLink(absPath: string): boolean { + try { + const osPath = convertToOSPath(absPath); + const stat = lstat(osPath); + if (stat?.isSymbolicLink()) { + // Add the directory containing the symlink to the symlinkRoots set + const symlinkRoot = toPosixPath(pathDirname(absPath)); + symlinkRoots.add(symlinkRoot); + return true; + } + return false; + } catch (e) { + // If we can't stat the file, assume it's not a symlink + return false; + } +} + +/** + * Check if a path is within any symlink root. + * + * This is used to determine if a path should use polling instead of native watching, + * even if it's not a symlink itself. + */ +function isWithinSymlinkRoot(absPath: string): boolean { + for (const root of symlinkRoots) { + // Check if absPath starts with root + '/' + if (absPath === root || (absPath.startsWith(root) && absPath.charAt(root.length) === '/')) { + return true; + } + } + return false; +} + /** * Ensure that the given directory is being watched by @parcel/watcher. * If it is not a directory or is unwatchable, it is immediately added to an ignore set. @@ -296,12 +342,18 @@ export const watch = Profile( callback: ChangeCallback ): SafeWatcher => { absPath = toPosixPath(absPath); + + // if (absPath.includes('sym')) { + // console.log("--> (safe-watcher.ts-Line: 318)\n absPath: ", absPath); + // } // If the path should be ignored, immediately return a noop SafeWatcher. if (shouldIgnorePath(absPath)) { return { close() {} }; } - // If native watching is disabled, use the polling strategy. - if (!watcherEnabled) { + // If native watching is disabled, the path is a symbolic link, or the path is within a symlink root, + // use the polling strategy. Symbolic links are not supported natively in some operating systems, + // and paths within symlink roots should also use polling for consistency. + if (!watcherEnabled || isWithinSymlinkRoot(absPath) || isSymbolicLink(absPath)) { return startPolling(absPath, callback); } // Try to reuse an existing entry if one was created before. From 253dccef7bb880f0319a68262efd84429e9e9e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 09:55:32 +0200 Subject: [PATCH 130/264] support symlinks with polling in direct node_modules packages --- tools/fs/safe-watcher.ts | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 969d20a54e..eb2c69e941 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -144,12 +144,28 @@ function shouldIgnorePath(absPath: string): boolean { // Otherwise, do not automatically ignore .meteor (which includes .meteor/packages, etc). } - // For project node_modules: ignore npm node_modules, rest are valid - if (isWithinCwd) { - return absPath.includes(`${cwd}/node_modules`); + // For project node_modules: check if it's a direct node_modules/ + if (isWithinCwd && absPath.includes(`${cwd}/node_modules`)) { + // Check if it's a direct node_modules/ path + const relPath = absPath.substring(cwd.length + 1); // +1 for the slash + const relParts = relPath.split('/'); + if (relParts.length >= 2 && relParts[0] === 'node_modules') { + // If it's a direct node_modules/, check if it's a symlink + // We'll return false here (don't ignore) so that the code can later decide to use polling + // based on isSymbolicLink check in the watch function + if (relParts.length === 2 && isSymbolicLink(absPath, false)) { + return false; + } + // Check if it's within a symlink root to not ignore + if (isWithinSymlinkRoot(absPath)) { + return false; + } + return true; + } + return true; } - // For external node_modules: ignore contents + // For external node_modules: check if it's a direct node_modules/ const nmIndex = parts.indexOf("node_modules"); if (nmIndex !== -1) { return true; @@ -168,14 +184,16 @@ function shouldIgnorePath(absPath: string): boolean { * * If a path is a symbolic link, its root is added to the symlinkRoots set. */ -function isSymbolicLink(absPath: string): boolean { +function isSymbolicLink(absPath: string, addToRoots = true): boolean { try { const osPath = convertToOSPath(absPath); const stat = lstat(osPath); if (stat?.isSymbolicLink()) { - // Add the directory containing the symlink to the symlinkRoots set - const symlinkRoot = toPosixPath(pathDirname(absPath)); - symlinkRoots.add(symlinkRoot); + if (addToRoots) { + // Add the directory containing the symlink to the symlinkRoots set + const symlinkRoot = toPosixPath(absPath); + symlinkRoots.add(symlinkRoot); + } return true; } return false; @@ -343,9 +361,6 @@ export const watch = Profile( ): SafeWatcher => { absPath = toPosixPath(absPath); - // if (absPath.includes('sym')) { - // console.log("--> (safe-watcher.ts-Line: 318)\n absPath: ", absPath); - // } // If the path should be ignored, immediately return a noop SafeWatcher. if (shouldIgnorePath(absPath)) { return { close() {} }; From 4a0e423d060d5892d086b9c1374b3e899badca7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 10:09:58 +0200 Subject: [PATCH 131/264] avoid register several watchers when polling --- tools/fs/safe-watcher.ts | 63 +++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index eb2c69e941..fcc3f3e0f8 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -53,6 +53,14 @@ interface Entry extends SafeWatcher { // Registry mapping normalized absolute paths to their watcher entry. const entries = new Map(); +// Registry mapping normalized absolute paths to their polling watchers. +// Each path can have multiple callbacks, but only one active watcher. +interface PollingWatcherInfo { + callbacks: Set; + pollCallback: (curr: Stats, prev: Stats) => void; +} +const pollingWatchers = new Map(); + function getEntry(path: string): Entry | null | undefined { return entries.get(path); } @@ -480,18 +488,53 @@ function startPolling(absPath: string, callback: ChangeCallback): SafeWatcher { const osPath = convertToOSPath(absPath); // Initial polling interval. let interval = getPollingInterval(absPath); - const pollCallback = (curr: Stats, prev: Stats) => { - // Compare modification times to detect a change. - if (+curr.mtime !== +prev.mtime) { - changedPaths.add(absPath); - callback("change"); - } - }; - watchFile(osPath, { interval }, pollCallback); + + // Check if we already have a polling watcher for this path + let watcherInfo = pollingWatchers.get(absPath); + + if (watcherInfo) { + // Add this callback to the existing watcher + watcherInfo.callbacks.add(callback); + } else { + // Create a new polling watcher + const pollCallback = (curr: Stats, prev: Stats) => { + // Compare modification times to detect a change. + if (+curr.mtime !== +prev.mtime) { + changedPaths.add(absPath); + // Notify all callbacks registered for this path + const info = pollingWatchers.get(absPath); + if (info) { + for (const cb of info.callbacks) { + cb("change"); + } + } + } + }; + + watchFile(osPath, { interval }, pollCallback); + + // Store the new watcher info + watcherInfo = { + callbacks: new Set([callback]), + pollCallback + }; + pollingWatchers.set(absPath, watcherInfo); + } + return { close() { - unwatchFile(osPath, pollCallback); - changedPaths.delete(absPath); + const info = pollingWatchers.get(absPath); + if (info) { + // Remove this callback + info.callbacks.delete(callback); + + // If no callbacks remain, remove the watcher + if (info.callbacks.size === 0) { + unwatchFile(osPath, info.pollCallback); + pollingWatchers.delete(absPath); + changedPaths.delete(absPath); + } + } } }; } From 1995445b7b20956b2ba6c21e5acd941bbc27bce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 10:20:43 +0200 Subject: [PATCH 132/264] avoid register several watchers when polling --- tools/fs/safe-watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index fcc3f3e0f8..c97b758c4b 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -161,7 +161,7 @@ function shouldIgnorePath(absPath: string): boolean { // If it's a direct node_modules/, check if it's a symlink // We'll return false here (don't ignore) so that the code can later decide to use polling // based on isSymbolicLink check in the watch function - if (relParts.length === 2 && isSymbolicLink(absPath, false)) { + if (relParts.length === 2 && isSymbolicLink(absPath)) { return false; } // Check if it's within a symlink root to not ignore From eaf78bca810e6ffdc9955630091de65293bf9feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 11:03:25 +0200 Subject: [PATCH 133/264] fix a test supporting non-npm node_modules watching --- tools/fs/safe-watcher.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index c97b758c4b..90ef55c3bc 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -153,24 +153,25 @@ function shouldIgnorePath(absPath: string): boolean { } // For project node_modules: check if it's a direct node_modules/ - if (isWithinCwd && absPath.includes(`${cwd}/node_modules`)) { - // Check if it's a direct node_modules/ path - const relPath = absPath.substring(cwd.length + 1); // +1 for the slash - const relParts = relPath.split('/'); - if (relParts.length >= 2 && relParts[0] === 'node_modules') { - // If it's a direct node_modules/, check if it's a symlink - // We'll return false here (don't ignore) so that the code can later decide to use polling - // based on isSymbolicLink check in the watch function - if (relParts.length === 2 && isSymbolicLink(absPath)) { - return false; + if (isWithinCwd) { + if (absPath.includes(`${cwd}/node_modules`)) { + // Check if it's a direct node_modules/ path + const relPath = absPath.substring(cwd.length + 1); // +1 for the slash + const relParts = relPath.split('/'); + if (relParts.length >= 2 && relParts[0] === 'node_modules') { + // If it's a direct node_modules/, check if it's a symlink + // We'll return false here (don't ignore) so that the code can later decide to use polling + // based on isSymbolicLink check in the watch function + if (relParts.length === 2 && isSymbolicLink(absPath)) { + return false; + } + // Check if it's within a symlink root to not ignore + if (isWithinSymlinkRoot(absPath)) { + return false; + } } - // Check if it's within a symlink root to not ignore - if (isWithinSymlinkRoot(absPath)) { - return false; - } - return true; } - return true; + return false; } // For external node_modules: check if it's a direct node_modules/ From 3fcfc8c5f30f2480aec003b77ab4f891108cdab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 11:04:43 +0200 Subject: [PATCH 134/264] fix a test supporting non-npm node_modules watching --- tools/fs/safe-watcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 90ef55c3bc..3f8c88dc28 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -170,8 +170,10 @@ function shouldIgnorePath(absPath: string): boolean { return false; } } + return true; + } else { + return false; } - return false; } // For external node_modules: check if it's a direct node_modules/ From 7460064bf3dcb63c229677431fc8b3e43f902b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 14:10:21 +0200 Subject: [PATCH 135/264] allow .swcrc override jsc.parser values --- packages/babel-compiler/babel-compiler.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index d323554201..837772a3cf 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -64,9 +64,6 @@ function compileWithSwc(source, swcOptions = {}, { inputFilePath, features, arch ? deepMerge(baseSwcConfig, swcOptions, [ 'env.targets', 'module.type', - 'jsc.parser.syntax', - 'jsc.parser.jsx', - 'jsc.parser.tsx', ]) : baseSwcConfig; From 37d54c441dec914ea6289ec64790ce283f9da34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 15:37:55 +0200 Subject: [PATCH 136/264] remove unused imports --- tools/fs/safe-watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 3f8c88dc28..a0b90c6c25 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -2,7 +2,7 @@ import { Stats } from 'fs'; import ParcelWatcher from "@parcel/watcher"; import { Profile } from "../tool-env/profile"; -import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname, realpathOrNull, readdir, pathJoin } from "./files"; +import { statOrNull, lstat, toPosixPath, convertToOSPath, pathRelative, watchFile, unwatchFile, pathResolve, pathDirname } from "./files"; // Register process exit handlers to ensure subscriptions are properly cleaned up const registerExitHandlers = () => { From c1e454df55f2b775773a48f7548a36a9feb71f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 16:34:18 +0200 Subject: [PATCH 137/264] improve SWC docs addressing feedback --- .../modern-transpiler-swc.md | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 278da99499..7783fde8e4 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -39,15 +39,18 @@ If all your code uses SWC, you're good and can turn off verbosity. But if you se ``` -There are a few things you can do. +This means SWC encountered syntax incompatibilities on the files. There are a few things you can do. -First, check the fallback details. It may show why SWC couldn’t handle the file. A common reason is nested imports, `import` statements inside a function. Moving them to the top level may fix it. These nested imports work via a Babel plugin specific to Meteor, which SWC doesn’t support. +First, check the fallback details to **fix the syntax**. They might explain why SWC failed. -Other reasons might involve features tied to Babel plugins. If so, you’ll need to find a similar plugin for SWC. See the [SWC plugin list](https://plugins.swc.rs/versions/range/271). +- A common cause is [**nested import statements** inside functions](#nested-imports). Move them to the top level. These work in Babel due to a Meteor-specific plugin, which SWC doesn’t support. +- Other issues may come from features tied to Babel plugins. You’ll need to find SWC equivalents. See the [SWC plugin list](https://plugins.swc.rs/versions/range/271). -Second, you might choose to ignore the fallback if those files are fine with Babel. Even with SWC enabled, Meteor will continue using Babel for those files on future rebuilds. +Second, **ignore the fallback** if those files run fine with Babel. SWC will still speed up other files. Meteor will keep using Babel for incompatible files on future builds. -Third, you can exclude files or contexts from SWC. For example, if you're using `babel-plugin-react-compiler`, which SWC doesn't support yet, you can exclude your app code adding this to `package.json`: +Third, **exclude files or contexts from SWC**. Even though it falls back automatically, you can skip the overhead of trying SWC on known-incompatible files. + +For example, if you're using `babel-plugin-react-compiler`, which [SWC doesn't support yet](https://react.dev/blog/2025/04/21/react-compiler-rc), you can exclude your app code adding this to `package.json`: ```json "meteor": { @@ -69,15 +72,32 @@ Or exclude only specific files like `.jsx`: You can also use `excludePackages`, `excludeNodeModules`, and `excludeLegacy` for finer control. See the [`modernTranspiler` config docs](#config-api) for more. -When no alternatives exist, these settings let you still get most of SWC’s speed benefits by limiting fallback use. +When no plugin exists, these settings let you still get most of SWC’s speed benefits by limiting fallback use. -We expect most apps will benefit just by enabling `modernTranspiler: true`. Most Meteor packages should work right away, except ones using nested imports. Node modules will mostly work too, since they follow common standards. Most app code should also work unless it depends on Babel-specific behavior. +Most apps will benefit just by enabling `modernTranspiler: true`. Most Meteor packages should work right away, except ones using nested imports. Node modules will mostly work too, since they follow common standards. Most app code should also work unless it depends on Babel-specific behavior. > Remember to turn off verbosity when you're done with optimizations. ## Custom .swcrc -You can use .swcrc config in the root of your project to describe specific [SWC plugins](https://github.com/swc-project/plugins) there, that will be applied to compile the entire files of your project. +You can use `.swcrc` config in the root of your project to describe specific [SWC plugins](https://github.com/swc-project/plugins) there, that will be applied to compile the entire files of your project. + +You can also configure other options using the `.swcrc` format. One common case is when your project uses `.js` files for React code. React typically uses `.jsx` for components. We still recommend following that convention for compatibility, but if you prefer `.js`, you can provide a custom `.swcrc` like this: + +``` json +{ + "jsc": { + "parser": { + "syntax": "emcascript", + "jsx": true + } + } +} +``` + +> You can also configure it for TypeScript, make sure to set `"syntax": "typescript"` and `"tsx": true` instead. + +This overrides Meteor's internal SWC config to apply your settings, ensuring SWC processes `.js` or `.ts` files with React components without falling back to Babel. ## Config API @@ -102,6 +122,35 @@ You can use .swcrc config in the root of your project to describe specific [SWC - `modernTranspiler.verbose: [true|false]` If true, the transpilation process for files is shown when running the app. This helps understand which transpiler is used for each file, what fallbacks are applied, and gives a chance to either exclude files to always use Babel or migrate fully to SWC. +## Related Topics + +### Nested imports + +Nested imports are a Meteor-specific feature in its bundler, unlike standard bundlers. Meteor introduced them during a time when bundling standards were still evolving and experimented with its own approach. This feature comes from the [`reify` module](https://github.com/benjamn/reify/tree/main) and works with Babel transpilation. SWC doesn't support them since they were never standardized. + +Example with a nested import: + +``` javascript +if (condition) { + import { a as b } from "./c"; + console.log(b); +} +``` + +Without a nested import (moved to top): + +``` javascript +import { a as b } from "./c"; + +if (condition) { + console.log(b); +} +``` + +For background, see: [Why nested import](https://github.com/benjamn/reify/blob/main/WHY_NEST_IMPORTS.md). + +With `"modernTranspiler": true`, if SWC finds one, it silently falls back to Babel (only shows in `"verbose": true`). Nested imports isn’t standard, most modern projects use other deferred loading methods. You might want to move imports to the top or use require instead, letting SWC handle the file and speeding up builds. Still, this decision is up to the devs, some Meteor devs use them for valid reasons. + ## Troubleshotting If you run into issues, try `meteor reset` or delete the `.meteor/local` folder in the project root. From 6024cbe773bdb46b7fe43208bb7ac57ec1b4fcf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 16:59:20 +0200 Subject: [PATCH 138/264] update 3.3.0 changelog to describe latest changes and feedback --- .../generators/changelog/versions/3.3.0.md | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/v3-docs/docs/generators/changelog/versions/3.3.0.md b/v3-docs/docs/generators/changelog/versions/3.3.0.md index 0e63e671fd..cba0b4aea3 100644 --- a/v3-docs/docs/generators/changelog/versions/3.3.0.md +++ b/v3-docs/docs/generators/changelog/versions/3.3.0.md @@ -3,36 +3,46 @@ ### Highlights - Support SWC transpiler for faster dev and builds [PR#13657](https://github.com/meteor/meteor/pull/13657) -- Default to modern architecture in new apps [PR#13665](https://github.com/meteor/meteor/pull/13665) +- Use `@parcel/watcher` for improved native file watching [PR#13699](https://github.com/meteor/meteor/pull/13699), [#13707](https://github.com/meteor/meteor/pull/13707) +- Default to modern architecture, skip legacy processing [PR#13665](https://github.com/meteor/meteor/pull/13665), [PR#13698](https://github.com/meteor/meteor/pull/13698) +- Optimize SQLite for faster startup and better performance [PR#13702](https://github.com/meteor/meteor/pull/13702) - Support CPU profiling in Meteor 3 bundler [PR#13650](https://github.com/meteor/meteor/pull/13650) -- Improve `meteor profile`@show rebuild steps, support `--build` [PR#13694](https://github.com/meteor/meteor/pull/13694) +- Improve `meteor profile`: show rebuild steps and total, support `--build` [PR#16](https://github.com/meteor/performance/pull/16) and [PR#13694](https://github.com/meteor/meteor/pull/13694) - Improve `useFind` and `useSubscribe` React hooks - Add `replaceEmailAsync` helper to Accounts [PR#13677](https://github.com/meteor/meteor/pull/13677) - Fix user agent detection and oplog collection filtering - Refine type definitions for Meteor methods and SSR's ServerSink -- Update Express to 5.1.0 +- Allow opting out of usage stats with `DO_NOT_TRACK` +- Update Node to 22.15.0 and Express to 5.1.0 -All merged PRs@[GitHub PRs 3.3](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3) - -React packages PRs@[PR#419](https://github.com/meteor/react-packages/pull/419), [PR#429](https://github.com/meteor/react-packages/pull/429) and [PR#430](https://github.com/meteor/react-packages/pull/430) +All Merged PRs@[GitHub PRs 3.3](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3) +React Packages Changelog: [react-meteor-data@4.0.0-beta.0](https://github.com/meteor/react-packages/blob/fb73eeb89ff59664a7a01769fa1c2c880e72a3e5/packages/react-meteor-data/CHANGELOG.md#v400-beta0-xxx) #### Breaking Changes -N/A +- `react-meteor-data@4.0.0-beta.0` + - Independent from the core, only applies if upgraded manually. + - useFind describes no deps by default [PR#431](https://github.com/meteor/react-packages/pull/431) #### Internal API changes -N/A +- `express@5.1.0` - Depends on Meteor’s `webapp` package. + - Deprecates non-native promise usage [#154](https://github.com/pillarjs/router/pull/154) + - Use `async/await` or `Promise.resolve` when defining endpoints to avoid deprecation warnings. #### Migration Steps Please run the following command to update your project: ```bash - meteor update --release 3.3-beta.0 +``` +To apply react-meteor-data changes: + +```bash +meteor add react-meteor-data@4.0.0-beta.0 ``` #### Bumped Meteor Packages From ebf733b4bb4e8e3f53638209a56d6aa48851ab04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 18:07:44 +0200 Subject: [PATCH 139/264] add new watcher docs and update changelog with internal and breaking changes --- .../modern-build-stack/modern-bundler.md | 20 ++++++++++++++++++- .../generators/changelog/versions/3.3.0.md | 5 +++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/v3-docs/docs/about/modern-build-stack/modern-bundler.md b/v3-docs/docs/about/modern-build-stack/modern-bundler.md index d5ebb5b55e..a240c67ec8 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-bundler.md +++ b/v3-docs/docs/about/modern-build-stack/modern-bundler.md @@ -6,7 +6,7 @@ Integration with a modern bundler is in progress for Meteor 3.4. Meanwhile, we'v ## Modern builds -Starting with Meteor 3.3, new apps skip `web.browser.legacy` and `web.cordova` by default in development mode (unless developing for native). +Starting with Meteor 3.3, new apps skip `web.browser.legacy` and `web.cordova` by default in development mode (unless developing for native). This results on getting a faster build process on development mode. For existing apps, enable this by adding to `package.json`: @@ -17,3 +17,21 @@ For existing apps, enable this by adding to `package.json`: ``` This works like using `--exclude-archs web.browser.legacy,web.cordova` with `meteor run`. + +## Modern watcher + +Starting with Meteor 3.3, development mode uses a modern, cross-platform watcher: [`@parcel/watcher`](https://github.com/parcel-bundler/watcher). It responds quickly to file changes using native file watching. Symbolic link changes and all traversed files are supported via polling. + +If the watcher doesn’t work properly, such as when using WSL with host, a volume, or a remote setup, switch fully to the polling strategy. + +To enable polling, run your Meteor app with: + +```shell +# enable polling +METEOR_WATCH_FORCE_POLLING=true meteor run + +# set polling interval (in ms) +METEOR_WATCH_POLLING_INTERVAL_MS=1000 METEOR_WATCH_FORCE_POLLING=true meteor run +``` + +> Polling uses more CPU and RAM, but it's the most reliable option in some environments. diff --git a/v3-docs/docs/generators/changelog/versions/3.3.0.md b/v3-docs/docs/generators/changelog/versions/3.3.0.md index cba0b4aea3..9dc6052aea 100644 --- a/v3-docs/docs/generators/changelog/versions/3.3.0.md +++ b/v3-docs/docs/generators/changelog/versions/3.3.0.md @@ -21,6 +21,11 @@ React Packages Changelog: [react-meteor-data@4.0.0-beta.0](https://github.com/me #### Breaking Changes +- File watching strategy switched to @parcel/watcher + - Most setups should be fine, but if issues appear, like when using WSL with host, volumes, or remote setups—switch to polling. + - Set `METEOR_WATCH_FORCE_POLLING=true` to enable polling. + - Set `METEOR_WATCH_POLLING_INTERVAL_MS=1000` to adjust the interval. + - `react-meteor-data@4.0.0-beta.0` - Independent from the core, only applies if upgraded manually. - useFind describes no deps by default [PR#431](https://github.com/meteor/react-packages/pull/431) From 2ba043fc8adbd413333e283290d87fe62991a1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 18:12:38 +0200 Subject: [PATCH 140/264] add starting with Meteor 3.3 on bundler docs --- v3-docs/docs/about/modern-build-stack/modern-bundler.md | 4 ++++ .../docs/about/modern-build-stack/modern-transpiler-swc.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/v3-docs/docs/about/modern-build-stack/modern-bundler.md b/v3-docs/docs/about/modern-build-stack/modern-bundler.md index a240c67ec8..c572951fbd 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-bundler.md +++ b/v3-docs/docs/about/modern-build-stack/modern-bundler.md @@ -1,5 +1,9 @@ # Modern Bundler +:::info +Starting with Meteor 3.3 +::: + Meteor handles watching and linking all project files into the final bundle. While we'd like to offload more of this to modern bundlers, we're still focused on keeping what's left in the Meteor context as fast as possible. Integration with a modern bundler is in progress for Meteor 3.4. Meanwhile, we've optimized existing processes for better performance. diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 7783fde8e4..135b3075b3 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -1,5 +1,9 @@ # Modern Transpiler: SWC +:::info +Starting with Meteor 3.3 +::: + Meteor has long used Babel, a mature and still widely adopted transpiler. However, it lags behind newer tools like SWC in terms of speed. SWC and others are not only faster but are growing in use and features, reaching parity with Babel. Since transpilation is one of the slowest steps in development, Meteor now gives you the option to use SWC for your apps. From 962584a814f7dcce2caf6c4eeffcb963e0afd145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Thu, 24 Apr 2025 18:28:55 +0200 Subject: [PATCH 141/264] clean --- .../about/modern-build-stack/modern-transpiler-swc.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md index 135b3075b3..cf452b9d93 100644 --- a/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md +++ b/v3-docs/docs/about/modern-build-stack/modern-transpiler-swc.md @@ -90,12 +90,12 @@ You can also configure other options using the `.swcrc` format. One common case ``` json { - "jsc": { - "parser": { - "syntax": "emcascript", - "jsx": true - } + "jsc": { + "parser": { + "syntax": "emcascript", + "jsx": true } + } } ``` From b013c3e33ea453a9355ddfbca62c3fa854c7fbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 25 Apr 2025 09:42:10 +0200 Subject: [PATCH 142/264] implement rewatching polling strategy --- tools/fs/safe-watcher.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index a0b90c6c25..32fe547e88 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -204,6 +204,8 @@ function isSymbolicLink(absPath: string, addToRoots = true): boolean { // Add the directory containing the symlink to the symlinkRoots set const symlinkRoot = toPosixPath(absPath); symlinkRoots.add(symlinkRoot); + // Rewatch using polling any existing watchers under this symlink root + rewatchPolling(symlinkRoot); } return true; } @@ -542,6 +544,35 @@ function startPolling(absPath: string, callback: ChangeCallback): SafeWatcher { }; } +/** + * Rewatch entries under a symlink root from native watchers to polling watchers. + * This is called when a new symlink root is discovered. + */ +function rewatchPolling(root: string) { + for (const [watchedPath, entry] of entries) { + // if it lives under the new symlink root... + if (watchedPath === root || + (watchedPath.startsWith(root) && watchedPath.charAt(root.length) === '/')) { + // Skip if entry is null or already closed + if (!entry) continue; + + // Store the callbacks before closing the entry + const callbacks = Array.from(entry.callbacks); + + // Tear down the old native watcher + entry.close(); + + // Remove it from the map + entries.delete(watchedPath); + + // Re-watch via polling for each callback + for (const cb of callbacks) { + startPolling(watchedPath, cb); + } + } + } +} + /** * Fall back to polling. If a critical error occurs, * we disable native watching and close all existing native watchers. From 90b5ccc5763a383e0e41a09eae697b65723f3528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 25 Apr 2025 10:19:13 +0200 Subject: [PATCH 143/264] don't ignore node_modules in packages linked externally --- tools/fs/safe-watcher.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 32fe547e88..dc03089bb2 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -154,6 +154,7 @@ function shouldIgnorePath(absPath: string): boolean { // For project node_modules: check if it's a direct node_modules/ if (isWithinCwd) { + // Check if it's the project node_modules if (absPath.includes(`${cwd}/node_modules`)) { // Check if it's a direct node_modules/ path const relPath = absPath.substring(cwd.length + 1); // +1 for the slash @@ -172,6 +173,7 @@ function shouldIgnorePath(absPath: string): boolean { } return true; } else { + // Otherwise, don't ignore non-npm node_modules return false; } } @@ -179,6 +181,12 @@ function shouldIgnorePath(absPath: string): boolean { // For external node_modules: check if it's a direct node_modules/ const nmIndex = parts.indexOf("node_modules"); if (nmIndex !== -1) { + // Don't ignore node_modules within .npm/package/ paths + const npmPackageIndex = parts.indexOf(".npm"); + if (npmPackageIndex !== -1 && parts[npmPackageIndex + 1] === "package" && + nmIndex > npmPackageIndex && parts[nmIndex - 1] === "package") { + return false; + } return true; } From 7e49bca0e8a62467805b77624c43dbb9f1276858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Fri, 25 Apr 2025 20:01:03 +0200 Subject: [PATCH 144/264] favor reify transpile all time --- packages/babel-compiler/babel-compiler.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index d323554201..c8681c674a 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -91,9 +91,7 @@ function compileWithSwc(source, swcOptions = {}, { inputFilePath, features, arch generateLetDeclarations: true, }), }); - if (!result.identical) { - content = result.code; - } + content = result.code; return { code: content, From 43af635d1b1355d7fb76c950cc9138b6a3dfd982 Mon Sep 17 00:00:00 2001 From: Welkin Wong Date: Sat, 26 Apr 2025 02:55:09 +0800 Subject: [PATCH 145/264] Bump mongodb dependency to version 6.16.0 in npm-mongo package (#13710) --- packages/npm-mongo/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/npm-mongo/package.js b/packages/npm-mongo/package.js index 8b45db0032..fad1d65554 100644 --- a/packages/npm-mongo/package.js +++ b/packages/npm-mongo/package.js @@ -8,7 +8,7 @@ Package.describe({ }); Npm.depends({ - mongodb: "6.9.0" + mongodb: "6.16.0" }); Package.onUse(function (api) { From 14a35d449f8341bb79a2b100ec53349e8c7516ab Mon Sep 17 00:00:00 2001 From: Welkin Wong Date: Mon, 28 Apr 2025 17:51:53 +0800 Subject: [PATCH 146/264] fix: handle all-zero versions in modern browser detection (#13712) --- packages/modern-browsers/modern-tests.js | 14 ++++++++++++++ packages/modern-browsers/modern.js | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/modern-browsers/modern-tests.js b/packages/modern-browsers/modern-tests.js index 4f658f0da2..a19fcad1c8 100644 --- a/packages/modern-browsers/modern-tests.js +++ b/packages/modern-browsers/modern-tests.js @@ -49,6 +49,13 @@ Tinytest.add('modern-browsers - versions - basic', function (test) { patch: 0, })); + test.isFalse(isModern({ + name: "mobileSafariUI/WKWebView", + major: 0, + minor: 0, + patch: 0, + })); + const oldPackageSettings = Meteor.settings.packages; Meteor.settings.packages = { @@ -64,6 +71,13 @@ Tinytest.add('modern-browsers - versions - basic', function (test) { patch: 0, })); + test.isTrue(isModern({ + name: "mobileSafariUI/WKWebView", + major: 0, + minor: 0, + patch: 0, + })); + Meteor.settings.packages = oldPackageSettings; }); diff --git a/packages/modern-browsers/modern.js b/packages/modern-browsers/modern.js index 801a83c271..0223f14909 100644 --- a/packages/modern-browsers/modern.js +++ b/packages/modern-browsers/modern.js @@ -93,7 +93,13 @@ function isModern(browser) { const entry = hasOwn.call(minimumVersions, lowerCaseName) ? minimumVersions[lowerCaseName] : undefined; - if (!entry) { + if ( + !entry || + // When all version numbers are 0, this typically comes from in-app WebView UAs (e.g., iOS WKWebView). + // We can let users decide whether to treat it as a modern browser + // via the packageSettings.unknownBrowsersAssumedModern option. + (browser.major === 0 && browser.minor === 0 && browser.patch === 0) + ) { const packageSettings = Meteor.settings.packages ? Meteor.settings.packages['modern-browsers'] : undefined; From 4a89a7fc0c513d207af746eccd5f3c4a1741b8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 30 Apr 2025 08:57:07 +0200 Subject: [PATCH 147/264] support filename references in source maps during SWC compilation to improve debugging --- packages/babel-compiler/babel-compiler.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index c8681c674a..f03765405a 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -35,7 +35,7 @@ function compileWithBabel(source, babelOptions, cacheOptions) { }); } -function compileWithSwc(source, swcOptions = {}, { inputFilePath, features, arch }) { +function compileWithSwc(source, swcOptions = {}, { inputFilePath, filename, sourceFileName, features, arch }) { return profile('SWC.compile', function () { // Determine file extension based syntax. const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); @@ -55,6 +55,8 @@ function compileWithSwc(source, swcOptions = {}, { inputFilePath, features, arch module: { type: 'es6' }, minify: false, sourceMaps: true, + filename, + sourceFileName, ...(isLegacyWebArch && { env: { targets: lastModifiedSwcLegacyConfig || {} }, }), @@ -336,10 +338,16 @@ BCp.processOneFileForTarget = function (inputFile, source) { } return compilation; } + + + const filename = packageName + ? `packages/${packageName}/${inputFilePath}` + : inputFilePath; + const sourceFileName = filename; compilation = compileWithSwc( source, lastModifiedSwcConfig, - { inputFilePath, features, arch }, + { inputFilePath, features, arch, filename, sourceFileName }, ); // Save result in cache this.writeToSwcCache({ cacheKey, compilation }); From f13a56a284d398c5c2057d7c521d8d2b35b8ca81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Codo=C3=B1er?= Date: Wed, 30 Apr 2025 09:16:18 +0200 Subject: [PATCH 148/264] support filename references in source maps during SWC compilation to improve debugging --- packages/socket-stream-client/sockjs-1.6.1-min-.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/socket-stream-client/sockjs-1.6.1-min-.js b/packages/socket-stream-client/sockjs-1.6.1-min-.js index 7850ea9d02..50cb4cb267 100644 --- a/packages/socket-stream-client/sockjs-1.6.1-min-.js +++ b/packages/socket-stream-client/sockjs-1.6.1-min-.js @@ -1,3 +1,2 @@ /* sockjs-client v1.6.1 | http://sockjs.org | MIT license */ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).SockJS=e()}}(function(){return function i(s,a,l){function u(t,e){if(!a[t]){if(!s[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var o=a[t]={exports:{}};s[t][0].call(o.exports,function(e){return u(s[t][1][e]||e)},o,o.exports,i,s,a,l)}return a[t].exports}for(var c="function"==typeof require&&require,e=0;e>>0;if(!a(e))throw new TypeError;for(;++i>>0;if(!r)return-1;var o=0;for(1>>0:function(e){return e>>>0}(t);(o=e.exec(n))&&!(u<(i=o.index+o[0].length)&&(a.push(n.slice(u,o.index)),!_&&1=t));)e.lastIndex===o.index&&e.lastIndex++;return u===n.length?!s&&e.test("")||a.push(""):a.push(n.slice(u)),a.length>t?a.slice(0,t):a}):"0".split(void 0,0).length&&(s.split=function(e,t){return void 0===e&&0===t?[]:E.call(this,e,t)});var S=s.substr,O="".substr&&"b"!=="0b".substr(-1);d(s,{substr:function(e,t){return S.call(this,e<0&&(e=this.length+e)<0?0:e,t)}},O)},{}],16:[function(e,t,n){"use strict";t.exports=[e("./transport/websocket"),e("./transport/xhr-streaming"),e("./transport/xdr-streaming"),e("./transport/eventsource"),e("./transport/lib/iframe-wrap")(e("./transport/eventsource")),e("./transport/htmlfile"),e("./transport/lib/iframe-wrap")(e("./transport/htmlfile")),e("./transport/xhr-polling"),e("./transport/xdr-polling"),e("./transport/lib/iframe-wrap")(e("./transport/xhr-polling")),e("./transport/jsonp-polling")]},{"./transport/eventsource":20,"./transport/htmlfile":21,"./transport/jsonp-polling":23,"./transport/lib/iframe-wrap":26,"./transport/websocket":38,"./transport/xdr-polling":39,"./transport/xdr-streaming":40,"./transport/xhr-polling":41,"./transport/xhr-streaming":42}],17:[function(o,f,e){(function(r){(function(){"use strict";var i=o("events").EventEmitter,e=o("inherits"),s=o("../../utils/event"),a=o("../../utils/url"),l=r.XMLHttpRequest,u=function(){};function c(e,t,n,r){u(e,t);var o=this;i.call(this),setTimeout(function(){o._start(e,t,n,r)},0)}e(c,i),c.prototype._start=function(e,t,n,r){var o=this;try{this.xhr=new l}catch(e){}if(!this.xhr)return u("no xhr"),this.emit("finish",0,"no xhr support"),void this._cleanup();t=a.addQuery(t,"t="+ +new Date),this.unloadRef=s.unloadAdd(function(){u("unload cleanup"),o._cleanup(!0)});try{this.xhr.open(e,t,!0),this.timeout&&"timeout"in this.xhr&&(this.xhr.timeout=this.timeout,this.xhr.ontimeout=function(){u("xhr timeout"),o.emit("finish",0,""),o._cleanup(!1)})}catch(e){return u("exception",e),this.emit("finish",0,""),void this._cleanup(!1)}if(r&&r.noCredentials||!c.supportsCORS||(u("withCredentials"),this.xhr.withCredentials=!0),r&&r.headers)for(var i in r.headers)this.xhr.setRequestHeader(i,r.headers[i]);this.xhr.onreadystatechange=function(){if(o.xhr){var e,t,n=o.xhr;switch(u("readyState",n.readyState),n.readyState){case 3:try{t=n.status,e=n.responseText}catch(e){}u("status",t),1223===t&&(t=204),200===t&&e&&0')}catch(e){var n=f.document.createElement("iframe");return n.name=t,n}}(r);o.id=r,o.style.display="none",s.appendChild(o);try{a.value=t}catch(e){}s.submit();function i(e){c("completed",r,e),o.onerror&&(o.onreadystatechange=o.onerror=o.onload=null,setTimeout(function(){c("cleaning up",r),o.parentNode.removeChild(o),o=null},500),a.value="",n(e))}return o.onerror=function(){c("onerror",r),i()},o.onload=function(){c("onload",r),i()},o.onreadystatechange=function(e){c("onreadystatechange",r,o.readyState,e),"complete"===o.readyState&&i()},function(){c("aborted",r),i(new Error("Aborted"))}}}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../../utils/random":50,"../../utils/url":52,"debug":void 0}],34:[function(r,u,e){(function(l){(function(){"use strict";var o=r("events").EventEmitter,e=r("inherits"),i=r("../../utils/event"),t=r("../../utils/browser"),s=r("../../utils/url"),a=function(){};function n(e,t,n){a(e,t);var r=this;o.call(this),setTimeout(function(){r._start(e,t,n)},0)}e(n,o),n.prototype._start=function(e,t,n){a("_start");var r=this,o=new l.XDomainRequest;t=s.addQuery(t,"t="+ +new Date),o.onerror=function(){a("onerror"),r._error()},o.ontimeout=function(){a("ontimeout"),r._error()},o.onprogress=function(){a("progress",o.responseText),r.emit("chunk",200,o.responseText)},o.onload=function(){a("load"),r.emit("finish",200,o.responseText),r._cleanup(!1)},this.xdr=o,this.unloadRef=i.unloadAdd(function(){r._cleanup(!0)});try{this.xdr.open(e,t),this.timeout&&(this.xdr.timeout=this.timeout),this.xdr.send(n)}catch(e){this._error()}},n.prototype._error=function(){this.emit("finish",0,""),this._cleanup(!1)},n.prototype._cleanup=function(e){if(a("cleanup",e),this.xdr){if(this.removeAllListeners(),i.unloadDel(this.unloadRef),this.xdr.ontimeout=this.xdr.onerror=this.xdr.onprogress=this.xdr.onload=null,e)try{this.xdr.abort()}catch(e){}this.unloadRef=this.xdr=null}},n.prototype.close=function(){a("close"),this._cleanup(!0)},n.enabled=!(!l.XDomainRequest||!t.hasDomain()),u.exports=n}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../../utils/browser":44,"../../utils/event":46,"../../utils/url":52,"debug":void 0,"events":3,"inherits":54}],35:[function(e,t,n){"use strict";var r=e("inherits"),o=e("../driver/xhr");function i(e,t,n,r){o.call(this,e,t,n,r)}r(i,o),i.enabled=o.enabled&&o.supportsCORS,t.exports=i},{"../driver/xhr":17,"inherits":54}],36:[function(e,t,n){"use strict";var r=e("events").EventEmitter;function o(){var e=this;r.call(this),this.to=setTimeout(function(){e.emit("finish",200,"{}")},o.timeout)}e("inherits")(o,r),o.prototype.close=function(){clearTimeout(this.to)},o.timeout=2e3,t.exports=o},{"events":3,"inherits":54}],37:[function(e,t,n){"use strict";var r=e("inherits"),o=e("../driver/xhr");function i(e,t,n){o.call(this,e,t,n,{noCredentials:!0})}r(i,o),i.enabled=o.enabled,t.exports=i},{"../driver/xhr":17,"inherits":54}],38:[function(e,t,n){"use strict";var i=e("../utils/event"),s=e("../utils/url"),r=e("inherits"),a=e("events").EventEmitter,l=e("./driver/websocket"),u=function(){};function c(e,t,n){if(!c.enabled())throw new Error("Transport created when disabled");a.call(this),u("constructor",e);var r=this,o=s.addPath(e,"/websocket");o="https"===o.slice(0,5)?"wss"+o.slice(5):"ws"+o.slice(4),this.url=o,this.ws=new l(this.url,[],n),this.ws.onmessage=function(e){u("message event",e.data),r.emit("message",e.data)},this.unloadRef=i.unloadAdd(function(){u("unload"),r.ws.close()}),this.ws.onclose=function(e){u("close event",e.code,e.reason),r.emit("close",e.code,e.reason),r._cleanup()},this.ws.onerror=function(e){u("error event",e),r.emit("close",1006,"WebSocket connection broken"),r._cleanup()}}r(c,a),c.prototype.send=function(e){var t="["+e+"]";u("send",t),this.ws.send(t)},c.prototype.close=function(){u("close");var e=this.ws;this._cleanup(),e&&e.close()},c.prototype._cleanup=function(){u("_cleanup");var e=this.ws;e&&(e.onmessage=e.onclose=e.onerror=null),i.unloadDel(this.unloadRef),this.unloadRef=this.ws=null,this.removeAllListeners()},c.enabled=function(){return u("enabled"),!!l},c.transportName="websocket",c.roundTrips=2,t.exports=c},{"../utils/event":46,"../utils/url":52,"./driver/websocket":19,"debug":void 0,"events":3,"inherits":54}],39:[function(e,t,n){"use strict";var r=e("inherits"),o=e("./lib/ajax-based"),i=e("./xdr-streaming"),s=e("./receiver/xhr"),a=e("./sender/xdr");function l(e){if(!a.enabled)throw new Error("Transport created when disabled");o.call(this,e,"/xhr",s,a)}r(l,o),l.enabled=i.enabled,l.transportName="xdr-polling",l.roundTrips=2,t.exports=l},{"./lib/ajax-based":24,"./receiver/xhr":32,"./sender/xdr":34,"./xdr-streaming":40,"inherits":54}],40:[function(e,t,n){"use strict";var r=e("inherits"),o=e("./lib/ajax-based"),i=e("./receiver/xhr"),s=e("./sender/xdr");function a(e){if(!s.enabled)throw new Error("Transport created when disabled");o.call(this,e,"/xhr_streaming",i,s)}r(a,o),a.enabled=function(e){return!e.cookie_needed&&!e.nullOrigin&&(s.enabled&&e.sameScheme)},a.transportName="xdr-streaming",a.roundTrips=2,t.exports=a},{"./lib/ajax-based":24,"./receiver/xhr":32,"./sender/xdr":34,"inherits":54}],41:[function(e,t,n){"use strict";var r=e("inherits"),o=e("./lib/ajax-based"),i=e("./receiver/xhr"),s=e("./sender/xhr-cors"),a=e("./sender/xhr-local");function l(e){if(!a.enabled&&!s.enabled)throw new Error("Transport created when disabled");o.call(this,e,"/xhr",i,s)}r(l,o),l.enabled=function(e){return!e.nullOrigin&&(!(!a.enabled||!e.sameOrigin)||s.enabled)},l.transportName="xhr-polling",l.roundTrips=2,t.exports=l},{"./lib/ajax-based":24,"./receiver/xhr":32,"./sender/xhr-cors":35,"./sender/xhr-local":37,"inherits":54}],42:[function(l,u,e){(function(a){(function(){"use strict";var e=l("inherits"),t=l("./lib/ajax-based"),n=l("./receiver/xhr"),r=l("./sender/xhr-cors"),o=l("./sender/xhr-local"),i=l("../utils/browser");function s(e){if(!o.enabled&&!r.enabled)throw new Error("Transport created when disabled");t.call(this,e,"/xhr_streaming",n,r)}e(s,t),s.enabled=function(e){return!e.nullOrigin&&(!i.isOpera()&&r.enabled)},s.transportName="xhr-streaming",s.roundTrips=2,s.needBody=!!a.document,u.exports=s}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"../utils/browser":44,"./lib/ajax-based":24,"./receiver/xhr":32,"./sender/xhr-cors":35,"./sender/xhr-local":37,"inherits":54}],43:[function(e,t,n){(function(n){(function(){"use strict";n.crypto&&n.crypto.getRandomValues?t.exports.randomBytes=function(e){var t=new Uint8Array(e);return n.crypto.getRandomValues(t),t}:t.exports.randomBytes=function(e){for(var t=new Array(e),n=0;n