eslint-plugin-greasemonkey 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,7 +46,7 @@ export default [
46
46
  rules: {
47
47
  ...greasemonkey.configs.recommended.rules,
48
48
  'greasemonkey/no-unused-resources': 'off', // selectively disable rule
49
- 'greasemonkey/meta-spacing': ['error', 2] // selectively tweak rule
49
+ 'greasemonkey/meta-spacing': ['error', { spaces: 2 }] // selectively tweak rule
50
50
  }
51
51
  }
52
52
  ]
@@ -73,7 +73,7 @@ Avaialble bundles: `recommended`, `strict`, `tampermonkey`, `violentmonkey`, `gr
73
73
  <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-e8357a?style=flat-square" align="middle"></a>
74
74
  <a title="Doesn't prevent bugs" href="#meta-order">
75
75
  <img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
76
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
76
+ <a title="Accepts array w/ options" href="#usage">
77
77
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
78
78
  <a title="Copy rule URL" target="_blank"
79
79
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23meta-order">
@@ -86,7 +86,7 @@ Avaialble bundles: `recommended`, `strict`, `tampermonkey`, `violentmonkey`, `gr
86
86
 
87
87
  #### Options
88
88
 
89
- - `string[]` (default `[]`) – metadata keys in custom order
89
+ - `order` (default `[]`) – metadata keys in custom order
90
90
 
91
91
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
92
92
 
@@ -148,7 +148,7 @@ If you don't care about the order of metadata fields.
148
148
  <img height=18 alt="[Auto-fixable]" src="https://img.shields.io/badge/Auto--fixable-e8357a?style=flat-square" align="middle"></a>
149
149
  <a title="Doesn't prevent bugs" href="#meta-spacing">
150
150
  <img height=18 alt="[Stylistic]" src="https://img.shields.io/badge/Stylistic-af68ff?style=flat-square" align="middle"></a>
151
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
151
+ <a title="Accepts array w/ options" href="#usage">
152
152
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
153
153
  <a title="Copy rule URL" target="_blank"
154
154
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23meta-spacing">
@@ -161,12 +161,12 @@ If you don't care about the order of metadata fields.
161
161
 
162
162
  #### Options
163
163
 
164
- - `number` (default `3`) – minimum spaces after the longest key
164
+ - `spaces` (default `3`) – minimum spaces after the longest key
165
165
 
166
166
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
167
167
 
168
168
  ```diff
169
- /* eslint greasemonkey/meta-spacing: ['error', 1] */
169
+ /* eslint greasemonkey/meta-spacing: ['error', { spaces: 1 }] */
170
170
 
171
171
  // ==UserScript==
172
172
  ! // @name My Script
@@ -178,7 +178,7 @@ If you don't care about the order of metadata fields.
178
178
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
179
179
 
180
180
  ```diff
181
- /* eslint greasemonkey/meta-spacing: ['error', 1] */
181
+ /* eslint greasemonkey/meta-spacing: ['error', { spaces: 1 }] */
182
182
 
183
183
  // ==UserScript==
184
184
  - // @name My Script
@@ -276,11 +276,11 @@ If you intentionally repeat single-value keys.
276
276
 
277
277
  </details>
278
278
 
279
- <h3 id="no-invalid-grants">
279
+ <h3 id="no-invalid-grants"><a deprecated id="valid-grants"></a>
280
280
  <code>no-invalid-grants</code>
281
281
  <a title="Included in recommended config" href="#usage">
282
282
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
283
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
283
+ <a title="Accepts array w/ options" href="#usage">
284
284
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
285
285
  <a title="Copy rule URL" target="_blank"
286
286
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23no-invalid-grants">
@@ -363,7 +363,7 @@ If you don't want `@grant` validation at all. For individual uncovered grants, p
363
363
  <code>no-invalid-headers</code>
364
364
  <a title="Included in recommended config" href="#usage">
365
365
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
366
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
366
+ <a title="Accepts array w/ options" href="#usage">
367
367
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
368
368
  <a title="Copy rule URL" target="_blank"
369
369
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23no-invalid-headers">
@@ -376,7 +376,7 @@ If you don't want `@grant` validation at all. For individual uncovered grants, p
376
376
 
377
377
  #### Options
378
378
 
379
- - `target` (default `'all'`) – manager(s) whose headers to validate against (`'all'`, `'tampermonkey'`, `'violentmonkey'`, `'greasemonkey'`), as a single string or array
379
+ - `target` (default `'all'`) – manager(s) whose headers to validate against (`'all'`, `'tampermonkey'`, `'violentmonkey'`, `'greasemonkey'`, `'scriptcat'`), as a single string or array
380
380
  - `allowedHeaders` (default `[]`) – additional headers to allow (strings or regexes), as a single value or array
381
381
 
382
382
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
@@ -701,7 +701,7 @@ If you rely on `@include` patterns that cannot be expressed using `@match`.
701
701
 
702
702
  </details>
703
703
 
704
- <h3 id="require-blank-line-after-meta-block">
704
+ <h3 id="require-blank-line-after-meta-block"><a deprecated id="require-meta-newline"></a>
705
705
  <code>require-blank-line-after-meta-block</code>
706
706
  <a title="Included in recommended config" href="#usage">
707
707
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
@@ -822,7 +822,7 @@ If your target manager does not fall back to `@namespace` when a homepage key is
822
822
  <code>require-matching-localized-fields</code>
823
823
  <a title="Included in recommended config" href="#usage">
824
824
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
825
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
825
+ <a title="Accepts array w/ options" href="#usage">
826
826
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
827
827
  <a title="Copy rule URL" target="_blank"
828
828
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23require-matching-localized-fields">
@@ -891,7 +891,7 @@ If you intentionally provide localized names without descriptions (or vice versa
891
891
  <code>require-required-fields</code>
892
892
  <a title="Included in recommended config" href="#usage">
893
893
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
894
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
894
+ <a title="Accepts array w/ options" href="#usage">
895
895
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
896
896
  <a title="Copy rule URL" target="_blank"
897
897
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23require-required-fields">
@@ -900,16 +900,17 @@ If you intentionally provide localized names without descriptions (or vice versa
900
900
 
901
901
  <details>
902
902
 
903
- <summary>Requires essential metadata fields (<code>@name</code>, <code>@version</code> by default) and at least one <code>@match</code> or <code>@include</code></summary>
903
+ <summary>Requires essential metadata fields (<code>@name</code>, <code>@version</code> and at least one <code>@match</code> or <code>@include</code>by default).</summary>
904
904
 
905
905
  #### Options
906
906
 
907
907
  - `fields` (default `['name', 'version']`) – override the required field list
908
+ - `requireMatchOrInclude` (default `true`) – require `@match` or `@include`
908
909
 
909
910
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
910
911
 
911
912
  ```diff
912
- /* eslint greasemonkey/require-required-fields: ['error', { fields: ['name', 'version', 'match'] }] */
913
+ /* eslint greasemonkey/require-required-fields: ['error', { fields: ['name', 'version', 'license'] }] */
913
914
 
914
915
  // ==UserScript==
915
916
  // @name My Script
@@ -919,11 +920,12 @@ If you intentionally provide localized names without descriptions (or vice versa
919
920
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
920
921
 
921
922
  ```diff
922
- /* eslint greasemonkey/require-required-fields: ['error', { fields: ['name', 'version', 'match'] }] */
923
+ /* eslint greasemonkey/require-required-fields: ['error', { fields: ['name', 'version', 'license'] }] */
923
924
 
924
925
  // ==UserScript==
925
926
  // @name My Script
926
927
  + // @version 1.0.0
928
+ + // @license MIT
927
929
  + // @match https://example.com/*
928
930
  // ==/UserScript==
929
931
  ```
@@ -934,7 +936,7 @@ If you use a subset of metadata fields that doesn't match the convention.
934
936
 
935
937
  #### Introduced in
936
938
 
937
- <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-greasemonkey/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026)
939
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-greasemonkey/releases/tag/v1.0.0">v1.0.0</a> (September 9, 2026) as `require-required-fields`
938
940
 
939
941
  #### See also
940
942
 
@@ -1014,7 +1016,7 @@ If you intentionally use a different prefix style.
1014
1016
  <code>require-sri-hashing</code>
1015
1017
  <a title="Only included in strict config" href="#usage">
1016
1018
  <img height=18 alt="[Strict only]" src="https://img.shields.io/badge/Strict_only-ff8c42?style=flat-square" align="middle"></a>
1017
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
1019
+ <a title="Accepts array w/ options" href="#usage">
1018
1020
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1019
1021
  <a title="Copy rule URL" target="_blank"
1020
1022
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23require-sri-hashing">
@@ -1180,9 +1182,9 @@ If you intentionally name your userscripts something other than `*.user.js`.
1180
1182
  <code>valid-field-values</code>
1181
1183
  <a title="Included in recommended config" href="#usage">
1182
1184
  <img height=18 alt="[Recommended]" src="https://img.shields.io/badge/Recommended-f1f20a?style=flat-square" align="middle"></a>
1183
- <a title="Accepts array w/ options" target="_blank" href="https://eslint.org/docs/latest/extend/custom-rules#option-defaults">
1185
+ <a title="Accepts array w/ options" href="#usage">
1184
1186
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1185
- <a title="Copy rule URL" target="_blank"
1187
+ <a title="Copy rule URL" target="_blank"
1186
1188
  href="https://copy-to-clipboard.onrender.com/copy/https%3A%2F%2Fcodeberg.org%2Fadamlui%2Feslint-plugin-greasemonkey%2F%23valid-field-values">
1187
1189
  <img height=16 width=21 alt="[Copy rule URL]" src="https://api.iconify.design/mdi/content-copy.svg?color=%23cccccc" align="middle"></a>
1188
1190
  </h3>
@@ -1194,6 +1196,8 @@ If you intentionally name your userscripts something other than `*.user.js`.
1194
1196
  #### Options
1195
1197
 
1196
1198
  - `versionType` (default `'semver'`) – version format to enforce (`'semver'`, `'date'`)
1199
+ - `allowFutureDate` (default `false`) – allow `@version` dates later than the current date
1200
+ - `timezone` (default `''`) – <a target="_blank" href="https://data.iana.org/time-zones/tzdb/zone1970.tab">IANA timezone</a> used to determine the current date (when omitted, only year/month are enforced)
1197
1201
 
1198
1202
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
1199
1203
 
@@ -1238,6 +1242,8 @@ If you use a custom versioning scheme or URLs that aren't standard.
1238
1242
 
1239
1243
  </div>
1240
1244
 
1245
+ </details>
1246
+
1241
1247
  ---
1242
1248
 
1243
1249
  ## Shield embed
@@ -1303,7 +1309,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
1303
1309
  <div align="center">
1304
1310
 
1305
1311
  <picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.staticdelivr.com/gl/adamlui/js-utils/6b0d399/assets/images/icons/tag/white/icon16.svg"><img height=14 src="https://cdn.staticdelivr.com/gl/adamlui/js-utils/6b0d399/assets/images/icons/tag/dark-gray/icon16.svg"></picture>
1306
- <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-greasemonkey/releases"><b>Latest releases</b></a> / <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-greasemonkey/issues/new">Report a bug</a> / <a target="_blank" href="https://npmx.dev/org/adamlui">More packages by author</a> / [Back to top ↑](#top)
1312
+ <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-greasemonkey/releases"><b>Latest releases</b></a> / <a target="_blank" href="https://codeberg.org/adamlui/eslint-plugin-greasemonkey/issues/new">Report a bug</a> / <a target="_blank" href="https://www.npmjs.com/~adamlui">More packages by author</a> / [Back to top ↑](#top)
1307
1313
 
1308
1314
  </div>
1309
1315
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-greasemonkey",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "ESLint rules for Greasemonkey/Tampermonkey userscripts",
5
5
  "type": "module",
6
6
  "engines": {
@@ -63,7 +63,7 @@
63
63
  "eslint": ">=9.0.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@e18e/eslint-plugin": "^0.8.0",
66
+ "@e18e/eslint-plugin": "^0.8.1",
67
67
  "@eslint/js": "^10.0.1",
68
68
  "@eslint/json": "^2.1.0",
69
69
  "@eslint/markdown": "^8.0.3",
@@ -1,6 +1,8 @@
1
+ // Delete a line to end support
2
+
1
3
  export const rules = { // old: new
2
- 'require-meta-newline': 'require-blank-line-after-meta-block', // 2026-9-13
3
- 'valid-grants': 'no-invalid-grants'
4
+ 'valid-grants': 'no-invalid-grants', // 2026-9-16 announced removal @ v2
5
+ 'require-meta-newline': 'require-blank-line-after-meta-block' // 2026-9-13 announced removal @ v2
4
6
  }
5
7
 
6
8
  export default { rules }
@@ -6,17 +6,23 @@ export const universalHeaders = new Set([
6
6
  ])
7
7
 
8
8
  export const managerHeaders = new Map([
9
- ['inject-into', ['violentmonkey']],
10
- ['top-level-await', ['violentmonkey']],
11
- ['user-agent', ['tampermonkey']],
12
- ['run-in', ['tampermonkey']],
9
+ ['run-in', ['tampermonkey', 'scriptcat']],
10
+ ['nocompat', ['tampermonkey']],
13
11
  ['sandbox', ['tampermonkey']],
12
+ ['user-agent', ['tampermonkey']],
14
13
  ['webRequest', ['tampermonkey']],
15
- ['nocompat', ['tampermonkey']]
14
+ ['inject-into', ['violentmonkey', 'scriptcat']],
15
+ ['top-level-await', ['violentmonkey']],
16
+ ['background', ['scriptcat']],
17
+ ['crontab', ['scriptcat']],
18
+ ['definition', ['scriptcat']],
19
+ ['early-start', ['scriptcat']],
20
+ ['require-css', ['scriptcat']],
21
+ ['storageName', ['scriptcat']]
16
22
  ])
17
23
 
18
24
  export const localizedHeaders = ['name', 'description', 'antifeature']
19
- export const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey']
25
+ export const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat']
20
26
  export const validHeaders = new Map()
21
27
 
22
28
  validHeaders.set('all', new Set([...universalHeaders, ...managerHeaders.keys()]))
package/src/index.js CHANGED
@@ -22,6 +22,8 @@ import requireUpdateDownloadPair from './rules/require-update-download-pair.js'
22
22
  import requireUserJsFilename from './rules/require-user-js-filename.js'
23
23
  import validFieldValues from './rules/valid-field-values.js'
24
24
 
25
+ import { initDeprecatedAlias } from './utils.js'
26
+
25
27
  const plugin = {
26
28
  rules: {
27
29
  'meta-order': metaOrder,
@@ -49,16 +51,8 @@ const plugin = {
49
51
  for (const [ruleId, rule] of Object.entries(plugin.rules))
50
52
  rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-greasemonkey/#${ruleId}`
51
53
 
52
- for (const [oldName, newName] of Object.entries(oldRules)) {
53
- const newRule = plugin.rules[newName]
54
- plugin.rules[oldName] = {
55
- ...newRule,
56
- meta: {
57
- ...newRule.meta, docs: { ...newRule.meta.docs, recommended: false },
58
- deprecated: { message: `Use \`${newName}\` instead.`, replacedBy: [{ rule: { name: newName }}] }
59
- }
60
- }
61
- }
54
+ for (const entry of Object.entries(oldRules))
55
+ initDeprecatedAlias(plugin, entry)
62
56
 
63
57
  plugin.configs = {
64
58
  recommended: {
@@ -93,7 +87,8 @@ for (const manager of ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptc
93
87
  rules: {
94
88
  ...plugin.configs.recommended.rules,
95
89
  'greasemonkey/no-invalid-grants': ['error', { target: manager }],
96
- 'greasemonkey/no-invalid-headers': ['error', { target: manager }]
90
+ 'greasemonkey/no-invalid-headers': ['error', { target: manager }],
91
+ ...(manager == 'tampermonkey' && { 'greasemonkey/require-update-download-pair': 'error' })
97
92
  }
98
93
  }
99
94
 
@@ -35,16 +35,31 @@ export default {
35
35
  },
36
36
  fixable: 'code',
37
37
  schema: [{
38
- type: 'array',
39
- items: { type: 'string' },
40
- description: 'Metadata keys in preferred order; unspecified keys are ignored'
38
+ oneOf: [
39
+ {
40
+ type: 'array', items: { type: 'string' },
41
+ description: 'Metadata keys in preferred order; unspecified keys are ignored'
42
+ },
43
+ {
44
+ type: 'object',
45
+ properties: {
46
+ order: {
47
+ type: 'array', items: { type: 'string' },
48
+ description: 'Metadata keys in preferred order; unspecified keys are ignored'
49
+ }
50
+ },
51
+ additionalProperties: false,
52
+ description: 'Options for metadata key ordering'
53
+ }
54
+ ]
41
55
  }],
42
56
  defaultOptions: [[]],
43
57
  messages: { wrongOrder: '@{{first}} should appear before @{{second}}' }
44
58
  },
45
59
 
46
60
  create(context) {
47
- const custom = context.options[0] ?? [],
61
+ const option = context.options[0] ?? [],
62
+ custom = Array.isArray(option) ? option : option.order ?? [],
48
63
  master = custom.length ? defaultOrder.filter(key => !custom.includes(key)) : defaultOrder
49
64
  return {
50
65
  Program() {
@@ -11,13 +11,32 @@ export default {
11
11
  recommended: true
12
12
  },
13
13
  fixable: 'whitespace',
14
- schema: [{ type: 'number', description: 'Minimum spaces after the longest key (default 3)' }],
14
+ schema: [{
15
+ oneOf: [
16
+ {
17
+ type: 'number',
18
+ description: 'Minimum spaces after the longest key (default 3)'
19
+ },
20
+ {
21
+ type: 'object',
22
+ properties: {
23
+ spaces: {
24
+ type: 'number',
25
+ description: 'Minimum spaces after the longest key (default 3)'
26
+ }
27
+ },
28
+ additionalProperties: false,
29
+ description: 'Options for metadata spacing'
30
+ }
31
+ ]
32
+ }],
15
33
  defaultOptions: [3],
16
34
  messages: { misaligned: 'Metadata values should be aligned at column {{column}}' }
17
35
  },
18
36
 
19
37
  create(context) {
20
- const minSpaces = context.options[0] ?? 3
38
+ const option = context.options[0] ?? 3,
39
+ minSpaces = typeof option == 'number' ? option : option.spaces ?? 3
21
40
  return {
22
41
  Program() {
23
42
  const sourceCode = context.sourceCode,
@@ -15,6 +15,10 @@ export default {
15
15
  type: 'array',
16
16
  items: { type: 'string' },
17
17
  description: 'Required metadata fields (default: name, version)'
18
+ },
19
+ requireMatchOrInclude: {
20
+ type: 'boolean',
21
+ description: 'Require a match or include metadata field (default: true)'
18
22
  }
19
23
  },
20
24
  additionalProperties: false
@@ -25,7 +29,8 @@ export default {
25
29
 
26
30
  create(context) {
27
31
  const options = context.options[0] || {},
28
- requiredFields = options.fields || ['name', 'version']
32
+ requiredFields = options.fields || ['name', 'version'],
33
+ requireMatchOrInclude = options.requireMatchOrInclude ?? true
29
34
  return {
30
35
  Program(node) {
31
36
  const metaEntries = extractMetadata(context.sourceCode),
@@ -33,7 +38,7 @@ export default {
33
38
  for (const field of requiredFields)
34
39
  if (!keys.has(field))
35
40
  context.report({ node, messageId: 'missingField', data: { field }})
36
- if (!metaEntries.some(entry => ['include', 'match'].includes(entry.key)))
41
+ if (requireMatchOrInclude && !['include', 'match'].some(field => keys.has(field)))
37
42
  context.report({ node, messageId: 'missingField', data: { field: 'match or include' }})
38
43
  }
39
44
  }
@@ -28,6 +28,14 @@ export default {
28
28
  type: 'array',
29
29
  items: { type: 'string' },
30
30
  description: 'Allowed @grant values to check against; if empty, no grant validation'
31
+ },
32
+ allowFutureDate: {
33
+ type: 'boolean',
34
+ description: 'Allow @version dates later than the current date (default: false)'
35
+ },
36
+ timezone: {
37
+ type: 'string',
38
+ description: 'IANA timezone for determining the current date (when omitted, only year and month are checked)'
31
39
  }
32
40
  },
33
41
  additionalProperties: false
@@ -35,6 +43,7 @@ export default {
35
43
  defaultOptions: [{}],
36
44
  messages: {
37
45
  invalidVersion: 'Invalid @version: must be a valid {{versionType}} (expected format: {{example}})',
46
+ futureVersion: 'Invalid @version: date cannot be in the future',
38
47
  invalidURL: 'Invalid URL for @{{field}}',
39
48
  invalidGrant: 'Unknown @grant: "{{grant}}"'
40
49
  }
@@ -44,7 +53,15 @@ export default {
44
53
  const options = context.options[0] || {},
45
54
  versionType = options.versionType || 'semver',
46
55
  allowedGrants = options.allowedGrants || [],
56
+ allowFutureDate = options.allowFutureDate ?? false,
57
+ timezone = options.timezone,
47
58
  example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
59
+ const now = timezone
60
+ ? Object.fromEntries(new Intl.DateTimeFormat('en', {
61
+ timeZone: timezone, year: 'numeric', month: 'numeric', day: 'numeric'
62
+ }).formatToParts().filter(({ type }) => ['year', 'month', 'day'].includes(type))
63
+ .map(({ type, value }) => [type, Number(value)]))
64
+ : { year: new Date().getUTCFullYear(), month: new Date().getUTCMonth() +1 }
48
65
  return {
49
66
  Program() {
50
67
  const metaEntries = extractMetadata(context.sourceCode)
@@ -66,9 +83,15 @@ export default {
66
83
  const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
67
84
  : versionType == 'date' ? re.dateVer.test(entry.value)
68
85
  : false
86
+ const [year, month, day] = versionType == 'date' ? entry.value.split('.').map(Number) : []
69
87
  if (!isValid)
70
88
  context.report({
71
89
  node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
90
+ else if (versionType == 'date' && !allowFutureDate && (
91
+ year > now.year || year == now.year && month > now.month
92
+ || timezone && year == now.year && month == now.month && day > now.day
93
+ ))
94
+ context.report({ node: entry.comment, messageId: 'futureVersion' })
72
95
  }
73
96
  }
74
97
  }
package/src/utils.js CHANGED
@@ -1,3 +1,5 @@
1
+ const re = { whitespace: /\s/ }
2
+
1
3
  export function extractMetadata(sourceCode) {
2
4
  const comments = sourceCode.getAllComments(),
3
5
  metaEntries = []
@@ -7,17 +9,55 @@ export function extractMetadata(sourceCode) {
7
9
  if (!inMetaBlock && text == '==UserScript==') inMetaBlock = true
8
10
  else if (inMetaBlock && text == '==/UserScript==') break
9
11
  else if (inMetaBlock && text.startsWith('@')) { // capture entry
10
- const spaceIdx = text.indexOf(' ')
11
- if (spaceIdx > 1) { // need space after key
12
- const key = text.slice(1, spaceIdx).trim(),
13
- value = text.slice(spaceIdx +1).trim()
14
- metaEntries.push({ key, value, comment, line: comment.loc.start.line })
15
- }
12
+ const spaceIdx = text.search(re.whitespace),
13
+ key = spaceIdx == -1 ? text.slice(1).trim() : text.slice(1, spaceIdx).trim(),
14
+ value = spaceIdx == -1 ? '' : text.slice(spaceIdx +1).trim()
15
+ if (key) metaEntries.push({ key, value, comment, line: comment.loc.start.line })
16
16
  }
17
17
  }
18
18
  return metaEntries
19
19
  }
20
20
 
21
+ export function initDeprecatedAlias(plugin, [oldName, alias]) { /* eslint-disable security/detect-object-injection */
22
+ const entries = typeof alias == 'string' ? [[alias, null]] // old: new
23
+ : Array.isArray(alias) ? alias.flatMap(entry =>
24
+ typeof entry == 'string' ? [[entry, null]] : Object.entries(entry)) // old: [new, ...]
25
+ : Object.entries(alias) // old: { new: { old-option: new-option }}
26
+ const newNames = entries.filter(([name]) => name != oldName).map(([name]) => name),
27
+ newRule = plugin.rules[newNames[0] || oldName]
28
+ plugin.rules[oldName] = {
29
+ ...newRule,
30
+ meta: {
31
+ ...newRule.meta,
32
+ schema: typeof alias == 'string' ? newRule.meta.schema : [{ type: 'object' }],
33
+ docs: { ...newRule.meta.docs, recommended: false },
34
+ deprecated: {
35
+ message: `Use ${newNames.map(name => `\`${name}\``).join(', ')} instead.`,
36
+ replacedBy: newNames.map(name => ({ rule: { name }}))
37
+ }
38
+ },
39
+ create(context) {
40
+ const oldOptions = context.options[0]
41
+ return Object.assign({}, ...entries.map(([name, mapping]) => {
42
+ const newRule = plugin.rules[name]
43
+ const options = mapping ? {
44
+ ...oldOptions,
45
+ ...Object.fromEntries(Object.entries(mapping).map(([oldKey, newKey]) => // old: new
46
+ [newKey, oldOptions?.[oldKey] ?? (newKey == 'reverseDefault' ? true : newKey)]))
47
+ } : oldOptions
48
+ const aliasContext = new Proxy({}, {
49
+ get(_, property) {
50
+ if (property == 'options') return [options]
51
+ const val = context[property]
52
+ return typeof val == 'function' ? val.bind(context) : val
53
+ }
54
+ })
55
+ return newRule.create(aliasContext)
56
+ }))
57
+ }
58
+ }
59
+ } /* eslint-enable security/detect-object-injection */
60
+
21
61
  export function resolveTemplate(node) {
22
62
  if (node.type == 'ConditionalExpression') {
23
63
  const branches = [node.consequent, node.alternate].map(resolveTemplate)