diff --git a/packages/eslint-plugin-ecmascript-compat/README.md b/packages/eslint-plugin-ecmascript-compat/README.md index 4d45779..15073d8 100644 --- a/packages/eslint-plugin-ecmascript-compat/README.md +++ b/packages/eslint-plugin-ecmascript-compat/README.md @@ -21,7 +21,15 @@ npm install --save-dev eslint-plugin-ecmascript-compat { "plugins": ["ecmascript-compat"], "rules": { - "ecmascript-compat/compat": "error" + "ecmascript-compat/compat": [ + "error", + { + // Optionally, specify provided polyfills + "polyfills": [ + "Array.prototype.includes" + ] + } + ] } } ``` @@ -32,6 +40,10 @@ Chrome >= 64 Firefox >= 58 ``` + + +The optional `polyfills` option is used to specify polyfills that your application loads. These features are therefore considered supported in all browsers. Features that are polyfillable and can be specified here can be found in the [rule schema](https://github.com/robatwilliams/es-compat/blob/master/packages/eslint-plugin-ecmascript-compat/lib/rule.js). + For example usage, see sibling directory: `eslint-plugin-ecmascript-compat-example` diff --git a/packages/eslint-plugin-ecmascript-compat/lib/compatibility.js b/packages/eslint-plugin-ecmascript-compat/lib/compatibility.js index 4834a5a..ddc9b3e 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/compatibility.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/compatibility.js @@ -1,6 +1,6 @@ /* eslint-disable camelcase, no-underscore-dangle */ -function forbiddenFeatures(features, targets) { +function unsupportedFeatures(features, targets) { return features.filter((feature) => !isFeatureSupportedByTargets(feature, targets)); } @@ -54,4 +54,4 @@ function interpretSupport(versionAdded) { }; } -module.exports = { forbiddenFeatures }; +module.exports = { unsupportedFeatures }; diff --git a/packages/eslint-plugin-ecmascript-compat/lib/compatibility.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/compatibility.spec.js index d9bb798..323c6eb 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/compatibility.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/compatibility.spec.js @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ -const { forbiddenFeatures } = require('./compatibility'); +const { unsupportedFeatures } = require('./compatibility'); -it('allows feature in version introduced', () => { +it('supports feature in version introduced', () => { const feature = { compatFeatures: [ { @@ -14,11 +14,11 @@ it('allows feature in version introduced', () => { ], }; - const forbidden = forbiddenFeatures([feature], [{ name: 'chrome', version: '73' }]); - expect(forbidden).toHaveLength(0); + const unsupported = unsupportedFeatures([feature], [{ name: 'chrome', version: '73' }]); + expect(unsupported).toHaveLength(0); }); -it('forbids feature in version before introduced', () => { +it('doesnt support feature in version before introduced', () => { const feature = { compatFeatures: [ { @@ -31,11 +31,11 @@ it('forbids feature in version before introduced', () => { ], }; - const forbidden = forbiddenFeatures([feature], [{ name: 'chrome', version: '72' }]); - expect(forbidden[0]).toBe(feature); + const unsupported = unsupportedFeatures([feature], [{ name: 'chrome', version: '72' }]); + expect(unsupported[0]).toBe(feature); }); -it('allows feature supported by family in unknown version', () => { +it('supports feature supported by family in unknown version', () => { const feature = { compatFeatures: [ { @@ -48,11 +48,11 @@ it('allows feature supported by family in unknown version', () => { ], }; - const forbidden = forbiddenFeatures([feature], [{ name: 'chrome', version: '73' }]); - expect(forbidden).toHaveLength(0); + const unsupported = unsupportedFeatures([feature], [{ name: 'chrome', version: '73' }]); + expect(unsupported).toHaveLength(0); }); -it('forbids feature not supported in any version of family', () => { +it('doesnt support feature not supported in any version of family', () => { const feature = { compatFeatures: [ { @@ -65,11 +65,11 @@ it('forbids feature not supported in any version of family', () => { ], }; - const forbidden = forbiddenFeatures([feature], [{ name: 'chrome', version: '73' }]); - expect(forbidden[0]).toBe(feature); + const unsupported = unsupportedFeatures([feature], [{ name: 'chrome', version: '73' }]); + expect(unsupported[0]).toBe(feature); }); -it('allows feature with unknown support by family', () => { +it('supports feature with unknown support by family', () => { const feature = { compatFeatures: [ { @@ -82,11 +82,11 @@ it('allows feature with unknown support by family', () => { ], }; - const forbidden = forbiddenFeatures([feature], [{ name: 'chrome', version: '73' }]); - expect(forbidden).toHaveLength(0); + const unsupported = unsupportedFeatures([feature], [{ name: 'chrome', version: '73' }]); + expect(unsupported).toHaveLength(0); }); -it('allows feature with omitted support entry for mobile target', () => { +it('supports feature with omitted support entry for mobile target', () => { const feature = { compatFeatures: [ { @@ -99,14 +99,14 @@ it('allows feature with omitted support entry for mobile target', () => { ], }; - const forbidden = forbiddenFeatures( + const unsupported = unsupportedFeatures( [feature], [{ name: 'chrome_android', version: '73' }] ); - expect(forbidden).toHaveLength(0); + expect(unsupported).toHaveLength(0); }); -it('forbids feature supported by one target but not another', () => { +it('doesnt support feature supported by one target but not another', () => { const feature = { compatFeatures: [ { @@ -120,14 +120,14 @@ it('forbids feature supported by one target but not another', () => { ], }; - const forbidden = forbiddenFeatures( + const unsupported = unsupportedFeatures( [feature], [ { name: 'chrome', version: '73' }, { name: 'firefox', version: '50' }, ] ); - expect(forbidden[0]).toBe(feature); + expect(unsupported[0]).toBe(feature); }); it('uses primary support record where multiple ones exist', () => { @@ -154,17 +154,17 @@ it('uses primary support record where multiple ones exist', () => { ], }; - const primaryForbidden = forbiddenFeatures( + const primaryUnsupported = unsupportedFeatures( [feature], [{ name: 'nodejs', version: '7.0.0' }] ); - expect(primaryForbidden).toHaveLength(0); + expect(primaryUnsupported).toHaveLength(0); - const secondaryForbidden = forbiddenFeatures( + const secondaryUnsupported = unsupportedFeatures( [feature], [{ name: 'nodejs', version: '6.7.0' }] ); - expect(secondaryForbidden[0]).toBe(feature); + expect(secondaryUnsupported[0]).toBe(feature); }); it('explains what the problem is when compat feature not found in MDN data', () => { @@ -191,6 +191,6 @@ it('explains what the problem is when compat feature not found in MDN data', () }; expect(() => { - forbiddenFeatures([feature], [{ name: 'chrome', version: '73' }]); + unsupportedFeatures([feature], [{ name: 'chrome', version: '73' }]); }).toThrow("Sparse compatFeatures for rule 'some rule': object,undefined"); }); diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.js index e43bc72..5175f55 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.js @@ -12,6 +12,7 @@ module.exports = [ options: noRestrictedSyntaxPrototypeMethod('Array.prototype.includes', 'ES2016'), }, compatFeatures: [compatData.javascript.builtins.Array.includes], + polyfill: 'Array.prototype.includes', }, { ruleConfig: { definition: esPlugin.rules['no-exponential-operators'] }, diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.spec.js index 306ee91..b36540d 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2016.spec.js @@ -11,7 +11,12 @@ const ruleTester = new RuleTester({ }); ruleTester.run('compat', require('../rule'), { - valid: [], + valid: [ + { + code: 'foo.includes();', + options: [{ polyfills: ['Array.prototype.includes'] }], + }, + ], invalid: [ { code: 'foo.includes();', diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.js index 213bc41..d23864a 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.js @@ -21,14 +21,17 @@ module.exports = [ { ruleConfig: { definition: esPlugin.rules['no-object-getownpropertydescriptors'] }, compatFeatures: [compatData.javascript.builtins.Object.getOwnPropertyDescriptors], + polyfill: 'Object.getOwnPropertyDescriptors', }, { ruleConfig: { definition: esPlugin.rules['no-object-entries'] }, compatFeatures: [compatData.javascript.builtins.Object.entries], + polyfill: 'Object.entries', }, { ruleConfig: { definition: esPlugin.rules['no-object-values'] }, compatFeatures: [compatData.javascript.builtins.Object.values], + polyfill: 'Object.values', }, { // Rule requires the ES2017 global, SharedArrayBuffer @@ -41,6 +44,7 @@ module.exports = [ options: noRestrictedSyntaxPrototypeMethod('String.prototype.padStart', 'ES2017'), }, compatFeatures: [compatData.javascript.builtins.String.padStart], + polyfill: 'String.prototype.padStart', }, { ruleConfig: { @@ -48,6 +52,7 @@ module.exports = [ options: noRestrictedSyntaxPrototypeMethod('String.prototype.padEnd', 'ES2017'), }, compatFeatures: [compatData.javascript.builtins.String.padEnd], + polyfill: 'String.prototype.padEnd', }, { ruleConfig: { definition: esPlugin.rules['no-trailing-function-commas'] }, diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.spec.js index da4daa0..a99a318 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2017.spec.js @@ -18,7 +18,28 @@ const ruleTester = new RuleTester({ }); ruleTester.run('compat', require('../rule'), { - valid: [], + valid: [ + { + code: 'Object.getOwnPropertyDescriptors();', + options: [{ polyfills: ['Object.getOwnPropertyDescriptors'] }], + }, + { + code: 'Object.entries();', + options: [{ polyfills: ['Object.entries'] }], + }, + { + code: 'Object.values();', + options: [{ polyfills: ['Object.values'] }], + }, + { + code: 'str.padStart();', + options: [{ polyfills: ['String.prototype.padStart'] }], + }, + { + code: 'str.padEnd();', + options: [{ polyfills: ['String.prototype.padEnd'] }], + }, + ], invalid: [ { code: 'async function foo() {}', diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.js index 18fe660..4259154 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.js @@ -26,6 +26,7 @@ module.exports = [ options: noRestrictedSyntaxPrototypeMethod('Promise.prototype.finally', 'ES2018'), }, compatFeatures: [compatData.javascript.builtins.Promise.finally], + polyfill: 'Promise.prototype.finally', }, { ruleConfig: { definition: esPlugin.rules['no-regexp-lookbehind-assertions'] }, diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.spec.js index 94ab6da..165e392 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2018.spec.js @@ -11,7 +11,12 @@ const ruleTester = new RuleTester({ }); ruleTester.run('compat', require('../rule'), { - valid: [], + valid: [ + { + code: 'foo.finally();', + options: [{ polyfills: ['Promise.prototype.finally'] }], + }, + ], invalid: [ { code: 'async function* asyncGenerator() {}', diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.js index 2e3e108..9f6758a 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.js @@ -9,15 +9,18 @@ module.exports = [ { ruleConfig: { definition: coreRules.get('no-restricted-syntax'), - options: [ - ...noRestrictedSyntaxPrototypeMethod('Array.prototype.flat', 'ES2019'), - ...noRestrictedSyntaxPrototypeMethod('Array.prototype.flatMap', 'ES2019'), - ], + options: noRestrictedSyntaxPrototypeMethod('Array.prototype.flat', 'ES2019'), }, - compatFeatures: [ - compatData.javascript.builtins.Array.flat, - compatData.javascript.builtins.Array.flatMap, - ], + compatFeatures: [compatData.javascript.builtins.Array.flat], + polyfill: 'Array.prototype.flat', + }, + { + ruleConfig: { + definition: coreRules.get('no-restricted-syntax'), + options: noRestrictedSyntaxPrototypeMethod('Array.prototype.flatMap', 'ES2019'), + }, + compatFeatures: [compatData.javascript.builtins.Array.flatMap], + polyfill: 'Array.prototype.flatMap', }, { ruleConfig: { definition: esPlugin.rules['no-json-superset'] }, @@ -26,6 +29,7 @@ module.exports = [ { ruleConfig: { definition: esPlugin.rules['no-object-fromentries'] }, compatFeatures: [compatData.javascript.builtins.Object.fromEntries], + polyfill: 'Object.fromEntries', }, { ruleConfig: { definition: esPlugin.rules['no-optional-catch-binding'] }, @@ -34,17 +38,33 @@ module.exports = [ { ruleConfig: { definition: coreRules.get('no-restricted-syntax'), - options: [ - ...noRestrictedSyntaxPrototypeMethod('String.prototype.trimLeft', 'ES2019'), - ...noRestrictedSyntaxPrototypeMethod('String.prototype.trimRight', 'ES2019'), - ...noRestrictedSyntaxPrototypeMethod('String.prototype.trimStart', 'ES2019'), - ...noRestrictedSyntaxPrototypeMethod('String.prototype.trimEnd', 'ES2019'), - ], + options: noRestrictedSyntaxPrototypeMethod('String.prototype.trimStart', 'ES2019'), + }, + compatFeatures: [compatData.javascript.builtins.String.trimStart], + polyfill: 'String.prototype.trimStart', + }, + { + ruleConfig: { + definition: coreRules.get('no-restricted-syntax'), + options: noRestrictedSyntaxPrototypeMethod('String.prototype.trimLeft', 'ES2019'), + }, + compatFeatures: [compatData.javascript.builtins.String.trimStart], // not a mistake; trimLeft is an alias for trimStart + polyfill: 'String.prototype.trimLeft', + }, + { + ruleConfig: { + definition: coreRules.get('no-restricted-syntax'), + options: noRestrictedSyntaxPrototypeMethod('String.prototype.trimEnd', 'ES2019'), + }, + compatFeatures: [compatData.javascript.builtins.String.trimEnd], + polyfill: 'String.prototype.trimEnd', + }, + { + ruleConfig: { + definition: coreRules.get('no-restricted-syntax'), + options: noRestrictedSyntaxPrototypeMethod('String.prototype.trimRight', 'ES2019'), }, - compatFeatures: [ - // trimRight and trimLeft are alternates of these - compatData.javascript.builtins.String.trimEnd, - compatData.javascript.builtins.String.trimStart, - ], + compatFeatures: [compatData.javascript.builtins.String.trimEnd], // not a mistake; trimRight is an alias for trimEnd + polyfill: 'String.prototype.trimRight', }, ]; diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.spec.js index cd5444e..fbf0bb3 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2019.spec.js @@ -18,6 +18,34 @@ ruleTester.run('compat', require('../rule'), { { code: 'residentialAddress.flat = flat;', }, + { + code: 'foo.flat();', + options: [{ polyfills: ['Array.prototype.flat'] }], + }, + { + code: 'foo.flatMap();', + options: [{ polyfills: ['Array.prototype.flatMap'] }], + }, + { + code: 'Object.fromEntries();', + options: [{ polyfills: ['Object.fromEntries'] }], + }, + { + code: 'foo.trimLeft();', + options: [{ polyfills: ['String.prototype.trimLeft'] }], + }, + { + code: 'foo.trimRight();', + options: [{ polyfills: ['String.prototype.trimRight'] }], + }, + { + code: 'foo.trimStart();', + options: [{ polyfills: ['String.prototype.trimStart'] }], + }, + { + code: 'foo.trimEnd();', + options: [{ polyfills: ['String.prototype.trimEnd'] }], + }, ], invalid: [ { @@ -46,7 +74,15 @@ ruleTester.run('compat', require('../rule'), { errors: [{ message: "ES2019 method 'String.prototype.trimLeft' is forbidden" }], }, { - code: 'String.prototype.trimEnd;', + code: 'String.prototype.trimRight;', + errors: [{ message: "ES2019 method 'String.prototype.trimRight' is forbidden" }], + }, + { + code: 'String.prototype.trimStart;', + errors: [{ message: "ES2019 method 'String.prototype.trimStart' is forbidden" }], + }, + { + code: 'foo.trimEnd();', errors: [{ message: "ES2019 method 'String.prototype.trimEnd' is forbidden" }], }, ], diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.js index 87d7023..18d91fa 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.js @@ -31,6 +31,7 @@ module.exports = [ { ruleConfig: { definition: esPlugin.rules['no-global-this'] }, compatFeatures: [compatData.javascript.builtins.globalThis], + polyfill: 'globalThis', }, { ruleConfig: { definition: esPlugin.rules['no-import-meta'] }, @@ -52,6 +53,7 @@ module.exports = [ // Rule requires the ES6 global, Promise ruleConfig: { definition: esPlugin.rules['no-promise-all-settled'] }, compatFeatures: [compatData.javascript.builtins.Promise.allSettled], + polyfill: 'Promise.prototype.allSettled', }, { // May false positive for Cache/Clients.matchAll() @@ -60,5 +62,6 @@ module.exports = [ options: noRestrictedSyntaxPrototypeMethod('String.prototype.matchAll', 'ES2020'), }, compatFeatures: [compatData.javascript.builtins.String.matchAll], + polyfill: 'String.prototype.matchAll', }, ]; diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.spec.js index b8b0e7f..d590e45 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2020.spec.js @@ -22,7 +22,20 @@ const ruleTester = new RuleTester({ }); ruleTester.run('compat', require('../rule'), { - valid: [], + valid: [ + { + code: 'globalThis.foo;', + options: [{ polyfills: ['globalThis'] }], + }, + { + code: 'Promise.allSettled();', + options: [{ polyfills: ['Promise.prototype.allSettled'] }], + }, + { + code: 'foo.matchAll();', + options: [{ polyfills: ['String.prototype.matchAll'] }], + }, + ], invalid: [ { code: 'Atomics.notify();', diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.js index 13ddf70..12a555e 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.js @@ -12,6 +12,7 @@ module.exports = [ options: noRestrictedSyntaxPrototypeMethod('String.prototype.replaceAll', 'ES2021'), }, compatFeatures: [compatData.javascript.builtins.String.replaceAll], + polyfill: 'String.prototype.replaceAll', }, { ruleConfig: { definition: esPlugin.rules['no-logical-assignment-operators'] }, @@ -28,6 +29,7 @@ module.exports = [ { ruleConfig: { definition: esPlugin.rules['no-promise-any'] }, compatFeatures: [compatData.javascript.builtins.Promise.any], + polyfill: 'Promise.prototype.any', }, { ruleConfig: { definition: esPlugin.rules['no-weakrefs'] }, diff --git a/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.spec.js b/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.spec.js index a289cf4..d728392 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.spec.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/features/es2021.spec.js @@ -18,7 +18,16 @@ const ruleTester = new RuleTester({ }); ruleTester.run('compat', require('../rule'), { - valid: [], + valid: [ + { + code: '"A dog".replaceAll("dog", "monkey");', + options: [{ polyfills: ['String.prototype.replaceAll'] }], + }, + { + code: 'const a = Promise.any([]);', + options: [{ polyfills: ['Promise.prototype.any'] }], + }, + ], invalid: [ { code: '"A dog".replaceAll("dog", "monkey");', diff --git a/packages/eslint-plugin-ecmascript-compat/lib/rule.js b/packages/eslint-plugin-ecmascript-compat/lib/rule.js index 337d106..f87d00e 100644 --- a/packages/eslint-plugin-ecmascript-compat/lib/rule.js +++ b/packages/eslint-plugin-ecmascript-compat/lib/rule.js @@ -4,17 +4,53 @@ const features = require('./features'); const targetRuntimes = require('./targetRuntimes'); const targets = targetRuntimes(); -const delegateeConfigs = compatibility - .forbiddenFeatures(features, targets) - .map((feature) => feature.ruleConfig); +const unsupportedFeatures = compatibility.unsupportedFeatures(features, targets); module.exports = { meta: { type: 'problem', - schema: [], // no options + schema: [ + { + type: 'object', + properties: { + polyfills: { + type: 'array', + items: { + type: 'string', + enum: [ + 'globalThis', + 'Array.prototype.flat', + 'Array.prototype.flatMap', + 'Array.prototype.includes', + 'Object.entries', + 'Object.fromEntries', + 'Object.getOwnPropertyDescriptors', + 'Object.values', + 'Promise.prototype.allSettled', + 'Promise.prototype.any', + 'Promise.prototype.finally', + 'String.prototype.matchAll', + 'String.prototype.padEnd', + 'String.prototype.padStart', + 'String.prototype.replaceAll', + 'String.prototype.trimEnd', + 'String.prototype.trimLeft', + 'String.prototype.trimRight', + 'String.prototype.trimStart', + ], + }, + }, + }, + additionalProperties: false, + }, + ], }, create(context) { - const visitors = delegateeConfigs.map((config) => createDelegatee(config, context)); + const polyfills = context.options?.[0]?.polyfills ?? []; + + const visitors = unsupportedFeatures + .filter((feature) => !polyfills.includes(feature.polyfill)) + .map((feature) => createDelegatee(feature.ruleConfig, context)); return delegatingVisitor(visitors); },