Skip to content

Commit b74a0a9

Browse files
danielroeantfu
andauthored
feat(eslint-config): add no-page-meta-runtime-values (#641)
* feat: add `no-page-meta-runtime-values` * chore: update * chore: lint * chore: migrate changes --------- Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 48b1915 commit b74a0a9

File tree

8 files changed

+416
-90
lines changed

8 files changed

+416
-90
lines changed

packages/eslint-config/src/configs/nuxt.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ export default function nuxt(options: NuxtESLintConfigOptions): Linter.Config[]
5050
},
5151
})
5252

53+
const filePages = [
54+
...(dirs.pages?.map(pagesDir => join(pagesDir, `**/*.${GLOB_EXTS}`)) || []),
55+
].sort()
56+
57+
if (filePages.length) {
58+
configs.push({
59+
name: 'nuxt/pages',
60+
files: filePages,
61+
rules: {
62+
'nuxt/no-page-meta-runtime-values': 'error',
63+
},
64+
})
65+
}
66+
5367
configs.push({
5468
name: 'nuxt/nuxt-config',
5569
files: [

packages/eslint-config/test/__snapshots__/flat-compose.test.ts.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ exports[`flat config composition > custom src dirs 1`] = `
7373
{
7474
"name": "nuxt/rules",
7575
},
76+
{
77+
"files": [
78+
"src1/pages/**/*.{js,ts,jsx,tsx,vue}",
79+
"src2/pages/**/*.{js,ts,jsx,tsx,vue}",
80+
],
81+
"name": "nuxt/pages",
82+
},
7683
{
7784
"files": [
7885
"**/.config/nuxt.?([cm])[jt]s?(x)",
@@ -171,6 +178,13 @@ exports[`flat config composition > empty 1`] = `
171178
{
172179
"name": "nuxt/rules",
173180
},
181+
{
182+
"files": [
183+
"app/pages/**/*.{js,ts,jsx,tsx,vue}",
184+
"pages/**/*.{js,ts,jsx,tsx,vue}",
185+
],
186+
"name": "nuxt/pages",
187+
},
174188
{
175189
"files": [
176190
"**/.config/nuxt.?([cm])[jt]s?(x)",
@@ -215,6 +229,13 @@ exports[`flat config composition > non-standalone 1`] = `
215229
{
216230
"name": "nuxt/rules",
217231
},
232+
{
233+
"files": [
234+
"app/pages/**/*.{js,ts,jsx,tsx,vue}",
235+
"pages/**/*.{js,ts,jsx,tsx,vue}",
236+
],
237+
"name": "nuxt/pages",
238+
},
218239
{
219240
"files": [
220241
"**/.config/nuxt.?([cm])[jt]s?(x)",
@@ -313,6 +334,13 @@ exports[`flat config composition > with stylistic 1`] = `
313334
{
314335
"name": "nuxt/rules",
315336
},
337+
{
338+
"files": [
339+
"app/pages/**/*.{js,ts,jsx,tsx,vue}",
340+
"pages/**/*.{js,ts,jsx,tsx,vue}",
341+
],
342+
"name": "nuxt/pages",
343+
},
316344
{
317345
"files": [
318346
"**/.config/nuxt.?([cm])[jt]s?(x)",

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { rule as noPageMetaRuntimeValuesRule } from './no-page-meta-runtime-values/no-page-meta-runtime-values'
12
import { rule as preferImportMetaRule } from './prefer-import-meta/prefer-import-meta'
23
import { rule as nuxtConfigOrderKeysRule } from './nuxt-config-keys-order/nuxt-config-keys-order'
34
import { rule as noNuxtConfigTestKeyRule } from './no-nuxt-config-test-key/no-nuxt-config-test-key'
@@ -6,4 +7,5 @@ export default {
67
'prefer-import-meta': preferImportMetaRule,
78
'nuxt-config-keys-order': nuxtConfigOrderKeysRule,
89
'no-nuxt-config-test-key': noNuxtConfigTestKeyRule,
10+
'no-page-meta-runtime-values': noPageMetaRuntimeValuesRule,
911
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { run } from 'eslint-vitest-rule-tester'
2+
import { rule } from './no-page-meta-runtime-values'
3+
4+
run({
5+
name: 'no-page-meta-runtime-values',
6+
rule,
7+
valid: [
8+
// ---- Static values (always fine) ----
9+
`definePageMeta({ layout: 'admin', name: 'home' })`,
10+
`definePageMeta({ layout: false, scrollToTop: true })`,
11+
`definePageMeta({ meta: { title: 'Home', description: 'Welcome' } })`,
12+
'definePageMeta({ name: `home` })',
13+
// String middleware references
14+
`definePageMeta({ middleware: ['auth', 'guest'] })`,
15+
16+
// ---- External variables/imports ARE fine (hoisted by the build) ----
17+
`
18+
const layout = 'admin'
19+
definePageMeta({ layout })
20+
`,
21+
`
22+
import { PAGE_TITLE } from './constants'
23+
definePageMeta({ name: PAGE_TITLE })
24+
`,
25+
`
26+
import myLayout from './layouts'
27+
definePageMeta({ layout: myLayout })
28+
`,
29+
`
30+
const baseMeta = { layout: 'default' }
31+
definePageMeta({ ...baseMeta })
32+
`,
33+
`
34+
const meta = { layout: 'admin' }
35+
definePageMeta(meta)
36+
`,
37+
38+
// ---- Inline functions for middleware/validate ----
39+
`definePageMeta({ middleware: (to, from) => { return true } })`,
40+
`definePageMeta({ validate: function (route) { return !!route.params.id } })`,
41+
`
42+
definePageMeta({
43+
middleware: (to) => {
44+
const path = to.path
45+
return path === '/admin'
46+
}
47+
})
48+
`,
49+
50+
// ---- Composables INSIDE inline functions are fine (they run at navigation time) ----
51+
`
52+
definePageMeta({
53+
middleware: () => {
54+
const route = useRoute()
55+
return route.path === '/'
56+
}
57+
})
58+
`,
59+
`
60+
definePageMeta({
61+
validate: () => {
62+
const app = useNuxtApp()
63+
return !!app
64+
}
65+
})
66+
`,
67+
`
68+
definePageMeta({
69+
middleware: () => {
70+
const config = useRuntimeConfig()
71+
return !!config.public.apiBase
72+
}
73+
})
74+
`,
75+
76+
// ---- await inside inline function is fine ----
77+
`
78+
definePageMeta({
79+
middleware: async (to) => {
80+
await someCheck()
81+
return true
82+
}
83+
})
84+
`,
85+
86+
// ---- this inside a function expression is fine ----
87+
`
88+
definePageMeta({
89+
middleware: function () {
90+
return this
91+
}
92+
})
93+
`,
94+
95+
// ---- No definePageMeta — nothing to check ----
96+
`const route = useRoute()`,
97+
`const data = ref('hello')`,
98+
99+
// ---- Non-matching function calls at eager level ----
100+
`definePageMeta({ name: someHelper() })`,
101+
`definePageMeta({ name: t('home') })`,
102+
],
103+
104+
invalid: [
105+
// ---- Vue composables at eager level ----
106+
{
107+
code: `definePageMeta({ layout: ref('admin') })`,
108+
errors: [{ messageId: 'composableCall', data: { name: 'ref' } }],
109+
},
110+
{
111+
code: `definePageMeta({ title: computed(() => 'hello') })`,
112+
errors: [{ messageId: 'composableCall', data: { name: 'computed' } }],
113+
},
114+
{
115+
code: `definePageMeta({ data: reactive({ foo: 'bar' }) })`,
116+
errors: [{ messageId: 'composableCall', data: { name: 'reactive' } }],
117+
},
118+
{
119+
code: `definePageMeta({ data: shallowRef(null) })`,
120+
errors: [{ messageId: 'composableCall', data: { name: 'shallowRef' } }],
121+
},
122+
123+
// ---- Nuxt composables at eager level ----
124+
{
125+
code: `definePageMeta({ name: useRoute().name })`,
126+
errors: [{ messageId: 'composableCall', data: { name: 'useRoute' } }],
127+
},
128+
{
129+
code: `definePageMeta({ title: useI18n().t('home') })`,
130+
errors: [{ messageId: 'composableCall', data: { name: 'useI18n' } }],
131+
},
132+
{
133+
code: `definePageMeta({ config: useRuntimeConfig() })`,
134+
errors: [{ messageId: 'composableCall', data: { name: 'useRuntimeConfig' } }],
135+
},
136+
{
137+
code: `definePageMeta({ app: useNuxtApp() })`,
138+
errors: [{ messageId: 'composableCall', data: { name: 'useNuxtApp' } }],
139+
},
140+
{
141+
code: `definePageMeta({ state: useState('key') })`,
142+
errors: [{ messageId: 'composableCall', data: { name: 'useState' } }],
143+
},
144+
{
145+
code: `definePageMeta({ cookie: useCookie('token') })`,
146+
errors: [{ messageId: 'composableCall', data: { name: 'useCookie' } }],
147+
},
148+
149+
// ---- Vue lifecycle hooks at eager level ----
150+
{
151+
code: `
152+
definePageMeta({
153+
middleware: [onMounted(() => console.log('mounted'))]
154+
})
155+
`,
156+
errors: [{ messageId: 'composableCall', data: { name: 'onMounted' } }],
157+
},
158+
159+
// ---- inject at eager level ----
160+
{
161+
code: `definePageMeta({ value: inject('key') })`,
162+
errors: [{ messageId: 'composableCall', data: { name: 'inject' } }],
163+
},
164+
165+
// ---- this at eager level ----
166+
{
167+
code: `definePageMeta({ layout: this.layout })`,
168+
errors: [{ messageId: 'thisExpression' }],
169+
},
170+
171+
// ---- await at eager level ----
172+
{
173+
code: `definePageMeta({ title: await getTitle() })`,
174+
errors: [{ messageId: 'awaitExpression' }],
175+
},
176+
177+
// ---- Multiple errors ----
178+
{
179+
code: `definePageMeta({ route: useRoute(), data: ref(null) })`,
180+
errors: [
181+
{ messageId: 'composableCall', data: { name: 'useRoute' } },
182+
{ messageId: 'composableCall', data: { name: 'ref' } },
183+
],
184+
},
185+
],
186+
})

0 commit comments

Comments
 (0)