Skip to main content

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.

Compiler Plugins

Compiler plugins allow packages to extend Meteor’s build system by registering handlers for specific file types. Unlike legacy source handlers, compiler plugins run in the context of an entire app.

Plugin Architecture

From compiler-plugin.js:
Unlike legacy source handlers (Plugin.registerSourceHandler), compilers run in the context of an entire app. They don’t run when you run meteor publish; whenever they run, they have access to all the files of their type across all packages as well as the app.

CompilerPluginProcessor

The main entry point for processing files with plugins:
export class CompilerPluginProcessor {
  constructor({
    unibuilds,           // Ordered list of packages
    arch,                // Target architecture
    sourceRoot,          // App source root
    buildMode,           // production/development/test
    isopackCache,        // Package cache
    linkerCacheDir,      // Cache directory for linker
    scannerCacheDir,     // Cache directory for import scanner
    minifyCssResource,   // CSS minification function
  }) {
    Object.assign(this, {
      unibuilds, arch, sourceRoot, buildMode,
      isopackCache, linkerCacheDir, scannerCacheDir,
      minifyCssResource,
    });
    
    if (linkerCacheDir) {
      files.mkdir_p(linkerCacheDir);
    }
    if (scannerCacheDir) {
      files.mkdir_p(scannerCacheDir);
    }
  }
}

Plugin Execution

async runCompilerPlugins() {
  buildmessage.assertInJob();
  
  // Create source batches for each unibuild
  const sourceBatches = [];
  for (const unibuild of this.unibuilds) {
    const { pkg: { name }, arch } = unibuild;
    const sourceRoot = name
      && this.isopackCache.getSourceRoot(name, arch)
      || this.sourceRoot;
    
    const batch = new PackageSourceBatch(unibuild, this, {
      sourceRoot,
      linkerCacheDir: this.linkerCacheDir,
      scannerCacheDir: this.scannerCacheDir,
    });
    
    await batch.init();
    sourceBatches.push(batch);
  }
  
  // Group files by source processor
  const sourceProcessorsWithSlots = {};
  _.each(sourceBatches, function (sourceBatch) {
    _.each(sourceBatch.resourceSlots, function (resourceSlot) {
      var sourceProcessor = resourceSlot.sourceProcessor;
      if (!sourceProcessor) return;
      
      if (!_.has(sourceProcessorsWithSlots, sourceProcessor.id)) {
        sourceProcessorsWithSlots[sourceProcessor.id] = {
          sourceProcessor: sourceProcessor,
          resourceSlots: []
        };
      }
      sourceProcessorsWithSlots[sourceProcessor.id].resourceSlots.push(
        resourceSlot);
    });
  });
  
  // Run each processor
  for (const [id, data] of Object.entries(sourceProcessorsWithSlots)) {
    var sourceProcessor = data.sourceProcessor;
    var resourceSlots = data.resourceSlots;
    
    var jobTitle = [
      "processing files with ",
      sourceProcessor.isopack.name,
      " (for target ", this.arch, ")"
    ].join('');
    
    await Profile.time("plugin "+sourceProcessor.isopack.name, async () => {
      await buildmessage.enterJob({ title: jobTitle }, async function () {
        var inputFiles = resourceSlots.map(
          resourceSlot => new InputFile(resourceSlot)
        );
        
        const markedMethod = buildmessage.markBoundary(
          sourceProcessor.userPlugin.processFilesForTarget,
          sourceProcessor.userPlugin
        );
        
        await markedMethod(inputFiles);
      });
    });
  }
  
  return sourceBatches;
}

InputFile API

The InputFile class is the documented API presented to compiler plugins:
class InputFile extends buildPluginModule.InputFile {
  constructor(resourceSlot) {
    super();
    this._resourceSlot = resourceSlot;
    
    // Cache maps
    this._statCache = Object.create(null);
    this._controlFileCache = Object.create(null);
    this._resolveCache = Object.create(null);
    
    // Feature flags
    this.supportsLazyCompilation = true;
    this.supportsTopLevelAwait = true;
  }
  
  getContentsAsBuffer() {
    return this._resourceSlot.inputResource.data;
  }
  
  getPackageName() {
    return this._resourceSlot.packageSourceBatch.unibuild.pkg.name;
  }
  
  isPackageFile() {
    return !!this.getPackageName();
  }
  
  isApplicationFile() {
    return !this.getPackageName();
  }
  
  getPathInPackage() {
    return this._resourceSlot.inputResource.path;
  }
  
  getSourceHash() {
    return this._resourceSlot.inputResource.hash;
  }
  
  getExtension() {
    return this._resourceSlot.inputResource.extension;
  }
  
  getDeclaredExports() {
    return this._resourceSlot.packageSourceBatch.unibuild.declaredExports;
  }
  
  getDisplayPath() {
    return this._resourceSlot.packageSourceBatch.unibuild.pkg._getServePath(
      this.getPathInPackage()
    );
  }
}

File Resolution

findControlFile(basename) {
  let absPath = this._controlFileCache[basename];
  if (typeof absPath === "string") {
    return absPath;
  }
  
  const sourceRoot = this.getSourceRoot(true);
  if (!_.isString(sourceRoot)) {
    return this._controlFileCache[basename] = null;
  }
  
  let dir = files.pathDirname(
    files.pathJoin(sourceRoot, this.getPathInPackage()));
  
  while (true) {
    absPath = files.pathJoin(dir, basename);
    
    const stat = this._stat(absPath);
    if (stat && stat.isFile()) {
      return this._controlFileCache[basename] = absPath;
    }
    
    // Don't escape node_modules
    if (files.pathBasename(dir) === "node_modules") {
      return this._controlFileCache[basename] = null;
    }
    
    if (dir === sourceRoot) break;
    let parentDir = files.pathDirname(dir);
    if (parentDir === dir) break;
    dir = parentDir;
  }
  
  return this._controlFileCache[basename] = null;
}

resolve(id, parentPath) {
  parentPath = parentPath || files.pathJoin(
    this.getSourceRoot(),
    this.getPathInPackage()
  );
  
  const resId = this._resolveCacheLookup(id, parentPath);
  if (resId) {
    return resId;
  }
  
  const batch = this._resourceSlot.packageSourceBatch;
  const resolver = batch.getResolver({
    // Use server architecture for resolving
    targetArch: archinfo.host(),
  });
  const resolved = resolver.resolve(id, parentPath);
  
  if (resolved === "missing") {
    const error = new Error("Cannot find module '" + id + "'");
    error.code = "MODULE_NOT_FOUND";
    throw error;
  }
  
  return this._resolveCacheStore(id, parentPath, resolved.id);
}

Adding Output

JavaScript

addJavaScript(options, lazyFinalizer) {
  // options.path - target path
  // options.data - compiled code
  // options.sourceMap - source map
  // lazyFinalizer - function to call for expensive computation
  this._resourceSlot.addJavaScript(options, lazyFinalizer);
}

CSS

addStylesheet(options, lazyFinalizer) {
  // options.path - target path
  // options.data - CSS content
  // options.sourceMap - source map
  this._resourceSlot.addStylesheet(options, lazyFinalizer);
}

Assets

addAsset(options) {
  // options.path - target path
  // options.data - asset content
  this._resourceSlot.addAsset(options);
}

Lazy Compilation

Plugins can defer expensive compilation:
// Compiler plugin example
processFilesForTarget(inputFiles) {
  inputFiles.forEach(file => {
    file.addJavaScript(
      {
        path: file.getPathInPackage(),
        // Don't include data yet
      },
      // Lazy finalizer - only called if file is actually used
      () => {
        const compiled = expensiveCompilation(file.getContentsAsBuffer());
        return {
          data: compiled.code,
          sourceMap: compiled.map
        };
      }
    );
  });
}

Linker Cache

The linker caches compilation results:
const CACHE_SIZE = process.env.METEOR_LINKER_CACHE_SIZE || 1024*1024*100;
const LINKER_CACHE_SALT = 26; // Increment to force relinking

const LINKER_CACHE = new LRUCache({
  max: CACHE_SIZE,
  // Measured in bytes
  length(files) {
    return files.reduce((soFar, current) => {
      return soFar + current.data.length + sourceMapLength(current.sourceMap);
    }, 0);
  }
});

Watch and Rebuild

readAndWatchFileWithHash(path) {
  const sourceBatch = this._resourceSlot.packageSourceBatch;
  return readAndWatchFileWithHash(
    sourceBatch.unibuild.watchSet,
    files.convertToPosixPath(path),
  );
}

readAndWatchFile(path) {
  return this.readAndWatchFileWithHash(path).contents;
}

HMR Support

hmrAvailable() {
  const fileOptions = this.getFileOptions() || {};
  return this._resourceSlot.hmrAvailable() && !fileOptions.bare;
}

Server-Lib Packages

Built-in packages available in build plugins:
const serverLibPackages = {
  fibers: true,
  // ... populated from dev_bundle/server-lib/node_modules
};

function populateServerLibPackages() {
  const devBundlePath = files.getDevBundle();
  const nodeModulesPath = files.pathJoin(
    devBundlePath, "server-lib", "node_modules"
  );
  
  files.readdir(nodeModulesPath).forEach(packageName => {
    const packagePath = files.pathJoin(nodeModulesPath, packageName);
    const packageStat = files.statOrNull(packagePath);
    if (packageStat && packageStat.isDirectory()) {
      serverLibPackages[packageName] = true;
    }
  });
}

Plugin Types

Three types of plugins can be registered:

Compiler Plugins

Plugin.registerCompiler({
  extensions: ['jsx', 'tsx'],
  archMatching: 'web',
  isTemplate: false
}, () => new MyCompiler());

Linter Plugins

Plugin.registerLinter({
  extensions: ['js', 'jsx'],
  archMatching: 'web'
}, () => new MyLinter());

Minifier Plugins

Plugin.registerMinifier({
  extensions: ['js', 'css']
}, () => new MyMinifier());

Source Processor Sets

// From isopack.js plugin initialization
self.sourceProcessors.compiler = new buildPluginModule.SourceProcessorSet(
  self.displayName(), 
  { hardcodeJs: true, singlePackage: true }
);

self.sourceProcessors.linter = new buildPluginModule.SourceProcessorSet(
  self.displayName(), 
  { singlePackage: true, allowConflicts: true }
);

self.sourceProcessors.minifier = new buildPluginModule.SourceProcessorSet(
  self.displayName(), 
  { singlePackage: true }
);

Plugin Loading

// Plugins are loaded with specific context
await plugin.load({
  Plugin,           // Plugin API object
  Profile,          // Profiling utilities
  __meteor_bootstrap__: {
    isFibersDisabled: true,
    startupHooks: null
  }
});