|
| 1 | +// Adapter: Fastify (Node). Scaffolds the Fastify guard plugin and registers it on the app — |
| 2 | +// `app.register(patchstackFastify)` right after the `fastify()` instance is created. |
| 3 | +// Dependency-free anchor + #region-marker patching (same approach as the express adapter). |
| 4 | + |
| 5 | +import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'; |
| 6 | +import { join, dirname, relative } from 'node:path'; |
| 7 | +import { read, log } from '../util.js'; |
| 8 | +import { scaffoldGeneric } from '../generic.js'; |
| 9 | +import type { Adapter, WireOptions, WireResult, VerifyResult } from '../types.js'; |
| 10 | + |
| 11 | +const APP_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:fastify|Fastify)\(/; |
| 12 | +const REGION = '// #region patchstack (managed by patchstack-connect protect — do not edit)'; |
| 13 | + |
| 14 | +function hasFastifyDep(cwd: string): boolean { |
| 15 | + try { |
| 16 | + const pkg = JSON.parse(read(join(cwd, 'package.json'))); |
| 17 | + return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.fastify); |
| 18 | + } catch { |
| 19 | + return false; |
| 20 | + } |
| 21 | +} |
| 22 | + |
| 23 | +// Find the source file + variable where the Fastify app is created (`const app = fastify()`). |
| 24 | +function findFastifyApp(cwd: string): { relPath: string; appVar: string } | null { |
| 25 | + const root = existsSync(join(cwd, 'src')) ? join(cwd, 'src') : cwd; |
| 26 | + let hit: { relPath: string; appVar: string } | null = null; |
| 27 | + const walk = (dir: string): void => { |
| 28 | + if (hit) return; |
| 29 | + let entries: string[]; |
| 30 | + try { |
| 31 | + entries = readdirSync(dir); |
| 32 | + } catch { |
| 33 | + return; |
| 34 | + } |
| 35 | + for (const name of entries) { |
| 36 | + if (hit) return; |
| 37 | + if (name === 'node_modules' || name === 'patchstack' || name.startsWith('.')) continue; |
| 38 | + const p = join(dir, name); |
| 39 | + let st; |
| 40 | + try { |
| 41 | + st = statSync(p); |
| 42 | + } catch { |
| 43 | + continue; |
| 44 | + } |
| 45 | + if (st.isDirectory()) walk(p); |
| 46 | + else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) { |
| 47 | + let src: string; |
| 48 | + try { |
| 49 | + src = readFileSync(p, 'utf8'); |
| 50 | + } catch { |
| 51 | + continue; |
| 52 | + } |
| 53 | + const m = APP_RE.exec(src); |
| 54 | + if (m) hit = { relPath: relative(cwd, p).replace(/\\/g, '/'), appVar: m[1]! }; |
| 55 | + } |
| 56 | + } |
| 57 | + }; |
| 58 | + walk(root); |
| 59 | + return hit; |
| 60 | +} |
| 61 | + |
| 62 | +function detect(cwd: string): boolean { |
| 63 | + return hasFastifyDep(cwd) && findFastifyApp(cwd) !== null; |
| 64 | +} |
| 65 | + |
| 66 | +function importSpecifier(fromRel: string, toRel: string): string { |
| 67 | + let spec = relative(dirname(fromRel), toRel).replace(/\\/g, '/').replace(/\.(?:ts|js)$/, ''); |
| 68 | + if (!spec.startsWith('.')) spec = `./${spec}`; |
| 69 | + return spec; |
| 70 | +} |
| 71 | + |
| 72 | +function wire(cwd: string, opts: WireOptions): WireResult { |
| 73 | + const { changed, dir } = scaffoldGeneric(cwd, opts, 'fastify-plugin.ts'); |
| 74 | + const entry = findFastifyApp(cwd); |
| 75 | + if (!entry) { |
| 76 | + log('fastify detected but no `= fastify()` site found — scaffolded plugin; register it yourself: app.register(patchstackFastify)'); |
| 77 | + return { ok: true, changed }; |
| 78 | + } |
| 79 | + |
| 80 | + const p = join(cwd, entry.relPath); |
| 81 | + let s = read(p); |
| 82 | + if (s.includes('patchstackFastify')) { |
| 83 | + log(`fastify entry ${entry.relPath} already wired`); |
| 84 | + return { ok: true, changed }; |
| 85 | + } |
| 86 | + |
| 87 | + const spec = importSpecifier(entry.relPath, `${dir}/guard`); |
| 88 | + const importLine = `import { patchstackFastify } from "${spec}";`; |
| 89 | + const lines = s.split('\n'); |
| 90 | + let lastImport = -1; |
| 91 | + for (let i = 0; i < lines.length; i++) if (/^\s*import\b/.test(lines[i] ?? '')) lastImport = i; |
| 92 | + lines.splice(lastImport + 1, 0, importLine); |
| 93 | + const appIdx = lines.findIndex((l) => APP_RE.test(l)); |
| 94 | + if (appIdx !== -1) { |
| 95 | + lines.splice(appIdx + 1, 0, REGION, `${entry.appVar}.register(patchstackFastify);`, '// #endregion patchstack'); |
| 96 | + } |
| 97 | + s = lines.join('\n'); |
| 98 | + writeFileSync(p, s); |
| 99 | + changed.push(entry.relPath); |
| 100 | + log(`patched ${entry.relPath} (${entry.appVar}.register(patchstackFastify))`); |
| 101 | + return { ok: true, changed: [...new Set(changed)] }; |
| 102 | +} |
| 103 | + |
| 104 | +function verify(cwd: string): VerifyResult { |
| 105 | + const dir = existsSync(join(cwd, 'src')) ? 'src/patchstack' : 'patchstack'; |
| 106 | + const scaffolded = existsSync(join(cwd, dir, 'guard.ts')); |
| 107 | + const entry = findFastifyApp(cwd); |
| 108 | + const wired = entry ? read(join(cwd, entry.relPath)).includes('patchstackFastify') : false; |
| 109 | + return { |
| 110 | + wired: scaffolded && wired, |
| 111 | + checks: [ |
| 112 | + { label: 'Fastify guard plugin scaffolded', ok: scaffolded, hint: 'run `patchstack-connect protect`' }, |
| 113 | + { |
| 114 | + label: 'app.register(patchstackFastify) wired into the Fastify app', |
| 115 | + ok: wired, |
| 116 | + hint: 'add `app.register(patchstackFastify)` right after you create your fastify() app', |
| 117 | + }, |
| 118 | + ], |
| 119 | + }; |
| 120 | +} |
| 121 | + |
| 122 | +export const fastifyAdapter: Adapter = { |
| 123 | + name: 'fastify', |
| 124 | + label: 'Fastify (Node)', |
| 125 | + detect, |
| 126 | + wire, |
| 127 | + verify, |
| 128 | +}; |
0 commit comments