Skip to content

Commit af09098

Browse files
committed
refactor: move utility functions to separate file for better organization and maintainability
1 parent c3919a2 commit af09098

File tree

2 files changed

+168
-189
lines changed

2 files changed

+168
-189
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { parse as parseSFC } from '@vue/compiler-sfc'
2+
import { minimatch } from 'minimatch'
3+
4+
export function shouldIgnoreFile(filename, ignorePatterns) {
5+
if (!ignorePatterns || !Array.isArray(ignorePatterns)) {
6+
return false
7+
}
8+
9+
return ignorePatterns.some((pattern) => {
10+
try {
11+
return minimatch(filename, pattern)
12+
} catch {
13+
return false
14+
}
15+
})
16+
}
17+
18+
export function parseOrderOption(options, defaultOrder) {
19+
if (Array.isArray(options.order) && options.order.length === 3 && options.order.every((section) => defaultOrder.includes(section))) {
20+
return options.order
21+
}
22+
return defaultOrder
23+
}
24+
25+
export function collectScriptBlocks(descriptor) {
26+
const scriptBlocks = []
27+
if (descriptor.scriptSetup && descriptor.scriptSetup.loc) {
28+
scriptBlocks.push({ type: 'setup', offset: descriptor.scriptSetup.loc.start.offset })
29+
}
30+
if (descriptor.script && descriptor.script.loc) {
31+
scriptBlocks.push({ type: 'regular', offset: descriptor.script.loc.start.offset })
32+
}
33+
return scriptBlocks
34+
}
35+
36+
export function validateScriptOrder(scriptBlocks, context, node) {
37+
if (scriptBlocks.length === 2) {
38+
const setupBlock = scriptBlocks.find((b) => b.type === 'setup')
39+
const regularBlock = scriptBlocks.find((b) => b.type === 'regular')
40+
if (setupBlock.offset > regularBlock.offset) {
41+
context.report({ node, messageId: 'scriptSetupBeforeScript' })
42+
return false
43+
}
44+
}
45+
return true
46+
}
47+
48+
export function addScriptBlocksToTags(scriptBlocks, tags) {
49+
scriptBlocks.forEach((block) => {
50+
tags.push({ name: 'script', index: block.offset })
51+
})
52+
}
53+
54+
export function collectStyleBlocks(descriptor) {
55+
const styleBlocks = []
56+
descriptor.styles.forEach((style) => {
57+
if (style.loc) {
58+
const isScoped = style.scoped === true
59+
styleBlocks.push({
60+
type: isScoped ? 'scoped' : 'global',
61+
offset: style.loc.start.offset,
62+
})
63+
}
64+
})
65+
return styleBlocks
66+
}
67+
68+
export function validateStyleOrder(styleBlocks, context, node) {
69+
const globalStyles = styleBlocks.filter((s) => s.type === 'global')
70+
const scopedStyles = styleBlocks.filter((s) => s.type === 'scoped')
71+
72+
if (globalStyles.length > 0 && scopedStyles.length > 0) {
73+
const lastGlobal = Math.max(...globalStyles.map((s) => s.offset))
74+
const firstScoped = Math.min(...scopedStyles.map((s) => s.offset))
75+
if (firstScoped < lastGlobal) {
76+
context.report({ node, messageId: 'scopedStyleAfterGlobal' })
77+
return false
78+
}
79+
}
80+
return true
81+
}
82+
83+
export function addStyleBlocksToTags(styleBlocks, tags) {
84+
styleBlocks.forEach((style) => {
85+
tags.push({ name: 'style', index: style.offset })
86+
})
87+
}
88+
89+
export function checkRequiredSections(tags, context, node) {
90+
const names = tags.map((t) => t.name)
91+
const hasTemplate = names.includes('template')
92+
const hasScript = names.includes('script')
93+
94+
if (!hasTemplate && !hasScript) {
95+
context.report({ node, messageId: 'missingScriptOrTemplate' })
96+
return false
97+
}
98+
return true
99+
}
100+
101+
export function groupSectionsByType(tags) {
102+
// Sort tags by their position in the file
103+
tags.sort((a, b) => a.index - b.index)
104+
105+
// Group consecutive blocks of the same type
106+
const sections = []
107+
let currentSection = null
108+
109+
for (const tag of tags) {
110+
if (currentSection && currentSection.name === tag.name) {
111+
continue // Skip, we already have this section type
112+
}
113+
currentSection = { name: tag.name }
114+
sections.push(currentSection)
115+
}
116+
return sections
117+
}
118+
119+
export function checkSectionOrder(sections, order, context, node) {
120+
const sectionOrder = sections.map((s) => order.indexOf(s.name)).filter((i) => i >= 0)
121+
122+
for (let i = 0; i + 1 < sectionOrder.length; i++) {
123+
if (sectionOrder[i] > sectionOrder[i + 1]) {
124+
context.report({ node, messageId: 'wrongOrder', data: { order: order.join(' -> ') } })
125+
return
126+
}
127+
}
128+
}
129+
130+
export function processVueFile(context, node, filename, order) {
131+
const source = context.getSourceCode().text
132+
const tags = []
133+
134+
try {
135+
const { descriptor } = parseSFC(source, { filename })
136+
137+
// Process template
138+
if (descriptor.template && descriptor.template.loc) {
139+
tags.push({ name: 'template', index: descriptor.template.loc.start.offset })
140+
}
141+
142+
// Process script blocks
143+
const scriptBlocks = collectScriptBlocks(descriptor)
144+
if (!validateScriptOrder(scriptBlocks, context, node)) {
145+
return
146+
}
147+
addScriptBlocksToTags(scriptBlocks, tags)
148+
149+
// Process style blocks
150+
const styleBlocks = collectStyleBlocks(descriptor)
151+
if (!validateStyleOrder(styleBlocks, context, node)) {
152+
return
153+
}
154+
addStyleBlocksToTags(styleBlocks, tags)
155+
156+
// Validate overall section order
157+
if (!checkRequiredSections(tags, context, node)) {
158+
return
159+
}
160+
161+
const sections = groupSectionsByType(tags)
162+
checkSectionOrder(sections, order, context, node)
163+
} catch {
164+
context.report({ node, messageId: 'parsingError' })
165+
return
166+
}
167+
}

src/rules/sfc-sections-order.js

Lines changed: 1 addition & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,11 @@
11
import { parseRuleOptions } from '../utils.js'
2-
import { parse as parseSFC } from '@vue/compiler-sfc'
3-
import { minimatch } from 'minimatch'
2+
import { shouldIgnoreFile, parseOrderOption, processVueFile } from './sfc-sections-order-utils.js'
43

54
const defaultOptions = {
65
order: ['script', 'template', 'style'],
76
ignore: [],
87
}
98

10-
function shouldIgnoreFile(filename, ignorePatterns) {
11-
if (!ignorePatterns || !Array.isArray(ignorePatterns)) {
12-
return false
13-
}
14-
15-
return ignorePatterns.some((pattern) => {
16-
try {
17-
return minimatch(filename, pattern)
18-
} catch {
19-
return false
20-
}
21-
})
22-
}
23-
24-
function parseOrderOption(options, defaultOrder) {
25-
if (Array.isArray(options.order) && options.order.length === 3 && options.order.every((section) => defaultOrder.includes(section))) {
26-
return options.order
27-
}
28-
return defaultOrder
29-
}
30-
31-
function processVueFile(context, node, filename, order) {
32-
const source = context.getSourceCode().text
33-
const tags = []
34-
35-
try {
36-
const { descriptor } = parseSFC(source, { filename })
37-
38-
// Process template
39-
if (descriptor.template && descriptor.template.loc) {
40-
tags.push({ name: 'template', index: descriptor.template.loc.start.offset })
41-
}
42-
43-
// Process script blocks with ordering validation
44-
if (!processScriptBlocks(descriptor, context, node, tags)) {
45-
return
46-
}
47-
48-
// Process style blocks with ordering validation
49-
if (!processStyleBlocks(descriptor, context, node, tags)) {
50-
return
51-
}
52-
53-
// Validate overall section order
54-
validateSectionOrder(tags, order, context, node)
55-
} catch {
56-
context.report({ node, messageId: 'parsingError' })
57-
return
58-
}
59-
}
60-
61-
function collectScriptBlocks(descriptor) {
62-
const scriptBlocks = []
63-
if (descriptor.scriptSetup && descriptor.scriptSetup.loc) {
64-
scriptBlocks.push({ type: 'setup', offset: descriptor.scriptSetup.loc.start.offset })
65-
}
66-
if (descriptor.script && descriptor.script.loc) {
67-
scriptBlocks.push({ type: 'regular', offset: descriptor.script.loc.start.offset })
68-
}
69-
return scriptBlocks
70-
}
71-
72-
function validateScriptOrder(scriptBlocks, context, node) {
73-
if (scriptBlocks.length === 2) {
74-
const setupBlock = scriptBlocks.find((b) => b.type === 'setup')
75-
const regularBlock = scriptBlocks.find((b) => b.type === 'regular')
76-
if (setupBlock.offset > regularBlock.offset) {
77-
context.report({ node, messageId: 'scriptSetupBeforeScript' })
78-
return false
79-
}
80-
}
81-
return true
82-
}
83-
84-
function addScriptBlocksToTags(scriptBlocks, tags) {
85-
scriptBlocks.forEach((block) => {
86-
tags.push({ name: 'script', index: block.offset })
87-
})
88-
}
89-
90-
function processScriptBlocks(descriptor, context, node, tags) {
91-
const scriptBlocks = collectScriptBlocks(descriptor)
92-
93-
if (!validateScriptOrder(scriptBlocks, context, node)) {
94-
return false
95-
}
96-
97-
addScriptBlocksToTags(scriptBlocks, tags)
98-
return true
99-
}
100-
101-
function collectStyleBlocks(descriptor) {
102-
const styleBlocks = []
103-
descriptor.styles.forEach((style) => {
104-
if (style.loc) {
105-
const isScoped = style.scoped === true
106-
styleBlocks.push({
107-
type: isScoped ? 'scoped' : 'global',
108-
offset: style.loc.start.offset,
109-
})
110-
}
111-
})
112-
return styleBlocks
113-
}
114-
115-
function validateStyleOrder(styleBlocks, context, node) {
116-
const globalStyles = styleBlocks.filter((s) => s.type === 'global')
117-
const scopedStyles = styleBlocks.filter((s) => s.type === 'scoped')
118-
119-
if (globalStyles.length > 0 && scopedStyles.length > 0) {
120-
const lastGlobal = Math.max(...globalStyles.map((s) => s.offset))
121-
const firstScoped = Math.min(...scopedStyles.map((s) => s.offset))
122-
if (firstScoped < lastGlobal) {
123-
context.report({ node, messageId: 'scopedStyleAfterGlobal' })
124-
return false
125-
}
126-
}
127-
return true
128-
}
129-
130-
function addStyleBlocksToTags(styleBlocks, tags) {
131-
styleBlocks.forEach((style) => {
132-
tags.push({ name: 'style', index: style.offset })
133-
})
134-
}
135-
136-
function processStyleBlocks(descriptor, context, node, tags) {
137-
const styleBlocks = collectStyleBlocks(descriptor)
138-
139-
if (!validateStyleOrder(styleBlocks, context, node)) {
140-
return false
141-
}
142-
143-
addStyleBlocksToTags(styleBlocks, tags)
144-
return true
145-
}
146-
147-
function checkRequiredSections(tags, context, node) {
148-
const names = tags.map((t) => t.name)
149-
const hasTemplate = names.includes('template')
150-
const hasScript = names.includes('script')
151-
152-
if (!hasTemplate && !hasScript) {
153-
context.report({ node, messageId: 'missingScriptOrTemplate' })
154-
return false
155-
}
156-
return true
157-
}
158-
159-
function groupSectionsByType(tags) {
160-
// Sort tags by their position in the file
161-
tags.sort((a, b) => a.index - b.index)
162-
163-
// Group consecutive blocks of the same type
164-
const sections = []
165-
let currentSection = null
166-
167-
for (const tag of tags) {
168-
if (currentSection && currentSection.name === tag.name) {
169-
continue // Skip, we already have this section type
170-
}
171-
currentSection = { name: tag.name }
172-
sections.push(currentSection)
173-
}
174-
return sections
175-
}
176-
177-
function checkSectionOrder(sections, order, context, node) {
178-
const sectionOrder = sections.map((s) => order.indexOf(s.name)).filter((i) => i >= 0)
179-
180-
for (let i = 0; i + 1 < sectionOrder.length; i++) {
181-
if (sectionOrder[i] > sectionOrder[i + 1]) {
182-
context.report({ node, messageId: 'wrongOrder', data: { order: order.join(' -> ') } })
183-
return
184-
}
185-
}
186-
}
187-
188-
function validateSectionOrder(tags, order, context, node) {
189-
if (!checkRequiredSections(tags, context, node)) {
190-
return
191-
}
192-
193-
const sections = groupSectionsByType(tags)
194-
checkSectionOrder(sections, order, context, node)
195-
}
196-
1979
export default {
19810
meta: {
19911
type: 'suggestion',

0 commit comments

Comments
 (0)