Skip to content

Commit

Permalink
feat: implement
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Aug 29, 2024
1 parent 790246b commit aae333d
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 32 deletions.
36 changes: 18 additions & 18 deletions docs/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
export default defineNuxtConfig({
extends: '@nuxt/ui-pro',

routeRules: {
'/guide': { redirect: '/guide/getting-started' },
},

site: {
url: 'https://eslint.nuxt.com',
},

modules: [
'@nuxt/image',
'@nuxt/content',
Expand All @@ -20,14 +12,30 @@ export default defineNuxtConfig({
'nuxt-og-image',
],

colorMode: {
preference: 'dark',
$production: {
nitro: {
experimental: {
wasm: true,
},
},
},

ui: {
icons: ['heroicons', 'simple-icons', 'ph'],
},

site: {
url: 'https://eslint.nuxt.com',
},

colorMode: {
preference: 'dark',
},

routeRules: {
'/guide': { redirect: '/guide/getting-started' },
},

nitro: {
prerender: {
routes: ['/api/search.json'],
Expand All @@ -46,12 +54,4 @@ export default defineNuxtConfig({
}
},
},

$production: {
nitro: {
experimental: {
wasm: true,
},
},
},
})
13 changes: 13 additions & 0 deletions packages/eslint-config/src/flat/configs/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,18 @@ export default function nuxt(options: NuxtESLintConfigOptions): Linter.Config[]
},
})

configs.push({
name: 'nuxt/config',
plugins: {
nuxt: nuxtPlugin,
},
files: [
'**/nuxt.config.?([cm])[jt]s?(x)',
],
rules: {
'nuxt/nuxt-config-keys-order': 'error',
},
})

return configs
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ exports[`flat config composition > custom src dirs 1`] = `
{
"name": "nuxt/rules",
},
{
"files": [
"**/nuxt.config.?([cm])[jt]s?(x)",
],
"name": "nuxt/config",
},
{
"files": [
"src1/app.{js,ts,jsx,tsx,vue}",
Expand Down Expand Up @@ -162,6 +168,12 @@ exports[`flat config composition > empty 1`] = `
{
"name": "nuxt/rules",
},
{
"files": [
"**/nuxt.config.?([cm])[jt]s?(x)",
],
"name": "nuxt/config",
},
{
"files": [
"app.{js,ts,jsx,tsx,vue}",
Expand Down Expand Up @@ -199,6 +211,12 @@ exports[`flat config composition > non-standalone 1`] = `
{
"name": "nuxt/rules",
},
{
"files": [
"**/nuxt.config.?([cm])[jt]s?(x)",
],
"name": "nuxt/config",
},
{
"files": [
"app.{js,ts,jsx,tsx,vue}",
Expand Down
202 changes: 199 additions & 3 deletions packages/eslint-plugin/src/rules/nuxt-config-keys-order/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { TSESTree as Tree, TSESLint } from '@typescript-eslint/utils'
import { createRule } from '../utils'
import { ORDER_KEYS } from './keys'

type MessageIds = 'default'

Expand All @@ -18,7 +20,201 @@ export const rule = createRule<MessageIds, Options>({
fixable: 'code',
},
defaultOptions: [],
create: context => ({
// TODO: Implement
}),
create(context) {
if (!context.filename.match(/nuxt\.config\.[mc]?[jt]sx?$/)) {
return {}
}

return {
ExportDefaultDeclaration(node) {
let object: Tree.ObjectExpression | undefined
if (node.declaration.type === 'ObjectExpression') {
object = node.declaration
}
else if (node.declaration.type === 'CallExpression' && node.declaration.arguments[0].type === 'ObjectExpression') {
object = node.declaration.arguments[0]
}
if (!object) {
return
}

const hasFixes = sort(context, object)
if (!hasFixes) {
const envProps = object.properties.filter(i => i.type === 'Property' && i.key.type === 'Identifier' && i.key.name.startsWith('$')) as Tree.Property[]
for (const prop of envProps) {
if (prop.value.type === 'ObjectExpression')
sort(context, prop.value)
}
}
},
}
},
})

function sort(context: TSESLint.RuleContext<MessageIds, Options>, node: Tree.ObjectExpression) {
return sortAst(
context,
node,
node.properties as Tree.Property[],
(prop) => {
if (prop.type === 'Property')
return getString(prop.key)
return null
},
sortKeys,
)
}

function sortKeys(a: string, b: string) {
const indexA = ORDER_KEYS.findIndex(k => typeof k === 'string' ? k === a : k.test(a))
const indexB = ORDER_KEYS.findIndex(k => typeof k === 'string' ? k === b : k.test(b))
if (indexA === -1 && indexB !== -1)
return 1
if (indexA !== -1 && indexB === -1)
return -1
if (indexA < indexB)
return -1
if (indexA > indexB)
return 1
return a.localeCompare(b)
}

// Ported from https://github.com/gauben/eslint-plugin-command/blob/04efa47a2319a5f9afb395cf0efccc9cb111058d/src/commands/keep-sorted.ts#L138-L144
function sortAst<T extends Tree.Node>(
context: TSESLint.RuleContext<MessageIds, Options>,
node: Tree.Node,
list: T[],
getName: (node: T) => string | (string | null)[] | null,
sort: (a: string, b: string) => number = (a, b) => a.localeCompare(b),
insertComma = true,
) {
const firstToken = context.sourceCode.getFirstToken(node)!
const lastToken = context.sourceCode.getLastToken(node)!
if (!firstToken || !lastToken)
return false

if (list.length < 2)
return false

const reordered = list.slice()
const ranges = new Map<typeof list[number], [number, number, string]>()
const names = new Map<typeof list[number], (string | null)[] | null>()

const rangeStart = Math.max(
firstToken.range[1],
context.sourceCode.getIndexFromLoc({
line: list[0].loc.start.line,
column: 0,
}),
)

let rangeEnd = rangeStart
for (let i = 0; i < list.length; i++) {
const item = list[i]
let name = getName(item)
if (typeof name === 'string')
name = [name]
names.set(item, name)

let lastRange = item.range[1]
const nextToken = context.sourceCode.getTokenAfter(item)
if (nextToken?.type === 'Punctuator' && nextToken.value === ',')
lastRange = nextToken.range[1]
const nextChar = context.sourceCode.getText()[lastRange]

// Insert comma if it's the last item without a comma
let text = getTextOf(context.sourceCode, [rangeEnd, lastRange])
if (nextToken === lastToken && insertComma)
text += ','

// Include subsequent newlines
if (nextChar === '\n') {
lastRange++
text += '\n'
}

ranges.set(item, [rangeEnd, lastRange, text])
rangeEnd = lastRange
}

const segments: [number, number][] = []
let segmentStart: number = -1
for (let i = 0; i < list.length; i++) {
if (names.get(list[i]) == null) {
if (segmentStart > -1)
segments.push([segmentStart, i])
segmentStart = -1
}
else {
if (segmentStart === -1)
segmentStart = i
}
}
if (segmentStart > -1 && segmentStart !== list.length - 1)
segments.push([segmentStart, list.length])

for (const [start, end] of segments) {
reordered.splice(
start,
end - start,
...reordered
.slice(start, end)
.sort((a, b) => {
const nameA: (string | null)[] = names.get(a)!
const nameB: (string | null)[] = names.get(b)!

const length = Math.max(nameA.length, nameB.length)
for (let i = 0; i < length; i++) {
const a = nameA[i]
const b = nameB[i]
if (a == null || b == null || a === b)
continue
return sort(a, b)
}
return 0
}),
)
}

const changed = reordered.some((prop, i) => prop !== list[i])
if (!changed)
return false

const newContent = reordered
.map(i => ranges.get(i)![2])
.join('')

// console.log({
// reordered,
// newContent,
// oldContent: ctx.context.sourceCode.text.slice(rangeStart, rangeEnd),
// })

context.report({
node,
messageId: 'default',
data: {
a: names.get(reordered[0])![0]!,
b: names.get(reordered[1])![0]!,
},
fix(fixer) {
return fixer.replaceTextRange([rangeStart, rangeEnd], newContent)
},
})
}

function getTextOf(sourceCode: TSESLint.SourceCode, node?: Tree.Node | Tree.Token | Tree.Range | null) {
if (!node)
return ''
if (Array.isArray(node))
return sourceCode.text.slice(node[0], node[1])
return sourceCode.getText(node)
}

function getString(node: Tree.Node): string | null {
if (node.type === 'Identifier')
return node.name
if (node.type === 'Literal')
return String(node.raw)
return null
}
27 changes: 22 additions & 5 deletions packages/eslint-plugin/src/rules/nuxt-config-keys-order/keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export const OFFICIAL_MODULES = {
client: [
'ui',
'site', // SEO module
'colorMode',
],

server: [
'hub',
],
}

export const ORDER_KEYS = [
// Ids
'appId',
Expand All @@ -11,11 +23,14 @@ export const ORDER_KEYS = [
'modules',
'plugins',

// Env ($production, $development, $test)
/^\$/,

// Nuxt Core Features
'ssr',
'pages',
'components',
'imports',
'pages',
'devtools',

// Client-side Integrations
Expand All @@ -24,6 +39,7 @@ export const ORDER_KEYS = [
'vue',
'router',
'unhead',
...OFFICIAL_MODULES.client,
'spaLoadingTemplate',

// Runtime Configs
Expand Down Expand Up @@ -70,6 +86,7 @@ export const ORDER_KEYS = [

// Nitro
'nitro',
...OFFICIAL_MODULES.server,
'serverHandlers',
'devServerHandlers',

Expand All @@ -79,14 +96,14 @@ export const ORDER_KEYS = [
'typescript',
'postcss',

// Logging
'debug',
'logLevel',

// Other Integrations
'test',
'telemetry',

// Logging
'debug',
'logLevel',

// Hooks
'hooks',
]
Loading

0 comments on commit aae333d

Please sign in to comment.