Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.21% covered (success)
90.21%
212 / 235
57.14% covered (warning)
57.14%
12 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodexModule
90.21% covered (success)
90.21%
212 / 235
57.14% covered (warning)
57.14%
12 / 21
71.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
81.82% covered (warning)
81.82%
27 / 33
0.00% covered (danger)
0.00%
0 / 1
11.73
 getIcons
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 getIconFilePath
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getMessages
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getPackageFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getStyleFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 processStyle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getDefinitionSummary
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 supportsURLLoading
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTheme
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 makeFilePath
78.57% covered (warning)
78.57%
22 / 28
0.00% covered (danger)
0.00%
0 / 1
5.25
 getCodexDirectory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isDevelopmentMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDevelopmentWarning
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getManifestFilePath
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 getCodexFiles
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 setupCodex
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 resolveDependencies
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 addComponentFiles
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
7
 loadFullCodexLibrary
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
4
 getType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\ResourceLoader;
8
9use InvalidArgumentException;
10use MediaWiki\Config\Config;
11use MediaWiki\Html\Html;
12use MediaWiki\Html\HtmlJsCode;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Registration\ExtensionRegistry;
15use RuntimeException;
16
17/**
18 * Module for codex that has direction-specific style files and a static helper
19 * function for embedding icons in package modules. This module also contains
20 * logic to support code-splitting (aka tree-shaking) of the Codex library to
21 * return only a subset of component JS and/or CSS files.
22 *
23 * @ingroup ResourceLoader
24 * @internal
25 */
26class CodexModule extends FileModule {
27    protected const CODEX_DEFAULT_LIBRARY_DIR = 'resources/lib/codex';
28    private const CODEX_MODULE_DEPENDENCIES = [ 'vue' ];
29
30    /** @var array<string,string>|null */
31    private static ?array $themeMap = null;
32
33    /**
34     * Cache for getCodexFiles(). Maps manifest file paths to outputs. For more information about
35     * the structure of the outputs, see the documentation comment for getCodexFiles().
36     *
37     * This array looks like:
38     *     [
39     *         '/path/to/manifest.json' => [ 'files' => [ ... ], 'components' => [ ... ] ],
40     *         '/path/to/manifest-rtl.json' => [ 'files' => [ ... ], 'components' => [ ... ] ],
41     *         ...
42     *     ]
43     *
44     * @var array<string,array{files:array<string,array{styles:string[],dependencies:string[]}>,components:array<string,string>}>
45     */
46    private static array $codexFilesCache = [];
47
48    /**
49     * Names of the requested components. Comes directly from the 'codexComponents' property in the
50     * module definition.
51     *
52     * @var string[]
53     */
54    private array $codexComponents = [];
55
56    private bool $isStyleOnly = false;
57    private bool $isScriptOnly = false;
58    private bool $codexFullLibrary = false;
59    private bool $setupComplete = false;
60
61    /**
62     * @param array $options [optional]
63     *  - codexComponents: array of Codex components to include
64     *  - codexFullLibrary: whether to load the entire Codex library
65     *  - codexStyleOnly: whether to include only style files
66     *  - codexScriptOnly: whether to include only script files
67     * @param string|null $localBasePath [optional]
68     * @param string|null $remoteBasePath [optional]
69     */
70    public function __construct( array $options = [], $localBasePath = null, $remoteBasePath = null ) {
71        if ( isset( $options[ 'codexComponents' ] ) ) {
72            if ( !is_array( $options[ 'codexComponents' ] ) || count( $options[ 'codexComponents' ] ) === 0 ) {
73                throw new InvalidArgumentException(
74                    "All 'codexComponents' properties in your module definition file " .
75                    'must either be omitted or be an array with at least one component name'
76                );
77            }
78
79            $this->codexComponents = $options[ 'codexComponents' ];
80        }
81
82        if ( isset( $options['codexFullLibrary'] ) ) {
83            if ( isset( $options[ 'codexComponents' ] ) ) {
84                throw new InvalidArgumentException(
85                    'ResourceLoader modules using the CodexModule class cannot ' .
86                    "use both 'codexFullLibrary' and 'codexComponents' options. " .
87                    "Instead, use 'codexFullLibrary' to load the entire library" .
88                    "or 'codexComponents' to load a subset of components."
89                );
90            }
91
92            $this->codexFullLibrary = $options[ 'codexFullLibrary' ];
93        }
94
95        if ( isset( $options[ 'codexStyleOnly' ] ) ) {
96            $this->isStyleOnly = $options[ 'codexStyleOnly' ];
97        }
98
99        if ( isset( $options[ 'codexScriptOnly' ] ) ) {
100            $this->isScriptOnly = $options[ 'codexScriptOnly' ];
101        }
102
103        // Normalize $options[ 'dependencies' ] to always make it an array. It could be unset, or
104        // it could be a string.
105        $options[ 'dependencies' ] = (array)( $options[ 'dependencies' ] ?? [] );
106
107        // Unlike when the entire @wikimedia/codex module is depended on, when the codexComponents
108        // option is used, Vue is not automatically included, though it is required. Add it and
109        // other dependencies here.
110        if ( !$this->isStyleOnly && count( $this->codexComponents ) > 0 ) {
111            $options[ 'dependencies' ] = array_unique( array_merge(
112                $options[ 'dependencies' ],
113                self::CODEX_MODULE_DEPENDENCIES
114            ) );
115        }
116
117        if ( in_array( '@wikimedia/codex', $options[ 'dependencies' ] ) ) {
118            throw new InvalidArgumentException(
119                'ResourceLoader modules using the CodexModule class cannot ' .
120                "list the '@wikimedia/codex' module as a dependency. " .
121                "Instead, use 'codexComponents' to require a subset of components."
122            );
123        }
124
125        parent::__construct( $options, $localBasePath, $remoteBasePath );
126    }
127
128    /**
129     * Retrieve the specified icon definitions from codex-icons.json. Intended as a convenience
130     * function to be used in packageFiles definitions.
131     *
132     * Example:
133     *     "packageFiles": [
134     *         {
135     *             "name": "icons.json",
136     *             "callback": "MediaWiki\\ResourceLoader\\CodexModule::getIcons",
137     *             "callbackParam": [
138     *                 "cdxIconClear",
139     *                 "cdxIconTrash"
140     *             ]
141     *         }
142     *     ]
143     *
144     * @param Context|null $context
145     * @param Config $config
146     * @param string[]|null $iconNames Names of icons to fetch, or null to fetch all icons
147     * @return array
148     */
149    public static function getIcons( ?Context $context, Config $config, ?array $iconNames = null ): array {
150        static $cachedIcons = null;
151        static $cachedIconFilePath = null;
152
153        $iconFile = self::getIconFilePath( $config );
154        if ( $cachedIcons === null || $cachedIconFilePath !== $iconFile ) {
155            $cachedIcons = json_decode( file_get_contents( $iconFile ), true );
156            $cachedIconFilePath = $iconFile;
157        }
158
159        if ( $iconNames === null ) {
160            return $cachedIcons;
161        }
162        return array_intersect_key( $cachedIcons, array_flip( $iconNames ) );
163    }
164
165    private static function getIconFilePath( Config $config ): string {
166        $devDir = $config->get( MainConfigNames::CodexDevelopmentDir );
167        $iconsDir = $devDir !== null ?
168            "$devDir/packages/codex-icons/dist" :
169            MW_INSTALL_PATH . '/resources/lib/codex-icons';
170        return "$iconsDir/codex-icons.json";
171    }
172
173    /** @inheritDoc */
174    public function getMessages() {
175        $messages = parent::getMessages();
176
177        // Add messages used inside Codex Vue components. The message keys are defined in the
178        // "messageKeys.json" file from the Codex package
179        if ( $this->codexFullLibrary || ( !$this->isStyleOnly && count( $this->codexComponents ) > 0 ) ) {
180            $messageKeyFilePath = $this->makeFilePath( 'messageKeys.json' )->getLocalPath();
181            $messageKeys = json_decode( file_get_contents( $messageKeyFilePath ), true );
182            $messages = array_merge( $messages, $messageKeys );
183        }
184
185        return $messages;
186    }
187
188    // These 3 public methods are the entry points to this class; depending on the
189    // circumstances any one of these might be called first.
190
191    /** @inheritDoc */
192    public function getPackageFiles( Context $context ) {
193        $this->setupCodex( $context );
194        return parent::getPackageFiles( $context );
195    }
196
197    /** @inheritDoc */
198    public function getStyleFiles( Context $context ) {
199        $this->setupCodex( $context );
200        return parent::getStyleFiles( $context );
201    }
202
203    /** @inheritDoc */
204    protected function processStyle( $style, $styleLang, $path, Context $context ) {
205        $pathAsString = $path instanceof FilePath ? $path->getLocalPath() : $path;
206        if ( str_starts_with( $pathAsString, $this->getCodexDirectory() ) ) {
207            // This is a Codex style file, don't do any processing.
208            // We need to avoid CSSJanus flipping in particular, because we're using RTL-specific
209            // files instead. Note that we're bypassing all of processStyle() when we really just
210            // care about bypassing flipping; that's fine for now, but could be a problem if
211            // processStyle() is ever expanded to do more than Less compilation, RTL flipping and
212            // image URL remapping.
213            return $style;
214        }
215
216        return parent::processStyle( $style, $styleLang, $path, $context );
217    }
218
219    /** @inheritDoc */
220    public function getDefinitionSummary( Context $context ) {
221        $this->setupCodex( $context );
222        return parent::getDefinitionSummary( $context );
223    }
224
225    /** @inheritDoc */
226    public function supportsURLLoading() {
227        // We need to override this explicitly. The parent method might return true if there are
228        // no 'packageFiles' set in the module definition and they're all generated by us.
229        // It's possible that this "should" return true in some circumstances (e.g. style-only use
230        // of CodexModule combined with non-packageFiles scripts), but those are edge cases that
231        // we're choosing not to support here.
232        return false;
233    }
234
235    /**
236     * Get the theme to use based on the current skin.
237     *
238     * @param Context $context
239     * @return string Name of the current theme
240     */
241    private function getTheme( Context $context ): string {
242        if ( self::$themeMap === null ) {
243            // Initialize self::$themeMap
244            $skinCodexThemes = ExtensionRegistry::getInstance()->getAttribute( 'SkinCodexThemes' );
245            self::$themeMap = [ 'default' => 'wikimedia-ui' ] + $skinCodexThemes;
246        }
247        return self::$themeMap[ $context->getSkin() ] ?? self::$themeMap[ 'default' ];
248    }
249
250    /**
251     * Build a FilePath object representing the path to a Codex file.
252     *
253     * If $wgCodexDevelopmentDir is set, the returned FilePath object points to
254     * $wgCodexDevelopmentDir/$file. Otherwise, it points to resources/lib/codex/$file.
255     *
256     * @param string $file File name
257     */
258    private function makeFilePath( string $file ): FilePath {
259        $remoteBasePath = $this->getConfig()->get( MainConfigNames::ResourceBasePath );
260        $devDir = $this->getConfig()->get( MainConfigNames::CodexDevelopmentDir );
261        if ( $devDir === null ) {
262            $filePath = new FilePath(
263                static::CODEX_DEFAULT_LIBRARY_DIR . '/' . $file,
264                MW_INSTALL_PATH,
265                $remoteBasePath
266            );
267            if ( !file_exists( $filePath->getLocalPath() ) ) {
268                throw new RuntimeException( "Could not find Codex file `{$filePath->getLocalPath()}`" );
269            }
270            return $filePath;
271        }
272
273        $modulesDir = $devDir . '/packages/codex/dist/modules';
274        if ( !file_exists( $modulesDir ) ) {
275            throw new RuntimeException(
276                "Could not find Codex development build, `$modulesDir` does not exist. " .
277                "You may need to run `npm run build-all` in the `$devDir` directory, " .
278                "or disable Codex development mode by setting \$wgCodexDevelopmentDir = null;"
279            );
280        }
281        $path = $devDir . '/packages/codex/dist/' . $file;
282        if ( !file_exists( $path ) ) {
283            throw new RuntimeException(
284                "Could not find Codex file `$path`. " .
285                "You may need to run `npm run build-all` in the `$devDir` directory, " .
286                "or you may be using a version of Codex that is too old for this version of MediaWiki."
287            );
288        }
289
290        // Return a modified FilePath object that bypasses LocalBasePath
291        // HACK: We do have to set LocalBasePath to a non-null value, otherwise
292        // FileModule::getLocalPath() will still prepend its own base path
293        return new class ( $path, '' ) extends FilePath {
294            public function getLocalPath(): string {
295                return $this->getPath();
296            }
297        };
298    }
299
300    /**
301     * Get the path to the directory that contains the Codex files.
302     *
303     * In production mode this is resources/lib/codex, but in development mode it can be
304     * somewhere else.
305     *
306     * @return string
307     */
308    private function getCodexDirectory() {
309        // Reuse the logic in makeFilePath by passing in an empty path
310        return $this->makeFilePath( '' )->getLocalPath();
311    }
312
313    private function isDevelopmentMode(): bool {
314        return $this->getConfig()->get( MainConfigNames::CodexDevelopmentDir ) !== null;
315    }
316
317    private function getDevelopmentWarning(): string {
318        return $this->isDevelopmentMode() ?
319            Html::encodeJsCall( 'mw.log.warn', [
320                "You are using a local development version of Codex, which may not match the latest version. " .
321                "To disable this, set \$wgCodexDevelopmentDir = null;"
322            ] ) :
323            '';
324    }
325
326    /**
327     * Decide which manifest file to use, based on the theme and the direction (LTR or RTL).
328     *
329     * @param Context $context
330     * @return string Name of the manifest file to use
331     */
332    private function getManifestFilePath( Context $context ): string {
333        $themeManifestNames = [
334            'wikimedia-ui' => [
335                'ltr' => 'manifest.json',
336                'rtl' => 'manifest-rtl.json',
337            ],
338            'wikimedia-ui-legacy' => [
339                'ltr' => 'manifest.json',
340                'rtl' => 'manifest-rtl.json',
341            ],
342            'experimental' => [
343                'ltr' => 'manifest.json',
344                'rtl' => 'manifest-rtl.json',
345            ]
346        ];
347
348        $theme = $this->getTheme( $context );
349        $direction = $context->getDirection();
350        if ( !isset( $themeManifestNames[ $theme ] ) ) {
351            throw new InvalidArgumentException( "Unknown Codex theme $theme" );
352        }
353        $manifestFile = $themeManifestNames[ $theme ][ $direction ];
354        $manifestFilePath = $this->makeFilePath( "modules/$manifestFile" );
355        return $manifestFilePath->getLocalPath();
356    }
357
358    /**
359     * Get information about all available Codex files.
360     *
361     * This transforms the Codex manifest to a more useful format, and caches it so that different
362     * instances of this class don't each parse the manifest separately.
363     *
364     * The returned data structure contains a 'files' key with dependency information about every
365     * file (both entry point and non-entry point files), and a 'component' key that is an array
366     * mapping component names to file names. The full data structure looks like this:
367     *
368     *     [
369     *         'files' => [
370     *             'CdxButtonGroup.js' => [
371     *                 'styles' => [ 'CdxButtonGroup.css' ],
372     *                 'dependencies' => [ 'CdxButton.js', 'buttonHelpers.js' ]
373     *             ],
374     *             // all files are listed here, both entry point and non-entry point files
375     *         ],
376     *         'components' => [
377     *             'CdxButtonGroup' => 'CdxButtonGroup.js',
378     *             // only entry point files are listed here
379     *         ]
380     *     ]
381     *
382     * @param Context $context
383     * @return array{files:array<string,array{styles:string[],dependencies:string[]}>,components:array<string,string>}
384     */
385    private function getCodexFiles( Context $context ): array {
386        $manifestFilePath = $this->getManifestFilePath( $context );
387
388        if ( isset( self::$codexFilesCache[ $manifestFilePath ] ) ) {
389            return self::$codexFilesCache[ $manifestFilePath ];
390        }
391
392        $manifest = json_decode( file_get_contents( $manifestFilePath ), true );
393        $files = [];
394        $components = [];
395        foreach ( $manifest as $key => $val ) {
396            $files[ $val[ 'file' ] ] = [
397                'styles' => $val[ 'css' ] ?? [],
398                // $val['imports'] is expressed as manifest keys, transform those to file names
399                'dependencies' => array_map( static function ( $manifestKey ) use ( $manifest ) {
400                    // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
401                    return $manifest[ $manifestKey ][ 'file' ];
402                }, $val[ 'imports' ] ?? [] )
403            ];
404
405            $isComponent = isset( $val[ 'isEntry' ] ) && $val[ 'isEntry' ];
406            if ( $isComponent ) {
407                $fileInfo = pathinfo( $val[ 'file' ] );
408                // $fileInfo[ 'filename' ] is the file name without the extension.
409                // This is the component name (e.g. CdxButton.cjs -> CdxButton).
410                $components[ $fileInfo[ 'filename' ] ] = $val[ 'file' ];
411            }
412        }
413
414        self::$codexFilesCache[ $manifestFilePath ] = [ 'files' => $files, 'components' => $components ];
415        return self::$codexFilesCache[ $manifestFilePath ];
416    }
417
418    /**
419     * There are several different use-cases for CodexModule. We may be dealing
420     * with:
421     *
422     * - A CSS-only or JS & CSS module for the entire component library
423     * - An otherwise standard module that needs one or more Codex icons
424     * - A CSS-only or CSS-and-JS module that has opted-in to Codex's
425     *   tree-shaking feature by specifying the "codexComponents" option
426     *
427     * Regardless of the kind of CodexModule we are dealing with, some kind of
428     * one-time setup operation may need to be performed.
429     *
430     * In the case of a full-library module, we need to ensure that the correct
431     * theme- and direction-specific CSS file is used.
432     *
433     * In the case of a tree-shaking module, we need to ensure that the CSS
434     * and/or JS files for the specified components (as well as all
435     * dependencies) are added to the module's packageFiles.
436     *
437     * @param Context $context
438     */
439    private function setupCodex( Context $context ) {
440        if ( $this->setupComplete ) {
441            return;
442        }
443
444        // If we are tree-shaking, add component-specific JS/CSS files
445        if ( count( $this->codexComponents ) > 0 ) {
446            $this->addComponentFiles( $context );
447        }
448
449        // If we want to load the entire Codex library (no tree shaking)
450        if ( $this->codexFullLibrary ) {
451            $this->loadFullCodexLibrary( $context );
452
453        }
454
455        $this->setupComplete = true;
456    }
457
458    /**
459     * Resolve the dependencies for a list of files, return flat arrays of both scripts and styles.
460     * The returned arrays include the files in $requestedFiles, plus their dependencies, their
461     * dependencies' dependencies, etc. The returned arrays are ordered in the order that the files
462     * should be loaded in; in other words, the files are ordered such that each file appears after
463     * all of its dependencies.
464     *
465     * @param string[] $requestedFiles File names whose dependencies to resolve
466     * @phpcs:ignore Generic.Files.LineLength, MediaWiki.Commenting.FunctionComment.MissingParamName
467     * @param array{files:array<string,array{styles:string[],dependencies:string[]}>,components:array<string,string>} $codexFiles
468     *   Data structure returned by getCodexFiles()
469     * @return array{scripts:string[],styles:string[]} Resolved dependencies, with script and style
470     *   files listed separately.
471     */
472    private function resolveDependencies( array $requestedFiles, array $codexFiles ) {
473        $scripts = [];
474        $styles = [];
475
476        $gatherDependencies = static function ( $file ) use ( &$scripts, &$styles, $codexFiles, &$gatherDependencies ) {
477            foreach ( $codexFiles[ 'files' ][ $file ][ 'dependencies' ] as $dep ) {
478                if ( !in_array( $dep, $scripts ) ) {
479                    $gatherDependencies( $dep );
480                }
481            }
482            $scripts[] = $file;
483            $styles = array_merge( $styles, $codexFiles[ 'files' ][ $file ][ 'styles' ] );
484        };
485
486        foreach ( $requestedFiles as $requestedFile ) {
487            $gatherDependencies( $requestedFile );
488        }
489
490        return [ 'scripts' => $scripts, 'styles' => $styles ];
491    }
492
493    /**
494     * For Codex modules that rely on tree-shaking, this method determines
495     * which CSS and/or JS files need to be included by consulting the
496     * appropriate manifest file.
497     *
498     * @param Context $context
499     */
500    private function addComponentFiles( Context $context ) {
501        $codexFiles = $this->getCodexFiles( $context );
502
503        $requestedFiles = array_map( static function ( $component ) use ( $codexFiles ) {
504            if ( !isset( $codexFiles[ 'components' ][ $component ] ) ) {
505                throw new InvalidArgumentException(
506                    "\"$component\" is not an export of Codex and cannot be included in the \"codexComponents\" array."
507                );
508            }
509            return $codexFiles[ 'components' ][ $component ];
510        }, $this->codexComponents );
511
512        [ 'scripts' => $scripts, 'styles' => $styles ] = $this->resolveDependencies( $requestedFiles, $codexFiles );
513
514        // Add the CSS files to the module's package file (unless this is a script-only module)
515        if ( !$this->isScriptOnly ) {
516            foreach ( $styles as $fileName ) {
517                $this->styles[] = $this->makeFilePath( "modules/$fileName" );
518            }
519        }
520
521        // Add the JS files to the module's package file (unless this is a style-only module).
522        if ( !$this->isStyleOnly ) {
523            // Add a synthetic top-level "exports" file
524            $exports = [];
525            foreach ( $this->codexComponents as $component ) {
526                $componentFile = $codexFiles[ 'components' ][ $component ];
527                $exports[ $component ] = new HtmlJsCode(
528                    'require( ' . Html::encodeJsVar( "./_codex/$componentFile" ) . ' )'
529                );
530            }
531
532            $syntheticExports = Html::encodeJsVar( HtmlJsCode::encodeObject( $exports ) );
533
534            // Add a console warning in development mode
535            $devWarning = $this->getDevelopmentWarning();
536
537            // Proxy the synthetic exports object so that we can throw a useful error if a component
538            // is not defined in the module definition
539            $proxiedSyntheticExports = <<<JAVASCRIPT
540            module.exports = new Proxy( $syntheticExports, {
541                get( target, prop ) {
542                    if ( !( prop in target ) ) {
543                        throw new Error(
544                            `Codex component "\${prop}" ` +
545                            'is not listed in the "codexComponents" array ' +
546                            'of the "{$this->getName()}" module in your module definition file'
547                        );
548                    }
549                    return target[ prop ];
550                }
551            } );
552            $devWarning
553            JAVASCRIPT;
554
555            $this->packageFiles[] = [
556                'name' => 'codex.js',
557                'content' => $proxiedSyntheticExports
558            ];
559
560            // Add each of the referenced scripts to the package
561            foreach ( $scripts as $fileName ) {
562                $this->packageFiles[] = [
563                    'name' => "_codex/$fileName",
564                    'file' => $this->makeFilePath( "modules/$fileName" )
565                ];
566            }
567        }
568    }
569
570    /**
571     * For loading the entire Codex library, rather than a subset module of it.
572     */
573    private function loadFullCodexLibrary( Context $context ) {
574        // Add all Codex JS files to the module's package
575        if ( !$this->isStyleOnly ) {
576            $jsFilePath = $this->makeFilePath( 'codex.umd.cjs' );
577
578            // Add a console warning in development mode
579            $devWarning = $this->getDevelopmentWarning();
580            if ( $devWarning ) {
581                $this->packageFiles[] = [
582                    'name' => 'codex.js',
583                    'callback' => static function () use ( $jsFilePath, $devWarning ) {
584                        return $devWarning . ';' . file_get_contents( $jsFilePath->getLocalPath() );
585                    },
586                    'versionCallback' => static function () use ( $jsFilePath ) {
587                        return $jsFilePath;
588                    }
589                ];
590            } else {
591                $this->packageFiles[] = [
592                    'name' => 'codex.js',
593                    'file' => $jsFilePath
594                ];
595            }
596        }
597
598        // Add all Codex CSS files to the module's package
599        if ( !$this->isScriptOnly ) {
600            // Theme-specific + direction style files
601            $themeStyles = [
602                'wikimedia-ui' => [
603                    'ltr' => 'codex.style.css',
604                    'rtl' => 'codex.style-rtl.css'
605                ],
606                'wikimedia-ui-legacy' => [
607                    'ltr' => 'codex.style.css',
608                    'rtl' => 'codex.style-rtl.css'
609                ],
610                'experimental' => [
611                    'ltr' => 'codex.style.css',
612                    'rtl' => 'codex.style-rtl.css'
613                ]
614            ];
615
616            $theme = $this->getTheme( $context );
617            $direction = $context->getDirection();
618            $styleFile = $themeStyles[ $theme ][ $direction ];
619            $this->styles[] = $this->makeFilePath( $styleFile );
620        }
621    }
622
623    /**
624     * @inheritDoc
625     */
626    public function getType(): string {
627        return $this->isStyleOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
628    }
629}