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 );
185 $this->setupCodex( $context );
186 return parent::getDefinitionSummary( $context );
210 private function getTheme(
Context $context ): string {
211 if ( self::$themeMap === null ) {
213 $skinCodexThemes = ExtensionRegistry::getInstance()->getAttribute(
'SkinCodexThemes' );
214 self::$themeMap = [
'default' =>
'wikimedia-ui' ] + $skinCodexThemes;
217 return self::$themeMap[ $context->
getSkin() ] ?? self::$themeMap[
'default' ];
226 private function getManifestFilePath( Context $context ): string {
227 $themeManifestNames = [
229 'ltr' =>
'manifest.json',
230 'rtl' =>
'manifest-rtl.json',
232 'wikimedia-ui-legacy' => [
233 'ltr' =>
'manifest-legacy.json',
234 'rtl' =>
'manifest-legacy-rtl.json',
237 'ltr' =>
'manifest-experimental.json',
238 'rtl' =>
'manifest-experimental-rtl.json',
242 $theme = $this->getTheme( $context );
243 $direction = $context->getDirection();
244 if ( !isset( $themeManifestNames[ $theme ] ) ) {
245 throw new InvalidArgumentException(
"Unknown Codex theme $theme" );
247 $manifestFile = $themeManifestNames[ $theme ][ $direction ];
248 $manifestFilePath = MW_INSTALL_PATH .
'/' . static::CODEX_MODULE_DIR . $manifestFile;
249 return $manifestFilePath;
279 private function getCodexFiles( Context $context ): array {
280 $manifestFilePath = $this->getManifestFilePath( $context );
282 if ( isset( self::$codexFilesCache[ $manifestFilePath ] ) ) {
283 return self::$codexFilesCache[ $manifestFilePath ];
286 $manifest = json_decode( file_get_contents( $manifestFilePath ),
true );
289 foreach ( $manifest as $key => $val ) {
290 $files[ $val[
'file' ] ] = [
291 'styles' => $val[
'css' ] ?? [],
293 'dependencies' => array_map(
static function ( $manifestKey ) use ( $manifest ) {
295 return $manifest[ $manifestKey ][
'file' ];
296 }, $val[
'imports' ] ?? [] )
299 $isComponent = isset( $val[
'isEntry' ] ) && $val[
'isEntry' ];
300 if ( $isComponent ) {
301 $fileInfo = pathinfo( $val[
'file' ] );
304 $components[ $fileInfo[
'filename' ] ] = $val[
'file' ];
308 self::$codexFilesCache[ $manifestFilePath ] = [
'files' => $files,
'components' => $components ];
309 return self::$codexFilesCache[ $manifestFilePath ];
333 private function setupCodex( Context $context ) {
334 if ( $this->setupComplete ) {
339 if ( count( $this->codexComponents ) > 0 ) {
340 $this->addComponentFiles( $context );
344 if ( $this->codexFullLibrary ) {
345 $this->loadFullCodexLibrary( $context );
349 $this->setupComplete =
true;
366 private function resolveDependencies( array $requestedFiles, array $codexFiles ) {
370 $gatherDependencies =
static function ( $file ) use ( &$scripts, &$styles, $codexFiles, &$gatherDependencies ) {
371 foreach ( $codexFiles[
'files' ][ $file ][
'dependencies' ] as $dep ) {
372 if ( !in_array( $dep, $scripts ) ) {
373 $gatherDependencies( $dep );
377 $styles = array_merge( $styles, $codexFiles[
'files' ][ $file ][
'styles' ] );
380 foreach ( $requestedFiles as $requestedFile ) {
381 $gatherDependencies( $requestedFile );
384 return [
'scripts' => $scripts,
'styles' => $styles ];
394 private function addComponentFiles( Context $context ) {
395 $remoteBasePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
396 $codexFiles = $this->getCodexFiles( $context );
398 $requestedFiles = array_map(
static function ( $component ) use ( $codexFiles ) {
399 if ( !isset( $codexFiles[
'components' ][ $component ] ) ) {
400 throw new InvalidArgumentException(
401 "\"$component\" is not an export of Codex and cannot be included in the \"codexComponents\" array."
404 return $codexFiles[
'components' ][ $component ];
405 }, $this->codexComponents );
407 [
'scripts' => $scripts,
'styles' => $styles ] = $this->resolveDependencies( $requestedFiles, $codexFiles );
410 if ( !$this->isScriptOnly ) {
411 foreach ( $styles as $fileName ) {
412 $this->styles[] =
new FilePath( static::CODEX_MODULE_DIR .
413 $fileName, MW_INSTALL_PATH, $remoteBasePath );
418 if ( !$this->isStyleOnly ) {
420 foreach ( $this->codexComponents as $component ) {
421 $componentFile = $codexFiles[
'components' ][ $component ];
422 $exports[ $component ] =
new HtmlJsCode(
423 'require( ' . Html::encodeJsVar(
"./_codex/$componentFile" ) .
' )'
428 $syntheticExports = Html::encodeJsVar( HtmlJsCode::encodeObject( $exports ) );
432 $proxiedSyntheticExports = <<<JAVASCRIPT
433 module.exports =
new Proxy( $syntheticExports, {
434 get( target, prop ) {
435 if ( !( prop in target ) ) {
437 `Codex component
"\${prop}" ` +
438 'is not listed in the "codexComponents" array ' +
439 'of the "{$this->getName()}" module in your module definition file'
442 return target[ prop ];
447 $this->packageFiles[] = [
448 'name' =>
'codex.js',
449 'content' => $proxiedSyntheticExports
453 foreach ( $scripts as $fileName ) {
454 $this->packageFiles[] = [
455 'name' =>
"_codex/$fileName",
456 'file' =>
new FilePath( static::CODEX_MODULE_DIR . $fileName, MW_INSTALL_PATH, $remoteBasePath )
467 private function loadFullCodexLibrary( Context $context ) {
468 $remoteBasePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
471 if ( !$this->isStyleOnly ) {
472 $this->packageFiles[] = [
473 'name' =>
'codex.js',
474 'file' =>
new FilePath(
'resources/lib/codex/codex.umd.cjs', MW_INSTALL_PATH, $remoteBasePath )
479 if ( !$this->isScriptOnly ) {
483 'ltr' =>
'resources/lib/codex/codex.style.css',
484 'rtl' =>
'resources/lib/codex/codex.style-rtl.css'
486 'wikimedia-ui-legacy' => [
487 'ltr' =>
'resources/lib/codex/codex.style-legacy.css',
488 'rtl' =>
'resources/lib/codex/codex.style-legacy-rtl.css'
491 'ltr' =>
'resources/lib/codex/codex.style-experimental.css',
492 'rtl' =>
'resources/lib/codex/codex.style-experimental-rtl.css'
496 $theme = $this->getTheme( $context );
497 $direction = $context->getDirection();
498 $styleFile = $themeStyles[ $theme ][ $direction ];
499 $this->styles[] =
new FilePath(