#!/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 ( 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) })