mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(www): lint next/link hrefs that point at another zone
This commit is contained in:
1 parent
04666b2559
commit
ab9ccf00a3
3 files changed
+120
No files matched your search
@@ -0,0 +1,74 @@
|
||||
const { readFileSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const vm = require('node:vm')
|
||||
|
||||
const SUPABASE_ORIGIN = 'https://supabase.com'
|
||||
|
||||
// Evaluated as production: the /docs rewrite only exists there, and /docs links must be flagged in every environment.
|
||||
function loadProductionRewrites() {
|
||||
const rewritesModule = { exports: [] }
|
||||
const source = readFileSync(join(__dirname, '../lib/rewrites.js'), 'utf8')
|
||||
vm.runInNewContext(source, {
|
||||
module: rewritesModule,
|
||||
process: { env: { NEXT_PUBLIC_VERCEL_ENV: 'production' } },
|
||||
})
|
||||
return rewritesModule.exports
|
||||
}
|
||||
|
||||
const crossZonePrefixes = loadProductionRewrites()
|
||||
.filter(({ destination }) => !destination.startsWith('/'))
|
||||
.map(({ source }) => source.replace(/\/(:path\*)?$/, ''))
|
||||
|
||||
function isCrossZonePath(href) {
|
||||
const path = href.startsWith(SUPABASE_ORIGIN) ? href.slice(SUPABASE_ORIGIN.length) : href
|
||||
const pathname = path.split(/[?#]/)[0]
|
||||
return crossZonePrefixes.some(
|
||||
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
|
||||
)
|
||||
}
|
||||
|
||||
function staticHrefPrefix(value) {
|
||||
if (!value) return null
|
||||
if (value.type === 'Literal') return typeof value.value === 'string' ? value.value : null
|
||||
if (value.type !== 'JSXExpressionContainer') return null
|
||||
|
||||
const { expression } = value
|
||||
if (expression.type === 'Literal' && typeof expression.value === 'string') return expression.value
|
||||
if (expression.type === 'TemplateLiteral') return expression.quasis[0].value.cooked
|
||||
return null
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
crossZone:
|
||||
"'{{href}}' is served by another zone. Use a plain <a>: Next.js can't prefetch or client-navigate across zones, so <Link> 404s its prefetch.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
const linkNames = new Set()
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source.value !== 'next/link') return
|
||||
for (const specifier of node.specifiers) {
|
||||
if (specifier.type === 'ImportDefaultSpecifier') linkNames.add(specifier.local.name)
|
||||
}
|
||||
},
|
||||
JSXOpeningElement(node) {
|
||||
if (node.name.type !== 'JSXIdentifier' || !linkNames.has(node.name.name)) return
|
||||
|
||||
const hrefAttribute = node.attributes.find(
|
||||
(attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'href'
|
||||
)
|
||||
const href = staticHrefPrefix(hrefAttribute?.value)
|
||||
if (!href || !isCrossZonePath(href)) return
|
||||
|
||||
context.report({ node: hrefAttribute, messageId: 'crossZone', data: { href } })
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { RuleTester } from 'eslint'
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
import noCrossZoneLink from './no-cross-zone-link.cjs'
|
||||
|
||||
RuleTester.describe = describe
|
||||
RuleTester.it = it
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
|
||||
})
|
||||
|
||||
ruleTester.run('no-cross-zone-link', noCrossZoneLink, {
|
||||
valid: [
|
||||
'import Link from \'next/link\'; <Link href="/pricing" />',
|
||||
'import Link from \'next/link\'; <Link href="/dashboards" />',
|
||||
"import Link from 'next/link'; <Link href={href} />",
|
||||
'<a href="/dashboard/sign-up" />',
|
||||
'import Link from \'somewhere-else\'; <Link href="/dashboard" />',
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import Link from \'next/link\'; <Link href="/dashboard/sign-up" />',
|
||||
errors: [{ messageId: 'crossZone' }],
|
||||
},
|
||||
{
|
||||
code: 'import Link from \'next/link\'; <Link href="https://supabase.com/docs/guides/auth" />',
|
||||
errors: [{ messageId: 'crossZone' }],
|
||||
},
|
||||
{
|
||||
code: "import Link from 'next/link'; <Link href={`/docs/guides/${slug}`} />",
|
||||
errors: [{ messageId: 'crossZone' }],
|
||||
},
|
||||
{
|
||||
code: 'import NextLink from \'next/link\'; <NextLink href="/humans.txt" />',
|
||||
errors: [{ messageId: 'crossZone' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
const { defineConfig } = require('eslint/config')
|
||||
const supabaseConfig = require('eslint-config-supabase/next')
|
||||
|
||||
const noCrossZoneLink = require('./eslint-rules/no-cross-zone-link.cjs')
|
||||
|
||||
module.exports = defineConfig([
|
||||
supabaseConfig,
|
||||
{
|
||||
@@ -19,4 +21,9 @@ module.exports = defineConfig([
|
||||
'react/no-children-prop': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{jsx,tsx}'],
|
||||
plugins: { www: { rules: { 'no-cross-zone-link': noCrossZoneLink } } },
|
||||
rules: { 'www/no-cross-zone-link': 'error' },
|
||||
},
|
||||
])
|
||||
Reference in new issue
Block a user