diff --git a/package.json b/package.json index 5974507e..d17af6ea 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "typecheck": "vue-tsc --noEmit", - "typecheck:runtime-server": "tsc --noEmit --pretty false -p src/runtime/server/tsconfig.json", + "typecheck:runtime-server": "cd test/cases/plugins-type-inference && nuxi prepare && vue-tsc --noEmit --pretty false -p .nuxt/tsconfig.server.json", "typecheck:playground": "pnpm -C playground exec nuxi prepare && pnpm -C playground exec vue-tsc --noEmit -p .nuxt/tsconfig.app.json", "test": "vitest run", "test:watch": "vitest watch" diff --git a/src/module.ts b/src/module.ts index a3e7a5b2..b24c7c92 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,6 +1,7 @@ import type { Nuxt } from '@nuxt/schema' import type { BetterAuthModuleOptions } from './runtime/config' import type { BetterAuthDatabaseProviderSetupContext } from './types/hooks' +import { existsSync, readFileSync } from 'node:fs' import { mkdir, writeFile } from 'node:fs/promises' import { addTemplate, createResolver, defineNuxtModule } from '@nuxt/kit' import { consola as _consola } from 'consola' @@ -18,6 +19,33 @@ import './types/hooks' const consola = _consola.withTag('nuxt-better-auth') +function isServerConfigSharedTypeSafe(serverConfigPath: string): boolean { + const resolvedPath = [ + serverConfigPath, + `${serverConfigPath}.ts`, + `${serverConfigPath}.mts`, + `${serverConfigPath}.cts`, + `${serverConfigPath}.js`, + `${serverConfigPath}.mjs`, + `${serverConfigPath}.cjs`, + ].find(path => existsSync(path)) + + if (!resolvedPath) + return false + + const contents = readFileSync(resolvedPath, 'utf8') + + return !( + /from\s+['"]#server/.test(contents) + || /from\s+['"]#layers\//.test(contents) + || /from\s+['"]~~/.test(contents) + || /from\s+['"]@@/.test(contents) + || /\bdb\b/.test(contents) + || /\bsessionHookAfter\b/.test(contents) + || /@nuxthub\/db/.test(contents) + ) +} + async function createDefaultAuthConfigFiles(nuxt: Nuxt): Promise { const configs = resolveAuthConfigDescriptors(nuxt) @@ -136,6 +164,7 @@ export default defineNuxtModule({ serverConfigPath: setup.serverTypes.serverConfigPath, hasHubDb: setup.serverTypes.hasHubDb, runtimeTypesPath: resolver.resolve('./runtime/types'), + sharedServerConfigSafe: isServerConfigSharedTypeSafe(setup.serverTypes.serverConfigPath), }) } diff --git a/src/module/hooks.ts b/src/module/hooks.ts index 59613945..2a8e986e 100644 --- a/src/module/hooks.ts +++ b/src/module/hooks.ts @@ -1,8 +1,9 @@ import type { Nuxt, NuxtPage } from '@nuxt/schema' import type { AuthRouteRules } from '../runtime/types' +import { existsSync, statSync } from 'node:fs' import { addComponentsDir, addImportsDir, addPlugin, addServerHandler, addServerImports, addServerImportsDir, addServerScanDir, extendPages, hasNuxtModule, installModule, updateTemplates } from '@nuxt/kit' import { defu } from 'defu' -import { join } from 'pathe' +import { isAbsolute, join } from 'pathe' import { createRouter, toRouteMatcher } from 'radix3' import { setupDevTools } from '../devtools' @@ -90,6 +91,15 @@ export function registerPrepareTypesHook(input: RegisterPrepareTypesHookInput): nodeTsConfig.compilerOptions.paths[key] = [value] } + for (const [key, value] of Object.entries(nuxt.options.alias)) { + if (typeof value !== 'string' || !isAbsolute(value)) + continue + + nodeTsConfig.compilerOptions.paths[key] ||= [value] + if (!key.includes('*') && existsSync(value) && statSync(value).isDirectory()) + nodeTsConfig.compilerOptions.paths[`${key}/*`] ||= [join(value, '*')] + } + nodeTsConfig.compilerOptions.paths['#server/*'] = [join(serverDir, '*')] for (const path of projectReferenceTypePaths) { diff --git a/src/module/templates.ts b/src/module/templates.ts index 85f41040..19b40095 100644 --- a/src/module/templates.ts +++ b/src/module/templates.ts @@ -97,7 +97,6 @@ export function createDatabase(event) { registerClientCleanup(event, client) return database } - const client = postgres(hyperdrive.connectionString, { prepare: false, onnotice: () => {}, diff --git a/src/module/type-templates.ts b/src/module/type-templates.ts index 1215477b..29de1d11 100644 --- a/src/module/type-templates.ts +++ b/src/module/type-templates.ts @@ -4,10 +4,14 @@ interface RegisterServerTypeTemplatesInput { serverConfigPath: string hasHubDb: boolean runtimeTypesPath: string + sharedServerConfigSafe: boolean } export function registerServerTypeTemplates(input: RegisterServerTypeTemplatesInput): void { - const { serverConfigPath, hasHubDb, runtimeTypesPath } = input + const { serverConfigPath, hasHubDb, runtimeTypesPath, sharedServerConfigSafe } = input + const serverConfigTypeTemplateOptions = sharedServerConfigSafe + ? { nuxt: true, nitro: true, node: true, shared: true } + : { nuxt: true, nitro: true, node: true } addTypeTemplate({ filename: 'types/auth-secondary-storage.d.ts', @@ -53,12 +57,40 @@ declare module '#auth/schema' { `, }, { nitro: true }) + addTypeTemplate({ + filename: 'types/nuxt-better-auth-server-context.d.ts', + getContents: () => ` +/// +/// +/// +/// +${hasHubDb ? '/// ' : ''} + +export {} +`, + }, { node: true }) + + addTypeTemplate({ + filename: 'types/nuxt-better-auth-config-context.d.ts', + getContents: () => ` +import type { RuntimeConfig } from 'nuxt/schema' + +declare module '@onmax/nuxt-better-auth/config' { + interface ServerAuthContextExtension { + runtimeConfig: RuntimeConfig + db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'undefined'} + requestOrigin?: string + } +} + +`, + }, { nuxt: true, nitro: true, node: true, shared: true }) + addTypeTemplate({ filename: 'types/nuxt-better-auth-infer.d.ts', getContents: () => ` import type { BetterAuthOptions, BetterAuthPlugin, InferPluginTypes, UnionToIntersection } from 'better-auth' import type { InferFieldsOutput } from 'better-auth/db' -import type { RuntimeConfig } from 'nuxt/schema' import type createServerAuth from '${serverConfigPath}' type _RawConfig = ReturnType @@ -88,30 +120,10 @@ type _SessionFallback = _InferModelFieldsFromPlugins<_RawPlugins, 'session'> & _ declare module '#nuxt-better-auth' { interface AuthUser extends _UserFallback {} interface AuthSession extends _SessionFallback {} - interface ServerAuthContext { - runtimeConfig: RuntimeConfig - db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'undefined'} - requestOrigin?: string - } type PluginTypes = InferPluginTypes<_Config> } - -interface _AugmentedServerAuthContext { - runtimeConfig: RuntimeConfig - db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'undefined'} - requestOrigin?: string -} - -declare module '@onmax/nuxt-better-auth/config' { - import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth' - type ServerAuthConfig = Omit & { - plugins?: readonly BetterAuthPlugin[] - } - export function defineServerAuth(config: (ctx: _AugmentedServerAuthContext) => R & ServerAuthConfig): (ctx: _AugmentedServerAuthContext) => R - export function defineServerAuth(config: R & ServerAuthConfig): (ctx: _AugmentedServerAuthContext) => R -} `, - }, { nuxt: true, nitro: true, node: true, shared: true }) + }, serverConfigTypeTemplateOptions) addTypeTemplate({ filename: 'types/nuxt-better-auth-social-providers.d.ts', @@ -128,7 +140,7 @@ declare module '#nuxt-better-auth' { } } `, - }, { nuxt: true, nitro: true, node: true, shared: true }) + }, serverConfigTypeTemplateOptions) addTypeTemplate({ filename: 'types/nuxt-better-auth-nitro.d.ts', @@ -326,7 +338,7 @@ declare module 'nitro/types' { } export {} `, - }, { nuxt: true, nitro: true, node: true }) + }, { nitro: true, node: true }) } interface RegisterSharedTypeTemplatesInput { diff --git a/src/runtime/config.ts b/src/runtime/config.ts index fac08b39..0745e591 100644 --- a/src/runtime/config.ts +++ b/src/runtime/config.ts @@ -1,11 +1,11 @@ import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth' import type { BetterAuthClientOptions } from 'better-auth/client' import type { Casing } from 'drizzle-orm/utils' -import type { ServerAuthContext } from './types/augment' +import type { ServerAuthContext as BaseServerAuthContext } from './types/augment' import { createAuthClient } from 'better-auth/vue' -// Re-export for declaration merging with generated types -export type { ServerAuthContext } +export interface ServerAuthContextExtension {} +export type ServerAuthContext = BaseServerAuthContext & ServerAuthContextExtension export interface ClientAuthContext { siteUrl: string diff --git a/test/cases/layer-server-auth-typecheck-base/server/auth.config.ts b/test/cases/layer-server-auth-typecheck-base/server/auth.config.ts index 52bbf3a8..e1588944 100644 --- a/test/cases/layer-server-auth-typecheck-base/server/auth.config.ts +++ b/test/cases/layer-server-auth-typecheck-base/server/auth.config.ts @@ -1,16 +1,20 @@ import { defineServerAuth } from '@onmax/nuxt-better-auth/config' -export default defineServerAuth(() => ({ - emailAndPassword: { - enabled: true, - }, - databaseHooks: { - session: { - create: { - async after() { - await sessionHookAfter() +export default defineServerAuth(({ db: _db }) => { + type _DbSelect = typeof _db.select + + return { + emailAndPassword: { + enabled: true, + }, + databaseHooks: { + session: { + create: { + async after() { + await sessionHookAfter() + }, }, }, }, - }, -})) + } +}) diff --git a/test/cases/plugins-type-inference/tsconfig.type-check.json b/test/cases/plugins-type-inference/tsconfig.type-check.json index 8df84722..d313d6fe 100644 --- a/test/cases/plugins-type-inference/tsconfig.type-check.json +++ b/test/cases/plugins-type-inference/tsconfig.type-check.json @@ -1,11 +1,28 @@ { - "extends": "./.nuxt/tsconfig.json", "compilerOptions": { - "noEmit": true + "target": "ESNext", + "lib": [ + "ESNext", + "DOM" + ], + "baseUrl": ".", + "module": "preserve", + "moduleResolution": "bundler", + "paths": { + "#auth/client": ["./app/auth.config"], + "#auth/server": ["./server/auth.config"], + "#nuxt-better-auth": ["../../../src/runtime/types/augment"] + }, + "types": [], + "strict": true, + "noEmit": true, + "skipLibCheck": true }, - "include": [ - "./.nuxt/nuxt.d.ts", + "files": [ "./virtual-modules.d.ts", + "./.nuxt/types/nuxt-better-auth-infer.d.ts", + "./.nuxt/types/nuxt-better-auth-social-providers.d.ts", + "./.nuxt/types/nuxt-better-auth-nitro.d.ts", "./typecheck-target.ts" ] } diff --git a/test/cases/server-auth-alias-typecheck/server/auth.config.ts b/test/cases/server-auth-alias-typecheck/server/auth.config.ts index dde456f0..94f51099 100644 --- a/test/cases/server-auth-alias-typecheck/server/auth.config.ts +++ b/test/cases/server-auth-alias-typecheck/server/auth.config.ts @@ -1,17 +1,21 @@ import { sessionHookAfter } from '#server/utils/hooks' import { defineServerAuth } from '@onmax/nuxt-better-auth/config' -export default defineServerAuth(() => ({ - emailAndPassword: { - enabled: true, - }, - databaseHooks: { - session: { - create: { - async after() { - await sessionHookAfter() +export default defineServerAuth(({ db: _db }) => { + type _DbSelect = typeof _db.select + + return { + emailAndPassword: { + enabled: true, + }, + databaseHooks: { + session: { + create: { + async after() { + await sessionHookAfter() + }, }, }, }, - }, -})) + } +}) diff --git a/test/server-auth-project-references-typecheck.test.ts b/test/server-auth-project-references-typecheck.test.ts index d8a76492..118e25f8 100644 --- a/test/server-auth-project-references-typecheck.test.ts +++ b/test/server-auth-project-references-typecheck.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { beforeAll, describe, expect, it } from 'vitest' @@ -39,12 +39,72 @@ function runProjectReferenceTypecheck(fixtureDir: string) { expect(typecheck.status, `vue-tsc -b failed:\n${typecheck.stdout}\n${typecheck.stderr}`).toBe(0) } +function readGeneratedJson(fixtureDir: string, filePath: string) { + return JSON.parse(readFileSync(`${fixtureDir}/.nuxt/${filePath}`, 'utf8')) +} + +function generatedReferences(fixtureDir: string, filePath: string) { + return (readGeneratedJson(fixtureDir, filePath).references || []) + .map((reference: { path?: string }) => reference.path) + .filter(Boolean) +} + +function expectSharedTypeReferencesToStayClientSafe(fixtureDir: string) { + const sharedReferences = generatedReferences(fixtureDir, 'tsconfig.shared.json') + const serverOnlyReferences = [ + 'types/nuxt-better-auth-server-context.d.ts', + 'types/nuxt-better-auth-infer.d.ts', + 'types/nuxt-better-auth-social-providers.d.ts', + 'types/nuxt-better-auth-nitro.d.ts', + 'types/nitro-imports.d.ts', + 'types/auth-database.d.ts', + 'types/auth-schema.d.ts', + 'types/auth-secondary-storage.d.ts', + 'hub/db.d.ts', + ] + + for (const reference of serverOnlyReferences) + expect(sharedReferences).not.toContain(reference) +} + +function expectServerContextToAvoidNuxthubAugmentation(fixtureDir: string) { + const contents = readFileSync(`${fixtureDir}/.nuxt/types/nuxt-better-auth-server-context.d.ts`, 'utf8') + expect(contents).not.toContain('declare module \'@nuxthub/db\'') + expect(contents).not.toContain('declare module "@nuxthub/db"') +} + +function expectNuxtTypesToStayClientSafe(fixtureDir: string) { + const contents = readFileSync(`${fixtureDir}/.nuxt/nuxt.d.ts`, 'utf8') + expect(contents).toContain('types/nuxt-better-auth-config-context.d.ts') + expect(contents).toContain('types/nuxt-better-auth-infer.d.ts') + expect(contents).toContain('types/nuxt-better-auth-social-providers.d.ts') + expect(contents).not.toContain('types/nuxt-better-auth-nitro.d.ts') +} + +function expectSharedTypesToIncludeOnlySafeConfigContext(fixtureDir: string) { + const contents = readFileSync(`${fixtureDir}/.nuxt/nuxt.shared.d.ts`, 'utf8') + expect(contents).toContain('types/nuxt-better-auth-config-context.d.ts') + expect(contents).not.toContain('types/nuxt-better-auth-infer.d.ts') + expect(contents).not.toContain('types/nuxt-better-auth-social-providers.d.ts') + expect(contents).not.toContain('types/nuxt-better-auth-nitro.d.ts') +} + describe('server auth config project-reference typecheck regression #309', () => { it('typechecks a layered auth config that uses Nitro auto-imported helpers', () => { - runProjectReferenceTypecheck(fileURLToPath(new URL('./cases/layer-server-auth-typecheck', import.meta.url))) + const fixtureDir = fileURLToPath(new URL('./cases/layer-server-auth-typecheck', import.meta.url)) + runProjectReferenceTypecheck(fixtureDir) + expectSharedTypeReferencesToStayClientSafe(fixtureDir) + expectServerContextToAvoidNuxthubAugmentation(fixtureDir) + expectNuxtTypesToStayClientSafe(fixtureDir) + expectSharedTypesToIncludeOnlySafeConfigContext(fixtureDir) }, 60_000) it('typechecks auth config imports that use the #server alias', () => { - runProjectReferenceTypecheck(fileURLToPath(new URL('./cases/server-auth-alias-typecheck', import.meta.url))) + const fixtureDir = fileURLToPath(new URL('./cases/server-auth-alias-typecheck', import.meta.url)) + runProjectReferenceTypecheck(fixtureDir) + expectSharedTypeReferencesToStayClientSafe(fixtureDir) + expectServerContextToAvoidNuxthubAugmentation(fixtureDir) + expectNuxtTypesToStayClientSafe(fixtureDir) + expectSharedTypesToIncludeOnlySafeConfigContext(fixtureDir) }, 60_000) })