Skip to main content

Migrating to 16.0.0

This release contains significant or breaking changes.

We've migrated our source code to ECMAScript modules (ESM) — a year-long effort to allow ESM plugins, custom syntaxes and formatters, and a step towards updating our pure ESM dependencies.

To give the community time to migrate to ESM, we'll publish a hybrid package to support the (now deprecated) CommonJS Node.js API until our next major release.

Significant changes

We've:

  • added support for ESM plugins
  • added support for ESM custom syntaxes
  • added support for ESM custom formatters
  • deprecated the CommonJS Node.js API
  • refactored to use .mjs and .cjs extensions internally

Added support for ESM plugins

You can now create ESM plugins.

For example:

import stylelint from "stylelint";

const {
createPlugin,
utils: { report, ruleMessages, validateOptions }
} = stylelint;

const ruleName = "foo-org/foo-bar";

const messages = ruleMessages(ruleName, {
rejected: (selector) => `Unexpected "foo"`
});

const meta = {
url: "https://foo.org/foo-bar"
};

const ruleFunction = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(/* .. */);

if (!validOptions) return;

/* .. */

report({
/* .. */
});
};
};

ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;
ruleFunction.meta = meta;

export default createPlugin(ruleName, ruleFunction);

We've updated our plugin developer guide with more examples of ESM syntax.

Added support for ESM custom syntaxes

You can now create ESM custom syntaxes.

For example:

import postcss from "postcss";

function parse(css, opts) {
/* .. */
}

function stringify(node, builder) {
/* .. */
}

export default { parse, stringify };

See the custom syntaxes section of our developer guide for more examples.

Added support for ESM custom formatters

You can now create ESM custom formatters.

For example:

export default function yourOwnFormatter(results) {
/* .. */
}

See the custom formatters section of our developer guide for more examples.

Deprecated CommonJS API

We've deprecated the CommonJS Node.js API and will remove it in the next major release so that we can update our pure ESM dependencies.

The deprecation will impact you if you're a plugin author or use stylelint.lint() to lint files. Custom syntaxes and formatters are not affected as they don't consume the Node.js API.

To migrate to ESM you should:

  • replace all require()/module.export with import/export
  • add "type": "module" to package.json if you are not using the .mjs extension
  • use only full relative file paths for imports, e.g., import x from '.';import x from './index.js';

We also recommend that you:

  • update the "engines" field in package.json to "node": ">=18.12.0"
  • remove 'use strict'; from all files
  • add "exports": "./index.js" in package.json
  • use the node: protocol for Node.js built-in imports

For more details, see the Node.js documentation for ESM.

Plugins

For example, to migrate your plugin to use import and export:

-const stylelint = require("stylelint");
+import stylelint from "stylelint";

const {
createPlugin,
utils: { report, validateOptions },
} = stylelint;

const ruleFunction = (primary, secondaryOptions) => { /* .. */ };

ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;
ruleFunction.meta = meta;

-module.exports = createPlugin(ruleName, ruleFunction);
+export default createPlugin(ruleName, ruleFunction);

If you test your plugin using our testRule schema, you can either:

jest-preset-stylelint

The preset needs the --experimental-vm-modules Node.js flag to support ESM. You can use the cross-env package to set the NODE_OPTIONS environment variable in your npm-run-script:

 {
"scripts": {
- "test": "jest",
+ "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --runInBand",
}
}

(cross-env is in maintenance-mode as it's considered finished.)

If you get an error (e.g. a segmentation fault while running the preset on Node.js 18), you can try using jest-light-runner in your Jest config:

 export default {
preset: "jest-preset-stylelint",
setupFiles: ["./jest.setup.js"],
+ runner: "jest-light-runner",
};

The runner has limited coverage support.

stylelint-test-rule-node

To try our new stylelint-test-rule-node package, you should import it into your test files:

+import { testRule } from "stylelint-test-rule-node";

testRule({
/* .. */
});

And update your npm-run-script:

 {
"scripts": {
- "test": "jest"
+ "test": "node --test"
}
}

The package uses node:test which is experimental within Node.js 18, but stable in 20. Coverage support is also experimental.

If you have other Jest tests, you'll need to adapt them to use node:test, e.g. expect() becomes assert().

stylelint.lint()

For example, to migrate your code that uses stylelint.lint() to use import and top-level await:

-const stylelint = require("stylelint");
+import stylelint from "stylelint";

-stylelint.lint(options).then((result) => { console.log(result); });
+const result = await stylelint.lint(options);
+console.log(result);

You'll find more details about the ESM Node.js API in the user guide.

Using the CommonJS Node.js API will trigger a deprecation warning. If you're not quite ready to migrate to ESM yet, you can use the quietDeprecationWarnings option to hide the warning.

 const stylelint = require("stylelint");

const resultPromise = stylelint.lint({
/* .. */
+ quietDeprecationWarnings: true
});

Refactored to use .mjs and .cjs extensions internally

We now use .mjs and .cjs extensions internally to support a hybrid package. This change doesn't affect our public API but will affect you if your plugin requires internal files.

We recommended copying files to your project instead of importing them as we'll remove access to them in the next major release.

However, you can unsafely continue to import or require the files before the next major release by updating your imports:

 // ESM
-import('stylelint/lib/utils/typeGuards.js');
+import('stylelint/lib/utils/typeGuards.mjs');

// CommonJS
-require('stylelint/lib/utils/typeGuards.js');
+require('stylelint/lib/utils/typeGuards.cjs');

Breaking changes

We've:

  • removed deprecated stylistic rules
  • removed support for Node.js less than 18.12.0
  • changed Node.js API returned resolved object
  • changed Node.js API stylelint.formatters object
  • changed Node.js API stylelint.rules object
  • changed Node.js API stylelint.utils.checkAgainstRule() function
  • changed CLI to print problems to stderr
  • changed CLI exit code for flag errors
  • changed default syntax behaviour to always use safe-parser with fix regardless of extension

Removed deprecated stylistic rules

We've removed the stylistic rules we deprecated in 15.0.0.

You should remove the rules from your configuration object. See the 15.0.0 migration guide for more details.

Removed support for Node.js less than 18.12.0

Node.js 14 and 16 have reached end-of-life. We've removed support for them to update some of our dependencies.

You should use the 18.12.0 or higher versions of Node.js.

Changed Node.js API returned resolved object

We've changed the resolved object of the Promise returned by stylelint.lint() so that a new:

  • report property contains the formatted problems
  • code property contains the autofixed code

We've deprecated the output property in favor of the new report and code ones, and we'll remove it in the next major version.

If you use stylelint.lint() to lint a source string and the fix option is true, the report property will contain the formatted problems and the code property will contain the fixed code.

 const result = await stylelint.lint({
code: "a {}",
fix: true
});
-const fixedCode = result.output;
+const formattedProblems = result.report;
+const fixedCode = result.code;

The code property will always be undefined if you use stylelint.lint() to lint files.

Changed Node.js API stylelint.formatters object

We've changed the stylelint.formatters object in the Node.js API so that every formatter is a Promise function.

-const formatter = stylelint.formatters.json;
+const formatter = await stylelint.formatters.json;

Changed Node.js API stylelint.rules object

We've changed the stylelint.rules object in the Node.js API so that every rule is a Promise function.

-const rule = stylelint.rules['block-no-empty'];
+const rule = await stylelint.rules['block-no-empty'];

Changed Node.js API stylelint.utils.checkAgainstRule() function

We've changed the stylelint.utils.checkAgainstRule() function in the Node.js API so that it's an async function.

-checkAgainstRule({ /* .. */ });
+await checkAgainstRule({ /* .. */ });

Changed CLI to print problems to stderr

We've changed the CLI to print problems to stderr instead of stdout.

If you use the --fix and --stdin options, the CLI will print the fixed code to stdout and any problems to stderr.

If you use > to redirect standard output, e.g. from a custom formatter, you can use the --output-file flag instead:

-npx stylelint "*.css" --custom-formatter custom-formatter.js > output.txt
+npx stylelint "*.css" --custom-formatter custom-formatter.js --output-file output.txt

Changed CLI exit code for flag errors

We've changed the exit code for CLI flag errors from 2 to 64 so that 2 is reserved for lint problems.

If you're an author of an editor integration that uses the CLI, you can now distinguish between flag errors and lint problems.

Changed default syntax behaviour to always use safe-parser with fix regardless of extension

We've changed the default syntax behaviour to always use postcss-safe-parser when autofixing CSS code, regardless of the file's extension. Previously, only .css, .pcss, or .postcss files were autofixed with postcss-safe-parser.