commit f87d25b06d368ccddf116e06e4d7a53e703fa23f
parent 2aee4803764a306053a363386eb40fcc9b187f4e
Author: Jon Oliver <jooliver@mozilla.com>
Date: Mon, 20 Oct 2025 16:41:55 +0000
Bug 1953499: Add stylelint rule to prevent importing browser CSS files in toolkit r=frontend-codestyle-reviewers,mkennedy,Gijs
- Add no-browser-refs-in-toolkit stylelint rule
- Configure rule to only apply to toolkit CSS files
- Add tests for no-browser-refs-in-toolkit rule
- Add docs for no-browser-refs-in-toolkit rule
Differential Revision: https://phabricator.services.mozilla.com/D268275
Diffstat:
6 files changed, 379 insertions(+), 0 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js
@@ -442,5 +442,11 @@ module.exports = {
"stylelint-plugin-mozilla/use-text-color-tokens": false,
},
},
+ {
+ files: ["toolkit/**/*.css", "toolkit/**/*.scss"],
+ rules: {
+ "stylelint-plugin-mozilla/no-browser-refs-in-toolkit": true,
+ },
+ },
],
};
diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.rst
@@ -0,0 +1,120 @@
+==========================
+no-browser-refs-in-toolkit
+==========================
+
+This rule prevents toolkit code from importing browser-specific CSS or SVG files.
+It is applied to both ``@import`` statements and CSS property values using the ``url`` function.
+
+This ensures that toolkit code remains independent and can be used in other applications/contexts
+where browser code is not necessarily available. Browser code can depend on toolkit code, but toolkit
+code should never depend on browser code, since toolkit code can be used in
+other applications/contexts where browser code may not be available.
+
+See additional documentation for more information on `internal URLs </toolkit/internal-urls.html>`_.
+
+Rule Scope
+----------
+
+This rule only applies to CSS files within the ``toolkit/`` directory. It is
+configured as an override in the stylelint configuration to target only
+``toolkit/**/*.css`` files.
+
+Examples of incorrect usage for this rule:
+------------------------------------------
+
+Incorrect usage of ``@import`` statements:
+
+.. code-block:: css
+
+ @import "chrome://browser/skin/example-file.css";
+
+.. code-block:: css
+
+ @import url("chrome://browser/skin/example-file.css");
+
+.. code-block:: css
+
+ @import "resource:///modules/example-file.css";
+
+.. code-block:: css
+
+ @import url("resource:///modules/example-file.css");
+
+.. code-block:: css
+
+ @import "resource://app/modules/example-file.css";
+
+.. code-block:: css
+
+ @import url("resource://app/modules/example-file.css");
+
+.. code-block:: css
+
+ @import "moz-src:///browser/example-file.css";
+
+.. code-block:: css
+
+ @import url("moz-src:///browser/example-file.css");
+
+Incorrect usage of property values:
+
+.. code-block:: css
+
+ .background-image {
+ background-image: url("chrome://browser/skin/example-file.svg");
+ }
+
+.. code-block:: css
+
+ .background-image {
+ background: url("moz-src:///browser/example-file.svg");
+ }
+
+.. code-block:: css
+
+ .background-image {
+ background: url("moz-src://foo/browser/example-file.svg");
+ }
+
+Examples of correct usage for this rule:
+----------------------------------------
+
+Correct usage of ``@import`` statements:
+
+.. code-block:: css
+
+ @import "chrome://global/skin/example-file.css";
+
+.. code-block:: css
+
+ @import url("chrome://global/skin/example-file.css");
+
+.. code-block:: css
+
+ @import "example-file.css";
+
+.. code-block:: css
+
+ @import url("example-file.css");
+
+.. code-block:: css
+
+ @import "resource://content-accessible/example-file.css"
+
+.. code-block:: css
+
+ @import url("resource://content-accessible/example-file.css");
+
+Correct usage of property values:
+
+.. code-block:: css
+
+ .background-image {
+ background: url("chrome://global/skin/example-file.svg");
+ }
+
+.. code-block:: css
+
+ .background-image {
+ background: url("example-file.svg");
+ }
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs
@@ -167,6 +167,9 @@ export const isFunction = node => node.type === "function";
export const isVariableFunction = node =>
isFunction(node) && node.value === "var";
+// checks if a node is a url() function
+export const isUrlFunction = node => isFunction(node) && node.value === "url";
+
/**
* Checks if CSS includes a named color, e.g. 'white' or 'rebeccapurple'
*
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs
@@ -5,6 +5,7 @@
*/
import noBaseDesignTokens from "./no-base-design-tokens.mjs";
+import noBrowserRefsInToolkit from "./no-browser-refs-in-toolkit.mjs";
import useBorderRadiusTokens from "./use-border-radius-tokens.mjs";
import useBorderColorTokens from "./use-border-color-tokens.mjs";
import useFontSizeTokens from "./use-font-size-tokens.mjs";
@@ -16,6 +17,7 @@ import useBoxShadowTokens from "./use-box-shadow-tokens.mjs";
export default {
"no-base-design-tokens": noBaseDesignTokens,
+ "no-browser-refs-in-toolkit": noBrowserRefsInToolkit,
"use-border-radius-tokens": useBorderRadiusTokens,
"use-border-color-tokens": useBorderColorTokens,
"use-font-size-tokens": useFontSizeTokens,
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.mjs
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-env node */
+
+import stylelint from "stylelint";
+import valueParser from "postcss-value-parser";
+import { namespace, isUrlFunction, isWord } from "../helpers.mjs";
+
+const {
+ utils: { report, ruleMessages, validateOptions },
+} = stylelint;
+
+const ruleName = namespace("no-browser-refs-in-toolkit");
+
+const messages = ruleMessages(ruleName, {
+ rejected: url =>
+ `${url} is part of Desktop Firefox and cannot be used by this code (which has to also work elsewhere).`,
+});
+
+const meta = {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-browser-refs-in-toolkit.html",
+ fixable: false,
+};
+
+function extractUrlFromNode(node) {
+ if (!node) {
+ return null;
+ }
+
+ if (node.type === "string") {
+ return node.value;
+ }
+
+ if (isUrlFunction(node)) {
+ const urlContent = node.nodes[0];
+ if (urlContent && (urlContent.type === "string" || isWord(urlContent))) {
+ return urlContent.value;
+ }
+ }
+
+ return null;
+}
+
+function isBrowserUrl(url) {
+ return (
+ url.startsWith("chrome://browser") ||
+ url.startsWith("resource:///") ||
+ url.startsWith("resource://app/") ||
+ /moz-src:\/\/\w*\/browser\//.test(url)
+ );
+}
+
+const ruleFunction = primaryOption => {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, {
+ actual: primaryOption,
+ possible: [true],
+ });
+
+ if (!validOptions) {
+ return;
+ }
+
+ root.walkAtRules("import", atRule => {
+ const params = valueParser(atRule.params);
+ const firstParam = params.nodes[0];
+ const importUrl = extractUrlFromNode(firstParam);
+
+ if (importUrl && isBrowserUrl(importUrl)) {
+ report({
+ message: messages.rejected(importUrl),
+ node: atRule,
+ result,
+ ruleName,
+ });
+ }
+ });
+
+ root.walkDecls(decl => {
+ const parsed = valueParser(decl.value);
+
+ parsed.nodes.forEach(node => {
+ const url = extractUrlFromNode(node);
+
+ if (url && isBrowserUrl(url)) {
+ report({
+ message: messages.rejected(url),
+ node: decl,
+ result,
+ ruleName,
+ });
+ }
+ });
+ });
+ };
+};
+
+ruleFunction.ruleName = ruleName;
+ruleFunction.messages = messages;
+ruleFunction.meta = meta;
+
+export default ruleFunction;
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/no-browser-refs-in-toolkit.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/no-browser-refs-in-toolkit.tests.mjs
@@ -0,0 +1,145 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/PL/2.0/.
+ */
+
+// Bug 1948378: remove this exception when the eslint import plugin fully
+// supports exports in package.json files
+// eslint-disable-next-line import/no-unresolved
+import { testRule } from "stylelint-test-rule-node";
+import stylelint from "stylelint";
+import noBrowserRefsInToolkit from "../rules/no-browser-refs-in-toolkit.mjs";
+
+let plugin = stylelint.createPlugin(
+ noBrowserRefsInToolkit.ruleName,
+ noBrowserRefsInToolkit
+);
+let {
+ ruleName,
+ rule: { messages },
+} = plugin;
+
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: [true],
+ fix: false,
+ accept: [
+ // @import statements
+ {
+ code: '@import "chrome://global/skin/global.css";',
+ description: "Using chrome://global is valid.",
+ },
+ {
+ code: '@import url("chrome://global/skin/global.css");',
+ description: "Using chrome://global with url function is valid.",
+ },
+ {
+ code: '@import "close-icon.css";',
+ description: "Using relative path is valid.",
+ },
+ {
+ code: '@import url("close-icon.css");',
+ description: "Using relative path with url function is valid.",
+ },
+ {
+ code: '@import "resource://content-accessible/viewsource.css";',
+ description: "Using non-app resource is valid.",
+ },
+ {
+ code: '@import url("resource://content-accessible/viewsource.css");',
+ description: "Using non-app resource with url function is valid.",
+ },
+ // property values
+ {
+ code: 'a { background: url("chrome://global/skin/global.css"); }',
+ description: "Using chrome://global in property value is valid.",
+ },
+ {
+ code: 'a { background: url("close-icon.svg"); }',
+ description: "Using relative path in property value is valid.",
+ },
+ ],
+
+ reject: [
+ // @import statements
+ {
+ code: '@import "chrome://browser/skin/browser-colors.css";',
+ message: messages.rejected("chrome://browser/skin/browser-colors.css"),
+ description: "Using chrome://browser import should be flagged.",
+ },
+ {
+ code: '@import url("chrome://browser/skin/browser-colors.css");',
+ message: messages.rejected("chrome://browser/skin/browser-colors.css"),
+ description:
+ "Using chrome://browser import with url function should be flagged.",
+ },
+ {
+ code: '@import "resource:///modules/testfile.css";',
+ message: messages.rejected("resource:///modules/testfile.css"),
+ description: "Using resource:/// import should be flagged.",
+ },
+ {
+ code: '@import url("resource:///modules/testfile.css");',
+ message: messages.rejected("resource:///modules/testfile.css"),
+ description:
+ "Using resource:/// import with url function should be flagged.",
+ },
+ {
+ code: '@import "resource://app/modules/testfile.css";',
+ message: messages.rejected("resource://app/modules/testfile.css"),
+ description: "Using resource://app/ import should be flagged.",
+ },
+ {
+ code: '@import url("resource://app/modules/testfile.css");',
+ message: messages.rejected("resource://app/modules/testfile.css"),
+ description:
+ "Using resource://app/ import with url function should be flagged.",
+ },
+ {
+ code: '@import "moz-src:///browser/testfile.css";',
+ message: messages.rejected("moz-src:///browser/testfile.css"),
+ description: "Using moz-src browser path should be flagged.",
+ },
+ {
+ code: '@import url("moz-src:///browser/testfile.css");',
+ message: messages.rejected("moz-src:///browser/testfile.css"),
+ description:
+ "Using moz-src browser path with url function should be flagged.",
+ },
+ {
+ code: "background: url(chrome://browser/skin/notification-fill-12.svg) no-repeat center;",
+ message: messages.rejected(
+ "chrome://browser/skin/notification-fill-12.svg"
+ ),
+ description:
+ "Using chrome://browser in property value should be flagged.",
+ },
+ // property values
+ {
+ code: 'a { background: url("chrome://browser/skin/browser-colors.css"); }',
+ message: messages.rejected("chrome://browser/skin/browser-colors.css"),
+ description:
+ "Using chrome://browser with quotes in property value should be flagged.",
+ },
+ {
+ code: "a { background: url(moz-src:///browser/testfile.svg); }",
+ message: messages.rejected("moz-src:///browser/testfile.svg"),
+ description:
+ "Using moz-src browser path in property value should be flagged.",
+ },
+ {
+ code: 'a { background: url("moz-src:///browser/testfile.svg"); }',
+ message: messages.rejected("moz-src:///browser/testfile.svg"),
+ description:
+ "Using moz-src browser path with quotes in property value should be flagged.",
+ },
+ {
+ code: 'background-image: url("moz-src://foo/browser/testfile.svg");',
+ message: messages.rejected("moz-src://foo/browser/testfile.svg"),
+ description:
+ "Using moz-src browser path in property value should be flagged.",
+ },
+ ],
+});