diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js index 39756afd124..9004a11a2e3 100644 --- a/tools/isobuild/compiler-plugin.js +++ b/tools/isobuild/compiler-plugin.js @@ -591,7 +591,7 @@ class ResourceSlot { return fileOptions && fileOptions[name]; } - _isLazy(options) { + _isLazy(options, isJavaScript) { let lazy = this._getOption("lazy", options); if (typeof lazy === "boolean") { @@ -616,9 +616,23 @@ class ResourceSlot { // test files should always be included, if we're running app // tests. return isInImports && !isTestFilePath(this.inputResource.path); - } else { - return isInImports; } + + if (isJavaScript && + // Set in PackageSource#_inferFileOptions (in package-source.js) + // to indicate that there was a mainModule, but this module was + // not the mainModule. It's important to wait until this point to + // make the final call, because we can finally tell whether the + // file was JavaScript or not (non-JS resources are not affected + // by the meteor.mainModule configuration option). + this._getOption("mainModule") === false) { + return true; + } + + // In other words, the imports directory remains relevant for non-JS + // resources, and for JS resources in the absence of an explicit + // meteor.mainModule configuration. + return isInImports; } addStylesheet(options) { @@ -637,7 +651,7 @@ class ResourceSlot { targetPath, servePath: self.packageSourceBatch.unibuild.pkg._getServePath(targetPath), hash: sha1(data), - lazy: this._isLazy(options), + lazy: this._isLazy(options, false), }; if (useMeteorInstall && resource.lazy) { @@ -711,7 +725,7 @@ class ResourceSlot { sourceMap: options.sourceMap, // intentionally preserve a possible `undefined` value for files // in apps, rather than convert it into `false` via `!!` - lazy: self._isLazy(options), + lazy: self._isLazy(options, true), bare: !! self._getOption("bare", options), mainModule: !! self._getOption("mainModule", options), }); @@ -739,7 +753,7 @@ class ResourceSlot { servePath: self.packageSourceBatch.unibuild.pkg._getServePath( options.path), hash: sha1(options.data), - lazy: self._isLazy(options), + lazy: self._isLazy(options, false), }); } @@ -763,7 +777,7 @@ class ResourceSlot { self.outputResources.push({ type: options.section, data: Buffer.from(files.convertToStandardLineEndings(options.data), 'utf8'), - lazy: self._isLazy(options), + lazy: self._isLazy(options, false), }); } diff --git a/tools/isobuild/package-api.js b/tools/isobuild/package-api.js index 1bd236889a5..c0f58852d38 100644 --- a/tools/isobuild/package-api.js +++ b/tools/isobuild/package-api.js @@ -48,7 +48,7 @@ function toArchArray (arch) { // 'client' -> 'web' // 'server' -> 'os' // '*' -> '*' -function mapWhereToArch (where) { +export function mapWhereToArch(where) { if (where === 'server') { return 'os'; } else if (where === 'client') { diff --git a/tools/isobuild/package-source.js b/tools/isobuild/package-source.js index 1fdad7561aa..ab41420d88c 100644 --- a/tools/isobuild/package-source.js +++ b/tools/isobuild/package-source.js @@ -837,7 +837,10 @@ _.extend(PackageSource.prototype, { constraint: constraint.constraintString }); }); - var projectWatchSet = projectContext.getProjectWatchSet(); + const projectWatchSet = projectContext.getProjectWatchSet(); + const mainModulesByArch = + projectContext.meteorConfig.getMainModulesByArch(); + projectWatchSet.merge(projectContext.meteorConfig.watchSet); _.each(compiler.ALL_ARCHES, function (arch) { // We don't need to build a Cordova SourceArch if there are no Cordova @@ -847,6 +850,9 @@ _.extend(PackageSource.prototype, { return; } + const mainModule = projectContext.meteorConfig + .getMainModuleForArch(arch, mainModulesByArch); + // XXX what about /web.browser/* etc, these directories could also // be for specific client targets. @@ -867,20 +873,42 @@ _.extend(PackageSource.prototype, { isApp: true, }; + // If this architecture has a mainModule defined in + // package.json, it's an error if _findSources doesn't find that + // module. If no mainModule is defined, anything goes. + let missingMainModule = !! mainModule; + + const sources = self._findSources(findOptions).sort( + loadOrderSort(sourceProcessorSet, arch) + ).map(relPath => { + if (relPath === mainModule) { + missingMainModule = false; + } + + const fileOptions = self._inferFileOptions(relPath, { + arch, + isApp: true, + mainModule, + }); + + return { + relPath, + fileOptions, + }; + }); + + if (missingMainModule) { + buildmessage.error([ + "Could not find mainModule for '" + arch + "' architecture: " + mainModule, + 'Check the "meteor" section of your package.json file?' + ].join("\n")); + } + + const assets = self._findAssets(findOptions); + return { - sources: self._findSources(findOptions).sort( - loadOrderSort(sourceProcessorSet, arch) - ).map(relPath => { - return { - relPath, - fileOptions: self._inferFileOptions(relPath, { - arch, - isApp: true, - }), - }; - }), - - assets: self._findAssets(findOptions), + sources, + assets, }; } }); @@ -919,7 +947,11 @@ _.extend(PackageSource.prototype, { } }), - _inferFileOptions(relPath, {arch, isApp}) { + _inferFileOptions(relPath, { + arch, + isApp, + mainModule, + }) { const fileOptions = {}; const isTest = global.testCommandMetadata && global.testCommandMetadata.isTest; @@ -974,6 +1006,17 @@ _.extend(PackageSource.prototype, { } } + if (isApp && mainModule) { + if (relPath === mainModule) { + fileOptions.lazy = false; + fileOptions.mainModule = true; + } else if (typeof fileOptions.lazy === "undefined") { + // Used in ResourceSlot#_isLazy (in compiler-plugin.js) to make a + // final determination of whether the file should be lazy. + fileOptions.mainModule = false; + } + } + return fileOptions; }, diff --git a/tools/project-context.js b/tools/project-context.js index 095ca9a83d0..738cf05a49c 100644 --- a/tools/project-context.js +++ b/tools/project-context.js @@ -17,6 +17,15 @@ var watch = require('./fs/watch.js'); var Profile = require('./tool-env/profile.js').Profile; import { KNOWN_ISOBUILD_FEATURE_PACKAGES } from './isobuild/compiler.js'; +import { + optimisticReadJsonOrNull, + optimisticHashOrNull, +} from "./fs/optimistic.js"; + +import { + mapWhereToArch, +} from "./isobuild/package-api.js"; + // The ProjectContext represents all the context associated with an app: // metadata files in the `.meteor` directory, the choice of package versions // used by it, etc. Any time you want to work with an app, create a @@ -363,6 +372,13 @@ _.extend(ProjectContext.prototype, { }); if (buildmessage.jobHasMessages()) return; + + self.meteorConfig = new MeteorConfig({ + packageJsonPath: files.pathJoin(self.projectDir, "package.json") + }); + if (buildmessage.jobHasMessages()) { + return; + } }); self._completedStage = STAGE.READ_PROJECT_METADATA; @@ -1580,3 +1596,81 @@ _.extend(exports.FinishedUpgraders.prototype, { files.appendFile(self.filename, appendText); } }); + +export class MeteorConfig { + constructor({ + packageJsonPath, + }) { + this.packageJsonPath = packageJsonPath; + this.watchSet = new watch.WatchSet; + } + + _ensureInitialized() { + if (! _.has(this, "config")) { + const json = optimisticReadJsonOrNull(this.packageJsonPath); + this.config = json && json.meteor || null; + this.watchSet.addFile( + this.packageJsonPath, + optimisticHashOrNull(this.packageJsonPath) + ); + } + + return this.config; + } + + // General utility for querying the "meteor" section of package.json. + // TODO Implement an API for setting these values? + get(...keys) { + let config = this._ensureInitialized(); + if (config) { + keys.every(key => { + if (config && _.has(config, key)) { + config = config[key]; + return true; + } + }); + return config; + } + } + + // Call this first if you plan to call getMainModuleForArch multiple + // times, so that you can avoid repeating this work each time. + getMainModulesByArch(arch) { + 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)] = + files.pathNormalize(configMainModule[where]); + }); + } + } + + return mainModulesByArch; + } + + // Given an architecture like web.browser, get the best mainModule for + // that architecture. For example, if this.config.mainModule.client is + // defined, then because mapWhereToArch("client") === "web", and "web" + // matches web.browser, return this.config.mainModule.client. + getMainModuleForArch( + arch, + mainModulesByArch = this.getMainModulesByArch(), + ) { + const mainMatch = archinfo.mostSpecificMatch( + arch, Object.keys(mainModulesByArch)); + + if (mainMatch) { + return mainModulesByArch[mainMatch]; + } + } +}