scripts/check_plugin_runtime_isolation.mjs
Rama main · réplica de lectura del repositorio original.
/**
* Fail if plugin_runtime imports any sibling _backend module outside itself,
* or if the CF plugin worker imports non-plugin_runtime backend code.
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const runtimeRoot = resolve(root, 'supabase/functions/_backend/plugin_runtime')
const pluginEntry = resolve(root, 'cloudflare_workers/plugin/index.ts')
const backendRoot = resolve(root, 'supabase/functions/_backend')
const errors = []
function walk(dir, out = []) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const abs = join(dir, entry.name)
if (entry.isDirectory())
walk(abs, out)
else if (entry.name.endsWith('.ts'))
out.push(abs)
}
return out
}
function resolveImport(from, spec) {
if (!spec.startsWith('.'))
return null
const base = resolve(dirname(from), spec)
for (const cand of [base, `${base}.ts`, `${base}.js`, join(base, 'index.ts')]) {
if (existsSync(cand) && cand.endsWith('.ts'))
return cand
}
return null
}
function collectImports(file) {
const text = readFileSync(file, 'utf8')
const specs = []
const re = /(?:from\s+|import\s*\(\s*)['"]([^'"]+)['"]/g
let m
while ((m = re.exec(text)))
specs.push(m[1])
return specs
}
for (const file of walk(runtimeRoot)) {
for (const spec of collectImports(file)) {
const resolved = resolveImport(file, spec)
if (!resolved)
continue
if (resolved.startsWith(`${backendRoot}/`) && !resolved.startsWith(`${runtimeRoot}/`)) {
errors.push(`${relative(root, file)} imports ${relative(root, resolved)} (outside plugin_runtime)`)
}
}
}
for (const spec of collectImports(pluginEntry)) {
const resolved = resolveImport(pluginEntry, spec)
if (!resolved)
continue
if (resolved.startsWith(`${backendRoot}/`) && !resolved.startsWith(`${runtimeRoot}/`)) {
errors.push(`cloudflare_workers/plugin/index.ts imports ${relative(root, resolved)} (must use plugin_runtime only)`)
}
}
const denoEntries = [
'supabase/functions/updates/index.ts',
'supabase/functions/stats/index.ts',
'supabase/functions/channel_self/index.ts',
'supabase/functions/updates_debug/index.ts',
]
for (const rel of denoEntries) {
const file = resolve(root, rel)
for (const spec of collectImports(file)) {
const resolved = resolveImport(file, spec)
if (!resolved)
continue
// Request-path code must come from plugin_runtime. The only allowed
// sibling _backend import is none — Deno supabase-js bridge lives in
// shared/plugin_deno_stats_fallbacks.ts.
if (resolved.startsWith(`${backendRoot}/`) && !resolved.startsWith(`${runtimeRoot}/`)) {
errors.push(`${rel} imports ${relative(root, resolved)} (use plugin_runtime or shared Deno bridge)`)
}
}
}
if (errors.length) {
console.error('plugin_runtime isolation check failed:\n')
for (const err of errors)
console.error(`- ${err}`)
process.exit(1)
}
console.log('plugin_runtime isolation check passed')