@maka/meteor-sdk
A programmatic Node.js API over Meteor's build
tool -- for driving Meteor's build system (bundle, run, test,
create, ...) from plain Node code instead of shelling out to a meteor
CLI subprocess. There is no CLI in this package; every operation is a
promise-based function you call directly.
Requirements
Node.js >=22.5.0. (One core module, node:sqlite, isn't available on
earlier versions -- Node 20 cannot run this package at all.)
Install
npm install @maka/meteor-sdk
Quick start
const sdk = require("@maka/meteor-sdk");
await sdk.init({ release: "METEOR@3.5.1" }); // call once per process,
// matching the app's own
// .meteor/release
const project = await sdk.loadProject("/path/to/app");
const result = await sdk.bundle(project, { outputPath: "/path/to/output" });
console.log(result.starManifest);
process.exit(0); // simplest way to end a one-shot script -- see close()
// below if your process needs to keep running afterward
API
init(options)
Call once per process before anything else. options.release must match
the target app's own .meteor/release (e.g. "METEOR@3.5.1") -- it pins
which Meteor release's package versions and build behavior this process
uses.
loadProject(appDir)
Resolves an app directory into a Project, ready to bundle or run.
Read-only: resolves package constraints and versions but doesn't build
anything yet.
bundle(project, options)
Produces a deployable bundle (a star.json manifest plus program
output), the same output meteor build produces.
await sdk.bundle(project, {
outputPath: "/path/to/output",
minifyMode: "production", // default; real minification, can take
// multiple minutes and multiple GB of RAM
// for a cold build -- pass "development"
// for a fast smoke build
});
createAppRunner(project, options)
Runs an app the way meteor run does, with a stoppable handle instead of
a blocking process.
const handle = await sdk.createAppRunner(project, {
port: 3000,
mongoUrl: "mongodb://127.0.0.1:27017/myapp", // required -- see below
once: false, // if true, exits after one run instead of rebuild-on-change
buildOptions: { minifyMode: "development" },
});
handle.on("exit", (result) => { /* a run ended: crash, rebuild, ... */ });
await handle.start(); // resolves once the proxy + app are both listening
// app is now serving http://localhost:3000
await handle.stop(); // stops the proxy, then the app; no orphaned processes
No Mongo lifecycle management. Unlike meteor run, createAppRunner()
does not start or stop a mongod for you -- supply a running Mongo's
connection string via mongoUrl.
There is no separate stdout/stderr event -- all app log output is
observable via sdk.on("log", ...) (see below).
testRun(project, options)
Runs an app's own tests, the way plain meteor test does (app test mode
-- not meteor test-packages' synthetic-app mode, which this package
doesn't cover).
const handle = await sdk.testRun(project, {
port: 3000,
mongoUrl: "mongodb://127.0.0.1:27017/myapp",
driverPackage: "test-in-browser", // required -- must already be in the project
fullApp: false, // matches `meteor test`'s --full-app
});
// same handle shape as createAppRunner(): start() / stop() / on("exit", ...)
create(targetDir, options)
Scaffolds a new Meteor app from a built-in skeleton template, the way
meteor create does.
const result = await sdk.create("/path/to/new-app", {
skeleton: "react", // default; see sdk.AVAILABLE_SKELETONS for the full list
appName: "my-app",
installDependencies: true,
});
// => { appPath, appName, skeleton }
connectDdp(url, options)
Opens a long-lived DDP client connection to a running Meteor server --
the same protocol and client (ddp-client, bundled in this package's
isopackets) the tool itself uses to talk to Meteor services. Resolves
once the first DDP handshake completes; after that, ddp-client's own
reconnect machinery (exponential backoff, automatic re-subscribe,
re-send of in-flight method calls) keeps the connection alive until you
close() it.
const conn = await sdk.connectDdp("https://app.example.com", {
headers: { Authorization: "Bearer ..." }, // extra WS handshake headers
tls: { ca, cert, key, rejectUnauthorized }, // Node tls.connect() options
firstConnectTimeoutMs: 30000, // reject if never connected by then
retry: true, // default; false = no auto-reconnect
});
const result = await conn.call("someMethod", arg1, arg2); // concurrent-safe
const sub = await conn.subscribe("pubName", ...args); // resolves on ready
conn.on("connected", ({ reconnect }) => { /* every (re)connect */ });
conn.on("disconnected", ({ error }) => { /* transport drop */ });
conn.status(); // ddp-client status snapshot
conn.disconnect(); // temporary offline (stops retrying)...
conn.reconnect(); // ...and back
conn.raw; // the underlying ddp-client connection
conn.close(); // permanent -- no reconnects after this
The URL's scheme picks the transport: https:///wss:// gives you a
TLS websocket; HTTPS_PROXY/NO_PROXY are honored. call() rejects
with the server's own Meteor.Error (structured error/reason/
details fields intact); subscribe() rejects with the server's sub
error if the subscription fails before first becoming ready. Logging in
to an accounts-enabled server is a plain method call:
await conn.call("login", { resume: token }).
close()
Releases file-watch resources opened by loadProject()/bundle()/etc.
For a one-shot script/CLI command that's about to exit anyway, prefer
skipping close() entirely and calling process.exit() directly once
your work is done, rather than awaiting close() first -- verified
to sidestep the issue below cleanly, with no downside for a process that
isn't doing anything else afterward. close() still exists for
longer-lived embedders that need to release watch handles mid-process
without exiting.
If you do call it: with the default native watcher backend, the process
can occasionally hang on Windows due to a @parcel/watcher native-addon
limitation outside this package's control (its background thread doesn't
release on unsubscribe(), and its public API has no lower-level
shutdown hook) -- and, observed specifically when the host process is
launched from Git Bash/MSYS2 on Windows (not from PowerShell/cmd), that
same untorn-down thread can crash the process outright (SIGSEGV)
during teardown instead of just hanging. Workarounds, in addition to
skipping close() above: call process.exit() after close() resolves
regardless of whether it hangs, or set METEOR_MODERN='{"watcher":false}'
to use the plain polling watcher instead, which doesn't exhibit either
symptom.
Events
sdk.on("log", ({ level, args }) => { /* ... */ });
sdk.off("log", listener);
Structured log events for the handful of informational messages Meteor's build tooling emits outside of any build result.
Errors
Every exported function rejects with a real Error -- MeteorSdkError
for usage errors (calling something before init(), an unknown
skeleton, ...) or MeteorBuildError for build failures (with a
.messages array of the underlying build errors) -- never a raw exit
code or an unstructured message set.
Known limitations
- No Cordova, no
publish/package-server admin operations, no springboarding/multi-release-per-process. Out of scope for this SDK. createAppRunner()/testRun()don't manage a Mongo process -- bring your ownmongoUrl.bundle()'s defaultminifyMode: "production"is slow -- pass"development"for fast iteration.- Two rare, environment-specific gaps, both out of scope for this
package to fix: a from-scratch install can hit an npm/Windows spawn
issue when a dependency needs a native rebuild for the first time; and
two processes concurrently installing npm dependencies for different
Meteor core packages at the same time can hit npm's own global-cache
database is lockederror.
License
MIT