diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a142b7..1a29f67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: - run: pnpm build + - name: Smoke-test built entrypoints load in raw Node + run: pnpm smoke + - name: Check type exports (attw) run: pnpm check-exports diff --git a/package.json b/package.json index 7e179a7..aef7382 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "lint": "eslint src", "lint:fix": "eslint src --fix", "prepare": "simple-git-hooks", + "smoke": "node scripts/smoke-load.mjs", "test": "vitest run --project unit --project nestjs", "test:e2e": "vitest run --project e2e", "test:watch": "vitest --project unit --project nestjs", diff --git a/scripts/smoke-load.mjs b/scripts/smoke-load.mjs new file mode 100644 index 0000000..a48ff5b --- /dev/null +++ b/scripts/smoke-load.mjs @@ -0,0 +1,59 @@ +// Loads every built entrypoint in a plain Node process — no transpiler in the +// chain — to catch output that fails to parse/load as a real consumer would +// (e.g. untranspiled decorator syntax; see issue #87). vitest can't catch this +// because it re-transpiles imported modules through swc/esbuild. +// +// Entrypoints are derived from package.json `exports` so this stays in sync as +// adapters are added. Run after `pnpm build`: `pnpm smoke`. + +import { createRequire } from 'node:module' +import { dirname, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { readFileSync } from 'node:fs' + +const require = createRequire(import.meta.url) +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) + +// Collect (subpath, format, absolute file) triples from every export condition +// that points at a built .mjs/.cjs file. +const targets = [] +for (const [subpath, entry] of Object.entries(pkg.exports ?? {})) { + if (typeof entry !== 'object') continue + const mjs = entry.import?.default + const cjs = entry.require?.default + if (mjs) targets.push({ subpath, format: 'esm', file: resolve(root, mjs) }) + if (cjs) targets.push({ subpath, format: 'cjs', file: resolve(root, cjs) }) +} + +if (targets.length === 0) { + console.error( + 'No entrypoints found in package.json exports — smoke test is testing nothing.', + ) + process.exit(1) +} + +const failures = [] +for (const { subpath, format, file } of targets) { + try { + if (format === 'esm') await import(pathToFileURL(file).href) + else require(file) + console.log(`ok ${format.padEnd(3)} ${subpath}`) + } catch (err) { + console.error(`FAIL ${format.padEnd(3)} ${subpath}`) + console.error(` ${err?.stack ?? err}`) + failures.push({ subpath, format, file }) + } +} + +if (failures.length > 0) { + console.error( + `\n${failures.length} built entrypoint(s) failed to load in raw Node.`, + ) + process.exit(1) +} + +console.log( + `\nAll ${targets.length} built entrypoints load cleanly in raw Node.`, +) diff --git a/src/adapters/nestjs/middleware.ts b/src/adapters/nestjs/middleware.ts index c9447d8..929a07e 100644 --- a/src/adapters/nestjs/middleware.ts +++ b/src/adapters/nestjs/middleware.ts @@ -87,7 +87,11 @@ function toWebRequest(req: NestRequestLike): Request { export function withSupabase( config?: Omit, ): Type { - @Injectable() + // Applied programmatically rather than as an `@Injectable()` decorator: + // the build tool (tsdown/oxc) does not lower legacy `experimentalDecorators`, + // so decorator syntax here would ship verbatim to `dist` and fail to parse + // under plain Node (CJS/ESM) at load time. Calling the decorator factory on + // the class produces identical DI metadata. See issue #87. class SupabaseAuthGuard implements CanActivate { async canActivate(executionContext: ExecutionContext): Promise { // Fail loudly on non-HTTP transports rather than silently allowing them @@ -123,5 +127,6 @@ export function withSupabase( } } + Injectable()(SupabaseAuthGuard) return SupabaseAuthGuard }