resourceloader: Support single-file component .vue files
Allows .vue files to be used in package modules as if they were .js files: they can be added to the 'packageFiles' array in module definitions, and require()d from JS files. In the load.php output, each .vue file is transformed to a function that contains the JS from the <script> tag, then a line that sets module.exports.template to the contents of the <template> tag (encoded as a string). The contents of the <style> tag are added to the module's styles. Internally, the type of a .vue file is inferred as 'script-vue', and the file is parsed with VueComponentParser, which extracts the three parts. After the transformation, the file's type is set to 'script+style', and files of this type contribute to both getScript() and getStyles(). This change also adds caching to getPackageFiles(), because it now needs to be called twice (in getScript() and getStyles()). Change-Id: Ic0a5c771901450a518eb7d24456053956507e1ed
This commit is contained in:
parent
1f1cff797a
commit
ca46126e98
9 changed files with 620 additions and 5 deletions
|
|
@ -1526,6 +1526,7 @@ $wgAutoloadLocalClasses = [
|
|||
'ViewCLI' => __DIR__ . '/maintenance/view.php',
|
||||
'VirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTService.php',
|
||||
'VirtualRESTServiceClient' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTServiceClient.php',
|
||||
'VueComponentParser' => __DIR__ . '/includes/resourceloader/VueComponentParser.php',
|
||||
'WANCacheReapUpdate' => __DIR__ . '/includes/deferred/WANCacheReapUpdate.php',
|
||||
'WANObjectCache' => __DIR__ . '/includes/libs/objectcache/wancache/WANObjectCache.php',
|
||||
'WANObjectCacheReaper' => __DIR__ . '/includes/libs/objectcache/wancache/WANObjectCacheReaper.php',
|
||||
|
|
|
|||
|
|
@ -115,6 +115,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
*/
|
||||
private $expandedPackageFiles = [];
|
||||
|
||||
/**
|
||||
* @var array Further expanded versions of $expandedPackageFiles, lazy-computed by
|
||||
* getPackageFiles(); keyed by context hash
|
||||
*/
|
||||
private $fullyExpandedPackageFiles = [];
|
||||
|
||||
/**
|
||||
* @var array List of modules this module depends on
|
||||
* @par Usage:
|
||||
|
|
@ -170,6 +176,11 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
*/
|
||||
protected $missingLocalFileRefs = [];
|
||||
|
||||
/**
|
||||
* @var VueComponentParser Lazy-created by getVueComponentParser()
|
||||
*/
|
||||
protected $vueComponentParser = null;
|
||||
|
||||
/**
|
||||
* Constructs a new module from an options array.
|
||||
*
|
||||
|
|
@ -390,6 +401,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
$deprecationScript = $this->getDeprecationInformation( $context );
|
||||
if ( $this->packageFiles !== null ) {
|
||||
$packageFiles = $this->getPackageFiles( $context );
|
||||
foreach ( $packageFiles['files'] as &$file ) {
|
||||
if ( $file['type'] === 'script+style' ) {
|
||||
$file['content'] = $file['content']['script'];
|
||||
$file['type'] = 'script';
|
||||
}
|
||||
}
|
||||
if ( $deprecationScript ) {
|
||||
$mainFile =& $packageFiles['files'][$packageFiles['main']];
|
||||
$mainFile['content'] = $deprecationScript . $mainFile['content'];
|
||||
|
|
@ -437,6 +454,21 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
$context
|
||||
);
|
||||
|
||||
if ( $this->packageFiles !== null ) {
|
||||
$packageFiles = $this->getPackageFiles( $context );
|
||||
foreach ( $packageFiles['files'] as $fileName => $file ) {
|
||||
if ( $file['type'] === 'script+style' ) {
|
||||
$style = $this->processStyle(
|
||||
$file['content']['style'],
|
||||
$file['content']['styleLang'],
|
||||
$fileName,
|
||||
$context
|
||||
);
|
||||
$styles['all'] = ( $styles['all'] ?? '' ) . "\n" . $style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track indirect file dependencies so that ResourceLoaderStartUpModule can check for
|
||||
// on-disk file changes to any of this files without having to recompute the file list
|
||||
$this->saveFileDependencies( $context, $this->localFileRefs );
|
||||
|
|
@ -670,6 +702,16 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VueComponentParser
|
||||
*/
|
||||
protected function getVueComponentParser() {
|
||||
if ( $this->vueComponentParser === null ) {
|
||||
$this->vueComponentParser = new VueComponentParser;
|
||||
}
|
||||
return $this->vueComponentParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|ResourceLoaderFilePath $path
|
||||
* @return string
|
||||
|
|
@ -720,12 +762,15 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
/**
|
||||
* Infer the file type from a package file path.
|
||||
* @param string $path
|
||||
* @return string 'script' or 'data'
|
||||
* @return string 'script', 'script-vue', or 'data'
|
||||
*/
|
||||
public static function getPackageFileType( $path ) {
|
||||
if ( preg_match( '/\.json$/i', $path ) ) {
|
||||
return 'data';
|
||||
}
|
||||
if ( preg_match( '/\.vue$/i', $path ) ) {
|
||||
return 'script-vue';
|
||||
}
|
||||
return 'script';
|
||||
}
|
||||
|
||||
|
|
@ -1187,7 +1232,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
$expanded = [ 'type' => $type ];
|
||||
if ( !empty( $fileInfo['main'] ) ) {
|
||||
$mainFile = $fileName;
|
||||
if ( $type !== 'script' ) {
|
||||
if ( $type !== 'script' && $type !== 'script-vue' ) {
|
||||
$msg = "Main file in package must be of type 'script', module " .
|
||||
"'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
|
||||
$this->getLogger()->error( $msg );
|
||||
|
|
@ -1274,7 +1319,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
if ( $expandedFiles && $mainFile === null ) {
|
||||
// The first package file that is a script is the main file
|
||||
foreach ( $expandedFiles as $path => $file ) {
|
||||
if ( $file['type'] === 'script' ) {
|
||||
if ( $file['type'] === 'script' || $file['type'] === 'script-vue' ) {
|
||||
$mainFile = $path;
|
||||
break;
|
||||
}
|
||||
|
|
@ -1294,16 +1339,20 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
* Resolves the package files defintion and generates the content of each package file.
|
||||
* @param ResourceLoaderContext $context
|
||||
* @return array Package files data structure, see ResourceLoaderModule::getScript()
|
||||
* @throws RuntimeException If a file doesn't exist
|
||||
* @throws RuntimeException If a file doesn't exist, or parsing a .vue file fails
|
||||
*/
|
||||
public function getPackageFiles( ResourceLoaderContext $context ) {
|
||||
if ( $this->packageFiles === null ) {
|
||||
return null;
|
||||
}
|
||||
$hash = $context->getHash();
|
||||
if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
|
||||
return $this->fullyExpandedPackageFiles[ $hash ];
|
||||
}
|
||||
$expandedPackageFiles = $this->expandPackageFiles( $context );
|
||||
|
||||
// Expand file contents
|
||||
foreach ( $expandedPackageFiles['files'] as &$fileInfo ) {
|
||||
foreach ( $expandedPackageFiles['files'] as $fileName => &$fileInfo ) {
|
||||
// Turn any 'filePath' or 'callback' key into actual 'content',
|
||||
// and remove the key after that. The callback could return a
|
||||
// ResourceLoaderFilePath object; if that happens, fall through
|
||||
|
|
@ -1336,6 +1385,34 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
$fileInfo['content'] = $content;
|
||||
unset( $fileInfo['filePath'] );
|
||||
}
|
||||
if ( $fileInfo['type'] === 'script-vue' ) {
|
||||
try {
|
||||
$parsedComponent = $this->getVueComponentParser()->parse( $fileInfo['content'] );
|
||||
} catch ( Exception $e ) {
|
||||
$msg = "Error parsing file '$fileName' in module '{$this->getName()}': " .
|
||||
$e->getMessage();
|
||||
$this->getLogger()->error( $msg );
|
||||
throw new RuntimeException( $msg );
|
||||
}
|
||||
$template = $context->getDebug() ?
|
||||
$parsedComponent['rawTemplate'] :
|
||||
$parsedComponent['template'];
|
||||
$encodedTemplate = json_encode( $template );
|
||||
if ( $context->getDebug() ) {
|
||||
// Replace \n (backslash-n) with space + backslash-newline in debug mode
|
||||
// We only replace \n if not preceded by a backslash, to avoid breaking '\\n'
|
||||
$encodedTemplate = preg_replace( '/(?<!\\\\)\\\\n/', " \\\n", $encodedTemplate );
|
||||
// Expand \t to real tabs in debug mode
|
||||
$encodedTemplate = strtr( $encodedTemplate, [ "\\t" => "\t" ] );
|
||||
}
|
||||
$fileInfo['content'] = [
|
||||
'script' => $parsedComponent['script'] .
|
||||
";\nmodule.exports.template = $encodedTemplate;",
|
||||
'style' => $parsedComponent['style'] ?? '',
|
||||
'styleLang' => $parsedComponent['styleLang'] ?? 'css'
|
||||
];
|
||||
$fileInfo['type'] = 'script+style';
|
||||
}
|
||||
|
||||
// Not needed for client response, exists for use by getDefinitionSummary().
|
||||
unset( $fileInfo['definitionSummary'] );
|
||||
|
|
@ -1343,6 +1420,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
|||
unset( $fileInfo['callbackParam'] );
|
||||
}
|
||||
|
||||
$this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
|
||||
return $expandedPackageFiles;
|
||||
}
|
||||
|
||||
|
|
|
|||
230
includes/resourceloader/VueComponentParser.php
Normal file
230
includes/resourceloader/VueComponentParser.php
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
<?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
|
||||
* @author Roan Kattouw
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parser for Vue single file components (.vue files). See parse() for usage.
|
||||
*
|
||||
* @ingroup ResourceLoader
|
||||
* @since 1.35
|
||||
*/
|
||||
class VueComponentParser {
|
||||
|
||||
/**
|
||||
* Parse a Vue single file component, and extract the script, template and style parts.
|
||||
*
|
||||
* Returns an associative array with the following keys:
|
||||
* - 'script': The JS code in the <script> tag
|
||||
* - 'template': The HTML in the <template> tag, with whitespace removed
|
||||
* - 'rawTemplate': The HTML in the <template> tag, unmodified (before whitespace removal)
|
||||
* - 'style': The CSS/LESS styles in the <style> tag, or null if the <style> tag was missing
|
||||
* - 'styleLang': The language used for 'style'; either 'css' or 'less', or null if no <style> tag
|
||||
*
|
||||
* @param string $html HTML with <script>, <template> and <style> tags at the top level
|
||||
* @return array
|
||||
* @throws Exception If the input is invalid
|
||||
*/
|
||||
public function parse( $html ) : array {
|
||||
$dom = $this->parseHTML( $html );
|
||||
|
||||
// Find the <script>, <template> and <style> tags. They can appear in any order, but they
|
||||
// must be at the top level, and there can only be one of each.
|
||||
$nodes = $this->findUniqueTags( $dom, [ 'script', 'template', 'style' ] );
|
||||
|
||||
// Throw an error if we didn't find a <script> or <template> tag. <style> is optional.
|
||||
foreach ( [ 'script', 'template' ] as $requiredTag ) {
|
||||
if ( !isset( $nodes[ $requiredTag ] ) ) {
|
||||
throw new Exception( "No <$requiredTag> tag found" );
|
||||
}
|
||||
}
|
||||
|
||||
$this->validateAttributes( $nodes['script'], [] );
|
||||
$this->validateAttributes( $nodes['template'], [] );
|
||||
if ( isset( $nodes['style'] ) ) {
|
||||
$this->validateAttributes( $nodes['style'], [ 'lang' ] );
|
||||
}
|
||||
|
||||
$rootTemplateNode = $this->getRootTemplateNode( $nodes['template'] );
|
||||
|
||||
$rawTemplate = $dom->saveHTML( $rootTemplateNode );
|
||||
$this->removeWhitespaceAndComments( $rootTemplateNode );
|
||||
$minifiedTemplate = $dom->saveHTML( $rootTemplateNode );
|
||||
|
||||
$styleData = isset( $nodes['style'] ) ? $this->getStyleAndLang( $nodes['style'] ) : null;
|
||||
|
||||
return [
|
||||
'script' => trim( $nodes['script']->nodeValue ),
|
||||
'template' => $minifiedTemplate,
|
||||
'rawTemplate' => $rawTemplate,
|
||||
'style' => $styleData ? $styleData['style'] : null,
|
||||
'styleLang' => $styleData ? $styleData['lang'] : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML, working around various annoying libxml behaviors.
|
||||
* @param string $html
|
||||
* @return DOMDocument
|
||||
*/
|
||||
private function parseHTML( $html ) : DOMDocument {
|
||||
$dom = new DOMDocument();
|
||||
// Disable external entities, see https://www.mediawiki.org/wiki/XML_External_Entity_Processing
|
||||
$loadEntities = libxml_disable_entity_loader( true );
|
||||
$useErrors = libxml_use_internal_errors( true );
|
||||
// Force UTF-8, and disable network access
|
||||
$dom->loadHTML( '<?xml encoding="utf-8">' . $html, LIBXML_NONET | LIBXML_HTML_NOIMPLIED );
|
||||
libxml_disable_entity_loader( $loadEntities );
|
||||
|
||||
$errors = array_filter( libxml_get_errors(), function ( $error ) {
|
||||
return $error->code !== 801; // XML_HTML_UNKNOWN_TAG
|
||||
} );
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors( $useErrors );
|
||||
|
||||
if ( $errors ) {
|
||||
throw new Exception( "HTML parse errors:\n" .
|
||||
implode( "\n", array_map( function ( $error ) {
|
||||
return $error->message . ' on line ' . $error->line;
|
||||
}, $errors ) )
|
||||
);
|
||||
}
|
||||
return $dom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find occurrences of specified tags in a DOM node, expecting at most one occurrence of each.
|
||||
* This method only looks at the top-level children of $rootNode, it doesn't descend into them.
|
||||
*
|
||||
* @param DOMNode $rootNode Node whose children to look at
|
||||
* @param string[] $tagNames Tag names to look for (must be all lowercase)
|
||||
* @return DOMElement[] Associative arrays whose keys are tag names and values are DOM nodes
|
||||
*/
|
||||
private function findUniqueTags( DOMNode $rootNode, array $tagNames ) : array {
|
||||
$nodes = [];
|
||||
foreach ( $rootNode->childNodes as $node ) {
|
||||
$tagName = strtolower( $node->nodeName );
|
||||
if ( in_array( $tagName, $tagNames ) ) {
|
||||
if ( isset( $nodes[ $tagName ] ) ) {
|
||||
throw new Exception( "More than one <$tagName> tag found" );
|
||||
}
|
||||
$nodes[ $tagName ] = $node;
|
||||
}
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
private function validateAttributes( DOMNode $node, array $allowedAttributes ) {
|
||||
if ( $allowedAttributes ) {
|
||||
foreach ( $node->attributes as $attr ) {
|
||||
if ( !in_array( $attr->name, $allowedAttributes ) ) {
|
||||
throw new Exception( "<{$node->nodeName}> may not have the " .
|
||||
"{$attr->name} attribute" );
|
||||
}
|
||||
}
|
||||
} elseif ( $node->attributes->length > 0 ) {
|
||||
throw new Exception( "<{$node->nodeName}> may not have any attributes" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root node of the template. This is the only child node of the <template> node.
|
||||
* If the <template> node has multiple children, or is empty, or contains (non-whitespace) text,
|
||||
* an exception is thrown.
|
||||
*
|
||||
* @param DOMNode $templateNode The <template> node
|
||||
* @return DOMNode The only child of the template node
|
||||
* @throws Exception If the contents of the <template> node are invalid
|
||||
*/
|
||||
private function getRootTemplateNode( DOMNode $templateNode ) : DOMNode {
|
||||
// Verify that the <template> tag only contains one tag, and put it in $rootTemplateNode
|
||||
// We can't use ->childNodes->length === 1 here because whitespace shows up as text nodes,
|
||||
// and comments are also allowed.
|
||||
$rootTemplateNode = null;
|
||||
foreach ( $templateNode->childNodes as $node ) {
|
||||
if ( $node->nodeType === XML_ELEMENT_NODE ) {
|
||||
if ( $rootTemplateNode !== null ) {
|
||||
throw new Exception( '<template> tag may not have multiple child tags' );
|
||||
}
|
||||
$rootTemplateNode = $node;
|
||||
} elseif ( $node->nodeType === XML_TEXT_NODE ) {
|
||||
// Text nodes are allowed, as long as they only contain whitespace
|
||||
if ( trim( $node->nodeValue ) !== '' ) {
|
||||
throw new Exception( '<template> tag may not contain text' );
|
||||
}
|
||||
} elseif ( $node->nodeType !== XML_COMMENT_NODE ) {
|
||||
// Comment nodes are allowed, anything else is not allowed
|
||||
throw new Exception( "<template> tag may only contain element and comment nodes, " .
|
||||
" found node of type {$node->nodeType}" );
|
||||
}
|
||||
}
|
||||
if ( $rootTemplateNode === null ) {
|
||||
throw new Exception( '<template> tag may not be empty' );
|
||||
}
|
||||
return $rootTemplateNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove whitespace from all text nodes in a DOM subtree, and remove all comment nodes
|
||||
* @param DOMNode $node
|
||||
*/
|
||||
private function removeWhitespaceAndComments( DOMNode $node ) {
|
||||
$toRemove = [];
|
||||
foreach ( $node->childNodes as $child ) {
|
||||
if ( $child->nodeType === XML_TEXT_NODE ) {
|
||||
$trimmedText = trim( $child->data );
|
||||
if ( $trimmedText === '' ) {
|
||||
$toRemove[] = $child;
|
||||
} else {
|
||||
$child->replaceData( 0, strlen( $child->data ), $trimmedText );
|
||||
}
|
||||
} elseif ( $child->nodeType === XML_COMMENT_NODE ) {
|
||||
$toRemove[] = $child;
|
||||
} elseif ( $child->nodeType === XML_ELEMENT_NODE ) {
|
||||
// Recurse, but don't strip whitespace inside <pre> tags
|
||||
if ( !in_array( strtolower( $child->nodeName ), [ 'pre', 'listing', 'textarea' ] ) ) {
|
||||
$this->removeWhitespaceAndComments( $child );
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $toRemove as $child ) {
|
||||
$node->removeChild( $child );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents and language of the <style> tag. The language can be 'css' or 'less'.
|
||||
* @param DOMElement $styleNode The <style> tag.
|
||||
* @return array [ 'style' => string, 'lang' => string ]
|
||||
* @throws Exception If an invalid language is used, or if the 'scoped' attribute is set.
|
||||
*/
|
||||
private function getStyleAndLang( DOMElement $styleNode ) : array {
|
||||
$style = $styleNode->nodeValue;
|
||||
$styleLang = $styleNode->hasAttribute( 'lang' ) ?
|
||||
$styleNode->getAttribute( 'lang' ) : 'css';
|
||||
if ( $styleLang !== 'css' && $styleLang !== 'less' ) {
|
||||
throw new Exception( "<style lang=\"$styleLang\"> is invalid," .
|
||||
" lang must be \"css\" or \"less\"" );
|
||||
}
|
||||
return [
|
||||
'style' => $style,
|
||||
'lang' => $styleLang,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
hello: 'world'
|
||||
};
|
||||
}
|
||||
};;
|
||||
module.exports.template = "<div class=\"mw-vue-test\"> \
|
||||
<!-- \
|
||||
Inner comment \
|
||||
with multiple lines \
|
||||
and tabs \
|
||||
--> \
|
||||
<p>Hello\\n<\/p> \
|
||||
<p>{{ hello }}<\/p> \
|
||||
<pre> \
|
||||
foo\\\n bar \
|
||||
<\/pre> \
|
||||
<\/div>";
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
hello: 'world'
|
||||
};
|
||||
}
|
||||
};;
|
||||
module.exports.template = "<div class=\"mw-vue-test\">\n<p>Hello\\n<\/p>\n<p>{{ hello }}<\/p>\n<pre>\n\t\t\tfoo\\\n\t\t\tbar\n\t\t<\/pre>\n<\/div>";
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
hello: 'world'
|
||||
};
|
||||
}
|
||||
};;
|
||||
module.exports.template = "<div class=\"mw-vue-test\"><p>Hello\\n<\/p><p>{{ hello }}<\/p><pre>\n\t\t\tfoo\\\n\t\t\tbar\n\t\t<\/pre><\/div>";
|
||||
32
tests/phpunit/data/resourceloader/vue-component.vue
Normal file
32
tests/phpunit/data/resourceloader/vue-component.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<!-- Outer comment -->
|
||||
<div class="mw-vue-test">
|
||||
<!--
|
||||
Inner comment
|
||||
with multiple lines
|
||||
and tabs
|
||||
-->
|
||||
<p>Hello\n</p>
|
||||
<p>{{ hello }}</p>
|
||||
<pre>
|
||||
foo\
|
||||
bar
|
||||
</pre>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
hello: 'world'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
.mw-vue-test {
|
||||
&:hover {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -539,6 +539,13 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
|
|||
$base = [ 'localBasePath' => $basePath ];
|
||||
$commentScript = file_get_contents( "$basePath/script-comment.js" );
|
||||
$nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
|
||||
$vueComponentDebug = trim( file_get_contents( "$basePath/vue-component-output-debug.js.txt" ) );
|
||||
// In PHP 7.2 and below, newlines are inserted between elements even if we try to strip them
|
||||
// Thankfully this was fixed in PHP 7.3, but it means we need different test cases for 7.2 vs 7.3+
|
||||
$vueComponentNonDebugFile = version_compare( PHP_VERSION, '7.3' ) < 0 ?
|
||||
"$basePath/vue-component-output-nondebug-php72.js.txt" :
|
||||
"$basePath/vue-component-output-nondebug.js.txt";
|
||||
$vueComponentNonDebug = trim( file_get_contents( $vueComponentNonDebugFile ) );
|
||||
$config = RequestContext::getMain()->getConfig();
|
||||
return [
|
||||
[
|
||||
|
|
@ -738,6 +745,45 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
|
|||
'lang' => 'nl'
|
||||
]
|
||||
],
|
||||
'.vue file in debug mode' => [
|
||||
$base + [
|
||||
'packageFiles' => [
|
||||
'vue-component.vue'
|
||||
]
|
||||
],
|
||||
[
|
||||
'files' => [
|
||||
'vue-component.vue' => [
|
||||
'type' => 'script',
|
||||
'content' => $vueComponentDebug
|
||||
]
|
||||
],
|
||||
'main' => 'vue-component.vue',
|
||||
],
|
||||
[
|
||||
'debug' => 'true'
|
||||
]
|
||||
],
|
||||
'.vue file in non-debug mode' => [
|
||||
$base + [
|
||||
'packageFiles' => [
|
||||
'vue-component.vue'
|
||||
],
|
||||
'name' => 'nondebug',
|
||||
],
|
||||
[
|
||||
'files' => [
|
||||
'vue-component.vue' => [
|
||||
'type' => 'script',
|
||||
'content' => $vueComponentNonDebug
|
||||
]
|
||||
],
|
||||
'main' => 'vue-component.vue'
|
||||
],
|
||||
[
|
||||
'debug' => 'false'
|
||||
]
|
||||
],
|
||||
[
|
||||
$base + [
|
||||
'packageFiles' => [
|
||||
|
|
|
|||
193
tests/phpunit/includes/resourceloader/VueComponentParserTest.php
Normal file
193
tests/phpunit/includes/resourceloader/VueComponentParserTest.php
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group ResourceLoader
|
||||
* @covers VueComponentParser
|
||||
*/
|
||||
class VueComponentParserTest extends PHPUnit\Framework\TestCase {
|
||||
/**
|
||||
* @dataProvider provideTestParse
|
||||
*/
|
||||
public function testParse( $html, $expectedResult, $message, $expectedException = '' ) {
|
||||
$parser = new VueComponentParser;
|
||||
if ( $expectedException !== '' ) {
|
||||
$this->expectExceptionMessage( $expectedException );
|
||||
}
|
||||
$actualResult = $parser->parse( $html );
|
||||
$this->assertEquals( $expectedResult, $actualResult, $message );
|
||||
}
|
||||
|
||||
public static function provideTestParse() {
|
||||
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
||||
return [
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style>baz</style>',
|
||||
[
|
||||
'script' => 'bar',
|
||||
'template' => '<p>{{foo}}</p>',
|
||||
'rawTemplate' => '<p>{{foo}}</p>',
|
||||
'style' => 'baz',
|
||||
'styleLang' => 'css',
|
||||
],
|
||||
'Basic test'
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><style>baz</style>',
|
||||
null,
|
||||
'Missing <script> tag',
|
||||
'No <script> tag found',
|
||||
],
|
||||
[
|
||||
'<script>bar</script><style>baz</style>',
|
||||
null,
|
||||
'Missing <template> tag',
|
||||
'No <template> tag found',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script>',
|
||||
[
|
||||
'script' => 'bar',
|
||||
'template' => '<p>{{foo}}</p>',
|
||||
'rawTemplate' => '<p>{{foo}}</p>',
|
||||
'style' => null,
|
||||
'styleLang' => null,
|
||||
],
|
||||
'Missing <style> tag'
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><template><p>{{quux}}</p></template><script>bar</script>',
|
||||
null,
|
||||
'Two template tags',
|
||||
'More than one <template> tag found'
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><script>quux</script>',
|
||||
null,
|
||||
'Two script tags',
|
||||
'More than one <script> tag found'
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style>baz</style><style>quux</style>',
|
||||
null,
|
||||
'Two style tags',
|
||||
'More than one <style> tag found'
|
||||
],
|
||||
[
|
||||
'<template></template><script>bar</script><style>baz</style>',
|
||||
null,
|
||||
'Empty <template> tag',
|
||||
'<template> tag may not be empty',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p><p></p></template><script>bar</script>',
|
||||
null,
|
||||
'Template with two root nodes',
|
||||
'<template> tag may not have multiple child tags',
|
||||
],
|
||||
[
|
||||
'<template><!-- Explanation --><p>{{foo}}</p></template><script>bar</script>',
|
||||
[
|
||||
'script' => 'bar',
|
||||
'template' => '<p>{{foo}}</p>',
|
||||
'rawTemplate' => '<p>{{foo}}</p>',
|
||||
'style' => null,
|
||||
'styleLang' => null,
|
||||
],
|
||||
'Template with comment outside',
|
||||
],
|
||||
[
|
||||
'<template><p><!-- Explanation -->{{foo}}</p></template><script>bar</script>',
|
||||
[
|
||||
'script' => 'bar',
|
||||
'template' => '<p>{{foo}}</p>',
|
||||
'rawTemplate' => '<p><!-- Explanation -->{{foo}}</p>',
|
||||
'style' => null,
|
||||
'styleLang' => null,
|
||||
],
|
||||
'Template with comment inside',
|
||||
],
|
||||
[
|
||||
'<template>blah</template><script>bar</script>',
|
||||
null,
|
||||
'Template with text',
|
||||
'<template> tag may not contain text',
|
||||
],
|
||||
[
|
||||
"<template>\n\t<div>\t\t<div> {{foo}}\n{{bar}} </div>\n\t</div>\n</template><script>blah</script>",
|
||||
[
|
||||
'script' => 'blah',
|
||||
'template' => "<div><div>{{foo}}\n{{bar}}</div></div>",
|
||||
'rawTemplate' => "<div>\t\t<div> {{foo}}\n{{bar}} </div>\n\t</div>",
|
||||
'style' => null,
|
||||
'styleLang' => null,
|
||||
],
|
||||
'Whitespace in <template> tag is stripped',
|
||||
],
|
||||
[
|
||||
"<template>\n\t<div>\t\t<pre> {{foo}}\n{{bar}} </pre>\n\t</div>\n</template><script>blah</script>",
|
||||
[
|
||||
'script' => 'blah',
|
||||
'template' => "<div><pre> {{foo}}\n{{bar}} </pre></div>",
|
||||
'rawTemplate' => "<div>\t\t<pre> {{foo}}\n{{bar}} </pre>\n\t</div>",
|
||||
'style' => null,
|
||||
'styleLang' => null,
|
||||
],
|
||||
'Whitespace stripping skips <pre> tags',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style lang="less">baz</style>',
|
||||
[
|
||||
'script' => 'bar',
|
||||
'template' => '<p>{{foo}}</p>',
|
||||
'rawTemplate' => '<p>{{foo}}</p>',
|
||||
'style' => 'baz',
|
||||
'styleLang' => 'less',
|
||||
],
|
||||
'Style tag with lang="less"',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style lang="quux">baz</style>',
|
||||
null,
|
||||
'Style tag with invalid language',
|
||||
'<style lang="quux"> is invalid, lang must be "css" or "less"',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style scoped>baz</style>',
|
||||
null,
|
||||
'Scoped style tag',
|
||||
'<style> may not have the scoped attribute',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style lang="less" scoped>baz</style>',
|
||||
null,
|
||||
'Scoped style tag with lang="less"',
|
||||
'<style> may not have the scoped attribute',
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script>bar</script><style module>baz</style>',
|
||||
null,
|
||||
'<style module> tag',
|
||||
'<style> may not have the module attribute',
|
||||
],
|
||||
[
|
||||
'<template functional><p>{{foo}}</p></template><script>bar</script><style>baz</style>',
|
||||
null,
|
||||
'<template functional> tag',
|
||||
'<template> may not have any attributes'
|
||||
],
|
||||
[
|
||||
'<template><p>{{foo}}</p></template><script foo>bar</script><style>baz</style>',
|
||||
null,
|
||||
'<script> tag with attribute',
|
||||
'<script> may not have any attributes'
|
||||
],
|
||||
[
|
||||
'<template><p @click="onClick">{{foo}}</p></template><script>bar</script>',
|
||||
null,
|
||||
'@click attribute',
|
||||
"HTML parse errors:\nerror parsing attribute name\n on line 1"
|
||||
]
|
||||
];
|
||||
// @codingStandardsIgnoreEnd Generic.Files.LineLength
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue