tools/static-assets/server/boot.js
var Fiber = require("fibers");
var fs = require("fs");
var path = require("path");
var Future = require("fibers/future");
var sourcemap_support = require('source-map-support');
var bootUtils = require('./boot-utils.js');
var files = require('./mini-files');
var npmRequire = require('./npm-require.js').require;
var Profile = require('./profile').Profile;
// This code is duplicated in tools/main.js.
var MIN_NODE_VERSION = 'v14.0.0';
var hasOwn = Object.prototype.hasOwnProperty;
// For now it's a function to ensure we don't get a falsy value.
// Once we figure out the best place to create this EV (maybe it's here),
// it won't need to be a function anymore.
global._isFibersEnabled = function () {
return !process.env.DISABLE_FIBERS;
};
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
process.stderr.write(
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');
process.exit(1);
}
// read our control files
var serverJsonPath = path.resolve(process.argv[2]);
var serverDir = path.dirname(serverJsonPath);
var serverJson = require("./server-json.js");
var configJson =
JSON.parse(fs.readFileSync(path.resolve(serverDir, 'config.json'), 'utf8'));
var programsDir = path.dirname(serverDir);
var buildDir = path.dirname(programsDir);
var starJson = JSON.parse(fs.readFileSync(path.join(buildDir, "star.json")));
// Set up environment
__meteor_bootstrap__ = {
startupHooks: [],
serverDir: serverDir,
configJson: configJson
};
__meteor_runtime_config__ = {
meteorRelease: configJson.meteorRelease,
gitCommitHash: starJson.gitCommitHash
};
if (!process.env.APP_ID) {
process.env.APP_ID = configJson.appId;
}
// Map from load path to its source map.
var parsedSourceMaps = {};
const meteorDebugFuture =
process.env.METEOR_INSPECT_BRK ? new Future : null;
function maybeWaitForDebuggerToAttach() {
if (meteorDebugFuture) {
const { pause } = require("./debug");
const pauseThresholdMs = 50;
const pollIntervalMs = 500;
const waitStartTimeMs = +new Date;
const waitLimitMinutes = 5;
const waitLimitMs = waitLimitMinutes * 60 * 1000;
// This setTimeout not only waits for the debugger to attach, but also
// keeps the process alive by preventing the event loop from running
// empty while the main Fiber yields.
setTimeout(function poll() {
const pauseStartTimeMs = +new Date;
if (pauseStartTimeMs - waitStartTimeMs > waitLimitMs) {
console.error(
`Debugger did not attach after ${waitLimitMinutes} minutes; continuing.`
);
meteorDebugFuture.return();
} else {
// This pause function contains a debugger keyword that will only
// act as a breakpoint once a debugging client has attached to the
// process, so we keep calling pause() until the first time it
// takes at least pauseThresholdMs, which indicates that a client
// must be attached. The only other signal of a client attaching
// is an unreliable "Debugger attached" message printed to stderr
// by native C++ code, which requires the parent process to listen
// for that message and then process.send a message back to this
// process. By comparison, this polling strategy tells us exactly
// what we want to know: "Is the debugger keyword enabled yet?"
pause();
if (new Date - pauseStartTimeMs > pauseThresholdMs) {
// If the pause() function call took a meaningful amount of
// time, we can conclude the debugger keyword must be active,
// which means a debugging client must be connected, which means
// we should stop polling and let the main Fiber continue.
meteorDebugFuture.return();
} else {
// If the pause() function call didn't take a meaningful amount
// of time to execute, then the debugger keyword must not have
// caused a pause, which means a debugging client must not be
// connected, which means we should keep polling.
setTimeout(poll, pollIntervalMs);
}
}
}, pollIntervalMs);
// The polling will continue while we wait here.
meteorDebugFuture.wait();
}
}
// Read all the source maps into memory once.
serverJson.load.forEach(function (fileInfo) {
if (fileInfo.sourceMap) {
var rawSourceMap = fs.readFileSync(
path.resolve(serverDir, fileInfo.sourceMap), 'utf8');
// Parse the source map only once, not each time it's needed. Also remove
// the anti-XSSI header if it's there.
var parsedSourceMap = JSON.parse(rawSourceMap.replace(/^\)\]\}'/, ''));
// source-map-support doesn't ever look at the sourcesContent field, so
// there's no point in keeping it in memory.
delete parsedSourceMap.sourcesContent;
var url;
if (fileInfo.sourceMapRoot) {
// Add the specified root to any root that may be in the file.
parsedSourceMap.sourceRoot = path.join(
fileInfo.sourceMapRoot, parsedSourceMap.sourceRoot || '');
}
parsedSourceMaps[path.resolve(__dirname, fileInfo.path)] = parsedSourceMap;
}
});
function retrieveSourceMap(pathForSourceMap) {
if (hasOwn.call(parsedSourceMaps, pathForSourceMap)) {
return { map: parsedSourceMaps[pathForSourceMap] };
}
return null;
}
var origWrapper = sourcemap_support.wrapCallSite;
var wrapCallSite = function (frame) {
var frame = origWrapper(frame);
var wrapGetter = function (name) {
var origGetter = frame[name];
frame[name] = function (arg) {
// replace a custom location domain that we set for better UX in Chrome
// DevTools (separate domain group) in source maps.
var source = origGetter(arg);
if (! source)
return source;
return source.replace(/(^|\()meteor:\/\/..app\//, '$1');
};
};
wrapGetter('getScriptNameOrSourceURL');
wrapGetter('getEvalOrigin');
return frame;
};
sourcemap_support.install({
// Use the source maps specified in program.json instead of parsing source
// code for them.
retrieveSourceMap: retrieveSourceMap,
// For now, don't fix the source line in uncaught exceptions, because we
// haven't fixed handleUncaughtExceptions in source-map-support to properly
// locate the source files.
handleUncaughtExceptions: false,
wrapCallSite: wrapCallSite
});
// As a replacement to the old keepalives mechanism, check for a running
// parent every few seconds. Exit if the parent is not running.
//
// Two caveats to this strategy:
// * Doesn't catch the case where the parent is CPU-hogging (but maybe we
// don't want to catch that case anyway, since the bundler not yielding
// is what caused #2536).
// * Could be fooled by pid re-use, i.e. if another process comes up and
// takes the parent process's place before the child process dies.
var startCheckForLiveParent = function (parentPid) {
if (parentPid) {
if (! bootUtils.validPid(parentPid)) {
console.error("METEOR_PARENT_PID must be a valid process ID.");
process.exit(1);
}
setInterval(function () {
try {
process.kill(parentPid, 0);
} catch (err) {
console.error("Parent process is dead! Exiting.");
process.exit(1);
}
}, 3000);
}
};
var specialArgPaths = {
"packages/modules-runtime.js": function () {
return {
npmRequire: npmRequire,
Profile: Profile
};
},
"packages/dynamic-import.js": function (file) {
var dynamicImportInfo = {};
var clientArchs = configJson.clientArchs ||
Object.keys(configJson.clientPaths);
clientArchs.forEach(function (arch) {
dynamicImportInfo[arch] = {
dynamicRoot: path.join(programsDir, arch, "dynamic")
};
});
dynamicImportInfo.server = {
dynamicRoot: path.join(serverDir, "dynamic")
};
return { dynamicImportInfo: dynamicImportInfo };
}
};
var loadServerBundles = Profile("Load server bundles", function () {
var infos = [];
serverJson.load.forEach(function (fileInfo) {
var code = fs.readFileSync(path.resolve(serverDir, fileInfo.path));
var nonLocalNodeModulesPaths = [];
function addNodeModulesPath(path) {
nonLocalNodeModulesPaths.push(
files.pathResolve(serverDir, path)
);
}
if (typeof fileInfo.node_modules === "string") {
addNodeModulesPath(fileInfo.node_modules);
} else if (fileInfo.node_modules) {
Object.keys(fileInfo.node_modules).forEach(function (path) {
const info = fileInfo.node_modules[path];
if (! info.local) {
addNodeModulesPath(path);
}
});
}
// Add dev_bundle/server-lib/node_modules.
addNodeModulesPath("node_modules");
function statOrNull(path) {
try {
return fs.statSync(path);
} catch (e) {
return null;
}
}
var Npm = {
/**
* @summary Require a package that was specified using
* `Npm.depends()`.
* @param {String} name The name of the package to require.
* @locus Server
* @memberOf Npm
*/
require: Profile(function getBucketName(name) {
return "Npm.require(" + JSON.stringify(name) + ")";
}, function (name, error) {
if (nonLocalNodeModulesPaths.length > 0) {
var fullPath;
// Replace all backslashes with forward slashes, just in case
// someone passes a Windows-y module identifier.
name = name.split("\\").join("/");
nonLocalNodeModulesPaths.some(function (nodeModuleBase) {
var packageBase = files.convertToOSPath(files.pathResolve(
nodeModuleBase,
name.split("/", 1)[0]
));
if (statOrNull(packageBase)) {
return fullPath = files.convertToOSPath(
files.pathResolve(nodeModuleBase, name)
);
}
});
if (fullPath) {
return require(fullPath);
}
}
var resolved = require.resolve(name);
if (resolved === name && ! path.isAbsolute(resolved)) {
// If require.resolve(id) === id and id is not an absolute
// identifier, it must be a built-in module like fs or http.
return require(resolved);
}
throw error || new Error(
"Cannot find module " + JSON.stringify(name)
);
})
};
var getAsset = function (assetPath, encoding, callback) {
var promiseResolver, promise;
if (! callback) {
promise = new Promise((resolve, reject) => {
promiseResolver = function (error, result) {
error ? reject(error) : resolve(result);
}
});
callback = promiseResolver;
}
// This assumes that we've already loaded the meteor package, so meteor
// itself can't call Assets.get*. (We could change this function so that
// it doesn't call bindEnvironment if you don't pass a callback if we need
// to.)
var _callback = Package.meteor.Meteor.bindEnvironment(function (err, result) {
if (result && ! encoding)
// Sadly, this copies in Node 0.10.
result = new Uint8Array(result);
callback(err, result);
}, function (e) {
console.log("Exception in callback of getAsset", e.stack);
});
// Convert a DOS-style path to Unix-style in case the application code was
// written on Windows.
assetPath = files.convertToStandardPath(assetPath);
// Unicode normalize the asset path to prevent string mismatches when
// using this string elsewhere.
assetPath = files.unicodeNormalizePath(assetPath);
if (! fileInfo.assets || ! hasOwn.call(fileInfo.assets, assetPath)) {
_callback(new Error("Unknown asset: " + assetPath));
} else {
var filePath = path.join(serverDir, fileInfo.assets[assetPath]);
fs.readFile(files.convertToOSPath(filePath), encoding, _callback);
}
if (promise)
return promise;
};
var Assets = {
getText: function (assetPath, callback) {
const result = getAsset(assetPath, "utf8", callback);
if (!callback) {
return Future.fromPromise(result).wait();
}
},
getTextAsync: function (assetPath) {
return getAsset(assetPath, "utf8");
},
getBinary: function (assetPath, callback) {
const result = getAsset(assetPath, undefined, callback);
if (!callback) {
return Future.fromPromise(result).wait();
}
},
getBinaryAsync: function (assetPath) {
return getAsset(assetPath, undefined);
},
/**
* @summary Get the absolute path to the static server asset. Note that assets are read-only.
* @locus Server [Not in build plugins]
* @memberOf Assets
* @param {String} assetPath The path of the asset, relative to the application's `private` subdirectory.
*/
absoluteFilePath: function (assetPath) {
// Unicode normalize the asset path to prevent string mismatches when
// using this string elsewhere.
assetPath = files.unicodeNormalizePath(assetPath);
assetPath = files.convertToStandardPath(assetPath);
if (! fileInfo.assets || ! hasOwn.call(fileInfo.assets, assetPath)) {
throw new Error("Unknown asset: " + assetPath);
}
var filePath = path.join(serverDir, fileInfo.assets[assetPath]);
return files.convertToOSPath(filePath);
},
getServerDir: function() {
return serverDir;
}
};
var wrapParts = ["(function(Npm,Assets"];
var specialArgs =
hasOwn.call(specialArgPaths, fileInfo.path) &&
specialArgPaths[fileInfo.path](fileInfo);
var specialKeys = Object.keys(specialArgs || {});
specialKeys.forEach(function (key) {
wrapParts.push("," + key);
});
// \n is necessary in case final line is a //-comment
wrapParts.push("){", code, "\n})");
var wrapped = wrapParts.join("");
// It is safer to use the absolute path when source map is present as
// different tooling, such as node-inspector, can get confused on relative
// urls.
// fileInfo.path is a standard path, convert it to OS path to join with
// __dirname
var fileInfoOSPath = files.convertToOSPath(fileInfo.path);
var absoluteFilePath = path.resolve(__dirname, fileInfoOSPath);
var scriptPath =
parsedSourceMaps[absoluteFilePath] ? absoluteFilePath : fileInfoOSPath;
var func = require('vm').runInThisContext(wrapped, {
filename: scriptPath,
displayErrors: true
});
var args = [Npm, Assets];
specialKeys.forEach(function (key) {
args.push(specialArgs[key]);
});
if (meteorDebugFuture) {
infos.push({
fn: Profile(fileInfo.path, func),
args
});
} else {
// Allows us to use code-coverage if the debugger is not enabled
Profile(fileInfo.path, func).apply(global, args);
}
});
maybeWaitForDebuggerToAttach();
infos.forEach(info => {
info.fn.apply(global, info.args);
});
});
var callStartupHooks = Profile("Call Meteor.startup hooks", function () {
// run the user startup hooks. other calls to startup() during this can still
// add hooks to the end.
while (__meteor_bootstrap__.startupHooks.length) {
var hook = __meteor_bootstrap__.startupHooks.shift();
Profile.time(hook.stack || "(unknown)", hook);
}
// Setting this to null tells Meteor.startup to call hooks immediately.
__meteor_bootstrap__.startupHooks = null;
});
var runMain = Profile("Run main()", function () {
// find and run main()
// XXX hack. we should know the package that contains main.
var mains = [];
var globalMain;
if ('main' in global) {
mains.push(main);
globalMain = main;
}
if (typeof Package !== "undefined") {
Object.keys(Package).forEach(function (name) {
const { main } = Package[name];
if (typeof main === "function" &&
main !== globalMain) {
mains.push(main);
}
});
}
if (! mains.length) {
process.stderr.write("Program has no main() function.\n");
process.exit(1);
}
if (mains.length > 1) {
process.stderr.write("Program has more than one main() function?\n");
process.exit(1);
}
var exitCode = mains[0].call({}, process.argv.slice(3));
// XXX hack, needs a better way to keep alive
if (exitCode !== 'DAEMON')
process.exit(exitCode);
if (process.env.METEOR_PARENT_PID) {
startCheckForLiveParent(process.env.METEOR_PARENT_PID);
}
});
Fiber(function () {
Profile.run("Server startup", function () {
loadServerBundles();
callStartupHooks();
runMain();
});
}).run();