Merge pull request #13657 from meteor/swc-replacement

SWC adoption as bundler transpiler
This commit is contained in:
Nacho Codoñer
2025-04-15 15:20:04 +02:00
committed by GitHub
27 changed files with 697 additions and 61 deletions

3
meteor
View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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');
}
}

View File

@@ -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);

View File

@@ -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);
},
}
}

View File

@@ -42,6 +42,7 @@ const packages = {
autopublish: {},
"babel-compiler": {
serverFiles: ["babel.js", "babel-compiler.js"],
ignoredFiles: ["babel-compiler.js"],
},
"babel-runtime": {},
"browser-policy": {},

View File

@@ -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",
}
};

View File

@@ -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<Record<string, ImportInfo>> {
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<string, ImportInfo>;
}
@@ -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;

View File

@@ -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') {

View File

@@ -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,
});

View File

@@ -22,6 +22,7 @@
"server": "server/main.js"
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -18,6 +18,7 @@
"server": "server/main.js"
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -25,6 +25,7 @@
"server": "server/main.js"
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -17,6 +17,7 @@
"server": "server/main.js"
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -19,6 +19,7 @@
"server": "server/main.js"
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -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",

View File

@@ -28,6 +28,7 @@
}
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -23,6 +23,7 @@
"server": "server/main.js"
},
"testModule": "tests/main.js",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -26,6 +26,7 @@
"server": "server/main.ts"
},
"testModule": "tests/main.ts",
"modernWebArchsOnly": true
"modernWebArchsOnly": true,
"modernTranspiler": true
}
}

View File

@@ -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,
},

View File

@@ -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 apps 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, were 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 apps code, supporting tree-shaking, popular plugins, and better features for both development and production. Meanwhile, Meteors 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.

View File

@@ -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`.

View File

@@ -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 <file> (<context>) Fallback
<more-details>
```
There are a few things you can do.
First, check the fallback details. It may show why SWC couldnt 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 doesnt support.
Other reasons might involve features tied to Babel plugins. If so, youll 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 SWCs 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 apps 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 apps 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 Meteors 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 Meteors 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). Were 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.**