Page MenuHomePhabricator

Standardise on how to access/register JavaScript interfaces
Closed, ResolvedPublic

Description

Right now the mw object is overloaded with various things and used as a place to register modules. This is confusing as it doesn't make it clear what can be initiated and what cannot and which ResourceLoader module stuff lives.

This leads to code such as mw.mobileFrontend, mw.cnBannerControllerLib, mw.webfonts, mw.uls and so on so on so on...
We already have jQuery and OO.ui but these can be extended in standard ways. Do we have a similar plan for mediawiki specific code?

What I would like to see is something similar to how our templates work and a cleaner way to register code that other extensions can use.

mw.template.get( 'moduleName', 'template.mustache' )

Mobilefrontend has a concept of mw.mobileFrontend.require and mw.mobileFrontend.define for controlling access to public interfaces [1]. Could we imagine a similar mechanism for core?
Proposed syntax: require and module.exports (methods scoped to module execution context, not a public interface)

T108655#1838005

// my-module-foo.js # scope( module )
module.exports = {
  doThings: function () { .. } 
};

// mediawiki.js # mw.loader
script( module );

// my-other-module.js # scope( require )
require( 'mediawiki.foo', function ( foo ) {
  foo.doThings();
} );

What are people's thoughts on this? I would like a cleaner way to access stuff that's not a jQuery module and not an OO.ui module.

[1] https://github.com/wikimedia/mediawiki-extensions-MobileFrontend/blob/master/resources/mobile.modules/modules.js

Related Objects

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes

Change 265793 had a related patch set uploaded (by Jdlrobson):
Modern module loading (2/2)

https://gerrit.wikimedia.org/r/265793

Thanks for all the work that already went into this proposal and implementation. I wonder how this idea interacts with the granularity of modules currently recommended for example by @Krinkle. In nodejs, require serves two different purposes:

  1. Load another component by name
  2. Load a local file by path

Currently, the Wikidata team matches files to rl modules one-to-one. That's something we've been regularly criticized for by @Krinkle, and probably for good reasons. As far as I understand this discussion, one if not the goal of it is to get rid of having to replicate the rl module hierarchy in global JS scope. How would that work out in a setup where each file does not be it's own module? So, for example, I have hypothetical files wikibase.view.snakview.js, wikibase.view.snakview.novaluevariation.js, wikibase.view.snakview.somevaluevariation.js, wikibase.view.snakview.valuevariation.js. If we have only one resource loader module wikibase.view.snakview, how would the three subordinate files attach themselves if not via global scope?

Thanks for all the work that already went into this proposal and implementation. I wonder how this idea interacts with the granularity of modules currently recommended for example by @Krinkle. In nodejs, require serves two different purposes:

  1. Load another component by name
  2. Load a local file by path

Currently, the Wikidata team matches files to rl modules one-to-one. That's something we've been regularly criticized for by @Krinkle, and probably for good reasons. As far as I understand this discussion, one if not the goal of it is to get rid of having to replicate the rl module hierarchy in global JS scope. How would that work out in a setup where each file does not be it's own module? So, for example, I have hypothetical files wikibase.view.snakview.js, wikibase.view.snakview.novaluevariation.js, wikibase.view.snakview.somevaluevariation.js, wikibase.view.snakview.valuevariation.js. If we have only one resource loader module wikibase.view.snakview, how would the three subordinate files attach themselves if not via global scope?

A module is defined as everything in the module so a file can refer to the module itself is contained in.

e.g.

my.module
foo.js

modules.exports = { a: 1 };

bar.js

var a = require( 'my.module' ).a;
modules.exports[b] = function( a ) { return a * 10 };

Per IRC conv, I'm assigning this to Roan (thanks Roan!). That has the added bonus of possibly taking care of Roan's ArchCom 2016-02-10 action item

@Jdlrobson So, if I have a resource loader module that consists of three files, these three files share a module.exports binding and can add to that independent of each other (or even dependent of each other if listed in the module definition the correct order)? That would solve my issue, although I still wonder if it is a good idea to take something from a one-file-per-module world and use it in a multiple-files-per-module world.

Change 260071 merged by jenkins-bot:
resourceloader: Implement modern module loading (1/2)

https://gerrit.wikimedia.org/r/260071

@Jdlrobson So, if I have a resource loader module that consists of three files, these three files share a module.exports binding and can add to that independent of each other (or even dependent of each other if listed in the module definition the correct order)? That would solve my issue

Yes, that's right. The way RL works is it takes the contents of all the files in the module (in order), concatenates them together, then wraps all of that in a function. So under this proposal, the result would be:

function( module ) {
// Contents of file1.js
// Contents of file2.js
// Contents of file3.js
}

(This is a simplification; it's currently function ( $, jQuery ) and it would change to function ( $, jQuery, module, require ).)

Both the proposal and the patches that implement it look good to me. However, now that we're introducing a module object with per-module state, let's first think about what other per-module things we want to do (now or in the future) and what those might look like.

At least two of these come to mind. First, we probably want to be able to serve config variables on a per-module basis instead of globally, which would reduce the size of the global config blob that's currently being loaded on every page. For example, VisualEditor exports the list of namespaces with subpages as mw.config.get( 'wgVisualEditorConfig' ).namespacesWithSubpages and a number of other things that are only used when VE is active. It's wasteful to have this on every page view. It's also ugly to have such specific variables be global. But with this module object, we could expose different config vars to different modules, for example with module.getConfig( 'foo' ).

Second, we could consider something similar for i18n messages, which are currently global (mw.msg( 'foo' )) but could also be made local (e.g. module.getMessage( 'foo' )).

My larger question is: how should we expose these kinds of things? Should we attach a bunch of stuff to the module object (module.getConfig(), module.getMessage(), etc.) ? Should we pass in more arguments to the closure like function( $, jQuery, module, require, config, msg ) so module code can call config( 'foo' ), msg( 'foo' ), etc.? Or should we do something else? Also, what other things might we want to add? My examples are both functions that involve state, but maybe we also want properties such as module.name?

Change 271921 had a related patch set uploaded (by Jdlrobson):
Fix use of require in debug mode

https://gerrit.wikimedia.org/r/271921

I understand that module.exports is going to become the preferred way of registering interfaces, instead of global variables.
Now I ask, will require() be asynchronous, perhaps replacing mw.loader.using(), or rather synchronous like Node.js' (only working for preloaded modules of course)?

I understand that module.exports is going to become the preferred way of registering interfaces, instead of global variables.
Now I ask, will require() be asynchronous, perhaps replacing mw.loader.using(), or rather synchronous like Node.js'

As you say, it'll become the preferred way of registering (and accessing interfaces) in favour of global variables. It will not take replace declaring dependencies or loading modules.

So where before:

foo:
  scripts: foo.js
  dependencies: mediawiki.util
mw.loader.load('foo');
// foo.js
alert(mw.util);

The latter could instead be done as:

// foo.js
var util = require('mediawiki.util');
alert(util);

Per T108655#1838005, the merged patch, and the current system with global variables: require() will be synchronous, and only work for modules that have loaded before the require. (Either with dependencies or inline mw.loader.using).

It's a straight up replacement for using a global variable and provides a way to keep and obtain references to the module's JavaScript value (whether an object, class or function etc.) Just like accessing a global will return the value as assigned to it, require will return the value as assigned to module.exports.

Per T108655#1838005, the merged patch, and the current system with global variables: require() will be synchronous, and only work for modules that have loaded before the require.

It's a straight up replacement for using a global variable and provides a way to keep and obtain references to the module's JavaScript value (whether an object, class or function etc.) Just like accessing a global will return the value as assigned to it, require will return the value as assigned to module.exports.

Thanks. Just to be sure, each module will be registered at most once and module.exports will be cached, am I right?

I put some more thought into this. I see that it's an improvement, but I doubt it's enough of an improvement to make it worthwhile for Wikidata to put work into (adopting) it. What I would like to see is something that would actually be compatible with other module systems, so that for example our code could also work under Node.js without re-implementing resource loader. The current proposal is not really an improvement in that regard.

I put some more thought into this. I see that it's an improvement, but I doubt it's enough of an improvement to make it worthwhile for Wikidata to put work into (adopting) it. What I would like to see is something that would actually be compatible with other module systems, so that for example our code could also work under Node.js without re-implementing resource loader. The current proposal is not really an improvement in that regard.

In what way is this not compatible with other systems? It's my impression that the module.exports pattern is used by other systems, and this moves us closer towards compatibility. At least for things with static dependencies (i.e. ignoring mw.loader.using()), you should now be able to use the same code between node.js and ResourceLoader, right?

Thanks. Just to be sure, each module will be registered at most once and module.exports will be cached, am I right?

The top-level code of each module (the code that attaches things to module.exports) will be executed at most once. RL already ensures that and I think it should stay that way.

I put some more thought into this. I see that it's an improvement, but I doubt it's enough of an improvement to make it worthwhile for Wikidata to put work into (adopting) it. What I would like to see is something that would actually be compatible with other module systems, so that for example our code could also work under Node.js without re-implementing resource loader. The current proposal is not really an improvement in that regard.

I'm personally interested in experiments into using package.json to generate ResourceLoader module definition and publishing code on npm that can be consumed by things outside the MediaWiki ecosystem to make it easier for node devs to build things for our projects.

I'm personally interested in experiments into using package.json to generate ResourceLoader module definition and publishing code on npm that can be consumed by things outside the MediaWiki ecosystem to make it easier for node devs to build things for our projects.

Interesting. It's probably pretty feasible to write a ResourceLoaderFileModule subclass/feature where the file info comes from a JSON file.

Things we agreed on
  • We will probably go ahead with the require/module.export API as proposed here (details below). This RFC is now in the last call phase.
  • Use of this API will be encouraged but not required; global symbols will be kept for backwards compatibility, and in some cases will be hard or impossible to eliminate (e.g. jQuery plugins that expect to be able to set $.fn.foo)
  • Attempting to require() a module that has not been declared as a dependency will throw an exception. This means that failing to declare a dependency will consistently result in an error, whereas this currently fails intermittently in some (relatively common) situations.
  • Asynchronous module loading / dynamic dependencies will use a combination of require() and mw.loader.using(), with simplified syntax available via require() (details below)
  • Future features attached to the module object will be attached to module.mw.* or another property name that minimizes the probability of naming collisions with other software. The module object is not standardized beyond module.exports and other platforms can and do add their own properties. If we decided to implement per-module config using module.config or module.getConfig for example, it's quite conceivable that another platform could use module.config for something different, and then code written for that platform will break in MW even if it uses feature detection.
  • mw.loader will have to stay global no matter what, for bootstrapping reasons, so there's no reason to use require( 'mediawiki.loader' )
  • We want to aim for CommonJS compatibility as much as possible, and avoid ending up with something that's almost compatible but not quite compatible enough to be useful.
  • We're not too concerned about perhaps wanting to migrate to ES6 modules in the future. CommonJS is popular, so automated migration paths from CommonJS to ES6 modules are likely to be available once this becomes an issue.
Code samples

Export:

// mediawiki.user.js
var User = {
    // ...
};

// Export global symbol for backwards compatibility
mw.user = User;
// Export to new API
module.exports = User;

Synchronous usage (requires the dependency to be declared in the module definition):

var mwUser = require( 'mediawiki.user' );
if ( mwUser.isAnon() ) {
    // ...
}

Asynchronous usage (long form):

mw.loader.using( 'mediawiki.user' ).done( function () {
    var mwUser = require( 'mediawiki.user' );
    if ( mwUser.isAnon() ) {
        // ...
    }
} );

Asynchronous usage (using wrapper):

require( 'mediawiki.user', function ( mwUser ) {
    if ( mwUser.isAnon() ) {
        // ...
    }
} );

The async versions will also let you load multiple modules:

// Long form:
mw.loader.using( [ 'foo', 'bar' ] ) .done( function () {
    var foo  = require( 'foo' ), bar = require( 'bar' );
    // ...
} );
// Short form:
require( [ 'foo', 'bar' ], function ( foo, bar ) {
    // ...
} );
  • Attempting to require() a module that has not been declared as a dependency will throw an exception. This means that failing to declare a dependency will consistently result in an error, whereas this currently fails intermittently in some (relatively common) situations.

[...]

mw.loader.using( 'mediawiki.user' ).done( function () {
    var mwUser = require( 'mediawiki.user' );
    if ( mwUser.isAnon() ) {
        // ...
    }
} );

I didn't notice this at the time, but these two things seem to me to be in conflict with each other. On the one hand, we said that attempting to call require( 'foo' ) when the module calling it does not declare a dependency on 'foo' should throw an exception, presumably even if 'foo' has in fact been loaded. This, by itself, is feasible, because we can pass each module a unique require() function that knows about that module's dependencies. It's also a very nice strong guarantee that if you forget to declare a dependency, it will fail deterministically, rather than succeed by accident most of the time because something else already loaded 'foo'. But on the other hand, we said that mw.loader.using( 'foo' ).done( function () { require( 'foo' ); } ); should work, and that seems impossible. The require() function is still the same function from the module's closure scope and knows about that module's dependencies, but mw.loader.using() can't know which module called it, so it can't tell the module's require function to unblock 'foo', because it doesn't know which one to tell that to. It could unblock 'foo' globally for all require functions in all modules, but that would weaken that strong guarantee.

I considered passing a new require function into the done callback of the mw.loader.using promise, but that's ugly, and it causes more problems: this new require function would have to agree to load 'foo' as well as anything that the outer require function would agree to load. But mw.loader.using can't build that function because it doesn't know which module it's in.

So instead, I think we should scrap the long-form using+require syntax, and only support the short-form require( 'foo', function ( foo ) { ... } ) syntax. It's much nicer anyway, and it avoids this problem.

It's unfortunate this meeting was scheduled an such short notice, otherwise I would have tried to be there. Anyway, on topic:

Asynchronous usage (using wrapper):

require( 'mediawiki.user', function ( mwUser ) {
    if ( mwUser.isAnon() ) {
        // ...
    }
} );

I suggest to implement this feature in a way that reduces the possibility of collisions and makes clear that this is a custom extension to CommonJS's require. For example:

require.mwAsync( 'mediawiki.user', function ( mwUser ) {
  if ( mwUser.isAnon() ) {
    // ...
  }
} );

In what way is this not compatible with other systems? It's my impression that the module.exports pattern is used by other systems, and this moves us closer towards compatibility. At least for things with static dependencies (i.e. ignoring mw.loader.using()), you should now be able to use the same code between node.js and ResourceLoader, right?

Well, as I stated earlier, this only covers one of the require use-cases: module loading, but not file loading.

So, have three files wikifoo/one.js, wikifoo/two.js, wikifoo/three.js and they form one resource loader module 'wikifoo' => array( 'scripts' => 'wikifoo/one.js', 'wikifoo/two.js', 'wikifoo/three.js' ). Then I also have a wikibar.js that's just it's own module and uses wikifoo. If they are implemented in the current style, in order to load them in Node.js, I could have a simple wrapper node_index.js:

global.wikifoo = {};
require('./wikifoo/one');
require('./wikifoo/two');
require('./wikifoo/three');

require('./wikibar'); // wikifoo can just be used from global scope

If I would switch wikifoo/*.js and wikibar.js to using the proposed require and module, wikibar.js would not use wikifoo from global scope, but instead do require('wikifoo'). My node_index.js wrapper would have to facilitate this:

var loadMwModule = ( function() {
  var cachedModules = [];
  var Module = require('module');
  var realResolve = Module._resolveFilename;
  Module._resolveFilename = function fakeResolve(request, parent) {
    if (cachedModules.indexOf(request) !== -1) {
      return request;
    }
    return realResolve(request, parent);
  };
  return function( moduleName, scripts ) {
    cachedModules.push( moduleName );
    require.cache[ moduleName ] = {
      id: moduleName,
      loaded: true,
      exports: scripts.reduce( function ( exports, fileName ) {
        return Object.assign( exports, require( fileName ) )
      }, {} )
    };
  }
} )();

loadMwModule(
  'wikifoo',
  [ './wikifoo/one', './wikifoo/two', './wikifoo/three' ]
);
require('./wikibar');

I know resource loader modules are not generally usable from other module systems, but I have the feeling that switching to this proposal would actually make things more difficult, because it would suggest to use require's calling convention for loading (external) modules for loading local modules. require only supports loading local code on a file level, not module level. Local modules would be directories with an index.js file for require. From my point of view, the trivial example I gave does support this impression.

It's unfortunate this meeting was scheduled an such short notice, otherwise I would have tried to be there. Anyway, on topic:

Sorry, that was entirely my fault. I had said I would announce it last week, and had forgotten.

I suggest to implement this feature in a way that reduces the possibility of collisions and makes clear that this is a custom extension to CommonJS's require. For example:

require.mwAsync( 'mediawiki.user', function ( mwUser ) {
  if ( mwUser.isAnon() ) {
    // ...
  }
} );

OK, I think that's a reasonable suggestion. There's value in explicitly calling out that this is MW-specific.

In what way is this not compatible with other systems? It's my impression that the module.exports pattern is used by other systems, and this moves us closer towards compatibility. At least for things with static dependencies (i.e. ignoring mw.loader.using()), you should now be able to use the same code between node.js and ResourceLoader, right?

Well, as I stated earlier, this only covers one of the require use-cases: module loading, but not file loading.

[...]

I know resource loader modules are not generally usable from other module systems, but I have the feeling that switching to this proposal would actually make things more difficult, because it would suggest to use require's calling convention for loading (external) modules for loading local modules. require only supports loading local code on a file level, not module level. Local modules would be directories with an index.js file for require. From my point of view, the trivial example I gave does support this impression.

Yes, file loading would not be supported. A related topic came up during the meeting: wouldn't it be easier to integrate 3rd party libraries if dependencies didn't have to be specified in Resources.php. Unfortunately, ResourceLoader does require that dependencies are known in advance as much as possible, for performance reasons. Using the dynamic dependency mechanism (mw.loader.using) for dependencies that are actually static (i.e. known ahead of time, not on-demand but always needed) dependencies degrades performance.

Applying this to your example: loading wikibar would cause a request to fetch the wikibar.js code, and when that code runs the first thing it'll do is ask for wikifoo, which causes another request to be sent for wikifoo/{one,two,three}.js. If wikifoo were structured differently internally, it's also possible that a few lines of one.js will be run before we realize that two.js is needed. In nodejs this is no problem, because 1) disk access is fast, so there is no performance issue; and 2) execution can pause while require() fetches from disk, so it can be synchronous. However, in our case, we can't do it that way because network requests are slow and async.

What we probably could do is find a way to make per-file loading work if the need for it is declared in the Resources.php definition. That could help us make 3rd party libraries work out of the box. I'll think a little bit about how that could be done. @Krinkle , maybe you could think about that as well?

http://webpack.github.io/ might be worth a closer look. It offers JS and other resource bundling, and supports several module styles and targets. There is also support for chunking of bundles.

While not a replacement for ResourceLoader, to me it looks like it could be potentially interesting as a component.

What we probably could do is find a way to make per-file loading work if the need for it is declared in the Resources.php definition. That could help us make 3rd party libraries work out of the box. I'll think a little bit about how that could be done. @Krinkle , maybe you could think about that as well?

Just to clarify: That is what I suggested. I wasn't hoping to get rid of dependency declarations in the resource loader modules. I just want some interface that also helps with loading local files in other engines.

One thing I came up with was having some cleverness in resource loader's require about how directories and resource loader module names map to each other. If dot-delimited parts in resource loader module names would correspond to directories, and a whole directory would be one module, we could easily map them. So, in my example (I added a root namespace just for clarification):

wikistuff/
  wikifoo/
    one.js
    two.js
    three.js
  wikibar.js
  wikibaz.js

wikibar.js can now require( './wikifoo' ), and resource loader would translate that (when called from wikistuff.wikibar) to wikistuff.wikifoo. wikistuff/wikifoo/two.js could now in turn require( '../wikibaz' ), and that would translate to wikistuff.wikibaz in resource loader. All these modules and dependencies have to be declared as usual.

If you follow this convention, you can basically re-use your (locally-depending) code in node.js. All you would need to do is add a wikistuff/wikifoo/index.js that merges the other files' exports in that directory.

I'm personally interested in experiments into using package.json to generate ResourceLoader module definition and publishing code on npm that can be consumed by things outside the MediaWiki ecosystem [..]

Interesting. It's probably pretty feasible to write a ResourceLoaderFileModule subclass/feature where the file info comes from a JSON file.

Agreed. Though any such reusable modules should be moved out of MediaWiki core into a separate repo with proper versioning and releases.

It'd be nice if we can reduce the Resources.php footprint to something that just consumes package.json, however that's not feasible currently. As pointed out by others, our loading aspect is still quite different from Node (no direct file relation). We require packages to be built into a single file or otherwise prepared for concatenation that anticipates for a single shared module object for each public module (whereas in Node, each file has its own module object). The package.json main property wouldn't work for RL. For the moment, using package.json would only add confusion. If we're able to concatenate files in ResourceLoader in a way that preserves where the files come from (so that relative file-path require calls work), then we can do this (with WebPack perhaps). At that time, we can also simplify configuration with package.json. But that's a simple step for convenience later on, not a first step.

Attempting to require() a module that has not been declared as a dependency will throw an exception. [..]

[..] attempting to call require( 'foo' ) when the module calling it does not declare a dependency on 'foo' should throw an exception, presumably even if 'foo' has in fact been loaded.

We rejected the idea of tolerating missing dependencies. We don't want to lazy-load (unless done explicitly via mw.loader.using or require() with callback). This preserves compatibility with CommonJS, AMD, and Node which feature (observed) synchronous return from plain require() calls. As such, I proposed we disallow such tolerance and throw exceptions instead.

However, race conditions when modules don't declare their dependencies, wil continue to be race conditions.

Asynchronous usage:

require( 'mediawiki.user', function ( user ) {
        // ...
} );

I suggest to implement this feature in a way that reduces the possibility of collisions and makes clear that this is a custom extension to CommonJS's require. For example:

require.mwAsync( 'mediawiki.user', function ( user ) {
    // ...
} );

OK, I think that's a reasonable suggestion.

I'll get back to the file-vs-module aspect later. However, this is not MediaWiki specific and should not suffer from separate methods or mw prefixes in the API. Both AMD and RequireJS promote this ability as well. See https://github.com/amdjs/amdjs-api/blob/master/require.md#requirearray-function-.

We rejected the idea of tolerating missing dependencies. We don't want to lazy-load (unless done explicitly via mw.loader.using or require() with callback). This preserves compatibility with CommonJS, AMD, and Node which feature (observed) synchronous return from plain require() calls. As such, I proposed we disallow such tolerance and throw exceptions instead.

However, race conditions when modules don't declare their dependencies, wil continue to be race conditions.

So I think that means that we mean different things by "missing dependency". You propose to throw an exception when a require()d module hasn't been loaded at the time require() is called, whereas what I thought was proposed was that modules that aren't declared as a dependency would be rejected. But as T108655#2062345 says that conflicts with the behavior you want for async loading.

Change 271921 merged by jenkins-bot:
resourceloader: Inverse hasOwn() check to fix require in debug mode

https://gerrit.wikimedia.org/r/271921

From the above patch:

FIXME: This makes OOJS un-loadable in debug mode, because it assumes module.exports doesn't exist in the browser.

Change 277289 had a related patch set uploaded (by Krinkle):
resourceloader: Add back-compat globals for requirable modules

https://gerrit.wikimedia.org/r/277289

Change 277289 merged by jenkins-bot:
resourceloader: Remove 'require' and 'module' from debug mode

https://gerrit.wikimedia.org/r/277289

@adrianheine @Catrope I'm not sure if I'm late but instead of using amd's require(dependencies, callback) we may want to consider using commonjs's form to comply with just one specification. It is similar than what you mentioned @adrianheine but instead of require.mwAsync it is require.ensure. More information in the commonjs.org page.

I've used it with webpack to great success in other projects and it may be worth staying compatible with the commonjs spec.

This work is looking awesome, thank you all.

Once this is finalized I'd love to help develop a standalone node tool that we could use to auto generate the modules dependencies information automatically for files that follow the commonjs module system. It should be pretty easy given all the work that's been done in webpack and browserify (example of getting the dependencies tree)

Change 265793 merged by jenkins-bot:
resourceloader: Implement modern module loading (2/2)

https://gerrit.wikimedia.org/r/265793

RobLa-WMF mentioned this in Unknown Object (Event).Apr 13 2016, 7:34 PM

A module is defined as everything in the module so a file can refer to the module itself is contained in.

e.g.

my.module
foo.js

modules.exports = { a: 1 };

bar.js

var a = require( 'my.module' ).a;
modules.exports[b] = function( a ) { return a * 10 };

In https://gerrit.wikimedia.org/r/#/c/286257/1/resources/mobile.startup/Page.js you are accessing the local module.exports directly, not calling require() with the name of the current module. Is that preferred?

I still wonder if it is a good idea to take something from a one-file-per-module world and use it in a multiple-files-per-module world.

Some issues began surfacing like T134097 and T134142. Since most libraries work as one-file-per-module, it seems impossible to include two or more of them into a single module: the last one in the bundled script will overwrite the others. Is there any way to opt out of module.exports, at least temporarily?

A module is defined as everything in the module so a file can refer to the module itself is contained in.

In mobile.startup/Page.js you are accessing the local module.exports directly, not calling require() with the name of the current module. Is that preferred?

Calling require() with the current module name inside the module is not supported. Not here and not other module systems. A module shouldn't even be aware of its own name. It doesn't make sense as the definition of a module can't depend on itself (i.e. an infinite loop).

MobileFrontend uses this as a transitional pattern due to limitations in the current system. The module pattern is supposed to work on a file-level, not a package level. Until the second RFC in this area is settled, the concatenation behaviour of ResourceLoader will mean that you can theoretically attach properties to the export object from multiple files. But one shouldn't do this. This should be considered undocumented and unsupported, and will likely cease to work after the second RFC.

@Krinkle How can I learn more about the second RFC and the transition to file-level pattern?

Second RFC about standard module access: T133462.