diff --git a/tools/isobuild/package-source.js b/tools/isobuild/package-source.js index 763844b5db..1db5906651 100644 --- a/tools/isobuild/package-source.js +++ b/tools/isobuild/package-source.js @@ -1014,6 +1014,12 @@ _.extend(PackageSource.prototype, { } if (typeof mainModule !== "undefined") { + // Note: if mainModule === false, no JavaScript modules will be + // loaded eagerly unless explicitly added with !fileOptions.lazy by + // a compiler plugin. This can be useful for building an app that + // does not run any application JS on the client (or the server). Of + // course, Meteor packages may still run JS on startup, but they + // have their own rules for lazy/eager loading of modules. if (relPath === mainModule) { fileOptions.lazy = false; fileOptions.mainModule = true; diff --git a/tools/project-context.js b/tools/project-context.js index 5eb25b43fd..dc65381f7f 100644 --- a/tools/project-context.js +++ b/tools/project-context.js @@ -1643,19 +1643,19 @@ export class MeteorConfig { const configMainModule = this.get("mainModule"); const mainModulesByArch = Object.create(null); - if (configMainModule) { - if (typeof configMainModule === "string") { - // If packageJson.meteor.mainModule is a string, use that string - // as the mainModule for all architectures. - mainModulesByArch["os"] = configMainModule; - mainModulesByArch["web"] = configMainModule; - } else if (typeof configMainModule === "object") { - // If packageJson.meteor.mainModule is an object, use its - // properties to select a mainModule for each architecture. - Object.keys(configMainModule).forEach(where => { - mainModulesByArch[mapWhereToArch(where)] = configMainModule[where]; - }); - } + if (typeof configMainModule === "string" || + configMainModule === false) { + // If packageJson.meteor.mainModule is a string or false, use that + // value as the mainModule for all architectures. + mainModulesByArch["os"] = configMainModule; + mainModulesByArch["web"] = configMainModule; + } else if (configMainModule && + typeof configMainModule === "object") { + // If packageJson.meteor.mainModule is an object, use its properties + // to select a mainModule for each architecture. + Object.keys(configMainModule).forEach(where => { + mainModulesByArch[mapWhereToArch(where)] = configMainModule[where]; + }); } return mainModulesByArch; @@ -1673,6 +1673,19 @@ export class MeteorConfig { arch, Object.keys(mainModulesByArch)); if (mainMatch) { + const mainModule = mainModulesByArch[mainMatch]; + + if (mainModule === false) { + // If meteor.mainModule.{client,server,...} === false, no modules + // will be loaded eagerly on the {client,server...}. This is useful + // if you have an app with no special app/{client,server} directory + // structure and you want to specify an entry point for just the + // client (or just the server), without accidentally loading + // everything on the other architecture. Instead of omitting + // meteor.mainModule for the other architecture, set it to false. + return mainModule; + } + if (! this._resolversByArch[arch]) { this._resolversByArch[arch] = new Resolver({ sourceRoot: this.appDirectory, @@ -1685,7 +1698,7 @@ export class MeteorConfig { // containing package.json or index.js files. const res = this._resolversByArch[arch].resolve( // Only relative paths are allowed (not top-level packages). - "./" + files.pathNormalize(mainModulesByArch[mainMatch]), + "./" + files.pathNormalize(mainModule), this.packageJsonPath ); diff --git a/tools/tests/app-config.js b/tools/tests/app-config.js index d6acbd7e8e..697cffc3d1 100644 --- a/tools/tests/app-config.js +++ b/tools/tests/app-config.js @@ -46,6 +46,26 @@ selftest.define("mainModule", function () { check({}); + check(false); + + check({ + client: false, + server: "abc", + }); + + check({ + client: "abc", + server: false, + }); + + check({ + web: false, + }); + + check({ + os: false, + }); + check({ client: "a", os: "bc", diff --git a/tools/tests/apps/app-config/tests.js b/tools/tests/apps/app-config/tests.js index 0882961844..0aca42ae14 100644 --- a/tools/tests/apps/app-config/tests.js +++ b/tools/tests/apps/app-config/tests.js @@ -9,6 +9,8 @@ const startupPromise = new Promise(resolve => { Meteor.startup(resolve); }); +const hasOwn = Object.prototype.hasOwnProperty; + describe("meteor.mainModule", () => { // These tests test the consequences of having various meteor.mainModule // configurations in package.json. @@ -50,7 +52,13 @@ describe("meteor.mainModule", () => { it("loads the right files", async () => { await startupPromise; - function checkNoMainModule() { + if (Meteor.isClient) { + console.log("client config:", config); + } else { + console.log("server config:", config); + } + + function checkDefaultLoadRules() { assert.deepEqual(ids, [ "/a.js", "/b.js", @@ -61,9 +69,22 @@ describe("meteor.mainModule", () => { ]); } + function checkEagerLoadingDisabled() { + // Eager loading of all modules is disabled. + assert.deepEqual(ids, []); + } + if (! config || - ! config.mainModule) { - return checkNoMainModule(); + ! hasOwn.call(config, "mainModule")) { + return checkDefaultLoadRules(); + } + + if (config.mainModule === false) { + return checkEagerLoadingDisabled(); + } + + if (! config.mainModule) { + return checkDefaultLoadRules(); } let mainId; @@ -72,16 +93,18 @@ describe("meteor.mainModule", () => { mainId = config.mainModule.client || config.mainModule.web; - console.log("client config:", config); } else if (Meteor.isServer) { mainId = config.mainModule.server || config.mainModule.os; - console.log("server config:", config); + } + + if (mainId === false) { + return checkEagerLoadingDisabled(); } if (! mainId) { - return checkNoMainModule(); + return checkDefaultLoadRules(); } const absId = require.resolve("./" + mainId);