diff --git a/meteor b/meteor index f131742641..21b45c3580 100755 --- a/meteor +++ b/meteor @@ -1,7 +1,6 @@ #!/usr/bin/env bash -BUNDLE_VERSION=22.14.0.8 - +BUNDLE_VERSION=22.14.0.9 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. 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..d323554201 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -1,5 +1,10 @@ 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'); /** * A compiler that can be instantiated with features and used inside @@ -24,12 +29,140 @@ 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 = {}, { 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: { + ...(!isLegacyWebArch && { target: 'es2015' }), + parser: { + syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript', + jsx: hasJSXSupport, + tsx: hasTSXSupport, + }, + }, + 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', + 'jsc.parser.tsx', + ]) + : baseSwcConfig; + + // Perform SWC transformation. + const transformed = SWC.transformSync(source, nextSwcConfig); + + 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), + sourceType: 'module', + }; + }); +} + +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; + + if (lastModifiedMeteorConfig?.modernTranspiler?.verbose) { + logConfigBlock('Meteor Config', lastModifiedMeteorConfig); + } + } + return lastModifiedMeteorConfig; +}; + +let lastModifiedSwcConfig; +let lastModifiedSwcConfigTime; +BCp.initializeMeteorAppSwcrc = function () { + if (!lastModifiedSwcConfig && !fs.existsSync(`${getMeteorAppDir()}/.swcrc`)) { + return; + } + const currentLastModifiedConfigTime = fs + .statSync(`${getMeteorAppDir()}/.swcrc`) + ?.mtime?.getTime(); + if (currentLastModifiedConfigTime !== lastModifiedSwcConfigTime) { + lastModifiedSwcConfigTime = currentLastModifiedConfigTime; + lastModifiedSwcConfig = getMeteorAppSwcrc(); + + if (lastModifiedMeteorConfig?.modernTranspiler?.verbose) { + logConfigBlock('SWC Config', lastModifiedSwcConfig); + } + } + 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; // 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 +184,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 +256,135 @@ 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); } 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?.modernTranspiler; + const hasModernTranspiler = config != null; + 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._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 (config?.verbose) { + logTranspilation({ + usedSwc: true, + inputFilePath, + packageName, + isNodeModulesCode, + cacheHit: true, + arch, + }); + } + return compilation; + } + compilation = compileWithSwc( + source, + lastModifiedSwcConfig, + { inputFilePath, features, arch }, + ); + // 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, + arch, + }); + } + } catch (e) { + this._swcIncompatible[cacheKey] = true; + // If SWC fails, fall back to Babel + compilation = compileWithBabel(source, babelOptions, cacheOptions); + if (config?.verbose) { + 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. @@ -564,3 +802,205 @@ 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() { + 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, + 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; +} diff --git a/packages/babel-compiler/package.js b/packages/babel-compiler/package.js index 03822c789b..0e9c3c420d 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', + "@meteorjs/swc-core": "1.1.2", }); 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..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/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/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": {}, diff --git a/scripts/dev-bundle-tool-package.js b/scripts/dev-bundle-tool-package.js index 04aaa1029a..2b92b9e5f6 100644 --- a/scripts/dev-bundle-tool-package.js +++ b/scripts/dev-bundle-tool-package.js @@ -67,7 +67,8 @@ var packageJson = { "anser": "2.1.1", 'xmlbuilder2': '1.8.1', "ws": "7.4.5", - "open":"8.4.2" + "open":"8.4.2", + "acorn": "8.14.1", } }; diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index fe6d9639bc..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, @@ -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; } @@ -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; diff --git a/tools/isobuild/js-analyze.js b/tools/isobuild/js-analyze.js index 35a929c083..2de961be63 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,9 @@ function isRegExp(value) { var AST_CACHE = new LRUCache({ max: Math.pow(2, 12), length(ast) { - return ast.loc.end.line; + // Estimate cached lines based on average length per character + const avgCharsPerLine = 40; + return Math.ceil(ast.end / avgCharsPerLine); } }); @@ -28,20 +31,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') { 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, }); 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/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 33389c324a..63a8507bac 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -167,6 +167,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..e16e814e24 --- /dev/null +++ b/v3-docs/docs/about/modern-build-stack.md @@ -0,0 +1,26 @@ +# 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. + +## Quick start + +Start using the new build stack by creating a Meteor app, or add this to your `package.json` in an existing one: + +```json +"meteor": { + "modernWebArchsOnly": true, + "modernTranspiler": true +} +``` + +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. 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..d5ebb5b55e --- /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..278da99499 --- /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, 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. + +## 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`, which SWC doesn't support yet, 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](#config-api) 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. + +## 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/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. + +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.**