ResourceLoader: Implement JavaScript source map support
In the debugger of Firefox and Chrome, without any special debug mode, you will be able to see the original unminified JavaScript source, and to set breakpoints in it and step through it. Main visible changes: * Add a config variable controlling the generation of source map links, off by default for now. * For script responses, move errors to the bottom of the response. This avoids disturbing the source map. * mw.loader.impl() calls will have less whitespace in debug mode, because minification is no longer done as a post-processing step on these calls. Details: * Use an index map when multiple responses are requested. This requires an update to the minify library. * Add a boolean "sourcemap" query parameter which causes load.php to deliver source map output instead of regular minified content. * Bundle sources into the source map and use two kinds of fake URL if a real debug URL is not available. "Open in new tab" on a fake URL is not functional. * In the source map mode, respond with 404 if the version is mismatched or if the content type is unimplemented. * Fix createLoaderURL() so that $extraQuery is not ignored when there are conflicting context parameters, so that we can successfully override the version. The source map version should match the delivered content, not the requested version. * Since minification with source map tracking can't use filter(), add a new cache for module source maps and minification. Add hit rate stats. Also: * Fix unnecessary array_map() in getCombinedVersion() Bug: T47514 Change-Id: I086e275148fdcac89f67a2fa0466d0dc063a17af
This commit is contained in:
parent
d713ab0716
commit
7c2c016e46
11 changed files with 551 additions and 185 deletions
|
|
@ -3740,6 +3740,12 @@ config-schema:
|
|||
Cache version for client-side ResourceLoader module storage. You can trigger
|
||||
invalidation of the contents of the module store by incrementing this value.
|
||||
@since 1.23
|
||||
ResourceLoaderEnableSourceMapLinks:
|
||||
default: false
|
||||
description: |-
|
||||
Whether to include source map URL comments in ResourceLoader responses
|
||||
for JavaScript modules.
|
||||
@since 1.41
|
||||
AllowSiteCSSOnRestrictedPages:
|
||||
default: false
|
||||
description: |-
|
||||
|
|
|
|||
|
|
@ -2107,6 +2107,12 @@ $wgResourceLoaderStorageEnabled = null;
|
|||
*/
|
||||
$wgResourceLoaderStorageVersion = null;
|
||||
|
||||
/**
|
||||
* Config variable stub for the ResourceLoaderEnableSourceMapLinks setting, for use by phpdoc and IDEs.
|
||||
* @see MediaWiki\MainConfigSchema::ResourceLoaderEnableSourceMapLinks
|
||||
*/
|
||||
$wgResourceLoaderEnableSourceMapLinks = null;
|
||||
|
||||
/**
|
||||
* Config variable stub for the AllowSiteCSSOnRestrictedPages setting, for use by phpdoc and IDEs.
|
||||
* @see MediaWiki\MainConfigSchema::AllowSiteCSSOnRestrictedPages
|
||||
|
|
|
|||
|
|
@ -2122,6 +2122,12 @@ class MainConfigNames {
|
|||
*/
|
||||
public const ResourceLoaderStorageVersion = 'ResourceLoaderStorageVersion';
|
||||
|
||||
/**
|
||||
* Name constant for the ResourceLoaderEnableSourceMapLinks setting, for use with Config::get()
|
||||
* @see MainConfigSchema::ResourceLoaderEnableSourceMapLinks
|
||||
*/
|
||||
public const ResourceLoaderEnableSourceMapLinks = 'ResourceLoaderEnableSourceMapLinks';
|
||||
|
||||
/**
|
||||
* Name constant for the AllowSiteCSSOnRestrictedPages setting, for use with Config::get()
|
||||
* @see MainConfigSchema::AllowSiteCSSOnRestrictedPages
|
||||
|
|
|
|||
|
|
@ -5913,6 +5913,16 @@ class MainConfigSchema {
|
|||
'default' => 1,
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether to include source map URL comments in ResourceLoader responses
|
||||
* for JavaScript modules.
|
||||
*
|
||||
* @since 1.41
|
||||
*/
|
||||
public const ResourceLoaderEnableSourceMapLinks = [
|
||||
'default' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether to allow site-wide CSS (MediaWiki:Common.css and friends) on
|
||||
* restricted pages like Special:UserLogin or Special:Preferences where
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ class Context implements MessageLocalizer {
|
|||
protected $version;
|
||||
/** @var bool */
|
||||
protected $raw;
|
||||
/** @var bool */
|
||||
protected $sourcemap;
|
||||
/** @var string|null */
|
||||
protected $image;
|
||||
/** @var string|null */
|
||||
|
|
@ -119,6 +121,7 @@ class Context implements MessageLocalizer {
|
|||
$this->only = $request->getRawVal( 'only' );
|
||||
$this->version = $request->getRawVal( 'version' );
|
||||
$this->raw = $request->getFuzzyBool( 'raw' );
|
||||
$this->sourcemap = $request->getFuzzyBool( 'sourcemap' );
|
||||
|
||||
// Image requests
|
||||
$this->image = $request->getRawVal( 'image' );
|
||||
|
|
@ -333,6 +336,14 @@ class Context implements MessageLocalizer {
|
|||
return $this->raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.41
|
||||
* @return bool
|
||||
*/
|
||||
public function isSourceMap(): bool {
|
||||
return $this->sourcemap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ class DerivativeContext extends Context {
|
|||
protected $version = self::INHERIT_VALUE;
|
||||
/** @var int|bool */
|
||||
protected $raw = self::INHERIT_VALUE;
|
||||
/** @var int|bool */
|
||||
protected $sourcemap = self::INHERIT_VALUE;
|
||||
/** @var int|callable|null */
|
||||
protected $contentOverrideCallback = self::INHERIT_VALUE;
|
||||
|
||||
|
|
@ -235,6 +237,17 @@ class DerivativeContext extends Context {
|
|||
$this->raw = $raw;
|
||||
}
|
||||
|
||||
public function isSourceMap(): bool {
|
||||
if ( $this->sourcemap === self::INHERIT_VALUE ) {
|
||||
return $this->context->isSourceMap();
|
||||
}
|
||||
return $this->sourcemap;
|
||||
}
|
||||
|
||||
public function setIsSourceMap( bool $sourcemap ) {
|
||||
$this->sourcemap = $sourcemap;
|
||||
}
|
||||
|
||||
public function getRequest(): WebRequest {
|
||||
return $this->context->getRequest();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ use Exception;
|
|||
use ExtensionRegistry;
|
||||
use HashBagOStuff;
|
||||
use HttpStatus;
|
||||
use IBufferingStatsdDataFactory;
|
||||
use InvalidArgumentException;
|
||||
use Less_Environment;
|
||||
use Less_Parser;
|
||||
|
|
@ -59,7 +60,13 @@ use WebRequest;
|
|||
use Wikimedia\DependencyStore\DependencyStore;
|
||||
use Wikimedia\DependencyStore\KeyValueDependencyStore;
|
||||
use Wikimedia\Minify\CSSMin;
|
||||
use Wikimedia\Minify\IdentityMinifierState;
|
||||
use Wikimedia\Minify\IndexMap;
|
||||
use Wikimedia\Minify\IndexMapOffset;
|
||||
use Wikimedia\Minify\JavaScriptMapperState;
|
||||
use Wikimedia\Minify\JavaScriptMinifier;
|
||||
use Wikimedia\Minify\JavaScriptMinifierState;
|
||||
use Wikimedia\Minify\MinifierState;
|
||||
use Wikimedia\RequestTimeout\TimeoutException;
|
||||
use Wikimedia\ScopedCallback;
|
||||
use Wikimedia\Timestamp\ConvertibleTimestamp;
|
||||
|
|
@ -113,6 +120,10 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
private $hookContainer;
|
||||
/** @var HookRunner */
|
||||
private $hookRunner;
|
||||
/** @var BagOStuff */
|
||||
private $srvCache;
|
||||
/** @var IBufferingStatsdDataFactory */
|
||||
private $stats;
|
||||
/** @var string */
|
||||
private $loadScript;
|
||||
/** @var int */
|
||||
|
|
@ -180,6 +191,9 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
$this->hookContainer = $services->getHookContainer();
|
||||
$this->hookRunner = new HookRunner( $this->hookContainer );
|
||||
|
||||
$this->srvCache = $services->getLocalServerObjectCache();
|
||||
$this->stats = $services->getStatsdDataFactory();
|
||||
|
||||
// Add 'local' source first
|
||||
$this->addSource( 'local', $this->loadScript );
|
||||
|
||||
|
|
@ -673,9 +687,10 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
if ( !$moduleNames ) {
|
||||
return '';
|
||||
}
|
||||
$hashes = array_map( function ( $module ) use ( $context ) {
|
||||
$hashes = [];
|
||||
foreach ( $moduleNames as $module ) {
|
||||
try {
|
||||
return $this->getModule( $module )->getVersionHash( $context );
|
||||
$hash = $this->getModule( $module )->getVersionHash( $context );
|
||||
} catch ( TimeoutException $e ) {
|
||||
throw $e;
|
||||
} catch ( Exception $e ) {
|
||||
|
|
@ -687,9 +702,10 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
'module' => $module,
|
||||
]
|
||||
);
|
||||
return '';
|
||||
$hash = '';
|
||||
}
|
||||
}, $moduleNames );
|
||||
$hashes[] = $hash;
|
||||
}
|
||||
return self::makeHash( implode( '', $hashes ) );
|
||||
}
|
||||
|
||||
|
|
@ -785,9 +801,40 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
return; // output handled (buffers cleared)
|
||||
}
|
||||
|
||||
if ( $context->isSourceMap() ) {
|
||||
// In source map mode, a version mismatch should be a 404
|
||||
if ( $context->getVersion() !== null && $versionHash !== $context->getVersion() ) {
|
||||
ob_end_clean();
|
||||
$this->sendSourceMapVersionMismatch( $versionHash );
|
||||
return;
|
||||
}
|
||||
// Can't generate a source map for image or only=styles requests, or in
|
||||
// debug mode
|
||||
if ( $context->getImage()
|
||||
|| $context->getOnly() === 'styles'
|
||||
|| $context->getDebug()
|
||||
) {
|
||||
ob_end_clean();
|
||||
$this->sendSourceMapTypeNotImplemented();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a response
|
||||
$response = $this->makeModuleResponse( $context, $modules, $missing );
|
||||
|
||||
// Determine the source map link URL while we still have an output buffer
|
||||
if ( $this->config->get( MainConfigNames::ResourceLoaderEnableSourceMapLinks )
|
||||
&& !$context->getImageObj()
|
||||
&& !$context->isSourceMap()
|
||||
&& $context->shouldIncludeScripts()
|
||||
&& !$context->getDebug()
|
||||
) {
|
||||
$sourceMapLinkUrl = $this->getSourceMapUrl( $context, $versionHash );
|
||||
} else {
|
||||
$sourceMapLinkUrl = null;
|
||||
}
|
||||
|
||||
// Capture any PHP warnings from the output buffer and append them to the
|
||||
// error list if we're in debug mode.
|
||||
if ( $context->getDebug() ) {
|
||||
|
|
@ -812,10 +859,19 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
$errorResponse .= 'if (window.console && console.error) { console.error('
|
||||
. $context->encodeJson( $errorText )
|
||||
. "); }\n";
|
||||
// Append the error info to the response
|
||||
// We used to prepend it, but that would corrupt the source map
|
||||
$response .= $errorResponse;
|
||||
} else {
|
||||
// For styles we can still prepend
|
||||
$response = $errorResponse . $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend error info to the response
|
||||
$response = $errorResponse . $response;
|
||||
// Add source map link
|
||||
if ( $sourceMapLinkUrl !== null ) {
|
||||
$response = self::ensureNewline( $response );
|
||||
$response .= "//# sourceMappingURL=$sourceMapLinkUrl";
|
||||
}
|
||||
|
||||
// @phan-suppress-next-line SecurityCheck-XSS
|
||||
|
|
@ -828,10 +884,9 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
*/
|
||||
protected function measureResponseTime() {
|
||||
$statStart = $_SERVER['REQUEST_TIME_FLOAT'];
|
||||
return new ScopedCallback( static function () use ( $statStart ) {
|
||||
return new ScopedCallback( function () use ( $statStart ) {
|
||||
$statTiming = microtime( true ) - $statStart;
|
||||
$stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
|
||||
$stats->timing( 'resourceloader.responseTime', $statTiming * 1000 );
|
||||
$this->stats->timing( 'resourceloader.responseTime', $statTiming * 1000 );
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
@ -879,6 +934,8 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
} else {
|
||||
$context->getImageObj()->sendResponseHeaders( $context );
|
||||
}
|
||||
} elseif ( $context->isSourceMap() ) {
|
||||
header( 'Content-Type: application/json' );
|
||||
} elseif ( $context->getOnly() === 'styles' ) {
|
||||
header( 'Content-Type: text/css; charset=utf-8' );
|
||||
header( 'Access-Control-Allow-Origin: *' );
|
||||
|
|
@ -942,6 +999,44 @@ class ResourceLoader implements LoggerAwareInterface {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL which will deliver the source map for the current response.
|
||||
*
|
||||
* @param Context $context
|
||||
* @param string $version The combined version hash
|
||||
* @return string
|
||||
*/
|
||||
private function getSourceMapUrl( Context $context, $version ) {
|
||||
return $this->createLoaderURL( 'local', $context, [
|
||||
'sourcemap' => '1',
|
||||
'version' => $version
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error page for a source map version mismatch
|
||||
*
|
||||
* @param string $currentVersion
|
||||
*/
|
||||
private function sendSourceMapVersionMismatch( $currentVersion ) {
|
||||
HttpStatus::header( 404 );
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
header( 'X-Content-Type-Options: nosniff' );
|
||||
echo "Can't deliver a source map for the requested version " .
|
||||
"since the version is now '$currentVersion'\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error page when a source map is requested but there is no
|
||||
* support for the specified content type
|
||||
*/
|
||||
private function sendSourceMapTypeNotImplemented() {
|
||||
HttpStatus::header( 404 );
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
header( 'X-Content-Type-Options: nosniff' );
|
||||
echo "Can't make a source map for this content type\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a CSS or JS comment block.
|
||||
*
|
||||
|
|
@ -1021,11 +1116,21 @@ MESSAGE;
|
|||
|
||||
$only = $context->getOnly();
|
||||
$debug = (bool)$context->getDebug();
|
||||
if ( $context->isSourceMap() && count( $modules ) > 1 ) {
|
||||
$indexMap = new IndexMap;
|
||||
} else {
|
||||
$indexMap = null;
|
||||
}
|
||||
|
||||
$out = '';
|
||||
foreach ( $modules as $name => $module ) {
|
||||
try {
|
||||
$out .= $this->getOneModuleResponse( $context, $name, $module );
|
||||
[ $response, $offset ] = $this->getOneModuleResponse( $context, $name, $module );
|
||||
if ( $indexMap ) {
|
||||
$indexMap->addEncodedMap( $response, $offset );
|
||||
} else {
|
||||
$out .= $response;
|
||||
}
|
||||
} catch ( TimeoutException $e ) {
|
||||
throw $e;
|
||||
} catch ( Exception $e ) {
|
||||
|
|
@ -1048,7 +1153,7 @@ MESSAGE;
|
|||
}
|
||||
|
||||
// Set the state of modules we didn't respond to with mw.loader.impl
|
||||
if ( $states ) {
|
||||
if ( $states && !$context->isSourceMap() ) {
|
||||
$stateScript = self::makeLoaderStateScript( $context, $states );
|
||||
if ( !$debug ) {
|
||||
$stateScript = self::filter( 'minify-js', $stateScript );
|
||||
|
|
@ -1064,7 +1169,11 @@ MESSAGE;
|
|||
. @$context->encodeJson( $states );
|
||||
}
|
||||
|
||||
return $out;
|
||||
if ( $indexMap ) {
|
||||
return $indexMap->getMap();
|
||||
} else {
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1073,18 +1182,101 @@ MESSAGE;
|
|||
* @param Context $context
|
||||
* @param string $name
|
||||
* @param Module $module
|
||||
* @return string
|
||||
* @return array{string,IndexMapOffset|null}
|
||||
*/
|
||||
private function getOneModuleResponse( Context $context, $name, Module $module ) {
|
||||
$only = $context->getOnly();
|
||||
$filter = $only === 'styles' ? 'minify-css' : 'minify-js';
|
||||
// Important: Do not cache minifications of embedded modules
|
||||
// This is especially for the private 'user.options' module,
|
||||
// which varies on every pageview and would explode the cache (T84960)
|
||||
$shouldCache = !$module->shouldEmbedModule( $context );
|
||||
if ( $only === 'styles' ) {
|
||||
$minifier = new IdentityMinifierState;
|
||||
$this->addOneModuleResponse( $context, $minifier, $name, $module, $this->extraHeaders );
|
||||
return [
|
||||
self::filter( 'minify-css', $minifier->getMinifiedOutput(),
|
||||
[ 'cache' => $shouldCache ] ),
|
||||
null
|
||||
];
|
||||
}
|
||||
|
||||
$minifier = new IdentityMinifierState;
|
||||
$this->addOneModuleResponse( $context, $minifier, $name, $module, $this->extraHeaders );
|
||||
$plainContent = $minifier->getMinifiedOutput();
|
||||
if ( $context->getDebug() ) {
|
||||
return [ $plainContent, null ];
|
||||
}
|
||||
|
||||
$isHit = true;
|
||||
$callback = function () use ( $context, $name, $module, &$isHit ) {
|
||||
$isHit = false;
|
||||
if ( $context->isSourceMap() ) {
|
||||
$minifier = ( new JavaScriptMapperState )
|
||||
->outputFile( $this->createLoaderURL( 'local', $context, [
|
||||
'modules' => self::makePackedModulesString( $context->getModules() ),
|
||||
'only' => $context->getOnly()
|
||||
] ) );
|
||||
} else {
|
||||
$minifier = new JavaScriptMinifierState;
|
||||
}
|
||||
// We only need to add one set of headers, and we did that for the identity response
|
||||
$discardedHeaders = null;
|
||||
$this->addOneModuleResponse( $context, $minifier, $name, $module, $discardedHeaders );
|
||||
if ( $context->isSourceMap() ) {
|
||||
$sourceMap = $minifier->getRawSourceMap();
|
||||
$generated = $minifier->getMinifiedOutput();
|
||||
$offset = IndexMapOffset::newFromText( $generated );
|
||||
return [ $sourceMap, $offset->toArray() ];
|
||||
} else {
|
||||
return [ $minifier->getMinifiedOutput(), null ];
|
||||
}
|
||||
};
|
||||
|
||||
if ( $shouldCache ) {
|
||||
[ $response, $offsetArray ] = $this->srvCache->getWithSetCallback(
|
||||
$this->srvCache->makeGlobalKey(
|
||||
'resourceloader-mapped',
|
||||
self::CACHE_VERSION,
|
||||
$name,
|
||||
$context->isSourceMap() ? '1' : '0',
|
||||
md5( $plainContent )
|
||||
),
|
||||
BagOStuff::TTL_DAY,
|
||||
$callback
|
||||
);
|
||||
$this->stats->increment( implode( '.', [
|
||||
"resourceloader_cache",
|
||||
$context->isSourceMap() ? 'map-js' : 'minify-js',
|
||||
$isHit ? 'hit' : 'miss'
|
||||
] ) );
|
||||
} else {
|
||||
[ $response, $offsetArray ] = $callback();
|
||||
}
|
||||
$offset = $offsetArray ? IndexMapOffset::newFromArray( $offsetArray ) : null;
|
||||
|
||||
return [ $response, $offset ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the response of a single module to the MinifierState
|
||||
*
|
||||
* @param Context $context
|
||||
* @param MinifierState $minifier
|
||||
* @param string $name
|
||||
* @param Module $module
|
||||
* @param array|null &$headers Array of headers. If it is not null, the
|
||||
* module's headers will be appended to this array.
|
||||
*/
|
||||
private function addOneModuleResponse(
|
||||
Context $context, MinifierState $minifier, $name, Module $module, &$headers
|
||||
) {
|
||||
$only = $context->getOnly();
|
||||
$debug = (bool)$context->getDebug();
|
||||
$content = $module->getModuleContent( $context );
|
||||
$implementKey = $name . '@' . $module->getVersionHash( $context );
|
||||
$strContent = '';
|
||||
$version = $module->getVersionHash( $context );
|
||||
|
||||
if ( isset( $content['headers'] ) ) {
|
||||
$this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
|
||||
if ( $headers !== null && isset( $content['headers'] ) ) {
|
||||
$headers = array_merge( $headers, $content['headers'] );
|
||||
}
|
||||
|
||||
// Append output
|
||||
|
|
@ -1098,11 +1290,13 @@ MESSAGE;
|
|||
}
|
||||
if ( isset( $scripts['plainScripts'] ) ) {
|
||||
// Add plain scripts
|
||||
$strContent .= self::concatenatePlainScripts( $scripts['plainScripts'] );
|
||||
$this->addPlainScripts( $minifier, $name, $scripts['plainScripts'] );
|
||||
} elseif ( isset( $scripts['files'] ) ) {
|
||||
// Add implement call if any
|
||||
$strContent .= self::makeLoaderImplementScript(
|
||||
$implementKey,
|
||||
$this->addImplementScript(
|
||||
$minifier,
|
||||
$name,
|
||||
$version,
|
||||
$scripts,
|
||||
[],
|
||||
null,
|
||||
|
|
@ -1116,7 +1310,9 @@ MESSAGE;
|
|||
// We no longer separate into media, they are all combined now with
|
||||
// custom media type groups into @media .. {} sections as part of the css string.
|
||||
// Module returns either an empty array or a numerical array with css strings.
|
||||
$strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
|
||||
if ( isset( $styles['css'] ) ) {
|
||||
$minifier->addOutput( implode( '', $styles['css'] ) );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$scripts = $content['scripts'] ?? '';
|
||||
|
|
@ -1132,8 +1328,10 @@ MESSAGE;
|
|||
$scripts = self::filter( 'minify-js', $scripts ); // T107377
|
||||
}
|
||||
}
|
||||
$strContent .= self::makeLoaderImplementScript(
|
||||
$implementKey,
|
||||
$this->addImplementScript(
|
||||
$minifier,
|
||||
$name,
|
||||
$version,
|
||||
$scripts,
|
||||
$content['styles'] ?? [],
|
||||
isset( $content['messagesBlob'] ) ? new HtmlJsCode( $content['messagesBlob'] ) : null,
|
||||
|
|
@ -1142,25 +1340,7 @@ MESSAGE;
|
|||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $debug ) {
|
||||
// In debug mode, separate each response by a new line.
|
||||
// For example, between 'mw.loader.impl();' statements.
|
||||
$strContent = self::ensureNewline( $strContent );
|
||||
} else {
|
||||
$strContent = self::filter( $filter, $strContent, [
|
||||
// Important: Do not cache minifications of embedded modules
|
||||
// This is especially for the private 'user.options' module,
|
||||
// which varies on every pageview and would explode the cache (T84960)
|
||||
'cache' => !$module->shouldEmbedModule( $context )
|
||||
] );
|
||||
}
|
||||
|
||||
if ( $only === 'scripts' ) {
|
||||
// Use a linebreak between module scripts (T162719)
|
||||
$strContent = self::ensureNewline( $strContent );
|
||||
}
|
||||
return $strContent;
|
||||
$minifier->ensureNewline();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1195,9 +1375,12 @@ MESSAGE;
|
|||
}
|
||||
|
||||
/**
|
||||
* Return JS code that calls mw.loader.impl with given module properties.
|
||||
* Generate JS code that calls mw.loader.impl with given module properties
|
||||
* and add it to the MinifierState.
|
||||
*
|
||||
* @param string $name Module name used as implement key (format "`[name]@[version]`")
|
||||
* @param MinifierState $minifier The minifier to which output should be appended
|
||||
* @param string $moduleName The module name
|
||||
* @param string $version The module version hash
|
||||
* @param array|string|string[] $scripts
|
||||
* - array: Package files array containing strings for individual JS files,
|
||||
* as produced by Module::getScript().
|
||||
|
|
@ -1211,30 +1394,39 @@ MESSAGE;
|
|||
* wrapped in an HtmlJsCode object.
|
||||
* @param array<string,string> $templates Map from template name to template source.
|
||||
* @param string|null $deprecationWarning
|
||||
* @return string JavaScript code
|
||||
*/
|
||||
private static function makeLoaderImplementScript(
|
||||
$name, $scripts, $styles, $messages, $templates, $deprecationWarning
|
||||
private function addImplementScript( MinifierState $minifier,
|
||||
$moduleName, $version, $scripts, $styles, $messages, $templates, $deprecationWarning
|
||||
) {
|
||||
$implementKey = "$moduleName@$version";
|
||||
// Plain functions are used instead of arrow functions to avoid
|
||||
// defeating lazy compilation on Chrome. (T343407)
|
||||
$minifier->addOutput( "mw.loader.impl(function(){return[" .
|
||||
Html::encodeJsVar( $implementKey ) . "," );
|
||||
|
||||
// Scripts
|
||||
if ( is_string( $scripts ) ) {
|
||||
// user/site script
|
||||
$minifier->addOutput( Html::encodeJsVar( $scripts ) );
|
||||
} elseif ( is_array( $scripts ) ) {
|
||||
if ( isset( $scripts['files'] ) ) {
|
||||
$files = self::encodeFiles( $scripts['files'] );
|
||||
$scripts = HtmlJsCode::encodeObject( [
|
||||
'main' => $scripts['main'],
|
||||
'files' => HtmlJsCode::encodeObject( $files, true )
|
||||
], true );
|
||||
$minifier->addOutput(
|
||||
"{\"main\":" .
|
||||
Html::encodeJsVar( $scripts['main'] ) .
|
||||
",\"files\":" );
|
||||
$this->addFiles( $minifier, $moduleName, $scripts['files'] );
|
||||
$minifier->addOutput( "}" );
|
||||
} elseif ( isset( $scripts['plainScripts'] ) ) {
|
||||
$plainScripts = self::concatenatePlainScripts( $scripts['plainScripts'] );
|
||||
if ( $plainScripts === '' ) {
|
||||
$scripts = null;
|
||||
if ( $this->isEmptyFileInfos( $scripts['plainScripts'] ) ) {
|
||||
$minifier->addOutput( 'null' );
|
||||
} else {
|
||||
$scripts = new HtmlJsCode(
|
||||
"function ( $, jQuery, require, module ) {\n{$plainScripts}}" );
|
||||
$minifier->addOutput( "function($,jQuery,require,module){" );
|
||||
$this->addPlainScripts( $minifier, $moduleName, $scripts['plainScripts'] );
|
||||
$minifier->addOutput( "}" );
|
||||
}
|
||||
} elseif ( $scripts === [] || isset( $scripts[0] ) ) {
|
||||
// Array of URLs
|
||||
$minifier->addOutput( Html::encodeJsVar( $scripts ) );
|
||||
} else {
|
||||
throw new InvalidArgumentException( 'Invalid script array: ' .
|
||||
'must contain files, plainScripts or be an array of URLs' );
|
||||
|
|
@ -1246,51 +1438,85 @@ MESSAGE;
|
|||
// mw.loader.impl requires 'styles', 'messages' and 'templates' to be objects (not
|
||||
// arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
|
||||
// of "{}". Force them to objects.
|
||||
$module = [
|
||||
$name,
|
||||
$scripts,
|
||||
$extraArgs = [
|
||||
(object)$styles,
|
||||
$messages ?? (object)[],
|
||||
(object)$templates,
|
||||
$deprecationWarning
|
||||
];
|
||||
self::trimArray( $module );
|
||||
|
||||
// We use pretty output unconditionally to make this method simpler.
|
||||
// Minification is taken care of closer to the output.
|
||||
// Plain functions are used instead of arrow functions to avoid
|
||||
// defeating lazy compilation on Chrome. (T343407)
|
||||
return 'mw.loader.impl(function(){return[' .
|
||||
Html::encodeJsList( $module, true ) . '];});';
|
||||
self::trimArray( $extraArgs );
|
||||
foreach ( $extraArgs as $arg ) {
|
||||
$minifier->addOutput( ',' . Html::encodeJsVar( $arg ) );
|
||||
}
|
||||
$minifier->addOutput( "];});" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the contents of an array of package files, and convert it to an
|
||||
* array of data which can be passed to HtmlJsCode::encodeObject(), with any
|
||||
* JS code wrapped in HtmlJsCode objects.
|
||||
* Extract the contents of an array of package files, and convert it to a
|
||||
* JavaScript array. Add the array to the minifier state.
|
||||
*
|
||||
* Package files can contain JSON data.
|
||||
*
|
||||
* @param MinifierState $minifier
|
||||
* @param string $moduleName
|
||||
* @param array $files
|
||||
* @return array
|
||||
*/
|
||||
private static function encodeFiles( $files ) {
|
||||
foreach ( $files as &$file ) {
|
||||
// $file is changed (by reference) from a descriptor array to the content of the file
|
||||
// All of these essentially do $file = $file['content'];, some just have wrapping around it
|
||||
if ( $file['type'] === 'script' ) {
|
||||
// Ensure that the script has a newline at the end to close any comment in the
|
||||
// last line.
|
||||
$content = self::ensureNewline( $file['content'] );
|
||||
private function addFiles( MinifierState $minifier, $moduleName, $files ) {
|
||||
$first = true;
|
||||
$minifier->addOutput( "{" );
|
||||
foreach ( $files as $fileName => $file ) {
|
||||
if ( $first ) {
|
||||
$first = false;
|
||||
} else {
|
||||
$minifier->addOutput( "," );
|
||||
}
|
||||
$minifier->addOutput( Html::encodeJsVar( $fileName ) . ':' );
|
||||
$this->addFileContent( $minifier, $moduleName, 'packageFile', $fileName, $file );
|
||||
}
|
||||
$minifier->addOutput( "}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a package file to a MinifierState
|
||||
*
|
||||
* @param MinifierState $minifier
|
||||
* @param string $moduleName
|
||||
* @param string $sourceType
|
||||
* @param string|int $sourceIndex
|
||||
* @param array $file The expanded file info array
|
||||
*/
|
||||
private function addFileContent( MinifierState $minifier,
|
||||
$moduleName, $sourceType, $sourceIndex, array $file
|
||||
) {
|
||||
$isScript = ( $file['type'] ?? 'script' ) === 'script';
|
||||
/** @var FilePath|null $filePath */
|
||||
$filePath = $file['filePath'] ?? $file['virtualFilePath'] ?? null;
|
||||
if ( $filePath !== null && $filePath->getRemoteBasePath() !== null ) {
|
||||
$url = $filePath->getRemotePath();
|
||||
} else {
|
||||
$ext = $isScript ? 'js' : 'json';
|
||||
$scriptPath = $this->config->has( MainConfigNames::ScriptPath )
|
||||
? $this->config->get( MainConfigNames::ScriptPath ) : '';
|
||||
$url = "$scriptPath/virtual-resource/$moduleName-$sourceType-$sourceIndex.$ext";
|
||||
}
|
||||
$content = $file['content'];
|
||||
if ( $isScript ) {
|
||||
if ( $sourceType === 'packageFile' ) {
|
||||
// Provide CJS `exports` (in addition to CJS2 `module.exports`) to package modules (T284511).
|
||||
// $/jQuery are simply used as globals instead.
|
||||
// TODO: Remove $/jQuery param from traditional module closure too (and bump caching)
|
||||
$file = new HtmlJsCode( "function ( require, module, exports ) {\n$content}" );
|
||||
$minifier->addOutput( "function(require,module,exports){" );
|
||||
$minifier->addSourceFile( $url, $content, true );
|
||||
$minifier->ensureNewline();
|
||||
$minifier->addOutput( "}" );
|
||||
} else {
|
||||
$file = $file['content'];
|
||||
$minifier->addSourceFile( $url, $content, true );
|
||||
$minifier->ensureNewline();
|
||||
}
|
||||
} else {
|
||||
$content = Html::encodeJsVar( $content, true );
|
||||
$minifier->addSourceFile( $url, $content, true );
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1310,6 +1536,34 @@ MESSAGE;
|
|||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add contents from a plainScripts array like [ [ 'content' => '...' ]
|
||||
* to a MinifierState
|
||||
*
|
||||
* @param MinifierState $minifier
|
||||
* @param string $moduleName
|
||||
* @param array[] $plainScripts
|
||||
*/
|
||||
private function addPlainScripts( MinifierState $minifier, $moduleName, $plainScripts ) {
|
||||
foreach ( $plainScripts as $index => $file ) {
|
||||
$this->addFileContent( $minifier, $moduleName, 'script', $index, $file );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an array of file info arrays has empty content
|
||||
*
|
||||
* @param array $infos
|
||||
* @return bool
|
||||
*/
|
||||
private function isEmptyFileInfos( $infos ) {
|
||||
$len = 0;
|
||||
foreach ( $infos as $info ) {
|
||||
$len += strlen( $info['content'] ?? '' );
|
||||
}
|
||||
return $len === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an associative array mapping media type to CSS into a
|
||||
* single stylesheet with "@media" blocks.
|
||||
|
|
@ -1764,7 +2018,9 @@ MESSAGE;
|
|||
if ( $printable ) {
|
||||
$query['printable'] = 1;
|
||||
}
|
||||
$query += $extraQuery;
|
||||
foreach ( $extraQuery as $name => $value ) {
|
||||
$query[$name] = $value;
|
||||
}
|
||||
|
||||
// Make queries uniform in order
|
||||
ksort( $query );
|
||||
|
|
|
|||
|
|
@ -682,6 +682,7 @@ return [
|
|||
'ResourceLoaderEnableJSProfiler' => false,
|
||||
'ResourceLoaderStorageEnabled' => true,
|
||||
'ResourceLoaderStorageVersion' => 1,
|
||||
'ResourceLoaderEnableSourceMapLinks' => false,
|
||||
'AllowSiteCSSOnRestrictedPages' => false,
|
||||
'VueDevelopmentMode' => false,
|
||||
'MetaNamespace' => false,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ abstract class ResourceLoaderTestCase extends MediaWikiIntegrationTestCase {
|
|||
'modules' => 'startup',
|
||||
'only' => 'scripts',
|
||||
'safemode' => null,
|
||||
'sourcemap' => null,
|
||||
];
|
||||
$resourceLoader = $rl ?: new ResourceLoader(
|
||||
MediaWikiServices::getInstance()->getMainConfig(),
|
||||
|
|
@ -57,6 +58,7 @@ abstract class ResourceLoaderTestCase extends MediaWikiIntegrationTestCase {
|
|||
'only' => $options['only'],
|
||||
'safemode' => $options['safemode'],
|
||||
'skin' => $options['skin'],
|
||||
'sourcemap' => $options['sourcemap'],
|
||||
'target' => 'phpunit',
|
||||
] );
|
||||
$ctx = $this->getMockBuilder( Context::class )
|
||||
|
|
@ -69,6 +71,9 @@ abstract class ResourceLoaderTestCase extends MediaWikiIntegrationTestCase {
|
|||
|
||||
public static function getSettings() {
|
||||
return [
|
||||
// For ResourceLoader::respond
|
||||
MainConfigNames::ResourceLoaderEnableSourceMapLinks => false,
|
||||
|
||||
// For Module
|
||||
MainConfigNames::ResourceLoaderValidateJS => false,
|
||||
|
||||
|
|
|
|||
|
|
@ -2600,7 +2600,8 @@ class OutputPageTest extends MediaWikiIntegrationTestCase {
|
|||
[ 'test.quux', RL\Module::TYPE_COMBINED ],
|
||||
"<script>(RLQ=window.RLQ||[]).push(function(){"
|
||||
. "mw.loader.impl(function(){return[\"test.quux@1b4i1\",function($,jQuery,require,module){"
|
||||
. "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
|
||||
. "mw.test.baz({token:123});\n"
|
||||
. "},{\"css\":[\".mw-icon{transition:none}"
|
||||
. "\"]}];});});</script>"
|
||||
],
|
||||
// Load no modules
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace MediaWiki\Tests\ResourceLoader;
|
||||
|
||||
use Config;
|
||||
use EmptyResourceLoader;
|
||||
use Exception;
|
||||
use ExtensionRegistry;
|
||||
use HashConfig;
|
||||
use InvalidArgumentException;
|
||||
use MediaWiki\Html\HtmlJsCode;
|
||||
use MediaWiki\MainConfigNames;
|
||||
|
|
@ -20,6 +22,7 @@ use ResourceLoaderTestCase;
|
|||
use ResourceLoaderTestModule;
|
||||
use RuntimeException;
|
||||
use UnexpectedValueException;
|
||||
use Wikimedia\Minify\IdentityMinifierState;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
|
||||
|
|
@ -401,109 +404,71 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
|
|||
|
||||
public static function provideLoaderImplement() {
|
||||
return [
|
||||
[ [
|
||||
'title' => 'Implement scripts, styles and messages',
|
||||
|
||||
'Implement scripts, styles and messages' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => 'mw.example();',
|
||||
'styles' => [ 'css' => [ '.mw-example {}' ] ],
|
||||
'messages' => [ 'example' => '' ],
|
||||
'templates' => [],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", function ( $, jQuery, require, module ) {
|
||||
mw.example();
|
||||
}, {
|
||||
"css": [
|
||||
".mw-example {}"
|
||||
]
|
||||
}, {
|
||||
"example": ""
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
||||
},{"css":[".mw-example {}"]},{"example":""}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement scripts',
|
||||
|
||||
'Implement scripts' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => 'mw.example();',
|
||||
'styles' => [],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", function ( $, jQuery, require, module ) {
|
||||
mw.example();
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
||||
}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement scripts with newline at end',
|
||||
|
||||
'Implement scripts with newline at end' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => "mw.example();\n",
|
||||
'styles' => [],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", function ( $, jQuery, require, module ) {
|
||||
mw.example();
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
||||
}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement scripts with comment at end',
|
||||
|
||||
'Implement scripts with comment at end' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => "mw.example();//Foo",
|
||||
'styles' => [],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", function ( $, jQuery, require, module ) {
|
||||
mw.example();//Foo
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();//Foo
|
||||
}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement styles',
|
||||
|
||||
'Implement styles' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => [],
|
||||
'styles' => [ 'css' => [ '.mw-example {}' ] ],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", [], {
|
||||
"css": [
|
||||
".mw-example {}"
|
||||
]
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",[],{"css":[".mw-example {}"]}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement scripts and messages',
|
||||
|
||||
'Implement scripts and messages' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => 'mw.example();',
|
||||
'messages' => [ 'example' => '' ],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", function ( $, jQuery, require, module ) {
|
||||
mw.example();
|
||||
}, {}, {
|
||||
"example": ""
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
||||
},{},{"example":""}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement scripts and templates',
|
||||
|
||||
'Implement scripts and templates' => [ [
|
||||
'name' => 'test.example',
|
||||
'scripts' => 'mw.example();',
|
||||
'templates' => [ 'example.html' => '' ],
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "test.example", function ( $, jQuery, require, module ) {
|
||||
mw.example();
|
||||
}, {}, {}, {
|
||||
"example.html": ""
|
||||
} ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["test.example@1",function($,jQuery,require,module){mw.example();
|
||||
},{},{},{"example.html":""}];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement unwrapped user script',
|
||||
|
||||
'Implement unwrapped user script' => [ [
|
||||
'name' => 'user',
|
||||
'scripts' => 'mw.example( 1 );',
|
||||
'wrap' => false,
|
||||
|
||||
'expected' => 'mw.loader.impl(function(){return[ "user", "mw.example( 1 );" ];});',
|
||||
'expected' => 'mw.loader.impl(function(){return["user@1","mw.example( 1 );"];});',
|
||||
] ],
|
||||
[ [
|
||||
'title' => 'Implement multi-file script',
|
||||
|
||||
'Implement multi-file script' => [ [
|
||||
'name' => 'test.multifile',
|
||||
'scripts' => [
|
||||
'files' => [
|
||||
|
|
@ -532,26 +497,13 @@ mw.example();
|
|||
],
|
||||
|
||||
'expected' => <<<END
|
||||
mw.loader.impl(function(){return[ "test.multifile", {
|
||||
"main": "five.js",
|
||||
"files": {
|
||||
"one.js": function ( require, module, exports ) {
|
||||
mw.example( 1 );
|
||||
},
|
||||
"two.json": {
|
||||
mw.loader.impl(function(){return["test.multifile@1",{"main":"five.js","files":{"one.js":function(require,module,exports){mw.example( 1 );
|
||||
},"two.json":{
|
||||
"n": 2
|
||||
},
|
||||
"three.js": function ( require, module, exports ) {
|
||||
mw.example( 3 ); // Comment
|
||||
},
|
||||
"four.js": function ( require, module, exports ) {
|
||||
mw.example( 4 );
|
||||
},
|
||||
"five.js": function ( require, module, exports ) {
|
||||
mw.example( 5 );
|
||||
}
|
||||
}
|
||||
} ];});
|
||||
},"three.js":function(require,module,exports){mw.example( 3 ); // Comment
|
||||
},"four.js":function(require,module,exports){mw.example( 4 );
|
||||
},"five.js":function(require,module,exports){mw.example( 5 );
|
||||
}}}];});
|
||||
END
|
||||
] ],
|
||||
];
|
||||
|
|
@ -560,36 +512,41 @@ END
|
|||
/**
|
||||
* @dataProvider provideLoaderImplement
|
||||
*/
|
||||
public function testMakeLoaderImplementScript( $case ) {
|
||||
public function testAddImplementScript( $case ) {
|
||||
$case += [
|
||||
'version' => '1',
|
||||
'wrap' => true,
|
||||
'styles' => [],
|
||||
'templates' => [],
|
||||
'messages' => new HtmlJsCode( '{}' ),
|
||||
'packageFiles' => [],
|
||||
];
|
||||
$rl = TestingAccessWrapper::newFromClass( ResourceLoader::class );
|
||||
$this->assertEquals(
|
||||
$case['expected'],
|
||||
$rl->makeLoaderImplementScript(
|
||||
$case['name'],
|
||||
( $case['wrap'] && is_string( $case['scripts'] ) )
|
||||
? [ 'plainScripts' => [ [ 'content' => $case['scripts'] ] ] ]
|
||||
: $case['scripts'],
|
||||
$case['styles'],
|
||||
$case['messages'],
|
||||
$case['templates'],
|
||||
$case['packageFiles']
|
||||
)
|
||||
$rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader );
|
||||
$minifier = new IdentityMinifierState;
|
||||
$rl->addImplementScript(
|
||||
$minifier,
|
||||
$case['name'],
|
||||
$case['version'],
|
||||
( $case['wrap'] && is_string( $case['scripts'] ) )
|
||||
? [ 'plainScripts' => [ [ 'content' => $case['scripts'] ] ] ]
|
||||
: $case['scripts'],
|
||||
$case['styles'],
|
||||
$case['messages'],
|
||||
$case['templates'],
|
||||
$case['packageFiles']
|
||||
);
|
||||
$this->assertEquals( $case['expected'], $minifier->getMinifiedOutput() );
|
||||
}
|
||||
|
||||
public function testMakeLoaderImplementScriptInvalid() {
|
||||
public function testAddImplementScriptInvalid() {
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
$this->expectExceptionMessage( 'Script must be a' );
|
||||
$rl = TestingAccessWrapper::newFromClass( ResourceLoader::class );
|
||||
$rl->makeLoaderImplementScript(
|
||||
$minifier = new IdentityMinifierState;
|
||||
$rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader );
|
||||
$rl->addImplementScript(
|
||||
$minifier,
|
||||
'test', // name
|
||||
'1', // version
|
||||
123, // scripts
|
||||
null, // styles
|
||||
null, // messages
|
||||
|
|
@ -1174,7 +1131,7 @@ END
|
|||
$rl
|
||||
);
|
||||
|
||||
$this->expectOutputRegex( '/^\/\*.+Cannot build private module/s' );
|
||||
$this->expectOutputRegex( '/\/\*.+Cannot build private module/s' );
|
||||
$rl->respond( $context );
|
||||
}
|
||||
|
||||
|
|
@ -1213,11 +1170,105 @@ END
|
|||
->with( $context, [ 'test' => $module ] )
|
||||
->willReturn( 'foo;' );
|
||||
// Internal errors should be caught and logged without affecting module output
|
||||
$this->expectOutputRegex( '/^\/\*.+Preload error.+Version error.+\*\/.*foo;/ms' );
|
||||
$this->expectOutputRegex( '/foo;.*\/\*.+Preload error.+Version error.+\*\//ms' );
|
||||
|
||||
$rl->respond( $context );
|
||||
}
|
||||
|
||||
private function getResourceLoaderWithTestModules( Config $config = null ) {
|
||||
$localBasePath = __DIR__ . '/../../data/resourceloader';
|
||||
$remoteBasePath = '/w';
|
||||
$rl = new EmptyResourceLoader( $config );
|
||||
$rl->register( 'test1', [
|
||||
'localBasePath' => $localBasePath,
|
||||
'remoteBasePath' => $remoteBasePath,
|
||||
'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
|
||||
] );
|
||||
$rl->register( 'test2', [
|
||||
'localBasePath' => $localBasePath,
|
||||
'remoteBasePath' => $remoteBasePath,
|
||||
'scripts' => [ 'script-nosemi-nonl.js' ],
|
||||
] );
|
||||
return $rl;
|
||||
}
|
||||
|
||||
public function testRespondSourceMap() {
|
||||
$rl = $this->getResourceLoaderWithTestModules();
|
||||
$context = $this->getResourceLoaderContext(
|
||||
[ 'modules' => 'test1', 'sourcemap' => '1', 'debug' => '' ],
|
||||
$rl
|
||||
);
|
||||
$this->expectOutputString( <<<JSON
|
||||
{
|
||||
"version": 3,
|
||||
"file": "/load.php?lang=en&modules=test1&only=scripts",
|
||||
"sources": ["/w/script-nosemi.js","/w/script-comment.js"],
|
||||
"sourcesContent": ["/* eslint-disable */\\nmw.foo()\\n","/* eslint-disable */\\nmw.foo()\\n// mw.bar();\\n"],
|
||||
"names": [],
|
||||
"mappings": "AACA,EAAE,CAAC,GAAG,CAAC;ACAP,EAAE,CAAC,GAAG,CAAC"
|
||||
}
|
||||
|
||||
JSON
|
||||
);
|
||||
$rl->respond( $context );
|
||||
}
|
||||
|
||||
public function testRespondIndexMap() {
|
||||
$rl = $this->getResourceLoaderWithTestModules();
|
||||
$context = $this->getResourceLoaderContext(
|
||||
[ 'modules' => 'test1|test2', 'sourcemap' => '1', 'debug' => '' ],
|
||||
$rl
|
||||
);
|
||||
$this->expectOutputString( <<<JSON
|
||||
{
|
||||
"version": 3,
|
||||
"sections": [
|
||||
{"offset":{"line":0,"column":0},"map":{
|
||||
"version": 3,
|
||||
"file": "/load.php?lang=en&modules=test1%2Ctest2&only=scripts",
|
||||
"sources": ["/w/script-nosemi.js","/w/script-comment.js"],
|
||||
"sourcesContent": ["/* eslint-disable */\\nmw.foo()\\n","/* eslint-disable */\\nmw.foo()\\n// mw.bar();\\n"],
|
||||
"names": [],
|
||||
"mappings": "AACA,EAAE,CAAC,GAAG,CAAC;ACAP,EAAE,CAAC,GAAG,CAAC"
|
||||
}
|
||||
},
|
||||
{"offset":{"line":2,"column":0},"map":{
|
||||
"version": 3,
|
||||
"file": "/load.php?lang=en&modules=test1%2Ctest2&only=scripts",
|
||||
"sources": ["/w/script-nosemi-nonl.js"],
|
||||
"sourcesContent": ["/* eslint-disable */\\nmw.foo()"],
|
||||
"names": [],
|
||||
"mappings": "AACA,EAAE,CAAC,GAAG,CAAC"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
);
|
||||
$rl->respond( $context );
|
||||
}
|
||||
|
||||
public function testRespondSourceMapLink() {
|
||||
$rl = $this->getResourceLoaderWithTestModules( new HashConfig(
|
||||
[
|
||||
MainConfigNames::ResourceLoaderEnableSourceMapLinks => true,
|
||||
]
|
||||
) );
|
||||
$context = $this->getResourceLoaderContext(
|
||||
[ 'modules' => 'test1|test2', 'debug' => '' ],
|
||||
$rl
|
||||
);
|
||||
$this->expectOutputString( <<<JS
|
||||
mw.foo()
|
||||
mw.foo()
|
||||
mw.foo()
|
||||
mw.loader.state({"test1":"ready","test2":"ready"});
|
||||
//# sourceMappingURL=/load.php?lang=en&modules=test1%2Ctest2&only=scripts&sourcemap=1&version=pq39u
|
||||
JS
|
||||
);
|
||||
$rl->respond( $context );
|
||||
}
|
||||
|
||||
public function testMeasureResponseTime() {
|
||||
$stats = $this->getMockBuilder( NullStatsdDataFactory::class )
|
||||
->onlyMethods( [ 'timing' ] )->getMock();
|
||||
|
|
|
|||
Loading…
Reference in a new issue