diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 5923b5e3..9060e178 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -290,6 +290,10 @@ export function buildRouter(appDir: string, opts: RouterOptions = {}): Router { const authz: ComponentRef[] = []; for (const f of scanDir(join(appDir, "authz"), [".js"])) { if (!/\.(ts|js)$/.test(f.file)) continue; + // Generated type files (permissions.gen.ts) live here too. Skip them quietly: + // they export types only, and isSafeIslandName would otherwise reject the dot + // and warn on every boot. + if (/[.]gen[.](ts|js)$/.test(f.file)) continue; const name = basename(f.file).replace(/\.(ts|js)$/, ""); if (!isSafeIslandName(name)) { console.warn(`[wrnexus] skipping authz declaration with unsafe name: ${name}`); diff --git a/packages/router/src/scan.ts b/packages/router/src/scan.ts index 7d6b7650..dce4d52d 100644 --- a/packages/router/src/scan.ts +++ b/packages/router/src/scan.ts @@ -33,10 +33,11 @@ function isIgnored(name: string): boolean { * Recursively collect allowed route files under `baseDir`. * Returns [] if the directory does not exist (a route kind may be unused). * - * `extraExtensions` widens the allow-list for callers that scan non-route - * directories (e.g. `app/schemas`, `app/authz`) and accept plain `.js` - * modules; it defaults to empty so route scanning (`app/pages`, `app/api`, - * `app/realtime`, ...) is unaffected. + * `extraExtensions` widens the allow-list for a caller that scans a non-route + * directory and accepts plain `.js` modules (currently only `app/authz`); it + * defaults to empty so every other caller — route scanning (`app/pages`, + * `app/api`, `app/realtime`, ...) as well as `app/schemas`, which does not + * pass it and so still only sees `.ts`/`.tsx`/`.wrn` — is unaffected. */ export function scanDir(baseDir: string, extraExtensions: readonly string[] = []): ScannedFile[] { if (!existsSync(baseDir)) return []; diff --git a/packages/router/test/authz-discovery.test.ts b/packages/router/test/authz-discovery.test.ts index 438e7fbe..196fdd47 100644 --- a/packages/router/test/authz-discovery.test.ts +++ b/packages/router/test/authz-discovery.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, spyOn, test } from "bun:test"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -41,4 +41,27 @@ describe("app/authz discovery", () => { mkdirSync(join(root, "app", "pages"), { recursive: true }); expect(buildRouter(join(root, "app")).authz).toEqual([]); }); + + test("quietly skips generated permissions.gen.ts without warning", () => { + const appDir = appWithAuthz({ + "permissions.gen.ts": "export type Foo = 1;", + "blog.ts": "export default {};", + }); + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); + try { + const router = buildRouter(appDir); + expect(router.authz.map((entry) => entry.name)).toEqual(["blog"]); + expect(warnSpy).not.toHaveBeenCalled(); + } finally { + warnSpy.mockRestore(); + } + }); + + test("skips permissions.gen.js too, while a legitimately named declaration is still discovered", () => { + const appDir = appWithAuthz({ + "permissions.gen.js": "export const x = 1;", + "billing.js": "export default {};", + }); + expect(buildRouter(appDir).authz.map((entry) => entry.name)).toEqual(["billing"]); + }); });