From 75e773ccc9fd729de5bd9e116cce049d28096011 Mon Sep 17 00:00:00 2001 From: Philipp Burckhardt Date: Sun, 15 Mar 2026 09:27:17 -0500 Subject: [PATCH 1/5] build: add `eslint.config.cjs` for JavaScript targets on ESLint v8 Add flat config alongside the legacy `.eslintrc.*` configuration: - Create `eslint.config.cjs` with base JS config, override blocks for benchmarks, examples, tests, and REPL namespace files - Inline stdlib rules as a runtime plugin object via `lib/node_modules/@stdlib/_tools/eslint/rules/scripts/plugin.js` - Reuse existing rule definitions from `etc/eslint/rules/` - Separate non-clonable rule options (remark plugin instances) from the main rules object to work around `structuredClone` limitations in ESLint's flat config internals - Add `globals` package for flat config environment definitions - Legacy `.eslintrc.*` files remain in place as the default workflow Usage (requires ESLint v8 with flat config enabled): ESLINT_USE_FLAT_CONFIG=true npx eslint -c eslint.config.cjs Note: files under `lib/node_modules/` are hard-ignored by ESLint v8 flat config. Full `lib/` linting via flat config requires ESLint v9, which allows `!**/node_modules/` ignore overrides. Ref: https://github.com/stdlib-js/metr-issue-tracker/issues/54 --- eslint.config.cjs | 161 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 162 insertions(+) create mode 100644 eslint.config.cjs diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 000000000000..2e9f0f64329f --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,161 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var globals = require( 'globals' ); +var pluginN = require( 'eslint-plugin-n' ); +var pluginCspell = require( '@cspell/eslint-plugin' ); +var pluginJsdoc = require( 'eslint-plugin-jsdoc' ); +var stdlibPlugin = require( './lib/node_modules/@stdlib/_tools/eslint/rules/scripts/plugin.js' ); +var allRules = require( './etc/eslint/rules' ); +var overrides = require( './etc/eslint/overrides' ); + + +// VARIABLES // + +var restrictedSyntaxConfig = overrides[ 2 ].rules[ 'no-restricted-syntax' ]; +var nonClonableRules = {}; +var rules = {}; +var val; +var key; +var i; + + +// FUNCTIONS // + +/** +* Tests whether a value can be structured-cloned. +* +* @private +* @param {*} value - value to test +* @returns {boolean} boolean indicating whether the value is clonable +*/ +function isClonable( value ) { + try { + // eslint-disable-next-line n/no-unsupported-features/es-builtins + if ( typeof structuredClone === 'function' ) { + structuredClone( value ); + } + return true; + } catch ( e ) { + return false; + } +} + + +// MAIN // + +// Separate rules containing non-clonable values: +for ( key in allRules ) { + val = allRules[ key ]; + if ( isClonable( val ) ) { + rules[ key ] = val; + } else { + nonClonableRules[ key ] = val; + } +} + +module.exports = [ + // Global ignores: + { + 'ignores': [ + '**/build/', + '**/reports/', + 'dist/', + '.git*' + ] + }, + + // Base JavaScript config: + { + 'files': [ '**/*.js' ], + 'languageOptions': { + 'ecmaVersion': 6, + 'sourceType': 'script', + 'globals': { + ...globals.browser, + ...globals.node, + ...globals.commonjs, + ...globals.worker + } + }, + 'plugins': { + 'n': pluginN, + 'jsdoc': pluginJsdoc, + '@cspell': pluginCspell, + 'stdlib': stdlibPlugin + }, + 'rules': rules + }, + + // REPL namespace files: + { + 'files': [ '**/lib/node_modules/@stdlib/**/lib/[a-z].js' ], + 'rules': { + 'stdlib/repl-namespace-order': 'error' + } + }, + + // Benchmarks: + { + 'files': [ '**/benchmark/**/*.js' ], + 'rules': { + 'no-new-wrappers': 'warn', + 'max-lines': [ 'warn', { + 'max': 1000, + 'skipBlankLines': true, + 'skipComments': true + }], + 'jsdoc/require-jsdoc': 'off', + 'no-restricted-syntax': restrictedSyntaxConfig + } + }, + + // Examples: + { + 'files': [ '**/examples/**/*.js' ], + 'rules': { + 'no-console': 'off', + 'vars-on-top': 'off', + 'jsdoc/require-jsdoc': 'off', + 'stdlib/jsdoc-private-annotation': 'off', + 'stdlib/require-order': 'off', + 'stdlib/require-file-extensions': 'off', + 'no-restricted-syntax': restrictedSyntaxConfig + } + }, + + // Tests: + { + 'files': [ '**/test/**/*.js' ], + 'rules': { + 'no-empty-function': 'off', + 'jsdoc/require-jsdoc': 'off', + 'no-undefined': 'off', + 'max-lines': [ 'warn', { + 'max': 1000, + 'skipBlankLines': true, + 'skipComments': true + }], + 'no-restricted-syntax': restrictedSyntaxConfig + } + } +]; diff --git a/package.json b/package.json index aab0d6a5bace..d6672b72b383 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsdoc": "^46.8.2", "eslint-plugin-stdlib": "file:./etc/eslint/plugin", + "globals": "^16.1.0", "exorcist": "^2.0.0", "factor-bundle": "^2.5.0", "gh-pages": "git+https://github.com/Planeshifter/gh-pages.git#main", From 88d798fddad9252e9f8fd17c84e1b32b47d62535 Mon Sep 17 00:00:00 2001 From: Philipp Burckhardt Date: Thu, 20 Aug 2026 00:13:01 -0500 Subject: [PATCH 2/5] build: make ESLint flat config clonable and opt-in Add a configPath option to stdlib/jsdoc-markdown-remark so the rule loads remark plugins internally while flat config receives only clonable data. Retain the existing config option and cover both forms in the rule tests. Use the complete rule catalog directly, share the restricted-syntax policy by name, avoid object spread, and keep flat config opt-in through eslint.flat.config.js. --- eslint.config.cjs => eslint.flat.config.js | 62 +++++------------- etc/eslint/overrides/index.js | 43 +----------- etc/eslint/overrides/restricted_syntax.js | 65 +++++++++++++++++++ etc/eslint/rules/stdlib.js | 4 +- .../rules/jsdoc-markdown-remark/README.md | 2 + .../rules/jsdoc-markdown-remark/lib/main.js | 16 +++-- .../test/fixtures/invalid.js | 48 +++++++++----- 7 files changed, 128 insertions(+), 112 deletions(-) rename eslint.config.cjs => eslint.flat.config.js (70%) create mode 100644 etc/eslint/overrides/restricted_syntax.js diff --git a/eslint.config.cjs b/eslint.flat.config.js similarity index 70% rename from eslint.config.cjs rename to eslint.flat.config.js index 2e9f0f64329f..7e87d1f7a498 100644 --- a/eslint.config.cjs +++ b/eslint.flat.config.js @@ -16,6 +16,8 @@ * limitations under the License. */ +/* eslint-disable n/no-unpublished-require */ + 'use strict'; // MODULES // @@ -24,56 +26,24 @@ var globals = require( 'globals' ); var pluginN = require( 'eslint-plugin-n' ); var pluginCspell = require( '@cspell/eslint-plugin' ); var pluginJsdoc = require( 'eslint-plugin-jsdoc' ); +var assign = require( './lib/node_modules/@stdlib/object/assign' ); var stdlibPlugin = require( './lib/node_modules/@stdlib/_tools/eslint/rules/scripts/plugin.js' ); -var allRules = require( './etc/eslint/rules' ); -var overrides = require( './etc/eslint/overrides' ); +var restrictedSyntaxConfig = require( './etc/eslint/overrides/restricted_syntax.js' ); +var rules = require( './etc/eslint/rules' ); // VARIABLES // -var restrictedSyntaxConfig = overrides[ 2 ].rules[ 'no-restricted-syntax' ]; -var nonClonableRules = {}; -var rules = {}; -var val; -var key; -var i; - - -// FUNCTIONS // - -/** -* Tests whether a value can be structured-cloned. -* -* @private -* @param {*} value - value to test -* @returns {boolean} boolean indicating whether the value is clonable -*/ -function isClonable( value ) { - try { - // eslint-disable-next-line n/no-unsupported-features/es-builtins - if ( typeof structuredClone === 'function' ) { - structuredClone( value ); - } - return true; - } catch ( e ) { - return false; - } -} +var globalVars; +var config; // MAIN // -// Separate rules containing non-clonable values: -for ( key in allRules ) { - val = allRules[ key ]; - if ( isClonable( val ) ) { - rules[ key ] = val; - } else { - nonClonableRules[ key ] = val; - } -} +globalVars = assign( {}, globals.browser, globals.node ); +globalVars = assign( globalVars, globals.commonjs, globals.worker ); -module.exports = [ +config = [ // Global ignores: { 'ignores': [ @@ -90,12 +60,7 @@ module.exports = [ 'languageOptions': { 'ecmaVersion': 6, 'sourceType': 'script', - 'globals': { - ...globals.browser, - ...globals.node, - ...globals.commonjs, - ...globals.worker - } + 'globals': globalVars }, 'plugins': { 'n': pluginN, @@ -159,3 +124,8 @@ module.exports = [ } } ]; + + +// EXPORTS // + +module.exports = config; diff --git a/etc/eslint/overrides/index.js b/etc/eslint/overrides/index.js index 8133780619d1..6b45d71d8051 100644 --- a/etc/eslint/overrides/index.js +++ b/etc/eslint/overrides/index.js @@ -21,48 +21,7 @@ // MODULES // var resolve = require( 'path' ).resolve; - - -// VARIABLES // - -var restrictedSyntaxConfig = [ 'error', - 'ArrowFunctionExpression', - 'ClassBody', - 'ClassDeclaration', - 'ClassExpression', - 'DebuggerStatement', - 'ExperimentalRestProperty', - 'ExperimentalSpreadProperty', - - // 'FunctionExpression', - 'LabeledStatement', - 'RestElement', - 'SpreadElement', - 'TaggedTemplateExpression', - 'TemplateElement', - 'TemplateLiteral', - 'WithStatement', - 'YieldExpression', - 'JSXIdentifier', - 'JSXNamespacedName', - 'JSXMemberExpression', - 'JSXEmptyExpression', - 'JSXExpressionContainer', - 'JSXElement', - 'JSXClosingElement', - 'JSXOpeningElement', - 'JSXAttribute', - 'JSXSpreadAttribute', - 'JSXText', - 'ExportDefaultDeclaration', - 'ExportNamedDeclaration', - 'ExportAllDeclaration', - 'ExportSpecifier', - 'ImportDeclaration', - 'ImportSpecifier', - 'ImportDefaultSpecifier', - 'ImportNamespaceSpecifier' -]; +var restrictedSyntaxConfig = require( './restricted_syntax.js' ); // MAIN // diff --git a/etc/eslint/overrides/restricted_syntax.js b/etc/eslint/overrides/restricted_syntax.js new file mode 100644 index 000000000000..d33a9d190bb7 --- /dev/null +++ b/etc/eslint/overrides/restricted_syntax.js @@ -0,0 +1,65 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MAIN // + +var rule = [ 'error', + 'ArrowFunctionExpression', + 'ClassBody', + 'ClassDeclaration', + 'ClassExpression', + 'DebuggerStatement', + 'ExperimentalRestProperty', + 'ExperimentalSpreadProperty', + + // 'FunctionExpression', + 'LabeledStatement', + 'RestElement', + 'SpreadElement', + 'TaggedTemplateExpression', + 'TemplateElement', + 'TemplateLiteral', + 'WithStatement', + 'YieldExpression', + 'JSXIdentifier', + 'JSXNamespacedName', + 'JSXMemberExpression', + 'JSXEmptyExpression', + 'JSXExpressionContainer', + 'JSXElement', + 'JSXClosingElement', + 'JSXOpeningElement', + 'JSXAttribute', + 'JSXSpreadAttribute', + 'JSXText', + 'ExportDefaultDeclaration', + 'ExportNamedDeclaration', + 'ExportAllDeclaration', + 'ExportSpecifier', + 'ImportDeclaration', + 'ImportSpecifier', + 'ImportDefaultSpecifier', + 'ImportNamespaceSpecifier' +]; + + +// EXPORTS // + +module.exports = rule; diff --git a/etc/eslint/rules/stdlib.js b/etc/eslint/rules/stdlib.js index 1c84b79de46c..806287f8f0cc 100644 --- a/etc/eslint/rules/stdlib.js +++ b/etc/eslint/rules/stdlib.js @@ -1,4 +1,4 @@ -/* eslint-disable stdlib/jsdoc-doctest-marker, stdlib/jsdoc-doctest, stdlib/jsdoc-example-require-spacing, stdlib/jsdoc-no-tabs */ +/* eslint-disable stdlib/jsdoc-doctest-marker, stdlib/jsdoc-example-require-spacing, stdlib/jsdoc-no-tabs */ /** * @license Apache-2.0 @@ -1720,7 +1720,7 @@ rules[ 'stdlib/jsdoc-list-item-spacing' ] = 'error'; */ rules[ 'stdlib/jsdoc-markdown-remark' ] = [ 'error', { - 'config': require( './../../remark/.remarkrc.jsdoc.js' ) + 'configPath': require.resolve( './../../remark/.remarkrc.jsdoc.js' ) } ]; diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md index 43dea465621b..c48984158735 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md @@ -40,6 +40,8 @@ var rule = require( '@stdlib/_tools/eslint/rules/jsdoc-markdown-remark' ); [ESLint rule][eslint-rules] to enforce that JSDoc descriptions are valid Markdown (using [remark][remark]). To configure the [remark][remark] processor to lint Markdown, set the `config` option with the lint configuration when enabling the [ESLint rule][eslint-rules]. +To keep rule options serializable (e.g., when using an ESLint flat config), set the `configPath` option to an absolute path for a CommonJS module which exports a remark configuration. + **Bad**: diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/lib/main.js b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/lib/main.js index 2081e110d0dd..88bba2133419 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/lib/main.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/lib/main.js @@ -46,18 +46,19 @@ var rule; */ function main( context ) { var options; + var config; var source; var lint; - var opts; options = context.options[ 0 ]; - opts = {}; - if ( hasOwnProp( options, 'config' ) ) { - opts.config = options.config; + if ( hasOwnProp( options, 'configPath' ) ) { + config = require( options.configPath ); // eslint-disable-line stdlib/no-dynamic-require + } else if ( hasOwnProp( options, 'config' ) ) { + config = options.config; } else { - opts.config = {}; + config = {}; } - lint = remark().use( opts.config ).processSync; + lint = remark().use( config ).processSync; source = context.sourceCode; return { @@ -151,6 +152,9 @@ rule = { 'properties': { 'config': { 'type': 'object' + }, + 'configPath': { + 'type': 'string' } }, 'additionalProperties': false diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js index 639ad6f1c220..5808a8f2deb2 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js @@ -25,8 +25,10 @@ var config = require( './config.js' ); // VARIABLES // +var errors; var invalid; var test; +var code; // MAIN // @@ -34,30 +36,44 @@ var test; // Create our test cases: invalid = []; +code = [ + '/**', + '* Beep boop.', + '*', + '* ## Beep', + '*', + '* ## Beep', + '*/', + 'function beep() {', + ' console.log( "boop" );', + '}' +].join( '\n' ); +errors = [ + { + 'message': '5:1-5:8 error Do not use headings with similar content (3:1) no-duplicate-headings remark-lint', + 'type': null + } +]; + test = { - 'code': [ - '/**', - '* Beep boop.', - '*', - '* ## Beep', - '*', - '* ## Beep', - '*/', - 'function beep() {', - ' console.log( "boop" );', - '}' - ].join( '\n' ), + 'code': code, 'options': [ { 'config': config } ], - 'errors': [ + 'errors': errors +}; +invalid.push( test ); + +test = { + 'code': code, + 'options': [ { - 'message': '5:1-5:8 error Do not use headings with similar content (3:1) no-duplicate-headings remark-lint', - 'type': null + 'configPath': require.resolve( './config.js' ) } - ] + ], + 'errors': errors }; invalid.push( test ); From e4ff82d6cdfa0547d65c7e9c80e25058af20ca1e Mon Sep 17 00:00:00 2001 From: Athan Date: Fri, 21 Aug 2026 02:59:14 -0700 Subject: [PATCH 3/5] test: inline declarations Signed-off-by: Athan --- .../test/fixtures/invalid.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js index 5808a8f2deb2..bafee125621c 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/test/fixtures/invalid.js @@ -23,20 +23,12 @@ var config = require( './config.js' ); -// VARIABLES // - -var errors; -var invalid; -var test; -var code; - - // MAIN // // Create our test cases: -invalid = []; +var invalid = []; -code = [ +var code = [ '/**', '* Beep boop.', '*', @@ -48,14 +40,14 @@ code = [ ' console.log( "boop" );', '}' ].join( '\n' ); -errors = [ +var errors = [ { 'message': '5:1-5:8 error Do not use headings with similar content (3:1) no-duplicate-headings remark-lint', 'type': null } ]; -test = { +var test = { 'code': code, 'options': [ { From 499416d6d2c4062d43032a0a1375064bbb3cfb9e Mon Sep 17 00:00:00 2001 From: Athan Date: Fri, 21 Aug 2026 03:00:44 -0700 Subject: [PATCH 4/5] docs: move content to notes section Signed-off-by: Athan --- .../eslint/rules/jsdoc-markdown-remark/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md index c48984158735..df51757a5808 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md @@ -40,8 +40,6 @@ var rule = require( '@stdlib/_tools/eslint/rules/jsdoc-markdown-remark' ); [ESLint rule][eslint-rules] to enforce that JSDoc descriptions are valid Markdown (using [remark][remark]). To configure the [remark][remark] processor to lint Markdown, set the `config` option with the lint configuration when enabling the [ESLint rule][eslint-rules]. -To keep rule options serializable (e.g., when using an ESLint flat config), set the `configPath` option to an absolute path for a CommonJS module which exports a remark configuration. - **Bad**: @@ -86,6 +84,16 @@ function beep() { +
+ +## Notes + +- To keep rule options serializable (e.g., when using an ESLint flat config), set the `configPath` option to an absolute path for a CommonJS module which exports a remark configuration. + +
+ + +
## Examples From 3f8fbc02eb854741a56c25189a7647c8cd84b2df Mon Sep 17 00:00:00 2001 From: Athan Date: Fri, 21 Aug 2026 03:01:14 -0700 Subject: [PATCH 5/5] docs: update example Signed-off-by: Athan --- .../_tools/eslint/rules/jsdoc-markdown-remark/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md index df51757a5808..d764a819c5ff 100644 --- a/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md +++ b/lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-markdown-remark/README.md @@ -107,12 +107,9 @@ var noDuplicateHeadings = require( 'remark-lint-no-duplicate-headings' ); var rule = require( '@stdlib/_tools/eslint/rules/jsdoc-markdown-remark' ); var linter = new Linter(); -var config; -var result; -var code; // Generate our source code containing a single lint error (a duplicate heading): -code = [ +var code = [ '/**', '* Beep boop.', '*', @@ -160,7 +157,7 @@ code = [ ].join( '\n' ); // Create a remark configuration: -config = { +var config = { 'plugins': [ [ remarkLint ], [ noDuplicateHeadings, [ 'error' ] ] @@ -171,7 +168,7 @@ config = { linter.defineRule( 'jsdoc-markdown-remark', rule ); // Lint the code: -result = linter.verify( code, { +var result = linter.verify( code, { 'rules': { 'jsdoc-markdown-remark': [ 'error', { 'config': config