If a generated chunk is listed in the codexComponents array, an exception will be thrown informing the developer that the item is not an export of Codex and cannot be required. Text fixtures and expected values were updated to match recent changes to the manifest that made composables explict entries. Bug: T353191 Change-Id: Ie3e35501993b1ad5d29ebecbcc8203e7f4ea8c81
358 lines
11 KiB
PHP
358 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
namespace MediaWiki\ResourceLoader;
|
|
|
|
use ExtensionRegistry;
|
|
use InvalidArgumentException;
|
|
use MediaWiki\Config\Config;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\Html\HtmlJsCode;
|
|
use MediaWiki\MainConfigNames;
|
|
|
|
/**
|
|
* Module for codex that has direction-specific style files and a static helper
|
|
* function for embedding icons in package modules. This module also contains
|
|
* logic to support code-splitting (aka tree-shaking) of the Codex library to
|
|
* return only a subset of component JS and/or CSS files.
|
|
*
|
|
* @ingroup ResourceLoader
|
|
* @internal
|
|
*/
|
|
class CodexModule extends FileModule {
|
|
protected const CODEX_MODULE_DIR = 'resources/lib/codex/modules/';
|
|
private const CODEX_MODULE_DEPENDENCIES = [ 'vue' ];
|
|
|
|
/** @var array<string,string> */
|
|
private array $themeMap = [];
|
|
|
|
/** @var array<string,array> */
|
|
private array $themeStyles = [];
|
|
|
|
/** @var array<string> */
|
|
private array $codexComponents = [];
|
|
|
|
private bool $hasThemeStyles = false;
|
|
private bool $isStyleOnly = false;
|
|
private bool $isScriptOnly = false;
|
|
private bool $setupComplete = false;
|
|
|
|
/**
|
|
* @param array $options [optional]
|
|
* - themeStyles: array of skin- or theme-specific files
|
|
* - codexComponents: array of Codex components to include
|
|
* - codexStyleOnly: whether to include only style files
|
|
* - codexScriptOnly: whether to include only script files
|
|
* @param string|null $localBasePath [optional]
|
|
* @param string|null $remoteBasePath [optional]
|
|
*/
|
|
public function __construct( array $options = [], $localBasePath = null, $remoteBasePath = null ) {
|
|
$skinCodexThemes = ExtensionRegistry::getInstance()->getAttribute( 'SkinCodexThemes' );
|
|
$this->themeMap = [ 'default' => 'wikimedia-ui' ] + $skinCodexThemes;
|
|
|
|
if ( isset( $options['themeStyles'] ) ) {
|
|
$this->hasThemeStyles = true;
|
|
$this->themeStyles = $options[ 'themeStyles' ];
|
|
}
|
|
|
|
if ( isset( $options[ 'codexComponents' ] ) ) {
|
|
$this->codexComponents = $options[ 'codexComponents' ];
|
|
}
|
|
|
|
if ( isset( $options[ 'codexStyleOnly' ] ) ) {
|
|
$this->isStyleOnly = $options[ 'codexStyleOnly' ];
|
|
}
|
|
|
|
if ( isset( $options[ 'codexScriptOnly' ] ) ) {
|
|
$this->isScriptOnly = $options[ 'codexScriptOnly' ];
|
|
}
|
|
|
|
// Unlike when the entire @wikimedia/codex module is depended on, when the codexComponents
|
|
// option is used, Vue is not automatically included, though it is required. Add it and
|
|
// other dependencies here.
|
|
if ( !$this->isStyleOnly && count( $this->codexComponents ) > 0 ) {
|
|
$options[ 'dependencies' ] = array_unique( array_merge(
|
|
$options[ 'dependencies' ] ?? [],
|
|
self::CODEX_MODULE_DEPENDENCIES
|
|
) );
|
|
}
|
|
|
|
parent::__construct( $options, $localBasePath, $remoteBasePath );
|
|
}
|
|
|
|
/**
|
|
* Retrieve the specified icon definitions from codex-icons.json. Intended as a convenience
|
|
* function to be used in packageFiles definitions.
|
|
*
|
|
* Example:
|
|
* "packageFiles": [
|
|
* {
|
|
* "name": "icons.json",
|
|
* "callback": "MediaWiki\\ResourceLoader\\CodexModule::getIcons",
|
|
* "callbackParam": [
|
|
* "cdxIconClear",
|
|
* "cdxIconTrash"
|
|
* ]
|
|
* }
|
|
* ]
|
|
*
|
|
* @param Context $context
|
|
* @param Config $config
|
|
* @param string[] $iconNames Names of icons to fetch
|
|
* @return array
|
|
*/
|
|
public static function getIcons( Context $context, Config $config, array $iconNames = [] ): array {
|
|
global $IP;
|
|
static $allIcons = null;
|
|
if ( $allIcons === null ) {
|
|
$allIcons = json_decode( file_get_contents( "$IP/resources/lib/codex-icons/codex-icons.json" ), true );
|
|
}
|
|
return array_intersect_key( $allIcons, array_flip( $iconNames ) );
|
|
}
|
|
|
|
// These 3 public methods are the entry points to this class; depending on the
|
|
// circumstances any one of these might be called first.
|
|
|
|
public function getPackageFiles( Context $context ) {
|
|
$this->setupCodex( $context );
|
|
return parent::getPackageFiles( $context );
|
|
}
|
|
|
|
public function getStyleFiles( Context $context ) {
|
|
$this->setupCodex( $context );
|
|
return parent::getStyleFiles( $context );
|
|
}
|
|
|
|
public function getDefinitionSummary( Context $context ) {
|
|
$this->setupCodex( $context );
|
|
return parent::getDefinitionSummary( $context );
|
|
}
|
|
|
|
/**
|
|
* @param Context $context
|
|
* @return string Name of the manifest file to use
|
|
*/
|
|
private function getManifestFile( Context $context ): string {
|
|
$isRtl = $context->getDirection() === 'rtl';
|
|
$isLegacy = $this->getTheme( $context ) === 'wikimedia-ui-legacy';
|
|
$manifestFile = null;
|
|
|
|
if ( $isRtl && $isLegacy ) {
|
|
$manifestFile = 'manifest-legacy-rtl.json';
|
|
} elseif ( $isRtl ) {
|
|
$manifestFile = 'manifest-rtl.json';
|
|
} elseif ( $isLegacy ) {
|
|
$manifestFile = 'manifest-legacy.json';
|
|
} else {
|
|
$manifestFile = 'manifest.json';
|
|
}
|
|
|
|
return $manifestFile;
|
|
}
|
|
|
|
/**
|
|
* @param Context $context
|
|
* @return string Name of the current theme
|
|
*/
|
|
private function getTheme( Context $context ): string {
|
|
return $this->themeMap[ $context->getSkin() ] ?? $this->themeMap[ 'default' ];
|
|
}
|
|
|
|
/**
|
|
* There are several different use-cases for CodexModule. We may be dealing
|
|
* with:
|
|
*
|
|
* - A CSS-only or JS & CSS module for the entire component library
|
|
* - An otherwise standard module that needs one or more Codex icons
|
|
* - A CSS-only or CSS-and-JS module that has opted-in to Codex's
|
|
* tree-shaking feature by specifying the "codexComponents" option
|
|
*
|
|
* Regardless of the kind of CodexModule we are dealing with, some kind of
|
|
* one-time setup operation may need to be performed.
|
|
*
|
|
* In the case of a full-library module, we need to ensure that the correct
|
|
* theme- and direction-specific CSS file is used.
|
|
*
|
|
* In the case of a tree-shaking module, we need to ensure that the CSS
|
|
* and/or JS files for the specified components (as well as all
|
|
* dependencies) are added to the module's packageFiles.
|
|
*
|
|
* @param Context $context
|
|
*/
|
|
private function setupCodex( Context $context ) {
|
|
if ( $this->setupComplete ) {
|
|
return;
|
|
}
|
|
|
|
// If we are tree-shaking, add component-specific JS/CSS files
|
|
if ( count( $this->codexComponents ) > 0 ) {
|
|
$this->addComponentFiles( $context );
|
|
}
|
|
|
|
// If themestyles are present, add them to the module styles
|
|
if ( $this->hasThemeStyles ) {
|
|
$theme = $this->getTheme( $context );
|
|
$dir = $context->getDirection();
|
|
$styles = $this->themeStyles[ $theme ][ $dir ];
|
|
$this->styles = array_merge( $this->styles, (array)$styles );
|
|
}
|
|
|
|
$this->setupComplete = true;
|
|
}
|
|
|
|
/**
|
|
* Resolve the dependencies for a list of keys in a given manifest,
|
|
* and return flat arrays of both scripts and styles. Dependencies
|
|
* are ordered.
|
|
*
|
|
* @param array<string> $keys
|
|
* @param array<string,array> $manifest
|
|
* @return array<string,array>
|
|
*/
|
|
private function resolveDependencies( $keys, $manifest ): array {
|
|
$resolvedKeys = [];
|
|
$scripts = [];
|
|
$styles = [];
|
|
|
|
$gatherDependencies = static function ( $key ) use ( &$resolvedKeys, $manifest, &$gatherDependencies ) {
|
|
foreach ( $manifest[ $key ][ 'imports' ] ?? [] as $dep ) {
|
|
if ( !in_array( $dep, $resolvedKeys ) ) {
|
|
$gatherDependencies( $dep );
|
|
}
|
|
}
|
|
$resolvedKeys[] = $key;
|
|
};
|
|
|
|
foreach ( $keys as $key ) {
|
|
$gatherDependencies( $key );
|
|
}
|
|
|
|
foreach ( $resolvedKeys as $key ) {
|
|
$scripts[] = $manifest[ $key ][ 'file' ];
|
|
foreach ( $manifest[ $key ][ 'css'] ?? [] as $css ) {
|
|
$styles[] = $css;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'scripts' => $scripts,
|
|
'styles' => $styles
|
|
];
|
|
}
|
|
|
|
/**
|
|
* For Codex modules that rely on tree-shaking, this method determines
|
|
* which CSS and/or JS files need to be included by consulting the
|
|
* appropriate manifest file.
|
|
*
|
|
* @param Context $context
|
|
*/
|
|
private function addComponentFiles( Context $context ) {
|
|
$remoteBasePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
|
|
|
|
// Manifest data structure representing all Codex components in the library
|
|
$manifest = json_decode(
|
|
file_get_contents( MW_INSTALL_PATH . '/' . static::CODEX_MODULE_DIR . $this->getManifestFile( $context ) ),
|
|
true
|
|
);
|
|
|
|
// Generate an array of manifest keys that meet the following conditions:
|
|
// * The "file" property has a ".js" file extension
|
|
// * Entry has a "file" property that matches one of the specified codexComponents (sans extension)
|
|
// * The manifest item is an intentional entry point and not a generated chunk
|
|
$manifestKeys = array_keys( array_filter( $manifest, function ( $val ) {
|
|
$file = pathinfo( $val[ 'file' ] );
|
|
|
|
if (
|
|
!array_key_exists( 'extension', $file ) ||
|
|
$file[ 'extension' ] !== 'js' ||
|
|
!in_array( $file[ 'filename' ], $this->codexComponents )
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if ( !$val[ 'isEntry' ] ) {
|
|
throw new InvalidArgumentException(
|
|
'"' . $file[ 'filename' ] . '"' .
|
|
' is not an export of Codex and cannot be included in the "codexComponents" array.'
|
|
);
|
|
}
|
|
|
|
return true;
|
|
} ) );
|
|
|
|
[ 'scripts' => $scripts, 'styles' => $styles ] = $this->resolveDependencies( $manifestKeys, $manifest );
|
|
|
|
// Add the CSS files to the module's package file (unless this is a script-only module)
|
|
if ( !( $this->isScriptOnly ) ) {
|
|
foreach ( $styles as $fileName ) {
|
|
$this->styles[] = new FilePath( static::CODEX_MODULE_DIR .
|
|
$fileName, MW_INSTALL_PATH, $remoteBasePath );
|
|
}
|
|
}
|
|
|
|
// Add the JS files to the module's package file (unless this is a style-only module)
|
|
if ( !( $this->isStyleOnly ) ) {
|
|
$exports = [];
|
|
foreach ( $this->codexComponents as $component ) {
|
|
$exports[ $component ] = new HtmlJsCode(
|
|
'require( ' . Html::encodeJsVar( "./_codex/$component.js" ) . ' )'
|
|
);
|
|
}
|
|
|
|
// Add a synthetic top-level "exports" file
|
|
$syntheticExports = Html::encodeJsVar( HtmlJsCode::encodeObject( $exports ) );
|
|
|
|
// Proxy the synthetic exports object so that we can throw a useful error if a component
|
|
// is not defined in the module definition
|
|
$proxiedSyntheticExports = <<<JAVASCRIPT
|
|
module.exports = new Proxy( $syntheticExports, {
|
|
get( target, prop ) {
|
|
if ( !(prop in target) ) {
|
|
throw new Error(
|
|
`Codex component "\${prop}" ` +
|
|
'is not listed in the "codexComponents" array ' +
|
|
'of the "{$this->getName()}" module in your module definition file'
|
|
);
|
|
}
|
|
|
|
return target [ prop ];
|
|
}
|
|
} );
|
|
JAVASCRIPT;
|
|
|
|
$this->packageFiles[] = [
|
|
'name' => 'codex.js',
|
|
'content' => $proxiedSyntheticExports
|
|
];
|
|
|
|
// Add each of the referenced scripts to the package
|
|
foreach ( $scripts as $fileName ) {
|
|
$this->packageFiles[] = [
|
|
'name' => "_codex/$fileName",
|
|
'file' => new FilePath( static::CODEX_MODULE_DIR . $fileName, MW_INSTALL_PATH, $remoteBasePath )
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @deprecated since 1.39 */
|
|
class_alias( CodexModule::class, 'ResourceLoaderCodexModule' );
|