Skip to content

Commit 9e45b42

Browse files
authored
fix: apply Injectable programmatically so NestJS adapter loads in raw Node (#107)
* fix: apply Injectable programmatically so NestJS adapter loads in raw Node The @Injectable() decorator on the guard in withSupabase() shipped untranspiled (tsdown/oxc does not lower legacy decorators), crashing `require`/`import` of the adapter with SyntaxError under plain Node. Apply it as Injectable()(SupabaseAuthGuard) instead, and add a raw-Node load smoke test (pnpm smoke) in CI to catch this class of regression. Fixes #87 * fix: fail smoke test when no entrypoints are found in exports
1 parent 8d5c781 commit 9e45b42

4 files changed

Lines changed: 69 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131

3232
- run: pnpm build
3333

34+
- name: Smoke-test built entrypoints load in raw Node
35+
run: pnpm smoke
36+
3437
- name: Check type exports (attw)
3538
run: pnpm check-exports
3639

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"lint": "eslint src",
114114
"lint:fix": "eslint src --fix",
115115
"prepare": "simple-git-hooks",
116+
"smoke": "node scripts/smoke-load.mjs",
116117
"test": "vitest run --project unit --project nestjs",
117118
"test:e2e": "vitest run --project e2e",
118119
"test:watch": "vitest --project unit --project nestjs",

scripts/smoke-load.mjs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Loads every built entrypoint in a plain Node process — no transpiler in the
2+
// chain — to catch output that fails to parse/load as a real consumer would
3+
// (e.g. untranspiled decorator syntax; see issue #87). vitest can't catch this
4+
// because it re-transpiles imported modules through swc/esbuild.
5+
//
6+
// Entrypoints are derived from package.json `exports` so this stays in sync as
7+
// adapters are added. Run after `pnpm build`: `pnpm smoke`.
8+
9+
import { createRequire } from 'node:module'
10+
import { dirname, resolve } from 'node:path'
11+
import { fileURLToPath, pathToFileURL } from 'node:url'
12+
import { readFileSync } from 'node:fs'
13+
14+
const require = createRequire(import.meta.url)
15+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
16+
17+
const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
18+
19+
// Collect (subpath, format, absolute file) triples from every export condition
20+
// that points at a built .mjs/.cjs file.
21+
const targets = []
22+
for (const [subpath, entry] of Object.entries(pkg.exports ?? {})) {
23+
if (typeof entry !== 'object') continue
24+
const mjs = entry.import?.default
25+
const cjs = entry.require?.default
26+
if (mjs) targets.push({ subpath, format: 'esm', file: resolve(root, mjs) })
27+
if (cjs) targets.push({ subpath, format: 'cjs', file: resolve(root, cjs) })
28+
}
29+
30+
if (targets.length === 0) {
31+
console.error(
32+
'No entrypoints found in package.json exports — smoke test is testing nothing.',
33+
)
34+
process.exit(1)
35+
}
36+
37+
const failures = []
38+
for (const { subpath, format, file } of targets) {
39+
try {
40+
if (format === 'esm') await import(pathToFileURL(file).href)
41+
else require(file)
42+
console.log(`ok ${format.padEnd(3)} ${subpath}`)
43+
} catch (err) {
44+
console.error(`FAIL ${format.padEnd(3)} ${subpath}`)
45+
console.error(` ${err?.stack ?? err}`)
46+
failures.push({ subpath, format, file })
47+
}
48+
}
49+
50+
if (failures.length > 0) {
51+
console.error(
52+
`\n${failures.length} built entrypoint(s) failed to load in raw Node.`,
53+
)
54+
process.exit(1)
55+
}
56+
57+
console.log(
58+
`\nAll ${targets.length} built entrypoints load cleanly in raw Node.`,
59+
)

src/adapters/nestjs/middleware.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ function toWebRequest(req: NestRequestLike): Request {
8787
export function withSupabase(
8888
config?: Omit<WithSupabaseConfig, 'cors'>,
8989
): Type<CanActivate> {
90-
@Injectable()
90+
// Applied programmatically rather than as an `@Injectable()` decorator:
91+
// the build tool (tsdown/oxc) does not lower legacy `experimentalDecorators`,
92+
// so decorator syntax here would ship verbatim to `dist` and fail to parse
93+
// under plain Node (CJS/ESM) at load time. Calling the decorator factory on
94+
// the class produces identical DI metadata. See issue #87.
9195
class SupabaseAuthGuard implements CanActivate {
9296
async canActivate(executionContext: ExecutionContext): Promise<boolean> {
9397
// Fail loudly on non-HTTP transports rather than silently allowing them
@@ -123,5 +127,6 @@ export function withSupabase(
123127
}
124128
}
125129

130+
Injectable()(SupabaseAuthGuard)
126131
return SupabaseAuthGuard
127132
}

0 commit comments

Comments
 (0)