Documentation Index
Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt
Use this file to discover all available pages before exploring further.
Bundler
The bundler is responsible for combining packages and application code into deployable bundles for different architectures.Bundle Format
Meteor creates site archives (.star files) with this structure:
star.json
// From bundler.js header comments
{
format: "site-archive-pre1",
builtBy: "Meteor 0.6.0",
programs: [
{
name: "web.browser",
arch: "web.browser",
path: "programs/web.browser"
},
{
name: "server",
arch: "os.linux.x86_64",
path: "programs/server"
}
],
plugins: [
{
name: "my-plugin",
arch: "os",
path: "programs/my-plugin/program.json"
}
],
meteorRelease: "METEOR@2.8.0"
}
Main Entry Point
// From bundler.js - contents of main.js in bundles
exports._mainJsContents = [
"",
"process.argv.splice(2, 0, 'program.json');",
"process.chdir(require('path').join(__dirname, 'programs', 'server'));",
'require("./programs/server/runtime.js")({ cachePath: process.env.METEOR_REIFY_CACHE_DIR });',
"require('./programs/server/boot.js');",
].join("\n");
Target Architecture
The bundler creates different targets for different platforms:ClientTarget (Web)
class ClientTarget extends Target {
constructor(options) {
super(options);
// CSS files in load order
this.css = [];
// HTML segments for head/body
this.head = [];
this.body = [];
if (!archinfo.matches(this.arch, 'web')) {
throw new Error('ClientTarget targeting something that isn\'t a client?');
}
}
}
Server Target
Server targets use the JsImage format:class JsImage {
constructor() {
// Files to load at startup
this.jsToLoad = [];
// Node modules to include
this.nodeModulesDirectories = Object.create(null);
// Target architecture
this.arch = null;
}
}
Web Program Format
// program.json for web targets
{
format: "web-program-pre1",
manifest: [
{
path: "app/template.myapp.js",
where: "client",
type: "js",
cacheable: true,
url: "/1a2b3c4d5e.js?hash=xyz",
size: 12345,
hash: "abc123...",
sri: "sha512-...",
sourceMap: "app/template.myapp.js.map"
},
{
where: "internal",
type: "head",
path: "head.html",
hash: "def456..."
}
]
}
Build Process
Themake() method orchestrates the entire build:
// From bundler.js - Target.prototype.make
async make({packages, minifyMode, addCacheBusters, minifiers, onJsOutputFiles}) {
buildmessage.assertInCapture();
await buildmessage.enterJob("building for " + this.arch, async () => {
// 1. Determine load order
await this._determineLoadOrder({ packages });
// 2. Run compiler plugins
const sourceBatches = await this._runCompilerPlugins({
minifiers,
minifyMode,
});
// 3. Link JavaScript and emit resources
await this._emitResources(sourceBatches, onJsOutputFiles);
// 4. Add direct Cordova dependencies
await this._addDirectCordovaDependencies();
// 5. Minify (client targets only)
if (this instanceof ClientTarget) {
if (minifiersByExt.js) {
await this.minifyJs(minifiersByExt.js, minifyMode);
}
if (minifiersByExt.css) {
await this.minifyCss(minifiersByExt.css, minifyMode);
}
}
// 6. Rewrite source maps
this.rewriteSourceMaps();
// 7. Add cache busters
if (addCacheBusters) {
this._addCacheBusters("js");
this._addCacheBusters("css");
}
});
}
Load Order Determination
The bundler uses a two-phase topological sort:async _determineLoadOrder({packages}) {
// Phase 1: Which unibuilds will be used?
const usedUnibuilds = {};
const addToGetsUsed = async function (unibuild) {
if (_.has(usedUnibuilds, unibuild.id)) return;
usedUnibuilds[unibuild.id] = unibuild;
if (unibuild.kind === 'main') {
this.usedPackages[unibuild.pkg.name] = true;
}
await compiler.eachUsedUnibuild({
dependencies: unibuild.uses,
arch: this.arch,
skipDebugOnly: this.buildMode === 'production',
skipProdOnly: this.buildMode !== 'production',
skipTestOnly: this.buildMode !== 'test',
}, addToGetsUsed);
}.bind(this);
// Phase 2: In what order should we load them?
const needed = _.clone(usedUnibuilds);
const onStack = {};
const add = async function (unibuild) {
if (!_.has(needed, unibuild.id)) return;
// Check for circular dependencies
var processUnibuild = async function (usedUnibuild) {
if (onStack[usedUnibuild.id]) {
buildmessage.error(
"circular dependency between packages " +
unibuild.pkg.name + " and " + usedUnibuild.pkg.name
);
return;
}
onStack[usedUnibuild.id] = true;
await add(usedUnibuild);
delete onStack[usedUnibuild.id];
};
await compiler.eachUsedUnibuild({
dependencies: unibuild.uses,
arch: this.arch,
skipUnordered: true,
acceptableWeakPackages: this.usedPackages,
}, processUnibuild);
this.unibuilds.push(unibuild);
delete needed[unibuild.id];
}.bind(this);
}
File Management
The bundler uses aFile class to represent resources:
class File {
constructor(options) {
// Source path on disk
this.sourcePath = options.sourcePath;
// Target path in bundle
this.targetPath = null;
// URL for serving over HTTP
this.url = null;
this.urlPrefix = "";
// Cache control
this.cacheable = options.cacheable || false;
// Hot Module Replacement
this.replaceable = options.replaceable;
// Node modules for this file
this.nodeModulesDirectories = Object.create(null);
// Server assets
this.assets = null;
// Contents and hashing
this._contents = options.data || null;
this._hash = null;
this._sri = null;
}
hash() {
if (!this._hash) {
const hashes = [String(File._salt())];
if (typeof this._inputHash === "string") {
hashes.push(this._inputHash);
}
if (!this._skipSri) {
hashes.push(this.sri());
}
this._hash = watch.sha1(...hashes);
}
return this._hash;
}
sri() {
if (!this._sri && !this._skipSri) {
this._sri = watch.sha512(this.contents());
}
return this._sri;
}
}
Cache Busting
addCacheBuster() {
if (!this.url) {
throw new Error("File must have a URL");
}
if (this.cacheable) {
return; // Already has hash in URL
}
if (/\?/.test(this.url)) {
throw new Error("URL already has a query string");
}
this.url += "?hash=" + this.hash();
this.cacheable = true;
}
setUrlToHash(fileAndUrlSuffix, urlSuffix) {
urlSuffix = urlSuffix || "";
this.url = this.urlPrefix + "/" +
this.hash() + fileAndUrlSuffix + urlSuffix;
this.cacheable = true;
this.targetPath = this.hash() + fileAndUrlSuffix;
}
Source Map Rewriting
rewriteSourceMaps() {
const rewriteSourceMap = function (sm) {
if (!sm.sources) return sm;
sm.sources = sm.sources.map(function (path) {
const prefix = "meteor://💻app";
if (path.slice(0, prefix.length) === prefix) {
return path;
}
// PERSONAL COMPUTER emoji ensures category is last in DevTools
return prefix + (path[0] === '/' ? '' : '/') + path;
});
return sm;
};
this.js?.forEach(js => {
if (js.sourceMap) {
js.sourceMap = rewriteSourceMap(js.sourceMap);
}
});
this.css?.forEach(css => {
if (css.sourceMap) {
css.sourceMap = rewriteSourceMap(css.sourceMap);
}
});
}
Minification
async minifyJs(minifierDef, minifyMode) {
const staticFiles = [];
const dynamicFiles = [];
const inputHashesByJsFile = new Map;
this.js.forEach(file => {
const jsf = new JsFile(file, { arch: this.arch });
inputHashesByJsFile.set(jsf, file.hash());
if (file.targetPath.startsWith("dynamic/")) {
dynamicFiles.push(jsf);
} else {
staticFiles.push(jsf);
}
});
var markedMinifier = buildmessage.markBoundary(
minifierDef.userPlugin.processFilesForBundle,
minifierDef.userPlugin
);
await buildmessage.enterJob('minifying app code', async function () {
await Promise.all([
markedMinifier(staticFiles, { minifyMode }),
...dynamicFiles.map(
file => markedMinifier([file], { minifyMode })
),
]);
});
}
Node Modules Handling
class NodeModulesDirectory {
getPreferredBundlePath(kind) {
let relPath = files.pathRelative(this.sourceRoot, this.sourcePath);
const isApp = !this.packageName;
if (!isApp) {
const relParts = relPath.split(files.pathSep);
const name = colonConverter.convert(
this.packageName.replace(/^local-test[:_]/, ""));
// Normalize .npm/package/node_modules paths
if (relParts[0] === ".npm") {
if (relParts[1] === "devPackage") {
relParts.splice(0, 2, 'dev');
} else if (relParts[1] === "package") {
relParts.splice(0, 2);
} else if (relParts[1] === "plugin") {
relParts.splice(0, 3);
}
}
if (kind === "bundle") {
relParts.unshift("node_modules", "meteor", name);
}
relPath = files.pathJoin(...relParts);
}
return files.pathJoin("npm", relPath);
}
}
Ignored Files
// From bundler.js
exports.ignoreFiles = [
/~$/, // Backup files
/^\.#/, // Emacs lock files
/^(\.meteor\/|\.git\/|Thumbs\.db|\.DS_Store\/?|Icon\r|ehthumbs\.db|\..*\.sw.|#.*#)$/,
];
Profiling
Key methods are profiled for performance analysis:[
'make',
'_runCompilerPlugins',
'_emitResources',
'minifyJs',
'rewriteSourceMaps',
].forEach((method) => {
Target.prototype[method] = Profile(`Target#${method}`, Target.prototype[method]);
});
Related Topics
- Isobuild - The build system
- Compiler Plugins - Code transformation
- Build Performance - Optimization strategies