diff --git a/.circleci/config.yml b/.circleci/config.yml index be2fe2cae4..8b714c9285 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,6 +115,9 @@ build_machine_environment: NUM_GROUPS: 12 RUNNING_AVG_LENGTH: 6 + # Force modern bundler test + METEOR_MODERN: true + jobs: Get Ready: <<: *build_machine_environment diff --git a/.github/workflows/meteor-selftest-windows.yml b/.github/workflows/meteor-selftest-windows.yml index c6831b7a6f..c328e7bf2d 100644 --- a/.github/workflows/meteor-selftest-windows.yml +++ b/.github/workflows/meteor-selftest-windows.yml @@ -18,6 +18,7 @@ env: TIMEOUT_SCALE_FACTOR: 20 METEOR_HEADLESS: true SELF_TEST_EXCLUDE: '^NULL-LEAVE-THIS-HERE-NULL$' + METEOR_MODERN: true jobs: test: diff --git a/.travis.yml b/.travis.yml index 5d5a1e7852..622ed6cadd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ dist: jammy sudo: required services: xvfb node_js: - - "22.14.0" + - "22.16.0" cache: directories: - ".meteor" @@ -17,6 +17,7 @@ env: - phantom=false - PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium - TEST_PACKAGES_EXCLUDE=stylus + - METEOR_MODERN=true addons: apt: sources: 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/history.md b/docs/history.md index e206711c49..7c142c2e11 100644 --- a/docs/history.md +++ b/docs/history.md @@ -8,6 +8,112 @@ [//]: # (go to meteor/docs/generators/changelog/docs) +## v3.3.0, 2025-06-11 + +### Highlights + +- Support SWC transpiler and minifier for faster dev and builds [PR#13657](https://github.com/meteor/meteor/pull/13657), [PR#13715](https://github.com/meteor/meteor/pull/13715) +- Switch to `@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 and total, support `--build` [PR#16](https://github.com/meteor/performance/pull/16), [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 +- Allow opting out of usage stats with `DO_NOT_TRACK` +- Update Node to 22.16.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 Changelog: [react-meteor-data@4.0.0](https://github.com/meteor/react-packages/tree/master/packages/react-meteor-data/CHANGELOG.md#v400-2025-06-11) + +#### 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` + - 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 + +- `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 +``` + +To apply react-meteor-data changes: + +```bash +meteor add react-meteor-data@4.0.0 +``` + +**Add this to your `package.json` to enable the new modern build stack:** + +```json +"meteor": { + "modern": true +} +``` + +> These settings are on by default for new apps. + +#### 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 +- minifier-js@3.0.2 +- modern-browsers@0.2.2 +- mongo@2.1.2 +- server-render@0.4.3 +- socket-stream-client@0.6.1 +- standard-minifier-js@3.1.0 +- typescript@5.6.4 +- 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) +- [@StorytellerCZ](https://github.com/StorytellerCZ) + +✨✨✨ + + ## v3.0.1, 2024-07-16 ### Highlights 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/meteor b/meteor index 745c3affbf..e452f7b273 100755 --- a/meteor +++ b/meteor @@ -1,7 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.4 - +BUNDLE_VERSION=22.16.0.1 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. 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-base/package.js b/packages/accounts-base/package.js index 6af3172a4e..31711cdf33 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", }); Package.onUse((api) => { diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index 31786541ba..447bfbefc0 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", }); Npm.depends({ 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..f34e172dc7 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1789,7 +1789,30 @@ if (Meteor.isServer) (() => { ]); }); - Tinytest.addAsync("passwords - remove email", + + +Tinytest.addAsync("accounts emails - replace email", async test => { + const origEmail = `originalemail@test.com`; + const userId = await Accounts.createUserAsync({ + email: origEmail, + password: 'password' + }); + + const newEmail = `newemail@test.com`; + + const u1 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u1.emails, [ + { address: origEmail, verified: false } + ]); + + 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/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/autoupdate/package.js b/packages/autoupdate/package.js index 4769c9e31b..514439ce1c 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', }); Package.onUse(function(api) { diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 11cf63f923..506f865117 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -1,14 +1,21 @@ var semver = Npm.require("semver"); var JSON5 = Npm.require("json5"); +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'); +var path = Npm.require('path'); +var vm = Npm.require('vm'); +var crypto = Npm.require('crypto'); /** * A compiler that can be instantiated with features and used inside * Plugin.registerCompiler * @param {Object} extraFeatures The same object that getDefaultOptions takes */ -BabelCompiler = function BabelCompiler(extraFeatures, modifyBabelConfig) { +BabelCompiler = function BabelCompiler(extraFeatures, modifyConfig) { this.extraFeatures = extraFeatures; - this.modifyBabelConfig = modifyBabelConfig; + this.modifyConfig = modifyConfig; this._babelrcCache = null; this._babelrcWarnings = Object.create(null); this.cacheDirectory = null; @@ -18,18 +25,166 @@ var BCp = BabelCompiler.prototype; var excludedFileExtensionPattern = /\.(es5|min)\.js$/i; var hasOwn = Object.prototype.hasOwnProperty; +// Check if verbose mode is enabled either in the provided config or in extraFeatures +BCp.isVerbose = function(config) { + if (config?.modern?.transpiler?.verbose) { + return true; + } + if (config?.verbose) { + return true; + } + return !!this.extraFeatures?.verbose; +}; + // There's no way to tell the current Meteor version, but we can infer // whether it's Meteor 1.4.4 or earlier by checking the Node version. 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, swcOptions = {}, { features }) { + return profile('SWC.compile', function () { + // Perform SWC transformation. + const transformed = SWC.transformSync(source, swcOptions); + + 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, + }), + }); + content = result.code; + + return { + code: content, + map: JSON.parse(transformed.map), + sourceType: 'module', + }; + }); +} +const DEFAULT_MODERN = { + transpiler: true, +}; + +const normalizeModern = (r = false) => Object.fromEntries( + Object.entries(DEFAULT_MODERN).map(([k, def]) => [ + k, + r === true + ? def + : r === false || r?.[k] === false + ? false + : typeof r?.[k] === 'object' + ? { ...r[k] } + : def, + ]), +); + +let modernForced = JSON.parse(process.env.METEOR_MODERN || "false"); + +let lastModifiedMeteorConfig; +let lastModifiedMeteorConfigTime; +BCp.initializeMeteorAppConfig = function () { + if (!lastModifiedMeteorConfig && !fs.existsSync(`${getMeteorAppDir()}/package.json`)) { + return; + } + const currentLastModifiedConfigTime = fs + .statSync(`${getMeteorAppDir()}/package.json`) + ?.mtime?.getTime(); + if (currentLastModifiedConfigTime !== lastModifiedMeteorConfigTime) { + lastModifiedMeteorConfigTime = currentLastModifiedConfigTime; + lastModifiedMeteorConfig = getMeteorAppPackageJson()?.meteor; + lastModifiedMeteorConfig = lastModifiedMeteorConfig != null ? { + ...lastModifiedMeteorConfig, + modern: normalizeModern(modernForced || lastModifiedMeteorConfig?.modern), + } : {}; + + if (this.isVerbose(lastModifiedMeteorConfig)) { + logConfigBlock('Meteor Config', lastModifiedMeteorConfig); + } + } + return lastModifiedMeteorConfig; +}; + +let lastModifiedSwcConfig; +let lastModifiedSwcConfigTime; +BCp.initializeMeteorAppSwcrc = function () { + const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`); + const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`); + if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) { + return; + } + const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc'; + const filePath = `${getMeteorAppDir()}/${swcFile}`; + const fileStats = fs.statSync(filePath); + const fileModTime = fileStats?.mtime?.getTime(); + + let currentLastModifiedConfigTime; + if (hasSwcJs) { + // For dynamic JS files, first get the resolved configuration + const resolvedConfig = lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile); + // Calculate a hash of the resolved configuration to detect changes + const contentHash = crypto + .createHash('sha256') + .update(JSON.stringify(resolvedConfig)) + .digest('hex'); + // Combine file modification time and content hash to create a unique identifier + currentLastModifiedConfigTime = `${fileModTime}-${contentHash}`; + // Store the resolved configuration + lastModifiedSwcConfig = resolvedConfig; + } else { + // For static JSON files, just use the file modification time + currentLastModifiedConfigTime = fileModTime; + } + + if (currentLastModifiedConfigTime !== lastModifiedSwcConfigTime) { + lastModifiedSwcConfigTime = currentLastModifiedConfigTime; + lastModifiedSwcConfig = getMeteorAppSwcrc(swcFile); + + if (this.isVerbose(lastModifiedMeteorConfig)) { + logConfigBlock('SWC Config', lastModifiedSwcConfig); + } + } + return lastModifiedSwcConfig; +}; + +let lastModifiedSwcLegacyConfig; +BCp.initializeMeteorAppLegacyConfig = function () { + const swcLegacyConfig = convertBabelTargetsForSwc(Babel.getMinimumModernBrowserVersions()); + if (this.isVerbose(lastModifiedMeteorConfig) && !lastModifiedSwcLegacyConfig) { + logConfigBlock('SWC Legacy Config', swcLegacyConfig); + } + lastModifiedSwcLegacyConfig = swcLegacyConfig; + return lastModifiedSwcConfig; +}; + BCp.processFilesForTarget = function (inputFiles) { var compiler = this; // Reset this cache for each batch processed. this._babelrcCache = null; + this.initializeMeteorAppConfig(); + this.initializeMeteorAppSwcrc(); + this.initializeMeteorAppLegacyConfig(); + inputFiles.forEach(function (inputFile) { if (inputFile.supportsLazyCompilation) { inputFile.addJavaScript({ @@ -51,6 +206,8 @@ BCp.processFilesForTarget = function (inputFiles) { // null to indicate there was an error, and nothing should be added. BCp.processOneFileForTarget = function (inputFile, source) { 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 @@ -121,32 +278,196 @@ BCp.processOneFileForTarget = function (inputFile, source) { }, }; - this.inferTypeScriptConfig( - features, inputFile, cacheOptions.cacheDeps); - - var babelOptions = Babel.getDefaultOptions(features); - babelOptions.caller = { name: "meteor", arch }; - - this.inferExtraBabelOptions( - inputFile, - babelOptions, - cacheOptions.cacheDeps - ); - - babelOptions.sourceMaps = true; - babelOptions.filename = - babelOptions.sourceFileName = packageName - ? "packages/" + packageName + "/" + inputFilePath + const filename = packageName + ? `packages/${packageName}/${inputFilePath}` : inputFilePath; - if (this.modifyBabelConfig) { - this.modifyBabelConfig(babelOptions, inputFile); - } + const setupBabelOptions = () => { + this.inferTypeScriptConfig(features, inputFile, cacheOptions.cacheDeps); + var babelOptions = Babel.getDefaultOptions(features); + babelOptions.caller = { name: "meteor", arch }; + + babelOptions.sourceMaps = true; + babelOptions.filename = babelOptions.sourceFileName = filename; + + this.inferExtraBabelOptions(inputFile, babelOptions, cacheOptions.cacheDeps); + + if (this.modifyConfig) { + this.modifyConfig(babelOptions, inputFile); + } + + return babelOptions; + }; + + const setupSWCOptions = () => { + const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx'); + const hasTSXSupport = inputFilePath.endsWith('.tsx'); + const hasJSXSupport = inputFilePath.endsWith('.jsx'); + const isLegacyWebArch = arch.includes('legacy'); + + var swcOptions = { + jsc: { + ...(!isLegacyWebArch && { target: 'es2015' }), + parser: { + syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', + jsx: hasJSXSupport, + tsx: hasTSXSupport, + }, + }, + module: { type: 'es6' }, + minify: false, + sourceMaps: true, + filename, + sourceFileName: filename, + ...(isLegacyWebArch && { + env: { targets: lastModifiedSwcLegacyConfig || {} }, + }), + }; + + // Merge with app-level SWC config + if (lastModifiedSwcConfig) { + swcOptions = deepMerge(swcOptions, lastModifiedSwcConfig, [ + 'env.targets', + 'module.type', + ]); + } + + this.inferExtraSWCOptions(inputFile, swcOptions, cacheOptions.cacheDeps); + + if (!!this.extraFeatures?.swc && this.modifyConfig) { + this.modifyConfig(swcOptions, inputFile); + } + + // Resolve custom baseUrl to an absolute path pointing to the project root + if (swcOptions.jsc && swcOptions.jsc.baseUrl) { + swcOptions.jsc.baseUrl = path.resolve(process.cwd(), swcOptions.jsc.baseUrl); + } + + return swcOptions; + }; + + var babelOptions = { filename }; try { - var result = profile('Babel.compile', function () { - return Babel.compile(source, babelOptions, cacheOptions); - }); + var result = (() => { + 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?.modern?.transpiler; + const hasModernTranspiler = config != null && config !== false; + const shouldSkipSwc = + !hasModernTranspiler || + (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 || [])) || + (isNodeModulesCode && + Array.isArray(config?.excludeNodeModules) && + (isExcludedConfig(inputFilePath, config?.excludeNodeModules || []) || + isExcludedConfig( + inputFilePath.replace('node_modules/', ''), + config?.excludeNodeModules || [], + true, + ))) || + (isPackageCode && + Array.isArray(config?.excludePackages) && + (isExcludedConfig(packageName, config?.excludePackages || []) || + isExcludedConfig( + `${packageName}/${inputFilePath}`, + config?.excludePackages || [], + ))); + + 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.extraFeatures?.swc) && + !this._swcIncompatible[cacheKey]; + let compilation; + try { + let usedSwc = false; + if (shouldUseSwc) { + // Create a cache key based on the source hash and the compiler used + // Check cache + compilation = this.readFromSwcCache({ cacheKey }); + // Return cached result if found. + if (compilation) { + if (this.isVerbose(config)) { + logTranspilation({ + usedSwc: true, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: true, + arch, + }); + } + return compilation; + } + + const swcOptions = setupSWCOptions(); + compilation = compileWithSwc( + source, + swcOptions, + { features }, + ); + // Save result in cache + this.writeToSwcCache({ cacheKey, compilation }); + usedSwc = true; + } else { + // Set up Babel options only when compiling with Babel + babelOptions = setupBabelOptions(); + + compilation = compileWithBabel(source, babelOptions, cacheOptions); + usedSwc = false; + } + + if (this.isVerbose(config)) { + logTranspilation({ + usedSwc, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: false, + arch, + }); + } + } catch (e) { + this._swcIncompatible[cacheKey] = true; + // If SWC fails, fall back to Babel + + babelOptions = setupBabelOptions(); + compilation = compileWithBabel(source, babelOptions, cacheOptions); + if (this.isVerbose(config)) { + logTranspilation({ + usedSwc: false, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: false, + arch, + errorMessage: e?.message, + ...(e?.message?.includes( + 'cannot be used outside of module code', + ) && { + tip: 'Remove nested imports or replace them with require to support SWC and improve speed.', + }), + }); + } + } + + return compilation; + })(); } catch (e) { if (e.loc) { // Error is from @babel/parser. @@ -256,6 +577,15 @@ BCp.inferExtraBabelOptions = function (inputFile, babelOptions, cacheDeps) { ); }; +BCp.inferExtraSWCOptions = function (inputFile, swcOptions, cacheDeps) { + if (! inputFile.require || + ! inputFile.findControlFile || + ! inputFile.readAndWatchFile) { + return false; + } + return this._inferFromSwcRc(inputFile, swcOptions, cacheDeps); +}; + BCp._inferFromBabelRc = function (inputFile, babelOptions, cacheDeps) { var babelrcPath = inputFile.findControlFile(".babelrc"); if (babelrcPath) { @@ -308,6 +638,65 @@ BCp._inferFromPackageJson = function (inputFile, babelOptions, cacheDeps) { } }; +BCp._inferFromSwcRc = function (inputFile, swcOptions, cacheDeps) { + var swcrcPath = inputFile.findControlFile(".swcrc"); + if (swcrcPath) { + if (! hasOwn.call(this._babelrcCache, swcrcPath)) { + try { + this._babelrcCache[swcrcPath] = { + controlFilePath: swcrcPath, + controlFileData: JSON.parse( + inputFile.readAndWatchFile(swcrcPath)), + deps: Object.create(null), + }; + } catch (e) { + if (e instanceof SyntaxError) { + e.message = ".swcrc is not a valid JSON file: " + e.message; + } + throw e; + } + } + + const cacheEntry = this._babelrcCache[swcrcPath]; + + if (this._inferHelperForSwc(inputFile, cacheEntry)) { + deepMerge(swcOptions, cacheEntry.controlFileData); + Object.assign(cacheDeps, cacheEntry.deps); + return true; + } + } +}; + +BCp._inferHelperForSwc = function (inputFile, cacheEntry) { + if (! cacheEntry.controlFileData) { + return false; + } + + if (hasOwn.call(cacheEntry, "finalInferHelperForSwcResult")) { + // We've already run _inferHelperForSwc and populated + // cacheEntry.controlFileData, so we can return early here. + return cacheEntry.finalInferHelperForSwcResult; + } + + // First, ensure that the current file path is not excluded. + if (cacheEntry.controlFileData.exclude) { + const exclude = cacheEntry.controlFileData.exclude; + const path = inputFile.getPathInPackage(); + + if (exclude instanceof Array) { + for (let i = 0; i < exclude.length; ++i) { + if (path.match(exclude[i])) { + return cacheEntry.finalInferHelperForSwcResult = false; + } + } + } else if (path.match(exclude)) { + return cacheEntry.finalInferHelperForSwcResult = false; + } + } + + return cacheEntry.finalInferHelperForSwcResult = true; +}; + BCp._inferHelper = function (inputFile, cacheEntry) { if (! cacheEntry.controlFileData) { return false; @@ -564,3 +953,245 @@ 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. + } + } +}; + +function getMeteorAppDir() { + return process.cwd(); +} + +function getMeteorAppPackageJson() { + return JSON.parse( + fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8'), + ); +} + +function getMeteorAppSwcrc(file = '.swcrc') { + try { + const filePath = `${getMeteorAppDir()}/${file}`; + if (file.endsWith('.js')) { + let content = fs.readFileSync(filePath, 'utf-8'); + // Check if the content uses ES module syntax (export default) + if (content.includes('export default')) { + // Transform ES module syntax to CommonJS + content = content.replace(/export\s+default\s+/, 'module.exports = '); + } + const script = new vm.Script(` + (function() { + const module = {}; + module.exports = {}; + (function(exports, module) { + ${content} + })(module.exports, module); + return module.exports; + })() + `); + const context = vm.createContext({ process }); + return script.runInContext(context); + } else { + // For .swcrc and other JSON files, parse as JSON + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } + } catch (e) { + console.error(`Error parsing ${file} 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; + }); +} + +const disableTextColors = Boolean(JSON.parse(process.env.METEOR_DISABLE_COLORS || "false")); + +function color(text, code) { + return disableTextColors ? text : `\x1b[${code}m${text}\x1b[0m`; +} + +function logTranspilation({ + packageName, + inputFilePath, + usedSwc, + cacheHit, + isNodeModulesCode, + arch, + 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) + : ''; + const archPart = arch ? color(` (${arch})`, 90) : ''; + console.log( + `${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}${archPart}`, + ); + if (errorMessage) { + console.log(); + console.log(` ↳ ${color('Error:', 31)} ${errorMessage}`); + if (tip) { + console.log(); + console.log(` ${color('💡 Tip:', 33)} ${tip}`); + } + console.log(); + } +} + +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; + + // 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; +} + +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; +} + +/** + * A compiler that extends BabelCompiler but always uses SWC + * @param {Object} extraFeatures Additional features to pass to BabelCompiler + * @param {Function} modifyConfig Function to modify the configuration + */ +SwcCompiler = function SwcCompiler(extraFeatures, modifyConfig) { + extraFeatures = extraFeatures || {}; + extraFeatures.swc = true; + BabelCompiler.call(this, extraFeatures, modifyConfig); +}; + +// Inherit from BabelCompiler +SwcCompiler.prototype = Object.create(BabelCompiler.prototype); +SwcCompiler.prototype.constructor = SwcCompiler; diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 03822c789b..4a45656d27 100644 --- a/packages/babel-compiler/package.js +++ b/packages/babel-compiler/package.js @@ -1,13 +1,14 @@ Package.describe({ name: "babel-compiler", summary: "Parser/transpiler for ECMAScript 2015+ syntax", - version: '7.11.3', + version: '7.12.0', }); Npm.depends({ '@meteorjs/babel': '7.20.1', 'json5': '2.2.3', - 'semver': '7.6.3' + 'semver': '7.6.3', + "@meteorjs/swc-core": "1.1.3", }); Package.onUse(function (api) { @@ -22,4 +23,5 @@ Package.onUse(function (api) { api.export('Babel', 'server'); api.export('BabelCompiler', 'server'); + api.export('SwcCompiler', 'server'); }); 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/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index 032ed11c8d..b1bed1ae86 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', }); Npm.depends({ 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, diff --git a/packages/ddp-client/package.js b/packages/ddp-client/package.js index 1b0e48c929..a9e4c534ef 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", documentation: null, }); diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index e74159738d..a8fd89363d 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', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); 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/ejson/package.js b/packages/ejson/package.js index 3ff1a14262..7cb875555d 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', }); Package.onUse(function onUse(api) { diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index 950f9bca9b..88f7661c70 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.2", + version: "3.3.0", }); Package.includeTool(); 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 **/ diff --git a/packages/meteor/package.js b/packages/meteor/package.js index a9ee2425dc..9b7ca6575a 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', }); Package.registerBuildPlugin({ diff --git a/packages/minifier-js/package.js b/packages/minifier-js/package.js index 80b0a54ff5..310391016c 100644 --- a/packages/minifier-js/package.js +++ b/packages/minifier-js/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "JavaScript minifier", - version: '3.0.1', + version: '3.0.2', }); Npm.depends({ 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; diff --git a/packages/modern-browsers/package.js b/packages/modern-browsers/package.js index 14146d9ab7..5187b13a29 100644 --- a/packages/modern-browsers/package.js +++ b/packages/modern-browsers/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'modern-browsers', - version: '0.2.1', + version: '0.2.2', summary: 'API for defining the boundary between modern and legacy ' + 'JavaScript clients', diff --git a/packages/mongo/collection/collection.js b/packages/mongo/collection/collection.js index 28776c9a93..ec2e3323b7 100644 --- a/packages/mongo/collection/collection.js +++ b/packages/mongo/collection/collection.js @@ -11,6 +11,7 @@ import { validateCollectionName } from './collection_utils'; import { ReplicationMethods } from './methods_replication'; +import { watchChangeStream } from './watch_change_stream'; /** * @summary Namespace for MongoDB-related items @@ -263,6 +264,10 @@ Mongo.Collection.ObjectID = Mongo.ObjectID; */ Meteor.Collection = Mongo.Collection; + // Allow deny stuff is now in the allow-deny package Object.assign(Mongo.Collection.prototype, AllowDeny.CollectionPrototype); +// Só agora que Mongo.Collection existe, adicionamos o método ao prototype +Object.assign(Mongo.Collection.prototype, { watchChangeStream }); + 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/packages/mongo/collection/watch_change_stream.js b/packages/mongo/collection/watch_change_stream.js new file mode 100644 index 0000000000..a4e8ae7b95 --- /dev/null +++ b/packages/mongo/collection/watch_change_stream.js @@ -0,0 +1,31 @@ +/** + * @summary Watches the MongoDB collection using Change Streams. + * @locus Server + * @memberof Mongo.Collection + * @instance + * @param {Array} [pipeline] Optional aggregation pipeline to filter Change Stream events. + * @param {Object} [options] Optional settings for the Change Stream. + * @returns {ChangeStream} The MongoDB ChangeStream instance. + * @throws {Error} If called on a client/minimongo collection. + * + * @example + * const changeStream = MyCollection.watchChangeStream([ + * { $match: { 'operationType': 'insert' } } + * ]); + * changeStream.on('change', (change) => { + * console.log('Change detected:', change); + * }); + */ + +export function watchChangeStream(pipeline = [], options = {}) { + // Only available on server + if (typeof Package === 'undefined' || !this.rawCollection) { + throw new Error('watchChangeStream is only available on server collections'); + } + const raw = this.rawCollection(); + if (!raw.watch) { + throw new Error('Underlying collection does not support watch (Change Streams)'); + } + console.log('[watchChangeStream] Chamando raw.watch() com pipeline:', JSON.stringify(pipeline, null, 2), 'e options:', JSON.stringify(options, null, 2)); + return raw.watch(pipeline, options); +} 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/package.js b/packages/mongo/package.js index 0013a658e3..cefaf18449 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", }); Npm.depends({ 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( diff --git a/packages/non-core/coffeescript-compiler/package.js b/packages/non-core/coffeescript-compiler/package.js index 91a790c4a4..c0ff2e50d8 100644 --- a/packages/non-core/coffeescript-compiler/package.js +++ b/packages/non-core/coffeescript-compiler/package.js @@ -3,7 +3,7 @@ Package.describe({ summary: 'Compiler for CoffeeScript code, supporting the coffeescript package', // This version of NPM `coffeescript` module, with _1, _2 etc. // If you change this, make sure to also update ../coffeescript/package.js to match. - version: '2.4.2' + version: '2.4.2', }); Npm.depends({ diff --git a/packages/non-core/coffeescript/package.js b/packages/non-core/coffeescript/package.js index 5d10541f80..63448dadfc 100644 --- a/packages/non-core/coffeescript/package.js +++ b/packages/non-core/coffeescript/package.js @@ -6,12 +6,12 @@ Package.describe({ // so bumping the version of this package will be how they get newer versions // of `coffeescript-compiler`. If you change this, make sure to also update // ../coffeescript-compiler/package.js to match. - version: '2.7.2' + version: '2.7.3', }); Package.registerBuildPlugin({ name: 'compile-coffeescript', - use: ['caching-compiler@2.0.0-rc300.2', 'ecmascript@0.16.9-rc300.2', 'coffeescript-compiler@2.4.1'], + use: ['caching-compiler@2.0.1', 'ecmascript@0.16.11', 'coffeescript-compiler@2.4.2'], sources: ['compile-coffeescript.js'], npmDependencies: { // A breaking change was introduced in @babel/runtime@7.0.0-beta.56 diff --git a/packages/server-render/package.js b/packages/server-render/package.js index fabcf15208..e71142714a 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', summary: "Generic support for server-side rendering in Meteor apps", documentation: "README.md" }); 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; diff --git a/packages/socket-stream-client/package.js b/packages/socket-stream-client/package.js index b67eafc1f9..28b94f29df 100644 --- a/packages/socket-stream-client/package.js +++ b/packages/socket-stream-client/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "socket-stream-client", - version: '0.6.0', + version: '0.6.1', summary: "Provides the ClientStream abstraction used by ddp-client", documentation: "README.md" }); 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