41 private const CODEX_MODULE_DEPENDENCIES = [
'vue' ];
44 private static ?array $themeMap =
null;
59 private static array $codexFilesCache = [];
67 private array $codexComponents = [];
69 private bool $isStyleOnly =
false;
70 private bool $isScriptOnly =
false;
71 private bool $codexFullLibrary =
false;
72 private bool $setupComplete =
false;
84 if ( isset( $options[
'codexComponents' ] ) ) {
85 if ( !is_array( $options[
'codexComponents' ] ) || count( $options[
'codexComponents' ] ) === 0 ) {
86 throw new InvalidArgumentException(
87 "All 'codexComponents' properties in your module definition file " .
88 'must either be omitted or be an array with at least one component name'
92 $this->codexComponents = $options[
'codexComponents' ];
95 if ( isset( $options[
'codexFullLibrary'] ) ) {
96 if ( isset( $options[
'codexComponents' ] ) ) {
97 throw new InvalidArgumentException(
98 'ResourceLoader modules using the CodexModule class cannot ' .
99 "use both 'codexFullLibrary' and 'codexComponents' options. " .
100 "Instead, use 'codexFullLibrary' to load the entire library" .
101 "or 'codexComponents' to load a subset of components."
105 $this->codexFullLibrary = $options[
'codexFullLibrary' ];
108 if ( isset( $options[
'codexStyleOnly' ] ) ) {
109 $this->isStyleOnly = $options[
'codexStyleOnly' ];
112 if ( isset( $options[
'codexScriptOnly' ] ) ) {
113 $this->isScriptOnly = $options[
'codexScriptOnly' ];
118 $options[
'dependencies' ] = (array)( $options[
'dependencies' ] ?? [] );
123 if ( !$this->isStyleOnly && count( $this->codexComponents ) > 0 ) {
124 $options[
'dependencies' ] = array_unique( array_merge(
125 $options[
'dependencies' ],
126 self::CODEX_MODULE_DEPENDENCIES
130 if ( in_array(
'@wikimedia/codex', $options[
'dependencies' ] ) ) {
131 throw new InvalidArgumentException(
132 'ResourceLoader modules using the CodexModule class cannot ' .
133 "list the '@wikimedia/codex' module as a dependency. " .
134 "Instead, use 'codexComponents' to require a subset of components."
164 static $allIcons =
null;
165 if ( $allIcons ===
null ) {
166 $allIcons = json_decode( file_get_contents(
"$IP/resources/lib/codex-icons/codex-icons.json" ),
true );
168 return array_intersect_key( $allIcons, array_flip( $iconNames ) );
175 $this->setupCodex( $context );
176 return parent::getPackageFiles( $context );
180 $this->setupCodex( $context );
181 return parent::getStyleFiles( $context );
186 if ( str_starts_with( $pathAsString, static::CODEX_LIBRARY_DIR ) ) {
196 return parent::processStyle( $style, $styleLang,
$path, $context );
200 $this->setupCodex( $context );
201 return parent::getDefinitionSummary( $context );
219 private function getTheme(
Context $context ): string {
220 if ( self::$themeMap === null ) {
222 $skinCodexThemes = ExtensionRegistry::getInstance()->getAttribute(
'SkinCodexThemes' );
223 self::$themeMap = [
'default' =>
'wikimedia-ui' ] + $skinCodexThemes;
226 return self::$themeMap[ $context->
getSkin() ] ?? self::$themeMap[
'default' ];
235 private function getManifestFilePath( Context $context ): string {
236 $themeManifestNames = [
238 'ltr' =>
'manifest.json',
239 'rtl' =>
'manifest-rtl.json',
241 'wikimedia-ui-legacy' => [
242 'ltr' =>
'manifest.json',
243 'rtl' =>
'manifest-rtl.json',
246 'ltr' =>
'manifest.json',
247 'rtl' =>
'manifest-rtl.json',
251 $theme = $this->getTheme( $context );
252 $direction = $context->getDirection();
253 if ( !isset( $themeManifestNames[ $theme ] ) ) {
254 throw new InvalidArgumentException(
"Unknown Codex theme $theme" );
256 $manifestFile = $themeManifestNames[ $theme ][ $direction ];
257 $manifestFilePath = MW_INSTALL_PATH .
'/' . static::CODEX_LIBRARY_DIR .
'modules/' . $manifestFile;
258 return $manifestFilePath;
288 private function getCodexFiles( Context $context ): array {
289 $manifestFilePath = $this->getManifestFilePath( $context );
291 if ( isset( self::$codexFilesCache[ $manifestFilePath ] ) ) {
292 return self::$codexFilesCache[ $manifestFilePath ];
295 $manifest = json_decode( file_get_contents( $manifestFilePath ),
true );
298 foreach ( $manifest as $key => $val ) {
299 $files[ $val[
'file' ] ] = [
300 'styles' => $val[
'css' ] ?? [],
302 'dependencies' => array_map(
static function ( $manifestKey ) use ( $manifest ) {
304 return $manifest[ $manifestKey ][
'file' ];
305 }, $val[
'imports' ] ?? [] )
308 $isComponent = isset( $val[
'isEntry' ] ) && $val[
'isEntry' ];
309 if ( $isComponent ) {
310 $fileInfo = pathinfo( $val[
'file' ] );
313 $components[ $fileInfo[
'filename' ] ] = $val[
'file' ];
317 self::$codexFilesCache[ $manifestFilePath ] = [
'files' => $files,
'components' => $components ];
318 return self::$codexFilesCache[ $manifestFilePath ];
342 private function setupCodex( Context $context ) {
343 if ( $this->setupComplete ) {
348 if ( count( $this->codexComponents ) > 0 ) {
349 $this->addComponentFiles( $context );
353 if ( $this->codexFullLibrary ) {
354 $this->loadFullCodexLibrary( $context );
358 $this->setupComplete =
true;
375 private function resolveDependencies( array $requestedFiles, array $codexFiles ) {
379 $gatherDependencies =
static function ( $file ) use ( &$scripts, &$styles, $codexFiles, &$gatherDependencies ) {
380 foreach ( $codexFiles[
'files' ][ $file ][
'dependencies' ] as $dep ) {
381 if ( !in_array( $dep, $scripts ) ) {
382 $gatherDependencies( $dep );
386 $styles = array_merge( $styles, $codexFiles[
'files' ][ $file ][
'styles' ] );
389 foreach ( $requestedFiles as $requestedFile ) {
390 $gatherDependencies( $requestedFile );
393 return [
'scripts' => $scripts,
'styles' => $styles ];
403 private function addComponentFiles( Context $context ) {
404 $remoteBasePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
405 $codexFiles = $this->getCodexFiles( $context );
407 $requestedFiles = array_map(
static function ( $component ) use ( $codexFiles ) {
408 if ( !isset( $codexFiles[
'components' ][ $component ] ) ) {
409 throw new InvalidArgumentException(
410 "\"$component\" is not an export of Codex and cannot be included in the \"codexComponents\" array."
413 return $codexFiles[
'components' ][ $component ];
414 }, $this->codexComponents );
416 [
'scripts' => $scripts,
'styles' => $styles ] = $this->resolveDependencies( $requestedFiles, $codexFiles );
419 if ( !$this->isScriptOnly ) {
420 foreach ( $styles as $fileName ) {
421 $this->styles[] =
new FilePath( static::CODEX_LIBRARY_DIR .
'modules/' .
422 $fileName, MW_INSTALL_PATH, $remoteBasePath );
427 if ( !$this->isStyleOnly ) {
429 foreach ( $this->codexComponents as $component ) {
430 $componentFile = $codexFiles[
'components' ][ $component ];
431 $exports[ $component ] =
new HtmlJsCode(
432 'require( ' . Html::encodeJsVar(
"./_codex/$componentFile" ) .
' )'
437 $syntheticExports = Html::encodeJsVar( HtmlJsCode::encodeObject( $exports ) );
441 $proxiedSyntheticExports = <<<JAVASCRIPT
442 module.exports =
new Proxy( $syntheticExports, {
443 get( target, prop ) {
444 if ( !( prop in target ) ) {
446 `Codex component
"\${prop}" ` +
447 'is not listed in the "codexComponents" array ' +
448 'of the "{$this->getName()}" module in your module definition file'
451 return target[ prop ];
456 $this->packageFiles[] = [
457 'name' =>
'codex.js',
458 'content' => $proxiedSyntheticExports
462 foreach ( $scripts as $fileName ) {
463 $this->packageFiles[] = [
464 'name' =>
"_codex/$fileName",
465 'file' =>
new FilePath(
466 static::CODEX_LIBRARY_DIR .
'modules/' . $fileName,
467 MW_INSTALL_PATH, $remoteBasePath
479 private function loadFullCodexLibrary( Context $context ) {
480 $remoteBasePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
483 if ( !$this->isStyleOnly ) {
484 $this->packageFiles[] = [
485 'name' =>
'codex.js',
486 'file' =>
new FilePath(
487 static::CODEX_LIBRARY_DIR .
'codex.umd.cjs',
488 MW_INSTALL_PATH, $remoteBasePath
494 if ( !$this->isScriptOnly ) {
498 'ltr' =>
'codex.style.css',
499 'rtl' =>
'codex.style-rtl.css'
501 'wikimedia-ui-legacy' => [
502 'ltr' =>
'codex.style.css',
503 'rtl' =>
'codex.style-rtl.css'
506 'ltr' =>
'codex.style.css',
507 'rtl' =>
'codex.style-rtl.css'
511 $theme = $this->getTheme( $context );
512 $direction = $context->getDirection();
513 $styleFile = $themeStyles[ $theme ][ $direction ];
514 $this->styles[] =
new FilePath(
515 static::CODEX_LIBRARY_DIR . $styleFile,