Skip to content

Commit

Permalink
Merge pull request meteor#9690 from meteor/meteor-config-mainModule
Browse files Browse the repository at this point in the history
Support a meteor.mainModule section in application package.json files.
  • Loading branch information
benjamn authored Feb 23, 2018
2 parents bbd6149 + eeec1bc commit 5cc7176
Show file tree
Hide file tree
Showing 29 changed files with 1,272 additions and 31 deletions.
17 changes: 17 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
## v.NEXT

* Applications may now specify client and server entry point modules in a
newly-supported `"meteor"` section of `package.json`:
```js
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
}
}
```
When specified, these entry points override Meteor's default module
loading semantics, rendering `imports` directories unnecessary. If
`mainModule` is left unspecified for either client or server, the
default rules will apply for that architecture, as before.
[Feature #135](https://github.com/meteor/meteor-feature-requests/issues/135)
[PR #9690](https://github.com/meteor/meteor/pull/9690)
* The `reify` npm package has been updated to version 0.14.2.
* The `meteor-babel` npm package has been updated to version
Expand Down
48 changes: 33 additions & 15 deletions tools/isobuild/compiler-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -607,18 +607,36 @@ class ResourceSlot {
return false;
}

const splitPath = this.inputResource.path.split(files.pathSep);
const isInImports = splitPath.indexOf("imports") >= 0;
const runningTests = global.testCommandMetadata &&
(global.testCommandMetadata.isTest ||
global.testCommandMetadata.isAppTest);

if (global.testCommandMetadata &&
(global.testCommandMetadata.isTest ||
global.testCommandMetadata.isAppTest)) {
// test files should always be included, if we're running app
// tests.
return isInImports && !isTestFilePath(this.inputResource.path);
} else {
return isInImports;
if (runningTests &&
isTestFilePath(this.inputResource.path)) {
// Test files are never lazy if we're running tests.
return false;
}

if (isJavaScript) {
// PackageSource#_inferFileOptions (in package-source.js) sets the
// mainModule option to false to indicate a meteor.mainModule was
// configured for this architecture, but this module was not it.
// It's important to wait until this point (ResourceSlot#_isLazy) to
// make the final call, because we can finally tell whether the
// output resource is JavaScript or not (non-JS resources are not
// affected by the meteor.mainModule option).
const mainModule = this._getOption("mainModule", options);
if (typeof mainModule === "boolean") {
return ! mainModule;
}
}

// 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 in package.json.
const splitPath = this.inputResource.path.split(files.pathSep);
const isInImports = splitPath.indexOf("imports") >= 0;
return isInImports;
}

addStylesheet(options) {
Expand All @@ -637,7 +655,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) {
Expand Down Expand Up @@ -711,7 +729,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),
});
Expand Down Expand Up @@ -739,7 +757,7 @@ class ResourceSlot {
servePath: self.packageSourceBatch.unibuild.pkg._getServePath(
options.path),
hash: sha1(options.data),
lazy: self._isLazy(options),
lazy: self._isLazy(options, false),
});
}

Expand All @@ -763,7 +781,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),
});
}

Expand Down
2 changes: 1 addition & 1 deletion tools/isobuild/package-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
73 changes: 58 additions & 15 deletions tools/isobuild/package-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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,
};
}
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
},

Expand Down
115 changes: 115 additions & 0 deletions tools/project-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ 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";

import Resolver from "./isobuild/resolver.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
Expand Down Expand Up @@ -363,6 +374,13 @@ _.extend(ProjectContext.prototype, {
});
if (buildmessage.jobHasMessages())
return;

self.meteorConfig = new MeteorConfig({
appDirectory: self.projectDir,
});
if (buildmessage.jobHasMessages()) {
return;
}
});

self._completedStage = STAGE.READ_PROJECT_METADATA;
Expand Down Expand Up @@ -1580,3 +1598,100 @@ _.extend(exports.FinishedUpgraders.prototype, {
files.appendFile(self.filename, appendText);
}
});

export class MeteorConfig {
constructor({
appDirectory,
}) {
this.appDirectory = appDirectory;
this.packageJsonPath = files.pathJoin(appDirectory, "package.json");
this.watchSet = new watch.WatchSet;
this._resolversByArch = Object.create(null);
}

_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)] = 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) {
if (! this._resolversByArch[arch]) {
this._resolversByArch[arch] = new Resolver({
sourceRoot: this.appDirectory,
targetArch: arch,
});
}

// Use a Resolver to allow the mainModule strings to omit .js or
// .json file extensions, and to enable resolving directories
// 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]),
this.packageJsonPath
);

if (res && typeof res === "object") {
return files.pathRelative(this.appDirectory, res.path);
}
}
}
}
Loading

0 comments on commit 5cc7176

Please sign in to comment.