diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index c83d81a4d..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,67 +0,0 @@ -// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has -// been set, it will still error even if it's not applicable to that version number. Since Google sets these -// rules, we have to turn them off ourselves. -var DISABLED_ES6_OPTIONS = { - 'no-var': 'off', - 'prefer-rest-params': 'off' -}; - -var SHAREDB_RULES = { - // Comma dangle is not supported in ES3 - 'comma-dangle': ['error', 'never'], - // We control our own objects and prototypes, so no need for this check - 'guard-for-in': 'off', - // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have - // to override ESLint's default of 0 indents for this. - indent: ['error', 2, { - SwitchCase: 1 - }], - // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code - 'max-len': ['error', - { - code: 120, - tabWidth: 2, - ignoreUrls: true - } - ], - // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused - // variables - 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], - // It's more readable to ensure we only have one statement per line - 'max-statements-per-line': ['error', {max: 1}], - // ES3 doesn't support spread - 'prefer-spread': 'off', - // as-needed quote props are easier to write - 'quote-props': ['error', 'as-needed'], - 'require-jsdoc': 'off', - 'valid-jsdoc': 'off' -}; - -module.exports = { - extends: 'google', - parserOptions: { - ecmaVersion: 3, - allowReserved: true - }, - rules: Object.assign( - {}, - DISABLED_ES6_OPTIONS, - SHAREDB_RULES - ), - ignorePatterns: [ - '/docs/' - ], - overrides: [ - { - files: ['examples/counter-json1-vite/*.js'], - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - allowReserved: false - }, - rules: { - quotes: ['error', 'single'] - } - } - ] -}; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0bea60da..6a45c29b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,9 +15,9 @@ jobs: strategy: matrix: node: - - 18 - 20 - 22 + - 24 services: mongodb: image: mongo:4.4 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..9cfb747e5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,87 @@ +const js = require("@eslint/js"); +const { + defineConfig, + globalIgnores, +} = require("eslint/config"); +const eslintConfigGoogle = require('eslint-config-google'); + +// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has +// been set, it will still error even if it's not applicable to that version number. Since Google sets these +// rules, we have to turn them off ourselves. +var DISABLED_ES6_OPTIONS = { + 'no-var': 'off', + 'prefer-rest-params': 'off' +}; + +var SHAREDB_RULES = { + // Comma dangle is not supported in ES3 + 'comma-dangle': ['error', 'never'], + // We control our own objects and prototypes, so no need for this check + 'guard-for-in': 'off', + // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have + // to override ESLint's default of 0 indents for this. + indent: ['error', 2, { + SwitchCase: 1 + }], + 'linebreak-style': 'off', + // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code + 'max-len': ['error', + { + code: 120, + tabWidth: 2, + ignoreUrls: true + } + ], + // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused + // variables + 'no-unused-vars': ['error', { + vars: 'all', + args: 'after-used', + // This can be removed once the minimum ES version is ES2019 or newer, and catch statements + // are updated to use optional catch binding. + caughtErrors: 'none', + }], + // It's more readable to ensure we only have one statement per line + 'max-statements-per-line': ['error', { max: 1 }], + // ES3 doesn't support spread + 'prefer-spread': 'off', + // as-needed quote props are easier to write + 'quote-props': ['error', 'as-needed'], + 'require-jsdoc': 'off', + 'valid-jsdoc': 'off', +}; + +module.exports = defineConfig([ + { + extends: [eslintConfigGoogle], + ignores: ['eslint.config.js'], + + languageOptions: { + ecmaVersion: 3, + sourceType: "commonjs", + + parserOptions: { + allowReserved: true, + }, + }, + + rules: Object.assign({}, DISABLED_ES6_OPTIONS, SHAREDB_RULES), + }, + globalIgnores(["docs/"]), + { + files: ["examples/counter-json1-vite/*.js"], + + languageOptions: { + ecmaVersion: 2015, + sourceType: "module", + + parserOptions: { + allowReserved: false, + }, + }, + + rules: { + quotes: ["error", "single"], + }, + } +]); diff --git a/package.json b/package.json index dd93b9422..74425271a 100644 --- a/package.json +++ b/package.json @@ -11,18 +11,18 @@ "ot-json0": "^1.1.0" }, "devDependencies": { - "chai": "^4.3.7", - "coveralls": "^3.1.1", - "eslint": "^8.47.0", + "@eslint/js": "^10.0.1", + "chai": "^6.2.2", + "eslint": "^10.1.0", "eslint-config-google": "^0.14.0", - "mocha": "^10.2.0", - "nyc": "^15.1.0", + "mocha": "^11.7.5", + "nyc": "^18.0.0", "ot-json0-v2": "https://github.com/ottypes/json0#90a3ae26364c4fa3b19b6df34dad46707a704421", "ot-json1": "^1.0.2", "rich-text": "^4.1.0", "sharedb-legacy": "npm:sharedb@1.1.0", - "sinon": "^15.2.0", - "sinon-chai": "^3.7.0" + "sinon": "^21.0.3", + "sinon-chai": "^4.0.1" }, "files": [ "lib/", @@ -34,8 +34,8 @@ "docs:start": "cd docs && bundle exec jekyll serve --livereload", "test": "mocha", "test-cover": "nyc --temp-dir=coverage -r text -r lcov npm test", - "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'", - "lint:fix": "npm run lint -- --fix" + "lint": "eslint", + "lint:fix": "eslint --fix" }, "repository": { "type": "git", diff --git a/test/.jshintrc b/test/.jshintrc deleted file mode 100644 index 1cb88cb41..000000000 --- a/test/.jshintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "node": true, - "laxcomma": true, - "eqnull": true, - "eqeqeq": true, - "indent": 2, - "newcap": true, - "quotmark": "single", - "undef": true, - "trailing": true, - "shadow": true, - "expr": true, - "boss": true, - "globals": { - "describe": false, - "it": false, - "before": false, - "after": false, - "beforeEach": false, - "afterEach": false - } -} diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 4cf690ff9..848db99ca 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -13,6 +13,9 @@ module.exports = function(options) { }); afterEach(function() { + if (sinon.clock) { + sinon.clock.uninstall(); + } sinon.restore(); }); @@ -517,7 +520,7 @@ function commonTests(options) { }); it('pollDebounce option reduces subsequent poll interval', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.db.canPollDoc = function() { return false; @@ -539,7 +542,7 @@ function commonTests(options) { connection.get('items', i.toString()).on('error', done).create({}, function(err) { if (err) return done(err); counter++; - if (counter === 9) clock.tickAsync(10000); + if (counter === 9) clock.tick(10000); }); } } @@ -561,12 +564,12 @@ function commonTests(options) { // event firing the first time, while sharedb is definitely // debouncing connection.get('items', '0').on('error', done).create({}, function() { - clock.tickAsync(3000); + clock.tick(3000); }); }); it('db.pollDebounce option reduces subsequent poll interval', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.db.canPollDoc = function() { return false; @@ -589,7 +592,7 @@ function commonTests(options) { connection.get('items', i.toString()).on('error', done).create({}, function(err) { if (err) return done(err); counter++; - if (counter === 9) clock.tickAsync(10000); + if (counter === 9) clock.tick(10000); }); } } @@ -611,12 +614,12 @@ function commonTests(options) { // event firing the first time, while sharedb is definitely // debouncing connection.get('items', '0').on('error', done).create({}, function() { - clock.tickAsync(3000); + clock.tick(3000); }); }); it('pollInterval updates a subscribed query after an unpublished create', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.suppressPublish = true; var query = connection.createSubscribeQuery( @@ -626,7 +629,7 @@ function commonTests(options) { function(err) { if (err) return done(err); connection.get('dogs', 'fido').on('error', done).create({}, function() { - clock.tickAsync(51); + clock.tick(51); }); } ); @@ -638,7 +641,7 @@ function commonTests(options) { }); it('db.pollInterval updates a subscribed query after an unpublished create', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.suppressPublish = true; this.backend.db.pollDebounce = 0; @@ -646,7 +649,7 @@ function commonTests(options) { var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) { if (err) return done(err); connection.get('dogs', 'fido').on('error', done).create({}, function() { - clock.tickAsync(51); + clock.tick(51); }); }); query.on('error', done); @@ -657,7 +660,7 @@ function commonTests(options) { }); it('pollInterval captures additional unpublished creates', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.suppressPublish = true; var count = 0; @@ -667,7 +670,7 @@ function commonTests(options) { var doc = connection.get('dogs', count.toString()).on('error', done); doc.create({}, function(e) { if (e) return done(e); - clock.tickAsync(2000); + clock.tick(2000); }); }); query.on('error', done); @@ -677,10 +680,10 @@ function commonTests(options) { var doc = connection.get('dogs', count.toString()).on('error', done); doc.create({}, function(e) { if (e) return done(e); - clock.tickAsync(10000); + clock.tick(10000); }); }); - clock.tickAsync(1); + clock.tick(1); }); it('query extra is returned to client', function(done) { diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 5ba12ee8d..1beaaf4fd 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -2,6 +2,7 @@ var Backend = require('../../lib/backend'); var expect = require('chai').expect; var MemoryDb = require('../../lib/db/memory'); var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); +var util = require('../util'); var sinon = require('sinon'); var async = require('async'); @@ -17,12 +18,12 @@ describe('SnapshotTimestampRequest', function() { var ONE_DAY = 1000 * 60 * 60 * 24; beforeEach(function() { - clock = sinon.useFakeTimers(day1); + clock = util.useFakeTimers(day1); backend = new Backend(); }); afterEach(function(done) { - clock.uninstall(); + sinon.restore(); backend.close(done); }); diff --git a/test/setup.js b/test/setup.js index d00aeee38..4d3aca185 100644 --- a/test/setup.js +++ b/test/setup.js @@ -3,7 +3,7 @@ var sinon = require('sinon'); var sinonChai = require('sinon-chai'); var chai = require('chai'); -chai.use(sinonChai); +chai.use(sinonChai.default); if (process.env.LOGGING !== 'true') { // Silence the logger for tests by setting all its methods to no-ops @@ -15,5 +15,8 @@ if (process.env.LOGGING !== 'true') { } afterEach(function() { + if (sinon.clock) { + sinon.clock.uninstall(); + } sinon.restore(); }); diff --git a/test/util.js b/test/util.js index 17fa6500f..996c5c42b 100644 --- a/test/util.js +++ b/test/util.js @@ -1,3 +1,4 @@ +var sinon = require('sinon'); exports.sortById = function(docs) { return docs.slice().sort(function(a, b) { @@ -15,6 +16,25 @@ exports.pluck = function(docs, key) { return values; }; +/** + * @param {Parameters[0]} config + * @see {@link sinon.useFakeTimers} + * @see {@link https://github.com/sinonjs/fake-timers#clocksettickmodemode} + */ +exports.useFakeTimers = function(config) { + if (config == null) { + config = {}; + } else if (typeof config !== 'object') { + // Number or Date + config = {now: config}; + } + if (config.toFake == null) { + config.toFake = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date']; + } + var clock = sinon.useFakeTimers(config); + return clock; +}; + // Wrap a done function to call back only after a specified number of calls. // For example, `var callbackAfter = callAfter(1, callback)` means that if // `callbackAfter` is called once, it won't call back. If it is called twice