From 858fc22a57aa64ad0423cedae0c4ed8ffddc39e3 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:12:15 +0100 Subject: [PATCH 1/2] feat: adding sort-scripts-elements rule --- .../eslint-plugin-svelte/src/rule-types.ts | 23 +++- .../src/rules/sort-scripts-elements.ts | 115 ++++++++++++++++++ .../eslint-plugin-svelte/src/utils/rules.ts | 2 + .../default/type-before-import-errors.yaml | 4 + .../default/type-before-import-input.svelte | 8 ++ .../default/type-before-import-output.svelte | 4 + .../valid/default/single-import-input.svelte | 3 + .../tests/src/rules/sort-scripts-elements.ts | 12 ++ 8 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/single-import-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/src/rules/sort-scripts-elements.ts diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index f58b636f9..0565b6759 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -382,6 +382,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/ */ 'svelte/sort-attributes'?: Linter.RuleEntry + /** + * enforce order of elements in Svelte scripts section + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-scripts-elements/ + */ + 'svelte/sort-scripts-sections'?: Linter.RuleEntry /** * enforce consistent spacing after the `` in a HTML comment * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/spaced-html-comment/ @@ -417,9 +422,9 @@ export interface RuleOptions { /* ======= Declarations ======= */ // ----- svelte/@typescript-eslint/no-unnecessary-condition ----- type SvelteTypescriptEslintNoUnnecessaryCondition = []|[{ - + allowConstantLoopConditions?: boolean - + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean }] // ----- svelte/block-lang ----- @@ -442,7 +447,7 @@ type SvelteCommentDirective = []|[{ // ----- svelte/consistent-selector-style ----- type SvelteConsistentSelectorStyle = []|[{ checkGlobal?: boolean - + style?: []|[("class" | "id" | "type")]|[("class" | "id" | "type"), ("class" | "id" | "type")]|[("class" | "id" | "type"), ("class" | "id" | "type"), ("class" | "id" | "type")] }] // ----- svelte/first-attribute-linebreak ----- @@ -537,11 +542,11 @@ type SvelteNoReactiveReassign = []|[{ }] // ----- svelte/no-restricted-html-elements ----- type SvelteNoRestrictedHtmlElements = [(string | { - + elements?: [string, ...(string)[]] message?: string }), ...((string | { - + elements?: [string, ...(string)[]] message?: string }))[]] @@ -557,7 +562,7 @@ type SvelteNoTrailingSpaces = []|[{ }] // ----- svelte/no-unknown-style-directive-property ----- type SvelteNoUnknownStyleDirectiveProperty = []|[{ - + ignoreProperties?: [string, ...(string)[]] ignorePrefixed?: boolean }] @@ -613,6 +618,12 @@ type SvelteSortAttributes = []|[{ })[] alphabetical?: boolean }] +// ----- svelte/sort-scripts-elements ----- +type SvelteSortScriptsElements = []|[{ + order?: (string | [string, ...(string)[]] | { + match: (string | [string, ...(string)[]]) + })[] +}] // ----- svelte/spaced-html-comment ----- type SvelteSpacedHtmlComment = []|[("always" | "never")] // ----- svelte/valid-compile ----- diff --git a/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts b/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts new file mode 100644 index 000000000..bf4f09115 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts @@ -0,0 +1,115 @@ +import { createRule } from '../utils/index.js'; +import type { RuleContext } from '../types.js'; +import type { AST } from 'svelte-eslint-parser'; + +const DEFAULT_ORDER: string[] = [ + 'ImportDeclaration', + 'TSTypeAliasDeclaration', + 'VariableDeclaration', + 'FunctionDeclaration' +]; + +// creating and exporting the rule +export default createRule('sort-scripts-elements', { + meta: { + docs: { + description: 'enforce order of elements in the Svelte scripts sections', + category: 'Stylistic Issues', + recommended: false, + conflictWithPrettier: false + }, + schema: [], + messages: { + scriptsIsNotSorted: 'Scripts is not sorted.' + }, + type: 'layout', + fixable: 'code' + }, + create(context: RuleContext) { + const sourceCode = context.sourceCode; + const MAPPING = new Map(DEFAULT_ORDER.map((value, index) => [value, index])); + + return { + SvelteScriptElement(node: AST.SvelteScriptElement) { + // do not accept scripts without closing tags + if (node.endTag === null) return; + const svelteEndTag: AST.SvelteEndTag = node.endTag; + + // collect scripts statement + const statements = node.body; + // do not sort when we only have one elements + if (statements.length <= 1) return; + + const seens = new Set(); + let current: string = statements[0].type; + + let sortingRequired = false; + for (let i = 0; i < statements.length; i++) { + // if we are the same as previous + if (seens.has(statements[i].type) && statements[i].type === current) { + continue; + } + + if (i > 0) { + const previousOrderIndex = MAPPING.get(current); + const currentOrderIndex = MAPPING.get(statements[i].type); + if (previousOrderIndex !== undefined && currentOrderIndex !== undefined) { + if (previousOrderIndex > currentOrderIndex) { + sortingRequired = true; + break; + } + } + } + + // mark the node type as seen + seens.add(statements[i].type); + current = statements[i].type; + } + + if (!sortingRequired) return; + + context.report({ + node, + messageId: 'scriptsIsNotSorted', + fix: (fixer) => { + const foo: { order: number; text: string }[] = []; + + for (let i = 0; i < statements.length; i++) { + // Getting the comments between previous and current statement + const comments = sourceCode + .getCommentsInside({ + type: 'Null', + range: [ + // Get comment between the last statement and current + i === 0 ? node.startTag.range[1] : statements[i - 1].range[1], + i === statements.length - 1 ? svelteEndTag.range[0] : statements[i].range[0] + ] + }) + .map((comment) => sourceCode.getText(comment)); + + /** + * We need to handle the missing \t\n + * Example when i === 0 we should take the startTag.range[1] up to the first statement if no comments + * Same between statements etc. + * sourceCode.getText(statements[i], (statements[i].range[0]) - (node.startTag.range[1] + 1)) + */ + foo.push({ + order: MAPPING.get(statements[i].type) ?? Number.MAX_SAFE_INTEGER, + text: comments.join('') + sourceCode.getText(statements[i]) + }); + } + + const text = foo + .sort((a, b) => a.order - b.order) + .map(({ text }) => text) + .join('\n'); + + return [ + fixer.replaceTextRange([node.startTag.range[1] + 1, svelteEndTag.range[0] - 1], text) + ]; + } + }); + } + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 4451367c2..f54ec2444 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -75,6 +75,7 @@ import requireStoresInit from '../rules/require-stores-init.js'; import shorthandAttribute from '../rules/shorthand-attribute.js'; import shorthandDirective from '../rules/shorthand-directive.js'; import sortAttributes from '../rules/sort-attributes.js'; +import sortScriptsElements from '../rules/sort-scripts-elements.js'; import spacedHtmlComment from '../rules/spaced-html-comment.js'; import system from '../rules/system.js'; import validCompile from '../rules/valid-compile.js'; @@ -156,6 +157,7 @@ export const rules = [ shorthandAttribute, shorthandDirective, sortAttributes, + sortScriptsElements, spacedHtmlComment, system, validCompile, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-errors.yaml new file mode 100644 index 000000000..1590fd280 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-errors.yaml @@ -0,0 +1,4 @@ +- message: Scripts is not sorted. + line: 1 + column: 1 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte new file mode 100644 index 000000000..dda961c7a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-output.svelte new file mode 100644 index 000000000..e05526c8a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-output.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/single-import-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/single-import-input.svelte new file mode 100644 index 000000000..c388cd9bd --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/single-import-input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/sort-scripts-elements.ts b/packages/eslint-plugin-svelte/tests/src/rules/sort-scripts-elements.ts new file mode 100644 index 000000000..f5da7b042 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/sort-scripts-elements.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/sort-scripts-elements.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion:"latest", + sourceType: 'module' + } +}); + +tester.run('sort-scripts-elements', rule as any, loadTestCases('sort-scripts-elements')); From 05cbda4fdc39f6d68b5e1e3993ef1943ecc6e70e Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:14:33 +0100 Subject: [PATCH 2/2] feat: adding sort-scripts-elements.ts rule Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --- .../src/rules/sort-scripts-elements.ts | 40 ++++++++++++------- .../state-before-interface-errors.yaml | 4 ++ .../state-before-interface-input.svelte | 6 +++ .../state-before-interface-output.svelte | 6 +++ .../default/type-before-import-input.svelte | 2 +- .../default/type-before-import-output.svelte | 4 ++ .../comment-before-import-input.svelte | 4 ++ 7 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/state-before-interface-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/state-before-interface-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/state-before-interface-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/comment-before-import-input.svelte diff --git a/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts b/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts index bf4f09115..09e4099ed 100644 --- a/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts +++ b/packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts @@ -1,10 +1,12 @@ import { createRule } from '../utils/index.js'; import type { RuleContext } from '../types.js'; import type { AST } from 'svelte-eslint-parser'; +import type { Range } from 'svelte-eslint-parser/lib/ast/common.js'; const DEFAULT_ORDER: string[] = [ 'ImportDeclaration', 'TSTypeAliasDeclaration', + 'TSInterfaceDeclaration', 'VariableDeclaration', 'FunctionDeclaration' ]; @@ -74,18 +76,25 @@ export default createRule('sort-scripts-elements', { fix: (fixer) => { const foo: { order: number; text: string }[] = []; + const ranges: Range[] = []; for (let i = 0; i < statements.length; i++) { - // Getting the comments between previous and current statement - const comments = sourceCode - .getCommentsInside({ - type: 'Null', - range: [ - // Get comment between the last statement and current - i === 0 ? node.startTag.range[1] : statements[i - 1].range[1], - i === statements.length - 1 ? svelteEndTag.range[0] : statements[i].range[0] - ] - }) - .map((comment) => sourceCode.getText(comment)); + // the first token start after the previous statement + let start: number; + if (i === 0) { + // special case: first statement, we copy everything from diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/state-before-interface-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/state-before-interface-output.svelte new file mode 100644 index 000000000..cab130e76 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/state-before-interface-output.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte index dda961c7a..860f687f6 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/invalid/default/type-before-import-input.svelte @@ -1,7 +1,7 @@ diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/comment-before-import-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/comment-before-import-input.svelte new file mode 100644 index 000000000..e2192ac97 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/sort-scripts-elements/valid/default/comment-before-import-input.svelte @@ -0,0 +1,4 @@ +