From f61ba60326a9434b4f2c12900e684cb9b4ea82ee Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Sat, 29 Jun 2019 18:44:16 -0400 Subject: [PATCH] Support meteor.nodeModules.recompile package.json configuration option. Example: "meteor": { "mainModule": ..., "testModule": ..., "nodeModules": { "recompile": { "very-modern-package": ["client", "server"], "alternate-notation-for-client-and-server": true, "somewhat-modern-package": "legacy", "another-package": ["legacy", "server"] } } } The keys of the meteor.nodeModules.recompile configuration object are npm package names, and the values specify for which bundles those packages should be recompiled using the Meteor compiler plugins system, as if the packages were part of the Meteor application. For example, if an npm package uses const/let syntax or arrow functions, that's fine for modern and server code, but you would probably want to recompile the package when building the legacy bundle. To accomplish this, specify "legacy" or ["legacy"] as the value of the package's property, similar to somewhat-modern-package above. These strings and arrays of strings have the same meaning as the second argument to api.addFiles(files, where) in a package.js file. This configuration serves pretty much the same purpose as symlinking an application directory into node_modules, but without any symlinking: https://forums.meteor.com/t/litelement-import-litelement-html/45042/8?u=benjamn The meteor.nodeModules.recompile configuration currently applies to the application node_modules directory only (not to Npm.depends dependencies in Meteor packages). Recompiled packages must be direct dependencies of the application. --- tools/isobuild/package-source.js | 36 ++++++++++++++------ tools/project-context.js | 56 +++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/tools/isobuild/package-source.js b/tools/isobuild/package-source.js index 210f6d03e8..ec0998d9fc 100644 --- a/tools/isobuild/package-source.js +++ b/tools/isobuild/package-source.js @@ -860,6 +860,9 @@ _.extend(PackageSource.prototype, { const testModulesByArch = projectContext.meteorConfig.getTestModulesByArch(); + const nodeModulesToRecompileByArch = + projectContext.meteorConfig.getNodeModulesToRecompileByArch(); + projectWatchSet.merge(projectContext.meteorConfig.watchSet); _.each(compiler.ALL_ARCHES, function (arch) { @@ -871,10 +874,13 @@ _.extend(PackageSource.prototype, { } const mainModule = projectContext.meteorConfig - .getMainModuleForArch(arch, mainModulesByArch); + .getMainModule(arch, mainModulesByArch); const testModule = projectContext.meteorConfig - .getTestModuleForArch(arch, testModulesByArch); + .getTestModule(arch, testModulesByArch); + + const nodeModulesToRecompile = projectContext.meteorConfig + .getNodeModulesToRecompile(arch, nodeModulesToRecompileByArch); // XXX what about /web.browser/* etc, these directories could also // be for specific client targets. @@ -895,6 +901,7 @@ _.extend(PackageSource.prototype, { ignoreFiles, isApp: true, testModule, + nodeModulesToRecompile, }; // If this architecture has a mainModule defined in @@ -1094,6 +1101,7 @@ _.extend(PackageSource.prototype, { isApp, sourceArch, testModule, + nodeModulesToRecompile = new Set, loopChecker = new SymlinkLoopChecker(this.sourceRoot), ignoreFiles = [] }) { @@ -1254,7 +1262,7 @@ _.extend(PackageSource.prototype, { // If we're in a node_modules directory, cache the results of the // find function for the duration of the process. - const cacheKey = inNodeModules && makeCacheKey(dir); + let cacheKey = inNodeModules && makeCacheKey(dir); if (cacheKey && cacheKey in self._findSourcesCache) { return self._findSourcesCache[cacheKey]; @@ -1273,14 +1281,22 @@ _.extend(PackageSource.prototype, { } } - const pkgJson = optimisticLookupPackageJson(self.sourceRoot, dir); - const hasModuleEntryPoint = pkgJson && ( - isWeb ? typeof pkgJson.module === "string" : pkgJson.type === "module" - ); + let readOptions = sourceReadOptions; + if (inNodeModules) { + const pkgJson = optimisticLookupPackageJson(self.sourceRoot, dir); + const shouldRecompile = + pkgJson && nodeModulesToRecompile.has(pkgJson.name); + const hasModuleEntryPoint = pkgJson && ( + typeof pkgJson.module === "string" || pkgJson.type === "module" + ); - const readOptions = inNodeModules && !hasModuleEntryPoint - ? nodeModulesReadOptions - : sourceReadOptions; + if (hasModuleEntryPoint || shouldRecompile) { + // Avoid caching node_modules code recompiled by Meteor. + cacheKey = false; + } else { + readOptions = nodeModulesReadOptions; + } + } const sources = _.difference( self._readAndWatchDirectory(dir, watchSet, readOptions), diff --git a/tools/project-context.js b/tools/project-context.js index 7806436baf..fdc136b9f2 100644 --- a/tools/project-context.js +++ b/tools/project-context.js @@ -1649,9 +1649,47 @@ export class MeteorConfig { } } - // Call this first if you plan to call getMainModuleForArch multiple + getNodeModulesToRecompileByArch() { + const packageNamesByArch = Object.create(null); + const recompile = this.get("nodeModules", "recompile"); + + if (recompile && typeof recompile === "object") { + const get = arch => packageNamesByArch[arch] || ( + packageNamesByArch[arch] = new Set); + + Object.keys(recompile).forEach(packageName => { + const info = recompile[packageName]; + if (! info) return; + if (info === true) { + get("web").add(packageName); + get("os").add(packageName); + } else if (typeof info === "string") { + mapWhereToArches(info).forEach(arch => { + get(arch).add(packageName); + }); + } else if (Array.isArray(info)) { + info.forEach(where => { + mapWhereToArches(where).forEach(arch => { + get(arch).add(packageName); + }); + }); + } + }); + } + + return packageNamesByArch; + } + + getNodeModulesToRecompile( + arch, + packageNamesByArch = this.getNodeModulesToRecompileByArch(), + ) { + return packageNamesByArch[arch]; + } + + // Call this first if you plan to call getMainModule multiple // times, so that you can avoid repeating this work each time. - getMainModulesByArch(arch) { + getMainModulesByArch() { return this._getEntryModulesByArch("mainModule"); } @@ -1659,24 +1697,24 @@ export class MeteorConfig { // 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( + getMainModule( arch, mainModulesByArch = this.getMainModulesByArch(), ) { - return this._getEntryModuleForArch(arch, mainModulesByArch); + return this._getEntryModule(arch, mainModulesByArch); } // Analogous to getMainModulesByArch, except for this.config.testModule. - getTestModulesByArch(arch) { + getTestModulesByArch() { return this._getEntryModulesByArch("testModule"); } - // Analogous to getMainModuleForArch, except for this.config.testModule. - getTestModuleForArch( + // Analogous to getMainModule, except for this.config.testModule. + getTestModule( arch, testModulesByArch = this.getTestModulesByArch(), ) { - return this._getEntryModuleForArch(arch, testModulesByArch); + return this._getEntryModule(arch, testModulesByArch); } _getEntryModulesByArch(...keys) { @@ -1703,7 +1741,7 @@ export class MeteorConfig { return entryModulesByArch; } - _getEntryModuleForArch( + _getEntryModule( arch, entryModulesByArch, ) {