resourceloader: Allow modules to mark themselves as ES6-only
Modules that set "es6": true in their module definition will error when a non-ES6 client tries to load them. To detect ES6 support, this looks for native Promise support, RegExp.prototype.flags, and non-BMP characters in variable names. All browsers that lack full ES6 support fail at least one of those checks. To flag modules as requiring ES6, this adds a ! to the end of their version string. This takes up much less space than adding another register() parameter (which would have to be at the end). It's hacky, but we expect this feature to be relatively temporary, until we require ES6 for running any JS at all (probably in about a year). For distinguishing different types of errors thrown from sortDependencies(), use e.name. We can't subclass Error properly because that requires ES6. Bug: T272104 Change-Id: I45670c910ff12eb422ae54c9fcf372e45c7b2bf1
This commit is contained in:
parent
deb624a6f8
commit
b267f7aa90
11 changed files with 170 additions and 18 deletions
|
|
@ -88,6 +88,9 @@ this is no longer recommended and the option has been removed.
|
|||
BoxedCommand. A BoxedCommand object can be obtained with
|
||||
MediaWikiServices::getInstance()->getCommandFactory()->createBoxed().
|
||||
(T260330)
|
||||
* ResourceLoader modules can now mark themselves as ES6-only by setting
|
||||
'es6' => true in their module definition. ES6-only modules will not be
|
||||
executed in browsers that don't support ES6, such as IE11.
|
||||
* …
|
||||
|
||||
=== External library changes in 1.36 ===
|
||||
|
|
|
|||
|
|
@ -307,6 +307,10 @@
|
|||
"items": {
|
||||
"type": ["string", "object"]
|
||||
}
|
||||
},
|
||||
"es6": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this module requires an ES6-capable browser. If set to true, loading this module in a non-ES6 browser will cause an error. Using ES6 syntax in modules is not yet supported, but will be in the near future. Default is false."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -317,6 +317,10 @@
|
|||
"items": {
|
||||
"type": ["string", "object"]
|
||||
}
|
||||
},
|
||||
"es6": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this module requires an ES6-capable browser. If set to true, loading this module in a non-ES6 browser will cause an error. Using ES6 syntax in modules is not yet supported, but will be in the near future. Default is false."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3908,6 +3908,16 @@ $wgMangleFlashPolicy = true;
|
|||
*
|
||||
* Default: `[]`
|
||||
*
|
||||
* - es6 `{bool}`:
|
||||
* If true, this module will only be executed in browsers that support ES6. You should set this
|
||||
* flag for modules that use ES6 in their JavaScript. Only use this for modules that provide
|
||||
* progressive enhancements that are safe to not load in browsers that are not modern but still
|
||||
* have a substantial user base, like IE11.
|
||||
*
|
||||
* Since: **MW 1.36**
|
||||
*
|
||||
* Default: `false`
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* @par Example: Using an alternate subclass
|
||||
|
|
|
|||
|
|
@ -156,6 +156,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
/** @var bool Whether CSSJanus flipping should be skipped for this module */
|
||||
protected $noflip = false;
|
||||
|
||||
/** @var bool Whether this module requires the client to support ES6 */
|
||||
protected $es6 = false;
|
||||
|
||||
/**
|
||||
* @var bool Whether getStyleURLsForDebug should return raw file paths,
|
||||
* or return load.php urls
|
||||
|
|
@ -260,6 +263,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
// Single booleans
|
||||
case 'debugRaw':
|
||||
case 'noflip':
|
||||
case 'es6':
|
||||
$this->{$member} = (bool)$option;
|
||||
break;
|
||||
}
|
||||
|
|
@ -514,6 +518,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
return $this->getFileContents( $localPath, 'skip function' );
|
||||
}
|
||||
|
||||
public function requiresES6() {
|
||||
return $this->es6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable module content versioning.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -447,6 +447,20 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the module requires ES6 support in the client.
|
||||
*
|
||||
* If the client does not support ES6, attempting to load a module that requires ES6 will
|
||||
* result in an error.
|
||||
*
|
||||
* @stable to override
|
||||
* @since 1.36
|
||||
* @return bool
|
||||
*/
|
||||
public function requiresES6() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indirect dependencies for this module persuant to the skin/language context
|
||||
*
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
|
|||
$registryData[$name] = [
|
||||
'version' => $versionHash,
|
||||
'dependencies' => $module->getDependencies( $context ),
|
||||
'es6' => $module->requiresES6(),
|
||||
'group' => $this->getGroupId( $module->getGroup() ),
|
||||
'source' => $module->getSource(),
|
||||
'skip' => $skipFunction,
|
||||
|
|
@ -260,7 +261,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
|
|||
// Call mw.loader.register(name, version, dependencies, group, source, skip)
|
||||
$registrations[] = [
|
||||
$name,
|
||||
$data['version'],
|
||||
// HACK: signify ES6 with a ! added at the end of the version
|
||||
// This avoids having to add another register() parameter, and generating
|
||||
// a bunch of nulls for ES6-only modules
|
||||
$data['version'] . ( $data['es6'] ? '!' : '' ),
|
||||
$data['dependencies'],
|
||||
$data['group'],
|
||||
// Swap default (local) for null
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
( function () {
|
||||
'use strict';
|
||||
|
||||
var mw, StringSet, log,
|
||||
var mw, StringSet, log, isES6Supported,
|
||||
hasOwn = Object.hasOwnProperty,
|
||||
console = window.console;
|
||||
|
||||
|
|
@ -340,6 +340,35 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Check whether the browser supports ES6.
|
||||
//
|
||||
// Most browsers that support native Promises also support all the ES6 features we need.
|
||||
// The exceptions are:
|
||||
// - Android 4.4.3, which supports almost no ES6 features besides Promise
|
||||
// - Edge 17 and 18, which don't support RegExp-related features
|
||||
// - Safari and iOS versions below 14, which don't support non-BMP characters in variable names
|
||||
// (older versions have other problems too)
|
||||
isES6Supported =
|
||||
// Check for Promise support (filters out most non-ES6 browsers)
|
||||
typeof Promise === 'function' &&
|
||||
// eslint-disable-next-line no-undef
|
||||
Promise.prototype.finally &&
|
||||
|
||||
// Check for RegExp.prototype.flags (filters out Android 4.4.3 and Edge <= 18)
|
||||
/./g.flags === 'g' &&
|
||||
|
||||
// Try a non-BMP variable name (filters out Safari < 14, iOS < 14)
|
||||
( function () {
|
||||
try {
|
||||
// \ud800\udec0 is U+102C0 CARIAN LETTER G
|
||||
// eslint-disable-next-line no-new, no-new-func
|
||||
new Function( 'var \ud800\udec0;' );
|
||||
return true;
|
||||
} catch ( e ) {
|
||||
return false;
|
||||
}
|
||||
}() );
|
||||
|
||||
/**
|
||||
* @class mw
|
||||
*/
|
||||
|
|
@ -488,6 +517,7 @@
|
|||
* 'moduleName': {
|
||||
* // From mw.loader.register()
|
||||
* 'version': '########' (hash)
|
||||
* 'requiresES6': bool
|
||||
* 'dependencies': ['required.foo', 'bar.also', ...]
|
||||
* 'group': string, integer, (or) null
|
||||
* 'source': 'local', (or) 'anotherwiki'
|
||||
|
|
@ -877,10 +907,19 @@
|
|||
* @throws {Error} If an unknown module or a circular dependency is encountered
|
||||
*/
|
||||
function sortDependencies( module, resolved, unresolved ) {
|
||||
var i, skip, deps;
|
||||
var e, i, skip, deps;
|
||||
|
||||
if ( !( module in registry ) ) {
|
||||
throw new Error( 'Unknown module: ' + module );
|
||||
e = new Error( 'Unknown module: ' + module );
|
||||
e.name = 'DependencyError';
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Check requiresES6 before skip, to avoid executing an ES6 skip function in an ES5 client
|
||||
if ( !isES6Supported && registry[ module ].requiresES6 ) {
|
||||
e = new Error( 'Module requires ES6 but ES6 is not supported: ' + module );
|
||||
e.name = 'ES6Error';
|
||||
throw e;
|
||||
}
|
||||
|
||||
if ( typeof registry[ module ].skip === 'string' ) {
|
||||
|
|
@ -905,9 +944,11 @@
|
|||
for ( i = 0; i < deps.length; i++ ) {
|
||||
if ( resolved.indexOf( deps[ i ] ) === -1 ) {
|
||||
if ( unresolved.has( deps[ i ] ) ) {
|
||||
throw new Error(
|
||||
e = new Error(
|
||||
'Circular reference detected: ' + module + ' -> ' + deps[ i ]
|
||||
);
|
||||
e.name = 'DependencyError';
|
||||
throw e;
|
||||
}
|
||||
|
||||
sortDependencies( deps[ i ], resolved, unresolved );
|
||||
|
|
@ -953,19 +994,28 @@
|
|||
try {
|
||||
sortDependencies( modules[ i ], resolved );
|
||||
} catch ( err ) {
|
||||
// This module is not currently known, or has invalid dependencies.
|
||||
// Most likely due to a cached reference after the module was
|
||||
// removed, otherwise made redundant, or omitted from the registry
|
||||
// by the ResourceLoader "target" system.
|
||||
resolved = saved;
|
||||
mw.log.warn( 'Skipped unresolvable module ' + modules[ i ] );
|
||||
if ( modules[ i ] in registry ) {
|
||||
// If the module was known but had unknown or circular dependencies,
|
||||
// also track it as an error.
|
||||
mw.trackError( 'resourceloader.exception', {
|
||||
exception: err,
|
||||
source: 'resolve'
|
||||
} );
|
||||
|
||||
if ( err.name === 'ES6Error' ) {
|
||||
// These errors are common, since trying to load ES6-only modules
|
||||
// in non-ES6 clients is OK and should fail gracefully. Don't track
|
||||
// them as errors, and display a custom warning message.
|
||||
mw.log.warn( 'Skipped ES6-only module ' + modules[ i ] );
|
||||
} else {
|
||||
// err.name === 'DependencyError'
|
||||
// This module is not currently known, or has invalid dependencies.
|
||||
// Most likely due to a cached reference after the module was
|
||||
// removed, otherwise made redundant, or omitted from the registry
|
||||
// by the ResourceLoader "target" system.
|
||||
mw.log.warn( 'Skipped unresolvable module ' + modules[ i ] );
|
||||
if ( modules[ i ] in registry ) {
|
||||
// If the module was known but had unknown or circular dependencies,
|
||||
// also track it as an error.
|
||||
mw.trackError( 'resourceloader.exception', {
|
||||
exception: err,
|
||||
source: 'resolve'
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1709,9 +1759,18 @@
|
|||
* @param {string} [skip]
|
||||
*/
|
||||
function registerOne( module, version, dependencies, group, source, skip ) {
|
||||
var requiresES6 = false;
|
||||
if ( module in registry ) {
|
||||
throw new Error( 'module already registered: ' + module );
|
||||
}
|
||||
|
||||
// requiresES6 is encoded as a ! at the end of version
|
||||
version = String( version || '' );
|
||||
if ( version.slice( -1 ) === '!' ) {
|
||||
version = version.slice( 0, -1 );
|
||||
requiresES6 = true;
|
||||
}
|
||||
|
||||
registry[ module ] = {
|
||||
// Exposed to execute() for mw.loader.implement() closures.
|
||||
// Import happens via require().
|
||||
|
|
@ -1720,7 +1779,8 @@
|
|||
},
|
||||
// module.export objects for each package file inside this module
|
||||
packageExports: {},
|
||||
version: String( version || '' ),
|
||||
version: version,
|
||||
requiresES6: requiresES6,
|
||||
dependencies: dependencies || [],
|
||||
group: typeof group === 'undefined' ? null : group,
|
||||
source: typeof source === 'string' ? source : 'local',
|
||||
|
|
@ -1872,6 +1932,7 @@
|
|||
* a list of arguments compatible with this method
|
||||
* @param {string|number} [version] Module version hash (falls backs to empty string)
|
||||
* Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
|
||||
* A version string that ends with '!' signifies that the module requires ES6 support.
|
||||
* @param {string[]} [dependencies] Array of module names on which this module depends.
|
||||
* @param {string} [group=null] Group which the module is in
|
||||
* @param {string} [source='local'] Name of the source
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class ResourceLoaderTestModule extends ResourceLoaderModule {
|
|||
protected $script = '';
|
||||
protected $styles = '';
|
||||
protected $skipFunction = null;
|
||||
protected $es6 = false;
|
||||
protected $isRaw = false;
|
||||
protected $isKnownEmpty = false;
|
||||
protected $type = ResourceLoaderModule::LOAD_GENERAL;
|
||||
|
|
@ -148,6 +149,10 @@ class ResourceLoaderTestModule extends ResourceLoaderModule {
|
|||
return $this->skipFunction;
|
||||
}
|
||||
|
||||
public function requiresES6() {
|
||||
return $this->es6;
|
||||
}
|
||||
|
||||
public function isRaw() {
|
||||
return $this->isRaw;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -867,4 +867,16 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
|
|||
$this->assertEquals( $expected, $module->getScript( $context ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ResourceLoaderFileModule::requiresES6
|
||||
*/
|
||||
public function testRequiresES6() {
|
||||
$module = new ResourceLoaderFileModule();
|
||||
$this->assertFalse( $module->requiresES6(), 'requiresES6 defaults to false' );
|
||||
$module = new ResourceLoaderFileModule( [ 'es6' => false ] );
|
||||
$this->assertFalse( $module->requiresES6(), 'requiresES6 is false when set to false' );
|
||||
$module = new ResourceLoaderFileModule( [ 'es6' => true ] );
|
||||
$this->assertTrue( $module->requiresES6(), 'requiresES6 is true when set to true' );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,6 +520,25 @@ mw.loader.register([
|
|||
2
|
||||
]
|
||||
]
|
||||
]);',
|
||||
] ],
|
||||
[ [
|
||||
'msg' => 'ES6-only module',
|
||||
'modules' => [
|
||||
'test.es6' => [
|
||||
'class' => ResourceLoaderTestModule::class,
|
||||
'es6' => true
|
||||
],
|
||||
],
|
||||
'out' => '
|
||||
mw.loader.addSource({
|
||||
"local": "/w/load.php"
|
||||
});
|
||||
mw.loader.register([
|
||||
[
|
||||
"test.es6",
|
||||
"{blankVer}!"
|
||||
]
|
||||
]);',
|
||||
] ],
|
||||
[ [
|
||||
|
|
@ -591,6 +610,10 @@ mw.loader.register([
|
|||
'source' => 'example',
|
||||
'targets' => [ 'x-foo' ],
|
||||
],
|
||||
'test.es6' => [
|
||||
'class' => ResourceLoaderTestModule::class,
|
||||
'es6' => true
|
||||
]
|
||||
],
|
||||
'out' => '
|
||||
mw.loader.addSource({
|
||||
|
|
@ -660,6 +683,10 @@ mw.loader.register([
|
|||
[],
|
||||
3,
|
||||
"example"
|
||||
],
|
||||
[
|
||||
"test.es6",
|
||||
"{blankVer}!"
|
||||
]
|
||||
]);'
|
||||
] ],
|
||||
|
|
|
|||
Loading…
Reference in a new issue