- lib/guard.ts: pure checks (isNoindex with Nitro route-rule patterns, findOffenders over the app's ssr flag, every merged route rule, .client.vue pages and experimental.inlineRouteRules) - module options seo.noindexRoutes and seo.indexable; declared noindex routes get X-Robots-Tag: noindex, nofollow through their route rule - a production build fails with the D-70 message; nuxt dev and nuxt prepare only warn - testing/guard-check.mjs builds four fixture apps (guard:check)
108 lines
4.7 KiB
JavaScript
108 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Build-guard check (D-70): builds the fixture apps under testing/guard,
|
|
// each extending this layer, and checks that the guard fails the build of
|
|
// every app with an indexable route that renders only in the browser and
|
|
// passes the one that declares that route noindex:
|
|
//
|
|
// indexable-ssr-false '/x/**' (Nuxt routeRules) and '/y/**' (Nitro
|
|
// routeRules) with ssr: false: exit 1 naming both
|
|
// client-page app/pages/panel.client.vue: exit 1 naming /panel
|
|
// global-ssr-false ssr: false for the whole app: exit 1
|
|
// noindex-ok '/x/**' with ssr: false declared in
|
|
// seo.noindexRoutes: exit 0; its server answers /x/a
|
|
// with X-Robots-Tag: noindex, nofollow and / without
|
|
// the header
|
|
//
|
|
// node testing/guard-check.mjs
|
|
//
|
|
// Each build runs the layer's own nuxt binary with the fixture as its
|
|
// working directory. Prints "guard-check: ok (<n> checks)" or one
|
|
// "guard-check: FAIL ..." line per failure and exits 1.
|
|
|
|
import { spawn } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { startNuxtServer } from './nuxt-server.mjs'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const layerRoot = resolve(here, '..')
|
|
const nuxtBin = join(layerRoot, 'node_modules', '.bin', 'nuxt')
|
|
const MESSAGE = '[vue-seo-layer] indexable routes must render on the server (D-70)'
|
|
|
|
/** @param {string} fixture @returns {Promise<{ code: number, log: string }>} */
|
|
function build(fixture) {
|
|
return new Promise((done, fail) => {
|
|
const child = spawn(nuxtBin, ['build'], {
|
|
cwd: join(here, 'guard', fixture),
|
|
env: { ...process.env, NODE_ENV: 'production', NUXT_TELEMETRY_DISABLED: '1' },
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
})
|
|
let log = ''
|
|
child.stdout.on('data', (b) => (log += b))
|
|
child.stderr.on('data', (b) => (log += b))
|
|
child.once('error', fail)
|
|
child.once('exit', (code) => done({ code: code ?? 1, log }))
|
|
})
|
|
}
|
|
|
|
async function main() {
|
|
if (!existsSync(nuxtBin)) throw new Error(`${nuxtBin} does not exist; run pnpm install in the layer first`)
|
|
/** @type {string[]} */
|
|
const failures = []
|
|
let checks = 0
|
|
const check = (/** @type {boolean} */ ok, /** @type {string} */ what) => {
|
|
checks++
|
|
if (!ok) failures.push(what)
|
|
}
|
|
|
|
/** @type {Array<{ fixture: string, names: string[] }>} */
|
|
const offenders = [
|
|
{ fixture: 'indexable-ssr-false', names: ['/x/**', '/y/**'] },
|
|
{ fixture: 'client-page', names: ['/panel'] },
|
|
{ fixture: 'global-ssr-false', names: ['ssr: false'] },
|
|
]
|
|
for (const { fixture, names } of offenders) {
|
|
const { code, log } = await build(fixture)
|
|
check(code !== 0, `${fixture}: the build passed, want it to fail`)
|
|
const line = log.split('\n').find((l) => l.includes(MESSAGE)) ?? ''
|
|
check(line !== '', `${fixture}: the build log lacks "${MESSAGE}"\n${log.slice(-2000)}`)
|
|
for (const name of names) check(line.includes(name), `${fixture}: the guard message does not name ${name}: ${line}`)
|
|
}
|
|
|
|
const ok = await build('noindex-ok')
|
|
check(ok.code === 0, `noindex-ok: the build failed (exit ${ok.code})\n${ok.log.slice(-2000)}`)
|
|
check(!ok.log.includes(MESSAGE), 'noindex-ok: the build log carries the guard message')
|
|
if (ok.code === 0) {
|
|
const server = await startNuxtServer({
|
|
outputDir: join(here, 'guard', 'noindex-ok', '.output'),
|
|
// No backend: the header comes from the route rule alone.
|
|
apiBase: 'http://127.0.0.1:1/_seo/api/v1',
|
|
})
|
|
try {
|
|
const declared = await fetch(`${server.url}/x/a`, { redirect: 'manual', headers: { Accept: 'text/html' } })
|
|
await declared.text()
|
|
check(declared.status === 200, `noindex-ok: /x/a answered ${declared.status}, want 200`)
|
|
const header = declared.headers.get('x-robots-tag')
|
|
check(header === 'noindex, nofollow', `noindex-ok: /x/a X-Robots-Tag is ${JSON.stringify(header)}, want "noindex, nofollow"`)
|
|
const home = await fetch(`${server.url}/`, { redirect: 'manual', headers: { Accept: 'text/html' } })
|
|
await home.text()
|
|
check(home.status === 200, `noindex-ok: / answered ${home.status}, want 200`)
|
|
check(home.headers.get('x-robots-tag') === null, `noindex-ok: / carries X-Robots-Tag ${JSON.stringify(home.headers.get('x-robots-tag'))}, want none`)
|
|
} finally {
|
|
await server.close()
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
for (const f of failures) console.error(`guard-check: FAIL ${f}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`guard-check: ok (${checks} checks)`)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`guard-check: FAIL setup: ${err instanceof Error ? err.message : String(err)}`)
|
|
process.exit(1)
|
|
})
|