From ad57737ce0c7192ac734541294620679629038be Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Tue, 12 Jul 2016 22:26:39 -0700 Subject: [PATCH] All tests passing for @plugin - Inline JavaScript disabled by default - Deprecated "preprocessor" option removed (preprocessor plugins still valid) --- Gruntfile.js | 10 +-- lib/less-browser/add-default-options.js | 2 + lib/less-browser/index.js | 3 +- lib/less-browser/plugin-loader.js | 12 ++- lib/less-node/index.js | 1 - lib/less-node/plugin-loader.js | 11 +-- lib/less/contexts.js | 2 +- lib/less/environment/abstract-file-manager.js | 3 +- .../environment/abstract-plugin-loader.js | 44 ++++++----- lib/less/import-manager.js | 6 +- lib/less/parser/parser.js | 42 +++++++--- lib/less/transform-tree.js | 4 +- lib/less/tree/import.js | 4 +- lib/less/tree/js-eval-node.js | 2 +- test/browser/common.js | 1 + test/browser/runner-browser-options.js | 2 +- test/browser/runner-errors-options.js | 4 +- test/browser/runner-postProcessor-options.js | 5 -- test/browser/runner-postProcessor.js | 3 - test/browser/test-runner-template.tmpl | 1 - test/index.js | 7 +- test/less-test.js | 78 ++++++++++--------- test/less/mixins-guards.less | 2 +- test/less/no-js-errors/no-js-errors.txt | 2 +- test/less/plugin.less | 2 +- test/less/plugin/plugin-local.js | 6 ++ test/less/plugin/plugin-transitive.less | 2 +- 27 files changed, 141 insertions(+), 120 deletions(-) delete mode 100644 test/browser/runner-postProcessor-options.js delete mode 100644 test/browser/runner-postProcessor.js diff --git a/Gruntfile.js b/Gruntfile.js index 5c2d00ad..92fea041 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -31,7 +31,7 @@ module.exports = function (grunt) { shell: { options: {stdout: true, failOnError: true}, test: { - command: 'node test' + command: 'node test/index.js' }, benchmark: { command: 'node benchmark/index.js' @@ -241,14 +241,6 @@ module.exports = function (grunt) { outfile: 'tmp/browser/test-runner-global-vars.html' } }, - postProcessor: { - src: ['test/browser/less/postProcessor/*.less'], - options: { - helpers: 'test/browser/runner-postProcessor-options.js', - specs: 'test/browser/runner-postProcessor.js', - outfile: 'tmp/browser/test-runner-post-processor.html' - } - }, postProcessorPlugin: { src: ['test/less/postProcessorPlugin/*.less'], options: { diff --git a/lib/less-browser/add-default-options.js b/lib/less-browser/add-default-options.js index b06c58f1..27fcb9e2 100644 --- a/lib/less-browser/add-default-options.js +++ b/lib/less-browser/add-default-options.js @@ -43,4 +43,6 @@ module.exports = function(window, options) { options.onReady = true; } + options.javascriptEnabled = (options.javascriptEnabled || options.inlineJavaScript) ? true : false; + }; diff --git a/lib/less-browser/index.js b/lib/less-browser/index.js index c8967125..5fc2a2b4 100644 --- a/lib/less-browser/index.js +++ b/lib/less-browser/index.js @@ -8,8 +8,7 @@ var addDataAttr = require("./utils").addDataAttr, module.exports = function(window, options) { var document = window.document; var less = require('../less')(); - - options.javascriptEnabled = !!(options.javascriptEnabled || options.inlineJavaScript); + less.options = options; var environment = less.environment, FileManager = require("./file-manager")(options, less.logger), diff --git a/lib/less-browser/plugin-loader.js b/lib/less-browser/plugin-loader.js index 9331f088..52bf6eb8 100644 --- a/lib/less-browser/plugin-loader.js +++ b/lib/less-browser/plugin-loader.js @@ -1,5 +1,4 @@ -var path = require("path"), - AbstractPluginLoader = require("../less/environment/abstract-plugin-loader.js"); +var AbstractPluginLoader = require("../less/environment/abstract-plugin-loader.js"); /** * Browser Plugin Loader @@ -11,7 +10,7 @@ var PluginLoader = function(less) { PluginLoader.prototype = new AbstractPluginLoader(); -PluginLoader.prototype.tryLoadPlugin = function(name, argument, basePath, callback) { +PluginLoader.prototype.tryLoadPlugin = function(name, basePath, callback) { var self = this; var prefix = name.slice(0, 1); var explicit = prefix === "." || prefix === "/" || name.slice(-3).toLowerCase() === ".js"; @@ -34,14 +33,13 @@ PluginLoader.prototype.tryLoadPlugin = function(name, argument, basePath, callba }; PluginLoader.prototype.tryLoadFromEnvironment = function(filename, basePath, explicit, callback) { - var fileManager; - + var fileManager = new this.less.FileManager(); + if (basePath) { - filename = path.join(basePath, filename); + filename = (fileManager.extractUrlParts(filename, basePath)).url; } if (filename) { - fileManager = new this.less.FileManager(); filename = fileManager.tryAppendExtension(filename, '.js'); diff --git a/lib/less-node/index.js b/lib/less-node/index.js index 55da4b22..ab278336 100644 --- a/lib/less-node/index.js +++ b/lib/less-node/index.js @@ -13,7 +13,6 @@ less.fs = require("./fs"); less.FileManager = FileManager; less.UrlFileManager = UrlFileManager; less.options = less.options || {}; -less.options.javascriptEnabled = false; less.formatError = function(ctx, options) { options = options || {}; diff --git a/lib/less-node/plugin-loader.js b/lib/less-node/plugin-loader.js index 8eaa19ae..d65880c9 100644 --- a/lib/less-node/plugin-loader.js +++ b/lib/less-node/plugin-loader.js @@ -36,9 +36,7 @@ PluginLoader.prototype.tryLoadPlugin = function(name, basePath, callback) { callback(null, data); } else { - self.tryLoadFromEnvironment(name, basePath, explicit, function(err2, data2) { - callback(err2, data2); - }); + self.tryLoadFromEnvironment(name, basePath, explicit, callback); } }); } @@ -46,11 +44,10 @@ PluginLoader.prototype.tryLoadPlugin = function(name, basePath, callback) { }; PluginLoader.prototype.tryLoadFromEnvironment = function(name, basePath, explicit, callback) { - var filename; + var filename = name; var self = this; function getFile(filename) { - var fileManager = new self.less.FileManager(); filename = fileManager.tryAppendExtension(filename, '.js'); @@ -60,7 +57,7 @@ PluginLoader.prototype.tryLoadFromEnvironment = function(name, basePath, explici self.require = self.requireRelative(filename); } catch(e) { - console.log(e.stack.toString()); + callback(e); } callback(null, data); }, @@ -103,7 +100,7 @@ PluginLoader.prototype.tryLoadFromEnvironment = function(name, basePath, explici catch(e) { } } - if (basePath && !filename) { + if (basePath) { filename = path.join(basePath, name); } if (filename) { diff --git a/lib/less/contexts.js b/lib/less/contexts.js index 545280b3..c7f6bc0f 100644 --- a/lib/less/contexts.js +++ b/lib/less/contexts.js @@ -48,7 +48,7 @@ var evalCopyProperties = [ 'sourceMap', // whether to output a source map 'importMultiple', // whether we are currently importing multiple copies 'urlArgs', // whether to add args into url tokens - 'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true + 'javascriptEnabled',// option - whether Inline JavaScript is enabled. if undefined, defaults to false 'pluginManager', // Used as the plugin manager for the session 'importantScope' // used to bubble up !important statements ]; diff --git a/lib/less/environment/abstract-file-manager.js b/lib/less/environment/abstract-file-manager.js index d9a2f90b..7c0e2063 100644 --- a/lib/less/environment/abstract-file-manager.js +++ b/lib/less/environment/abstract-file-manager.js @@ -35,13 +35,14 @@ abstractFileManager.prototype.alwaysMakePathsAbsolute = function() { abstractFileManager.prototype.isPathAbsolute = function(filename) { return (/^(?:[a-z-]+:|\/|\\|#)/i).test(filename); }; - +// TODO: pull out - this is part of Node & Browserify abstractFileManager.prototype.join = function(basePath, laterPath) { if (!basePath) { return laterPath; } return basePath + laterPath; }; + abstractFileManager.prototype.pathDiff = function pathDiff(url, baseUrl) { // diff between two paths to create a relative path diff --git a/lib/less/environment/abstract-plugin-loader.js b/lib/less/environment/abstract-plugin-loader.js index 3f14be18..9d86fcf2 100644 --- a/lib/less/environment/abstract-plugin-loader.js +++ b/lib/less/environment/abstract-plugin-loader.js @@ -36,6 +36,7 @@ AbstractPluginLoader.prototype.evalPlugin = function(contents, context, pluginOp pluginObj = pluginManager.get(filename); if (pluginObj) { + this.trySetOptions(pluginObj, filename, pluginOptions); if (pluginObj.use) { pluginObj.use(this.less); } @@ -58,24 +59,27 @@ AbstractPluginLoader.prototype.evalPlugin = function(contents, context, pluginOp pluginObj = localModule.exports; } - pluginObj = this.validatePlugin(pluginObj, filename, pluginOptions); + pluginObj = this.validatePlugin(pluginObj, filename); + if (pluginObj) { // Run on first load pluginManager.addPlugin(pluginObj, fileInfo.filename); pluginObj.functions = registry.getLocalFunctions(); + this.trySetOptions(pluginObj, filename, pluginOptions); + // Run every @plugin call if (pluginObj.use) { pluginObj.use(this.less); } } else { - throw new Error(); + throw new SyntaxError("Not a valid plugin"); } } catch(e) { // TODO pass the error - console.log(e.stack.toString()); + console.log(e); return new this.less.LessError({ message: "Plugin evaluation error: '" + e.name + ': ' + e.message.replace(/["]/g, "'") + "'" , filename: filename, @@ -88,7 +92,24 @@ AbstractPluginLoader.prototype.evalPlugin = function(contents, context, pluginOp }; -AbstractPluginLoader.prototype.validatePlugin = function(plugin, filename, options) { +AbstractPluginLoader.prototype.trySetOptions = function(plugin, filename, options) { + var name = require('path').basename(filename); + if (options) { + if (!plugin.setOptions) { + error("Options have been provided but the plugin " + name + " does not support any options."); + return null; + } + try { + plugin.setOptions(options); + } + catch(e) { + error("Error setting options on plugin " + name + '\n' + e.message); + return null; + } + } +}; + +AbstractPluginLoader.prototype.validatePlugin = function(plugin, filename) { if (plugin) { // support plugins being a function // so that the plugin can be more usable programmatically @@ -98,20 +119,7 @@ AbstractPluginLoader.prototype.validatePlugin = function(plugin, filename, optio var name = require('path').basename(filename); if (plugin.minVersion) { if (this.compareVersion(plugin.minVersion, this.less.version) < 0) { - error("plugin " + name + " requires version " + this.versionToString(plugin.minVersion)); - return null; - } - } - if (options) { - if (!plugin.setOptions) { - error("options have been provided but the plugin " + name + "does not support any options"); - return null; - } - try { - plugin.setOptions(options); - } - catch(e) { - error("Error setting options on plugin " + name + '\n' + e.message); + error("Plugin " + name + " requires version " + this.versionToString(plugin.minVersion)); return null; } } diff --git a/lib/less/import-manager.js b/lib/less/import-manager.js index 7ef6171c..3cbc4e21 100644 --- a/lib/less/import-manager.js +++ b/lib/less/import-manager.js @@ -107,8 +107,7 @@ module.exports = function(environment) { } if (importOptions.isPlugin) { - - plugin = pluginLoader.evalPlugin(contents, newEnv, importOptions, newFileInfo); + plugin = pluginLoader.evalPlugin(contents, newEnv, importOptions.pluginArgs, newFileInfo); if (plugin instanceof LessError) { fileParsedFunc(plugin, null, resolvedFilename); } @@ -132,9 +131,8 @@ module.exports = function(environment) { } }; if (importOptions.isPlugin) { - // TODO: implement options for plugins try { - pluginLoader.tryLoadPlugin(path, null, currentFileInfo.currentDirectory, done); + pluginLoader.tryLoadPlugin(path, currentFileInfo.currentDirectory, done); } catch(e) { callback(e); diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index 761ee6f3..11231e83 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -1384,22 +1384,27 @@ var Parser = function Parser(context, imports, fileInfo) { }, // - // A @plugin directive, used to import scoped extensions dynamically. + // A @plugin directive, used to import plugins dynamically. // - // @plugin "lib"; - // - // Depending on our environment, importing is done differently: - // In the browser, it's an XHR request, in Node, it would be a - // file-system operation. The function used for importing is - // stored in `import`, which we pass to the Import constructor. + // @plugin (args) "lib"; // plugin: function () { - var path, + var path, args, options, index = parserInput.i, dir = parserInput.$re(/^@plugin?\s+/); if (dir) { - var options = { isPlugin : true }; + args = this.pluginArgs(); + + if (args) { + options = { + pluginArgs: args, + isPlugin: true + }; + } + else { + options = { isPlugin: true }; + } if ((path = this.entities.quoted() || this.entities.url())) { @@ -1407,7 +1412,6 @@ var Parser = function Parser(context, imports, fileInfo) { parserInput.i = index; error("missing semi-colon on @plugin"); } - return new(tree.Import)(path, null, options, index, fileInfo); } else { @@ -1417,6 +1421,24 @@ var Parser = function Parser(context, imports, fileInfo) { } }, + pluginArgs: function() { + // list of options, surrounded by parens + parserInput.save(); + if (! parserInput.$char('(')) { + parserInput.restore(); + return null; + } + var args = parserInput.$re(/^\s*([^\);]+)\)\s*/); + if (args[1]) { + parserInput.forget(); + return args[1].trim(); + } + else { + parserInput.restore(); + return null; + } + }, + // // A CSS Directive // diff --git a/lib/less/transform-tree.js b/lib/less/transform-tree.js index f1fc37c2..1835462a 100644 --- a/lib/less/transform-tree.js +++ b/lib/less/transform-tree.js @@ -63,7 +63,9 @@ module.exports = function(root, options) { if (options.pluginManager) { visitorIterator.first(); while ((v = visitorIterator.get())) { - v.run(root); + if (!v.isPreEvalVisitor) { + v.run(evaldRoot); + } } } diff --git a/lib/less/tree/import.js b/lib/less/tree/import.js index eeb280e1..ef9636a9 100644 --- a/lib/less/tree/import.js +++ b/lib/less/tree/import.js @@ -127,8 +127,8 @@ Import.prototype.doEval = function (context) { features = this.features && this.features.eval(context); if (this.options.isPlugin) { - if (this.root && this.root.setContext) { - this.root.setContext(context); + if (this.root && this.root.eval) { + this.root.eval(context); } registry = context.frames[0] && context.frames[0].functionRegistry; if ( registry && this.root && this.root.functions ) { diff --git a/lib/less/tree/js-eval-node.js b/lib/less/tree/js-eval-node.js index 3b37378e..a4410b23 100644 --- a/lib/less/tree/js-eval-node.js +++ b/lib/less/tree/js-eval-node.js @@ -10,7 +10,7 @@ JsEvalNode.prototype.evaluateJavaScript = function (expression, context) { that = this, evalContext = {}; - if (context.javascriptEnabled !== undefined && !context.javascriptEnabled) { + if (!context.javascriptEnabled) { throw { message: "Inline JavaScript is not enabled. Is it set in your options?", filename: this.currentFileInfo.filename, index: this.index }; diff --git a/test/browser/common.js b/test/browser/common.js index 887e9cc6..58ef5060 100644 --- a/test/browser/common.js +++ b/test/browser/common.js @@ -1,6 +1,7 @@ /* Add js reporter for sauce */ jasmine.getEnv().addReporter(new jasmine.JSReporter2()); +jasmine.getEnv().defaultTimeoutInterval = 3000; /* record log messages for testing */ diff --git a/test/browser/runner-browser-options.js b/test/browser/runner-browser-options.js index 64037bbb..b90ec3b0 100644 --- a/test/browser/runner-browser-options.js +++ b/test/browser/runner-browser-options.js @@ -1,4 +1,4 @@ -var less = {logLevel: 4, errorReporting: "console"}; +var less = {logLevel: 4, errorReporting: "console", javascriptEnabled: true}; // There originally run inside describe method. However, since they have not // been inside it, they run at jasmine compile time (not runtime). It all diff --git a/test/browser/runner-errors-options.js b/test/browser/runner-errors-options.js index 8ba00e27..97b211d9 100644 --- a/test/browser/runner-errors-options.js +++ b/test/browser/runner-errors-options.js @@ -1,4 +1,6 @@ var less = { strictUnits: true, strictMath: true, - logLevel: 4 }; + logLevel: 4, + javascriptEnabled: true +}; diff --git a/test/browser/runner-postProcessor-options.js b/test/browser/runner-postProcessor-options.js deleted file mode 100644 index fe7111b6..00000000 --- a/test/browser/runner-postProcessor-options.js +++ /dev/null @@ -1,5 +0,0 @@ -var less = {logLevel: 4, - errorReporting: "console"}; -less.postProcessor = function(styles) { - return 'hr {height:50px;}\n' + styles; -}; diff --git a/test/browser/runner-postProcessor.js b/test/browser/runner-postProcessor.js deleted file mode 100644 index 64937669..00000000 --- a/test/browser/runner-postProcessor.js +++ /dev/null @@ -1,3 +0,0 @@ -describe("less.js postProcessor (deprecated)", function() { - testLessEqualsInDocument(); -}); diff --git a/test/browser/test-runner-template.tmpl b/test/browser/test-runner-template.tmpl index c02c38f3..9fef5680 100644 --- a/test/browser/test-runner-template.tmpl +++ b/test/browser/test-runner-template.tmpl @@ -3,7 +3,6 @@ Jasmine Spec Runner - <% var generateScriptTags = function(allScripts) { allScripts.forEach(function(script){ %> diff --git a/test/index.js b/test/index.js index a7aab0eb..c239e2fe 100644 --- a/test/index.js +++ b/test/index.js @@ -16,10 +16,11 @@ function getErrorPathReplacementFunction(dir) { } console.log("\n" + stylize("Less", 'underline') + "\n"); + lessTester.prepBomTest(); -lessTester.runTestSet({strictMath: true, relativeUrls: true, silent: true}); -lessTester.runTestSet({strictMath: true, strictUnits: true}, "errors/", - lessTester.testErrors, null, getErrorPathReplacementFunction("errors")); +lessTester.runTestSet({strictMath: true, relativeUrls: true, silent: true, javascriptEnabled: true}); +lessTester.runTestSet({strictMath: true, strictUnits: true, javascriptEnabled: true}, "errors/", + lessTester.testErrors, null, getErrorPathReplacementFunction("errors")); lessTester.runTestSet({strictMath: true, strictUnits: true, javascriptEnabled: false}, "no-js-errors/", lessTester.testErrors, null, getErrorPathReplacementFunction("no-js-errors")); lessTester.runTestSet({strictMath: true, dumpLineNumbers: 'comments'}, "debug/", null, diff --git a/test/less-test.js b/test/less-test.js index 7623a076..0d71a37c 100644 --- a/test/less-test.js +++ b/test/less-test.js @@ -58,7 +58,8 @@ module.exports = function() { var totalTests = 0, failedTests = 0, - passedTests = 0; + passedTests = 0, + finishTimer = setInterval(endTest, 500); less.functions.functionRegistry.addMultiple({ add: function (a, b) { @@ -221,48 +222,49 @@ module.exports = function() { var doubleCallCheck = false; queue(function() { toCSS(options, path.join(baseFolder, foldername + file), function (err, result) { - if (doubleCallCheck) { - totalTests++; - fail("less is calling back twice"); - process.stdout.write(doubleCallCheck + "\n"); - process.stdout.write((new Error()).stack + "\n"); - return; - } - doubleCallCheck = (new Error()).stack; + + if (doubleCallCheck) { + totalTests++; + fail("less is calling back twice"); + process.stdout.write(doubleCallCheck + "\n"); + process.stdout.write((new Error()).stack + "\n"); + return; + } + doubleCallCheck = (new Error()).stack; - if (verifyFunction) { - var verificationResult = verifyFunction(name, err, result && result.css, doReplacements, result && result.map, baseFolder); - release(); - return verificationResult; - } - if (err) { - fail("ERROR: " + (err && err.message)); - if (isVerbose) { - process.stdout.write("\n"); - if (err.stack) { - process.stdout.write(err.stack + "\n"); - } else { - //this sometimes happen - show the whole error object - console.log(err); + if (verifyFunction) { + var verificationResult = verifyFunction(name, err, result && result.css, doReplacements, result && result.map, baseFolder); + release(); + return verificationResult; + } + if (err) { + fail("ERROR: " + (err && err.message)); + if (isVerbose) { + process.stdout.write("\n"); + if (err.stack) { + process.stdout.write(err.stack + "\n"); + } else { + //this sometimes happen - show the whole error object + console.log(err); + } } + release(); + return; } - release(); - return; - } - var css_name = name; - if (nameModifier) { css_name = nameModifier(name); } - fs.readFile(path.join('test/css', css_name) + '.css', 'utf8', function (e, css) { - process.stdout.write("- " + path.join(baseFolder, css_name) + ": "); + var css_name = name; + if (nameModifier) { css_name = nameModifier(name); } + fs.readFile(path.join('test/css', css_name) + '.css', 'utf8', function (e, css) { + process.stdout.write("- " + path.join(baseFolder, css_name) + ": "); - css = css && doReplacements(css, path.join(baseFolder, foldername)); - if (result.css === css) { ok('OK'); } - else { - difference("FAIL", css, result.css); - } - release(); + css = css && doReplacements(css, path.join(baseFolder, foldername)); + if (result.css === css) { ok('OK'); } + else { + difference("FAIL", css, result.css); + } + release(); + }); }); }); - }); }); } @@ -305,8 +307,8 @@ module.exports = function() { function endTest() { if (isFinished && ((failedTests + passedTests) >= totalTests)) { + clearInterval(finishTimer); var leaked = checkGlobalLeaks(); - process.stdout.write("\n"); if (failedTests > 0) { process.stdout.write(failedTests + stylize(" Failed", "red") + ", " + passedTests + " passed\n"); diff --git a/test/less/mixins-guards.less b/test/less/mixins-guards.less index 883bb57a..31ad088e 100644 --- a/test/less/mixins-guards.less +++ b/test/less/mixins-guards.less @@ -150,7 +150,7 @@ .generic(abc, "abc"); .generic('abc', "abd"); .generic(6, e("6")); - .generic(`9`, 8); + .generic(9, 8); .generic(a, b); .generic(1 2, 3); } diff --git a/test/less/no-js-errors/no-js-errors.txt b/test/less/no-js-errors/no-js-errors.txt index d81dd2bd..10b3cd15 100644 --- a/test/less/no-js-errors/no-js-errors.txt +++ b/test/less/no-js-errors/no-js-errors.txt @@ -1,4 +1,4 @@ -SyntaxError: You are using JavaScript, which has been disabled. in {path}no-js-errors.less on line 2, column 6: +SyntaxError: Inline JavaScript is not enabled. Is it set in your options? in {path}no-js-errors.less on line 2, column 6: 1 .a { 2 a: `1 + 1`; 3 } diff --git a/test/less/plugin.less b/test/less/plugin.less index 66775ea9..d4dc0105 100644 --- a/test/less/plugin.less +++ b/test/less/plugin.less @@ -40,7 +40,7 @@ ruleset-shadow : test-shadow(); }; #ns { - @plugin "./plugin/plugin-local"; + @plugin (test=test) "./plugin/plugin-local"; .mixin() { ns-mixin-global : test-global(); ns-mixin-local : test-local(); diff --git a/test/less/plugin/plugin-local.js b/test/less/plugin/plugin-local.js index b54ca97a..d074b56c 100644 --- a/test/less/plugin/plugin-local.js +++ b/test/less/plugin/plugin-local.js @@ -6,3 +6,9 @@ functions.addMultiple({ return new tree.Anonymous( "local" ); } }); + +return { + setOptions: function(raw) { + // do nothing + } +} \ No newline at end of file diff --git a/test/less/plugin/plugin-transitive.less b/test/less/plugin/plugin-transitive.less index a0c20283..8e4ca00b 100644 --- a/test/less/plugin/plugin-transitive.less +++ b/test/less/plugin/plugin-transitive.less @@ -1,4 +1,4 @@ -@plugin "extension-transitive"; +@plugin "plugin-transitive"; .other { trans : test-transitive();