diff --git a/modules/eslint-plugin/schematics/ng-add/schema.json b/modules/eslint-plugin/schematics/ng-add/schema.json index 95dc57d9fe..924cd9df70 100644 --- a/modules/eslint-plugin/schematics/ng-add/schema.json +++ b/modules/eslint-plugin/schematics/ng-add/schema.json @@ -7,62 +7,42 @@ "config": { "description": "The config to be used.", "type": "string", - "default": "recommended", + "default": "all", "enum": [ - "recommended", "all", - "store-recommended", - "store-all", - "effects-recommended", - "effects-all", - "component-store-recommended", - "component-store-all", - "operators-recommended", - "operators-all" + "component-store", + "effects", + "operators", + "signals", + "store" ], "x-prompt": { "message": "Which ESLint configuration would you like to use?", "type": "list", "items": [ - { - "value": "recommended", - "label": "recommended" - }, { "value": "all", "label": "all" }, { - "value": "store-recommended", - "label": "store-recommended" - }, - { - "value": "store-all", - "label": "store-all" - }, - { - "value": "effects-recommended", - "label": "effects-recommended" - }, - { - "value": "effects-all", - "label": "effects-all" + "value": "component-store", + "label": "component-store" }, { - "value": "component-store-recommended", - "label": "component-store-recommended" + "value": "effects", + "label": "effects" }, { - "value": "component-store-all", - "label": "component-store-all" + "value": "operators", + "label": "operators" }, { - "value": "operators-recommended", - "label": "operators-recommended" + "value": "signals", + "label": "signals" }, { - "value": "operators-all", - "label": "operators-all" + "value": "store", + "label": "store" } ] } diff --git a/modules/eslint-plugin/schematics/ng-add/schema.ts b/modules/eslint-plugin/schematics/ng-add/schema.ts index 12d33bda60..b147cd10e8 100644 --- a/modules/eslint-plugin/schematics/ng-add/schema.ts +++ b/modules/eslint-plugin/schematics/ng-add/schema.ts @@ -1,10 +1,4 @@ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Schema { - config: - | 'recommended' - | 'strict' - | 'store' - | 'effects' - | 'component-store' - | 'all'; + config: 'all' | 'store' | 'effects' | 'component-store' | 'signals'; } diff --git a/modules/eslint-plugin/scripts/generate-config.ts b/modules/eslint-plugin/scripts/generate-config.ts index 182202f0e6..ed1165e9ad 100644 --- a/modules/eslint-plugin/scripts/generate-config.ts +++ b/modules/eslint-plugin/scripts/generate-config.ts @@ -9,56 +9,24 @@ const prettierConfig = resolveConfig.sync(__dirname); const RULE_MODULE = '@ngrx'; const CONFIG_DIRECTORY = './modules/eslint-plugin/src/configs/'; -writeConfig('recommended', (rule) => !rule.meta.docs?.requiresTypeChecking); writeConfig('all', (_rule) => true); - -writeConfig( - 'store-recommended', - (rule) => - rule.meta.ngrxModule === 'store' && !rule.meta.docs?.requiresTypeChecking -); -writeConfig('store-all', (rule) => rule.meta.ngrxModule === 'store'); - -writeConfig( - 'effects-recommended', - (rule) => - rule.meta.ngrxModule === 'effects' && !rule.meta.docs?.requiresTypeChecking -); -writeConfig('effects-all', (rule) => rule.meta.ngrxModule === 'effects'); - +writeConfig('store', (rule) => rule.meta.ngrxModule === 'store'); +writeConfig('effects', (rule) => rule.meta.ngrxModule === 'effects'); writeConfig( - 'component-store-recommended', - (rule) => - rule.meta.ngrxModule === 'component-store' && - !rule.meta.docs?.requiresTypeChecking -); - -writeConfig( - 'component-store-all', + 'component-store', (rule) => rule.meta.ngrxModule === 'component-store' ); - -writeConfig( - 'operators-recommended', - (rule) => - rule.meta.ngrxModule === 'operators' && - !rule.meta.docs?.requiresTypeChecking -); - -writeConfig('operators-all', (rule) => rule.meta.ngrxModule === 'operators'); +writeConfig('operators', (rule) => rule.meta.ngrxModule === 'operators'); +writeConfig('signals', (rule) => rule.meta.ngrxModule === 'signals'); function writeConfig( configName: | 'all' - | 'recommended' - | 'store-recommended' - | 'store-all' - | 'effects-recommended' - | 'effects-all' - | 'component-store-recommended' - | 'component-store-all' - | 'operators-recommended' - | 'operators-all', + | 'store' + | 'effects' + | 'component-store' + | 'operators' + | 'signals', predicate: (rule: NgRxRuleModule<[], string>) => boolean ) { const rulesForConfig = Object.entries(rulesForGenerate).filter(([_, rule]) => diff --git a/modules/eslint-plugin/spec/rules/signals/signal-state-no-arrays-at-root-level.spec.ts b/modules/eslint-plugin/spec/rules/signals/signal-state-no-arrays-at-root-level.spec.ts new file mode 100644 index 0000000000..3d965a5042 --- /dev/null +++ b/modules/eslint-plugin/spec/rules/signals/signal-state-no-arrays-at-root-level.spec.ts @@ -0,0 +1,34 @@ +import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'; +import * as path from 'path'; +import rule, { + messageId, +} from '../../../src/rules/signals/signal-state-no-arrays-at-root-level'; +import { ruleTester, fromFixture } from '../../utils'; + +type MessageIds = ESLintUtils.InferMessageIdsTypeFromRule; +type Options = readonly ESLintUtils.InferOptionsTypeFromRule[]; +type RunTests = TSESLint.RunTests; + +const valid: () => RunTests['valid'] = () => [ + `const state = signalState({ numbers: [1, 2, 3] });`, + `const state = state([1, 2, 3]);`, +]; + +const invalid: () => RunTests['invalid'] = () => [ + fromFixture(` +const state = signalState([1, 2, 3]); + ~~~~~~~~~ [${messageId}]`), + fromFixture(` +class Fixture { + state = signalState([{ foo: 'bar' }]); + ~~~~~~~~~~~~~~~~ [${messageId}] +}`), + fromFixture(` +const state = signalState([]); + ~~ [${messageId}]`), +]; + +ruleTester().run(path.parse(__filename).name, rule, { + valid: valid(), + invalid: invalid(), +}); diff --git a/modules/eslint-plugin/spec/rules/signals/with-state-no-arrays-at-root-level.spec.ts b/modules/eslint-plugin/spec/rules/signals/with-state-no-arrays-at-root-level.spec.ts new file mode 100644 index 0000000000..bc6ee84a97 --- /dev/null +++ b/modules/eslint-plugin/spec/rules/signals/with-state-no-arrays-at-root-level.spec.ts @@ -0,0 +1,42 @@ +import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'; +import * as path from 'path'; +import rule, { + messageId, +} from '../../../src/rules/signals/with-state-no-arrays-at-root-level'; +import { ruleTester, fromFixture } from '../../utils'; + +type MessageIds = ESLintUtils.InferMessageIdsTypeFromRule; +type Options = readonly ESLintUtils.InferOptionsTypeFromRule[]; +type RunTests = TSESLint.RunTests; + +const valid: () => RunTests['valid'] = () => [ + `const store = withState({ foo: 'bar' })`, + `const Store = signalStore(withState(initialState));`, + ` + const initialState = {}; + const Store = signalStore(withState(initialState)); + `, +]; + +const invalid: () => RunTests['invalid'] = () => [ + fromFixture(` +const store = withState([1, 2, 3]); + ~~~~~~~~~ [${messageId}]`), + fromFixture(` +class Fixture { + store = withState([{ foo: 'bar' }]); + ~~~~~~~~~~~~~~~~ [${messageId}] +}`), + fromFixture(` +const store = withState([]); + ~~ [${messageId}]`), + fromFixture(` + const initialState = []; + const store = withState(initialState); + ~~~~~~~~~~~~ [${messageId}]`), +]; + +ruleTester().run(path.parse(__filename).name, rule, { + valid: valid(), + invalid: invalid(), +}); diff --git a/modules/eslint-plugin/spec/schematics/ng-add.spec.ts b/modules/eslint-plugin/spec/schematics/ng-add.spec.ts index aff025ead1..bb32c18de4 100644 --- a/modules/eslint-plugin/spec/schematics/ng-add.spec.ts +++ b/modules/eslint-plugin/spec/schematics/ng-add.spec.ts @@ -10,7 +10,7 @@ const schematicRunner = new SchematicTestRunner( path.join(__dirname, '../../schematics/collection.json') ); -test('registers the plugin with the recommended config', async () => { +test('registers the plugin with the all config', async () => { const appTree = new UnitTestTree(Tree.empty()); const initialConfig = {}; @@ -21,7 +21,7 @@ test('registers the plugin with the recommended config', async () => { const eslintContent = appTree.readContent(`.eslintrc.json`); const eslintJson = JSON.parse(eslintContent); expect(eslintJson).toEqual({ - overrides: [{ files: ['*.ts'], extends: [`plugin:@ngrx/recommended`] }], + overrides: [{ files: ['*.ts'], extends: [`plugin:@ngrx/all`] }], }); }); @@ -31,7 +31,7 @@ test('registers the plugin with a different config', async () => { const initialConfig = {}; appTree.create('./.eslintrc.json', JSON.stringify(initialConfig, null, 2)); - const options = { config: 'recommended' }; + const options = { config: 'store' }; await schematicRunner.runSchematic('ng-add', options, appTree); const eslintContent = appTree.readContent(`.eslintrc.json`); @@ -110,7 +110,7 @@ test('registers the plugin in overrides when it supports TS', async () => { }, { files: ['*.ts'], - extends: [`plugin:@ngrx/recommended`], + extends: [`plugin:@ngrx/all`], }, ], }); @@ -120,7 +120,7 @@ test('does not add the plugin if it is already added manually', async () => { const appTree = new UnitTestTree(Tree.empty()); const initialConfig = { - extends: ['plugin:@ngrx/recommended'], + extends: ['plugin:@ngrx/all'], }; appTree.create('.eslintrc.json', JSON.stringify(initialConfig, null, 2)); @@ -137,7 +137,7 @@ test('does not add the plugin if it is already added manually as an override', a const initialConfig = { overrides: [ { - extends: ['plugin:@ngrx/recommended'], + extends: ['plugin:@ngrx/all'], }, ], }; diff --git a/modules/eslint-plugin/src/configs/all.json b/modules/eslint-plugin/src/configs/all.json index a3eb49d5f6..51593254ef 100644 --- a/modules/eslint-plugin/src/configs/all.json +++ b/modules/eslint-plugin/src/configs/all.json @@ -13,6 +13,8 @@ "@ngrx/prefer-effect-callback-in-block-statement": "error", "@ngrx/use-effects-lifecycle-interface": "error", "@ngrx/prefer-concat-latest-from": "error", + "@ngrx/signal-state-no-arrays-at-root-level": "error", + "@ngrx/with-state-no-arrays-at-root-level": "error", "@ngrx/avoid-combining-selectors": "error", "@ngrx/avoid-dispatching-multiple-actions-sequentially": "error", "@ngrx/avoid-duplicate-actions-in-reducer": "error", diff --git a/modules/eslint-plugin/src/configs/all.ts b/modules/eslint-plugin/src/configs/all.ts index 482c96671a..50889d2e4d 100644 --- a/modules/eslint-plugin/src/configs/all.ts +++ b/modules/eslint-plugin/src/configs/all.ts @@ -41,6 +41,8 @@ export default ( '@ngrx/prefer-effect-callback-in-block-statement': 'error', '@ngrx/use-effects-lifecycle-interface': 'error', '@ngrx/prefer-concat-latest-from': 'error', + '@ngrx/signal-state-no-arrays-at-root-level': 'error', + '@ngrx/with-state-no-arrays-at-root-level': 'error', '@ngrx/avoid-combining-selectors': 'error', '@ngrx/avoid-dispatching-multiple-actions-sequentially': 'error', '@ngrx/avoid-duplicate-actions-in-reducer': 'error', diff --git a/modules/eslint-plugin/src/configs/component-store-all.json b/modules/eslint-plugin/src/configs/component-store-all.json deleted file mode 100644 index f228983515..0000000000 --- a/modules/eslint-plugin/src/configs/component-store-all.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "plugins": ["@ngrx"], - "rules": { - "@ngrx/avoid-combining-component-store-selectors": "error", - "@ngrx/avoid-mapping-component-store-selectors": "error", - "@ngrx/updater-explicit-return-type": "error" - }, - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" - } -} diff --git a/modules/eslint-plugin/src/configs/component-store-recommended.json b/modules/eslint-plugin/src/configs/component-store.json similarity index 100% rename from modules/eslint-plugin/src/configs/component-store-recommended.json rename to modules/eslint-plugin/src/configs/component-store.json diff --git a/modules/eslint-plugin/src/configs/component-store-all.ts b/modules/eslint-plugin/src/configs/component-store.ts similarity index 94% rename from modules/eslint-plugin/src/configs/component-store-all.ts rename to modules/eslint-plugin/src/configs/component-store.ts index 5e16b4a52c..a0c4afc6d5 100644 --- a/modules/eslint-plugin/src/configs/component-store-all.ts +++ b/modules/eslint-plugin/src/configs/component-store.ts @@ -20,7 +20,7 @@ export default ( }, }, { - name: 'ngrx/component-store-all', + name: 'ngrx/component-store', languageOptions: { parser, }, diff --git a/modules/eslint-plugin/src/configs/effects-recommended.json b/modules/eslint-plugin/src/configs/effects-recommended.json deleted file mode 100644 index 28dc17386a..0000000000 --- a/modules/eslint-plugin/src/configs/effects-recommended.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "plugins": ["@ngrx"], - "rules": { - "@ngrx/no-dispatch-in-effects": "error", - "@ngrx/no-effects-in-providers": "error", - "@ngrx/prefer-action-creator-in-of-type": "error", - "@ngrx/prefer-effect-callback-in-block-statement": "error", - "@ngrx/use-effects-lifecycle-interface": "error" - } -} diff --git a/modules/eslint-plugin/src/configs/effects-recommended.ts b/modules/eslint-plugin/src/configs/effects-recommended.ts deleted file mode 100644 index 26c18b3633..0000000000 --- a/modules/eslint-plugin/src/configs/effects-recommended.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * DO NOT EDIT - * This file is generated - */ - -import type { TSESLint } from '@typescript-eslint/utils'; - -export default ( - plugin: TSESLint.FlatConfig.Plugin, - parser: TSESLint.FlatConfig.Parser -): TSESLint.FlatConfig.ConfigArray => [ - { - name: 'ngrx/base', - languageOptions: { - parser, - sourceType: 'module', - }, - plugins: { - '@ngrx': plugin, - }, - }, - { - name: 'ngrx/effects-recommended', - languageOptions: { - parser, - }, - rules: { - '@ngrx/no-dispatch-in-effects': 'error', - '@ngrx/no-effects-in-providers': 'error', - '@ngrx/prefer-action-creator-in-of-type': 'error', - '@ngrx/prefer-effect-callback-in-block-statement': 'error', - '@ngrx/use-effects-lifecycle-interface': 'error', - }, - }, -]; diff --git a/modules/eslint-plugin/src/configs/effects-all.json b/modules/eslint-plugin/src/configs/effects.json similarity index 79% rename from modules/eslint-plugin/src/configs/effects-all.json rename to modules/eslint-plugin/src/configs/effects.json index c34abc383e..e8dfbaf7fe 100644 --- a/modules/eslint-plugin/src/configs/effects-all.json +++ b/modules/eslint-plugin/src/configs/effects.json @@ -9,10 +9,5 @@ "@ngrx/prefer-action-creator-in-of-type": "error", "@ngrx/prefer-effect-callback-in-block-statement": "error", "@ngrx/use-effects-lifecycle-interface": "error" - }, - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" } } diff --git a/modules/eslint-plugin/src/configs/effects-all.ts b/modules/eslint-plugin/src/configs/effects.ts similarity index 96% rename from modules/eslint-plugin/src/configs/effects-all.ts rename to modules/eslint-plugin/src/configs/effects.ts index 8b28b16bb6..c3b386f40b 100644 --- a/modules/eslint-plugin/src/configs/effects-all.ts +++ b/modules/eslint-plugin/src/configs/effects.ts @@ -20,7 +20,7 @@ export default ( }, }, { - name: 'ngrx/effects-all', + name: 'ngrx/effects', languageOptions: { parser, parserOptions: { diff --git a/modules/eslint-plugin/src/configs/operators-all.json b/modules/eslint-plugin/src/configs/operators-all.json deleted file mode 100644 index e0e9804fe9..0000000000 --- a/modules/eslint-plugin/src/configs/operators-all.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "plugins": ["@ngrx"], - "rules": { - "@ngrx/prefer-concat-latest-from": "error" - }, - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" - } -} diff --git a/modules/eslint-plugin/src/configs/operators-recommended.ts b/modules/eslint-plugin/src/configs/operators-recommended.ts deleted file mode 100644 index 0461d7f9c9..0000000000 --- a/modules/eslint-plugin/src/configs/operators-recommended.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * DO NOT EDIT - * This file is generated - */ - -import type { TSESLint } from '@typescript-eslint/utils'; - -export default ( - plugin: TSESLint.FlatConfig.Plugin, - parser: TSESLint.FlatConfig.Parser -): TSESLint.FlatConfig.ConfigArray => [ - { - name: 'ngrx/base', - languageOptions: { - parser, - sourceType: 'module', - }, - plugins: { - '@ngrx': plugin, - }, - }, - { - name: 'ngrx/operators-recommended', - languageOptions: { - parser, - }, - rules: { - '@ngrx/prefer-concat-latest-from': 'error', - }, - }, -]; diff --git a/modules/eslint-plugin/src/configs/operators-recommended.json b/modules/eslint-plugin/src/configs/operators.json similarity index 100% rename from modules/eslint-plugin/src/configs/operators-recommended.json rename to modules/eslint-plugin/src/configs/operators.json diff --git a/modules/eslint-plugin/src/configs/operators-all.ts b/modules/eslint-plugin/src/configs/operators.ts similarity index 94% rename from modules/eslint-plugin/src/configs/operators-all.ts rename to modules/eslint-plugin/src/configs/operators.ts index 8fd0cb549f..ea6cdbff06 100644 --- a/modules/eslint-plugin/src/configs/operators-all.ts +++ b/modules/eslint-plugin/src/configs/operators.ts @@ -20,7 +20,7 @@ export default ( }, }, { - name: 'ngrx/operators-all', + name: 'ngrx/operators', languageOptions: { parser, }, diff --git a/modules/eslint-plugin/src/configs/recommended.json b/modules/eslint-plugin/src/configs/recommended.json deleted file mode 100644 index 435850f6bc..0000000000 --- a/modules/eslint-plugin/src/configs/recommended.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "plugins": ["@ngrx"], - "rules": { - "@ngrx/avoid-combining-component-store-selectors": "error", - "@ngrx/avoid-mapping-component-store-selectors": "error", - "@ngrx/updater-explicit-return-type": "error", - "@ngrx/no-dispatch-in-effects": "error", - "@ngrx/no-effects-in-providers": "error", - "@ngrx/prefer-action-creator-in-of-type": "error", - "@ngrx/prefer-effect-callback-in-block-statement": "error", - "@ngrx/use-effects-lifecycle-interface": "error", - "@ngrx/prefer-concat-latest-from": "error", - "@ngrx/avoid-combining-selectors": "error", - "@ngrx/avoid-dispatching-multiple-actions-sequentially": "error", - "@ngrx/avoid-duplicate-actions-in-reducer": "error", - "@ngrx/avoid-mapping-selectors": "error", - "@ngrx/good-action-hygiene": "error", - "@ngrx/no-multiple-global-stores": "error", - "@ngrx/no-reducer-in-key-names": "error", - "@ngrx/no-store-subscription": "error", - "@ngrx/no-typed-global-store": "error", - "@ngrx/on-function-explicit-return-type": "error", - "@ngrx/prefer-action-creator-in-dispatch": "error", - "@ngrx/prefer-action-creator": "error", - "@ngrx/prefer-inline-action-props": "error", - "@ngrx/prefer-one-generic-in-create-for-feature-selector": "error", - "@ngrx/prefer-selector-in-select": "error", - "@ngrx/prefix-selectors-with-select": "error", - "@ngrx/select-style": "error", - "@ngrx/use-consistent-global-store-name": "error" - } -} diff --git a/modules/eslint-plugin/src/configs/recommended.ts b/modules/eslint-plugin/src/configs/recommended.ts deleted file mode 100644 index b038463197..0000000000 --- a/modules/eslint-plugin/src/configs/recommended.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * DO NOT EDIT - * This file is generated - */ - -import type { TSESLint } from '@typescript-eslint/utils'; - -export default ( - plugin: TSESLint.FlatConfig.Plugin, - parser: TSESLint.FlatConfig.Parser -): TSESLint.FlatConfig.ConfigArray => [ - { - name: 'ngrx/base', - languageOptions: { - parser, - sourceType: 'module', - }, - plugins: { - '@ngrx': plugin, - }, - }, - { - name: 'ngrx/recommended', - languageOptions: { - parser, - }, - rules: { - '@ngrx/avoid-combining-component-store-selectors': 'error', - '@ngrx/avoid-mapping-component-store-selectors': 'error', - '@ngrx/updater-explicit-return-type': 'error', - '@ngrx/no-dispatch-in-effects': 'error', - '@ngrx/no-effects-in-providers': 'error', - '@ngrx/prefer-action-creator-in-of-type': 'error', - '@ngrx/prefer-effect-callback-in-block-statement': 'error', - '@ngrx/use-effects-lifecycle-interface': 'error', - '@ngrx/prefer-concat-latest-from': 'error', - '@ngrx/avoid-combining-selectors': 'error', - '@ngrx/avoid-dispatching-multiple-actions-sequentially': 'error', - '@ngrx/avoid-duplicate-actions-in-reducer': 'error', - '@ngrx/avoid-mapping-selectors': 'error', - '@ngrx/good-action-hygiene': 'error', - '@ngrx/no-multiple-global-stores': 'error', - '@ngrx/no-reducer-in-key-names': 'error', - '@ngrx/no-store-subscription': 'error', - '@ngrx/no-typed-global-store': 'error', - '@ngrx/on-function-explicit-return-type': 'error', - '@ngrx/prefer-action-creator-in-dispatch': 'error', - '@ngrx/prefer-action-creator': 'error', - '@ngrx/prefer-inline-action-props': 'error', - '@ngrx/prefer-one-generic-in-create-for-feature-selector': 'error', - '@ngrx/prefer-selector-in-select': 'error', - '@ngrx/prefix-selectors-with-select': 'error', - '@ngrx/select-style': 'error', - '@ngrx/use-consistent-global-store-name': 'error', - }, - }, -]; diff --git a/modules/eslint-plugin/src/configs/signals.json b/modules/eslint-plugin/src/configs/signals.json new file mode 100644 index 0000000000..b532bafe0c --- /dev/null +++ b/modules/eslint-plugin/src/configs/signals.json @@ -0,0 +1,8 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@ngrx"], + "rules": { + "@ngrx/signal-state-no-arrays-at-root-level": "error", + "@ngrx/with-state-no-arrays-at-root-level": "error" + } +} diff --git a/modules/eslint-plugin/src/configs/component-store-recommended.ts b/modules/eslint-plugin/src/configs/signals.ts similarity index 63% rename from modules/eslint-plugin/src/configs/component-store-recommended.ts rename to modules/eslint-plugin/src/configs/signals.ts index 3ac0da619c..2331faed81 100644 --- a/modules/eslint-plugin/src/configs/component-store-recommended.ts +++ b/modules/eslint-plugin/src/configs/signals.ts @@ -20,14 +20,18 @@ export default ( }, }, { - name: 'ngrx/component-store-recommended', + name: 'ngrx/signals', languageOptions: { parser, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + project: './tsconfig.json', + }, }, rules: { - '@ngrx/avoid-combining-component-store-selectors': 'error', - '@ngrx/avoid-mapping-component-store-selectors': 'error', - '@ngrx/updater-explicit-return-type': 'error', + '@ngrx/signal-state-no-arrays-at-root-level': 'error', + '@ngrx/with-state-no-arrays-at-root-level': 'error', }, }, ]; diff --git a/modules/eslint-plugin/src/configs/store-all.json b/modules/eslint-plugin/src/configs/store-all.json deleted file mode 100644 index aa9c7fe06f..0000000000 --- a/modules/eslint-plugin/src/configs/store-all.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "plugins": ["@ngrx"], - "rules": { - "@ngrx/avoid-combining-selectors": "error", - "@ngrx/avoid-dispatching-multiple-actions-sequentially": "error", - "@ngrx/avoid-duplicate-actions-in-reducer": "error", - "@ngrx/avoid-mapping-selectors": "error", - "@ngrx/good-action-hygiene": "error", - "@ngrx/no-multiple-global-stores": "error", - "@ngrx/no-reducer-in-key-names": "error", - "@ngrx/no-store-subscription": "error", - "@ngrx/no-typed-global-store": "error", - "@ngrx/on-function-explicit-return-type": "error", - "@ngrx/prefer-action-creator-in-dispatch": "error", - "@ngrx/prefer-action-creator": "error", - "@ngrx/prefer-inline-action-props": "error", - "@ngrx/prefer-one-generic-in-create-for-feature-selector": "error", - "@ngrx/prefer-selector-in-select": "error", - "@ngrx/prefix-selectors-with-select": "error", - "@ngrx/select-style": "error", - "@ngrx/use-consistent-global-store-name": "error" - }, - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" - } -} diff --git a/modules/eslint-plugin/src/configs/store-recommended.ts b/modules/eslint-plugin/src/configs/store-recommended.ts deleted file mode 100644 index 7128563170..0000000000 --- a/modules/eslint-plugin/src/configs/store-recommended.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * DO NOT EDIT - * This file is generated - */ - -import type { TSESLint } from '@typescript-eslint/utils'; - -export default ( - plugin: TSESLint.FlatConfig.Plugin, - parser: TSESLint.FlatConfig.Parser -): TSESLint.FlatConfig.ConfigArray => [ - { - name: 'ngrx/base', - languageOptions: { - parser, - sourceType: 'module', - }, - plugins: { - '@ngrx': plugin, - }, - }, - { - name: 'ngrx/store-recommended', - languageOptions: { - parser, - }, - rules: { - '@ngrx/avoid-combining-selectors': 'error', - '@ngrx/avoid-dispatching-multiple-actions-sequentially': 'error', - '@ngrx/avoid-duplicate-actions-in-reducer': 'error', - '@ngrx/avoid-mapping-selectors': 'error', - '@ngrx/good-action-hygiene': 'error', - '@ngrx/no-multiple-global-stores': 'error', - '@ngrx/no-reducer-in-key-names': 'error', - '@ngrx/no-store-subscription': 'error', - '@ngrx/no-typed-global-store': 'error', - '@ngrx/on-function-explicit-return-type': 'error', - '@ngrx/prefer-action-creator-in-dispatch': 'error', - '@ngrx/prefer-action-creator': 'error', - '@ngrx/prefer-inline-action-props': 'error', - '@ngrx/prefer-one-generic-in-create-for-feature-selector': 'error', - '@ngrx/prefer-selector-in-select': 'error', - '@ngrx/prefix-selectors-with-select': 'error', - '@ngrx/select-style': 'error', - '@ngrx/use-consistent-global-store-name': 'error', - }, - }, -]; diff --git a/modules/eslint-plugin/src/configs/store-recommended.json b/modules/eslint-plugin/src/configs/store.json similarity index 100% rename from modules/eslint-plugin/src/configs/store-recommended.json rename to modules/eslint-plugin/src/configs/store.json diff --git a/modules/eslint-plugin/src/configs/store-all.ts b/modules/eslint-plugin/src/configs/store.ts similarity index 98% rename from modules/eslint-plugin/src/configs/store-all.ts rename to modules/eslint-plugin/src/configs/store.ts index 0dc071c465..0543e60e13 100644 --- a/modules/eslint-plugin/src/configs/store-all.ts +++ b/modules/eslint-plugin/src/configs/store.ts @@ -20,7 +20,7 @@ export default ( }, }, { - name: 'ngrx/store-all', + name: 'ngrx/store', languageOptions: { parser, }, diff --git a/modules/eslint-plugin/src/index.ts b/modules/eslint-plugin/src/index.ts index 6077b3a60d..a72996db40 100644 --- a/modules/eslint-plugin/src/index.ts +++ b/modules/eslint-plugin/src/index.ts @@ -1,27 +1,19 @@ import { rules } from './rules'; import all from './configs/all.json'; -import recommended from './configs/recommended.json'; -import componentStoreRecommended from './configs/component-store-recommended.json'; -import componentStoreAll from './configs/component-store-all.json'; -import effectsRecommended from './configs/effects-recommended.json'; -import effectsAll from './configs/effects-all.json'; -import storeRecommended from './configs/store-recommended.json'; -import storeAll from './configs/store-all.json'; -import operatorsRecommended from './configs/operators-recommended.json'; -import operatorsAll from './configs/operators-all.json'; +import componentStore from './configs/component-store.json'; +import effects from './configs/effects.json'; +import store from './configs/store.json'; +import operators from './configs/operators.json'; +import signals from './configs/signals.json'; export = { configs: { all, - recommended, - 'component-store-recommended': componentStoreRecommended, - 'component-store-all': componentStoreAll, - 'effects-recommended': effectsRecommended, - 'effects-all': effectsAll, - 'store-recommended': storeRecommended, - 'store-all': storeAll, - 'operators-recommended': operatorsRecommended, - 'operators-all': operatorsAll, + 'component-store': componentStore, + effects: effects, + store: store, + operators: operators, + signals: signals, }, rules, }; diff --git a/modules/eslint-plugin/src/rules/signals/signal-state-no-arrays-at-root-level.ts b/modules/eslint-plugin/src/rules/signals/signal-state-no-arrays-at-root-level.ts new file mode 100644 index 0000000000..9d0d6a7aa8 --- /dev/null +++ b/modules/eslint-plugin/src/rules/signals/signal-state-no-arrays-at-root-level.ts @@ -0,0 +1,40 @@ +import { type TSESTree } from '@typescript-eslint/utils'; +import * as path from 'path'; +import { createRule } from '../../rule-creator'; +import { isArrayExpression } from '../../utils'; + +export const messageId = 'signalStateNoArraysAtRootLevel'; + +type MessageIds = typeof messageId; +type Options = readonly []; + +export default createRule({ + name: path.parse(__filename).name, + meta: { + type: 'problem', + ngrxModule: 'signals', + docs: { + description: `signalState should accept a record or dictionary as an input argument.`, + }, + schema: [], + messages: { + [messageId]: `Wrap the array in an record or dictionary.`, + }, + }, + defaultOptions: [], + create: (context) => { + return { + [`CallExpression[callee.name=signalState]`]( + node: TSESTree.CallExpression + ) { + const [argument] = node.arguments; + if (isArrayExpression(argument)) { + context.report({ + node: argument, + messageId, + }); + } + }, + }; + }, +}); diff --git a/modules/eslint-plugin/src/rules/signals/with-state-no-arrays-at-root-level.ts b/modules/eslint-plugin/src/rules/signals/with-state-no-arrays-at-root-level.ts new file mode 100644 index 0000000000..e5c19833b4 --- /dev/null +++ b/modules/eslint-plugin/src/rules/signals/with-state-no-arrays-at-root-level.ts @@ -0,0 +1,50 @@ +import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils'; +import * as path from 'path'; +import { createRule } from '../../rule-creator'; +import { isArrayExpression } from '../../utils'; + +export const messageId = 'withStateNoArraysAtRootLevel'; + +type MessageIds = typeof messageId; +type Options = readonly []; + +export default createRule({ + name: path.parse(__filename).name, + meta: { + type: 'problem', + ngrxModule: 'signals', + docs: { + description: `withState should accept a record or dictionary as an input argument.`, + requiresTypeChecking: true, + }, + schema: [], + messages: { + [messageId]: `Wrap the array in an record or dictionary.`, + }, + }, + defaultOptions: [], + create: (context) => { + return { + [`CallExpression[callee.name=withState]`](node: TSESTree.CallExpression) { + const [argument] = node.arguments; + if (isArrayExpression(argument)) { + context.report({ + node: argument, + messageId, + }); + } else if (argument) { + const services = ESLintUtils.getParserServices(context); + const typeChecker = services.program.getTypeChecker(); + const type = services.getTypeAtLocation(argument); + + if (typeChecker.isArrayType(type)) { + context.report({ + node: argument, + messageId, + }); + } + } + }, + }; + }, +}); diff --git a/modules/eslint-plugin/src/utils/helper-functions/guards.ts b/modules/eslint-plugin/src/utils/helper-functions/guards.ts index 9c5c212cea..11e7d71312 100644 --- a/modules/eslint-plugin/src/utils/helper-functions/guards.ts +++ b/modules/eslint-plugin/src/utils/helper-functions/guards.ts @@ -45,8 +45,8 @@ export const isTSTypeReference = isNodeOfType(AST_NODE_TYPES.TSTypeReference); export const isTSInstantiationExpression = isNodeOfType( AST_NODE_TYPES.TSInstantiationExpression ); -export const isObjectExpression = isNodeOfType(AST_NODE_TYPES.ObjectExpression); export const isProperty = isNodeOfType(AST_NODE_TYPES.Property); +export const isArrayExpression = isNodeOfType(AST_NODE_TYPES.ArrayExpression); export function isIdentifierOrMemberExpression( node: TSESTree.Node diff --git a/modules/eslint-plugin/src/utils/helper-functions/ngrx-modules.ts b/modules/eslint-plugin/src/utils/helper-functions/ngrx-modules.ts index 43e24d52b5..2fd8c6c9f1 100644 --- a/modules/eslint-plugin/src/utils/helper-functions/ngrx-modules.ts +++ b/modules/eslint-plugin/src/utils/helper-functions/ngrx-modules.ts @@ -3,6 +3,7 @@ export const NGRX_MODULE_PATHS = { effects: '@ngrx/effects', store: '@ngrx/store', operators: '@ngrx/operators', + signals: '@ngrx/signals', } as const; export type NGRX_MODULE = keyof typeof NGRX_MODULE_PATHS; diff --git a/modules/eslint-plugin/v9/index.ts b/modules/eslint-plugin/v9/index.ts index 6f0215d28e..abb59d7bf4 100644 --- a/modules/eslint-plugin/v9/index.ts +++ b/modules/eslint-plugin/v9/index.ts @@ -5,16 +5,12 @@ import { name as packageName, version as packageVersion, } from '../package.json'; -import recommended from '../src/configs/recommended'; import all from '../src/configs/all'; -import storeRecommended from '../src/configs/store-recommended'; -import storeAll from '../src/configs/store-all'; -import effectsRecommended from '../src/configs/effects-recommended'; -import effectsAll from '../src/configs/effects-all'; -import componentStoreRecommended from '../src/configs/component-store-recommended'; -import componentStoreAll from '../src/configs/component-store-all'; -import operatorsRecommended from '../src/configs/operators-recommended'; -import operatorsAll from '../src/configs/operators-all'; +import store from '../src/configs/store'; +import effects from '../src/configs/effects'; +import componentStore from '../src/configs/component-store'; +import operators from '../src/configs/operators'; +import signals from '../src/configs/signals'; const meta = { name: packageName, version: packageVersion }; @@ -23,20 +19,12 @@ const tsPlugin: TSESLint.FlatConfig.Plugin = { }; const configs = { - recommended: recommended(tsPlugin, parser), all: all(tsPlugin, parser), - - storeRecommended: storeRecommended(tsPlugin, parser), - storeAll: storeAll(tsPlugin, parser), - - effectsRecommended: effectsRecommended(tsPlugin, parser), - effectsAll: effectsAll(tsPlugin, parser), - - componentStoreRecommended: componentStoreRecommended(tsPlugin, parser), - componentStoreAll: componentStoreAll(tsPlugin, parser), - - operatorsRecommended: operatorsRecommended(tsPlugin, parser), - operatorsAll: operatorsAll(tsPlugin, parser), + store: store(tsPlugin, parser), + effects: effects(tsPlugin, parser), + componentStore: componentStore(tsPlugin, parser), + operators: operators(tsPlugin, parser), + signals: signals(tsPlugin, parser), }; /* diff --git a/projects/ngrx.io/content/guide/eslint-plugin/index.md b/projects/ngrx.io/content/guide/eslint-plugin/index.md index 82182abc4f..b72dcd8f33 100644 --- a/projects/ngrx.io/content/guide/eslint-plugin/index.md +++ b/projects/ngrx.io/content/guide/eslint-plugin/index.md @@ -73,6 +73,13 @@ Instead of manually configuring the rules, there are also [preconfigured configu | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- | --------------- | ------------ | ------------------------- | | [@ngrx/prefer-concat-latest-from](/guide/eslint-plugin/rules/prefer-concat-latest-from) | Use `concatLatestFrom` instead of `withLatestFrom` to prevent the selector from firing until the correct `Action` is dispatched. | problem | Yes | No | Yes | No | +### signals + +| Name | Description | Category | Fixable | Has suggestions | Configurable | Requires type information | +| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------- | ------- | --------------- | ------------ | ------------------------- | +| [@ngrx/signal-state-no-arrays-at-root-level](/guide/eslint-plugin/rules/signal-state-no-arrays-at-root-level) | signalState should accept a record or dictionary as an input argument. | problem | No | No | No | No | +| [@ngrx/with-state-no-arrays-at-root-level](/guide/eslint-plugin/rules/with-state-no-arrays-at-root-level) | withState should accept a record or dictionary as an input argument. | problem | No | No | No | Yes | + ### store | Name | Description | Category | Fixable | Has suggestions | Configurable | Requires type information | @@ -103,17 +110,7 @@ Instead of manually configuring the rules, there are also [preconfigured configu -| Name | -| -------------------------------------------------------------------------------------------------------------------------------------------- | -| [all](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/all.json) | -| [component-store-all](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/component-store-all.json) | -| [component-store-recommended](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/component-store-recommended.json) | -| [effects-all](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/effects-all.json) | -| [effects-recommended](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/effects-recommended.json) | -| [operators-all](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/operators-all.json) | -| [operators-recommended](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/operators-recommended.json) | -| [recommended](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/recommended.json) | -| [store-all](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/store-all.json) | -| [store-recommended](https://github.com/ngrx/platform/blob/main/modules/eslint-plugin/src/configs/store-recommended.json) | +| Name | +| ---- | diff --git a/projects/ngrx.io/content/guide/eslint-plugin/rules/prefer-concat-latest-from.md b/projects/ngrx.io/content/guide/eslint-plugin/rules/prefer-concat-latest-from.md index 7c2f42dcbc..13889b1cda 100644 --- a/projects/ngrx.io/content/guide/eslint-plugin/rules/prefer-concat-latest-from.md +++ b/projects/ngrx.io/content/guide/eslint-plugin/rules/prefer-concat-latest-from.md @@ -1,7 +1,5 @@ # prefer-concat-latest-from -> Required NgRx Version Range: ${meta.version} - Use `concatLatestFrom` instead of `withLatestFrom` to prevent the selector from firing until the correct `Action` is dispatched. - **Type**: problem diff --git a/projects/ngrx.io/content/guide/eslint-plugin/rules/signal-state-no-arrays-at-root-level.md b/projects/ngrx.io/content/guide/eslint-plugin/rules/signal-state-no-arrays-at-root-level.md new file mode 100644 index 0000000000..507888d4bc --- /dev/null +++ b/projects/ngrx.io/content/guide/eslint-plugin/rules/signal-state-no-arrays-at-root-level.md @@ -0,0 +1,12 @@ +# signal-state-no-arrays-at-root-level + +signalState should accept a record or dictionary as an input argument. + +- **Type**: problem +- **Fixable**: No +- **Suggestion**: No +- **Requires type checking**: No +- **Configurable**: No + + + diff --git a/projects/ngrx.io/content/guide/eslint-plugin/rules/with-state-no-arrays-at-root-level.md b/projects/ngrx.io/content/guide/eslint-plugin/rules/with-state-no-arrays-at-root-level.md new file mode 100644 index 0000000000..5157b8d9dc --- /dev/null +++ b/projects/ngrx.io/content/guide/eslint-plugin/rules/with-state-no-arrays-at-root-level.md @@ -0,0 +1,12 @@ +# with-state-no-arrays-at-root-level + +withState should accept a record or dictionary as an input argument. + +- **Type**: problem +- **Fixable**: No +- **Suggestion**: No +- **Requires type checking**: Yes +- **Configurable**: No + + +