Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 29 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<void> {
const configs = resolveAuthConfigDescriptors(nuxt)

Expand Down Expand Up @@ -136,6 +164,7 @@ export default defineNuxtModule<BetterAuthModuleOptions>({
serverConfigPath: setup.serverTypes.serverConfigPath,
hasHubDb: setup.serverTypes.hasHubDb,
runtimeTypesPath: resolver.resolve('./runtime/types'),
sharedServerConfigSafe: isServerConfigSharedTypeSafe(setup.serverTypes.serverConfigPath),
})
}

Expand Down
12 changes: 11 additions & 1 deletion src/module/hooks.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion src/module/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export function createDatabase(event) {
registerClientCleanup(event, client)
return database
}

const client = postgres(hyperdrive.connectionString, {
prepare: false,
onnotice: () => {},
Expand Down
62 changes: 37 additions & 25 deletions src/module/type-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -53,12 +57,40 @@ declare module '#auth/schema' {
`,
}, { nitro: true })

addTypeTemplate({
filename: 'types/nuxt-better-auth-server-context.d.ts',
getContents: () => `
/// <reference path="./nitro-imports.d.ts" />
/// <reference path="./auth-database.d.ts" />
/// <reference path="./auth-schema.d.ts" />
/// <reference path="./auth-secondary-storage.d.ts" />
${hasHubDb ? '/// <reference path="../hub/db.d.ts" />' : ''}

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<typeof createServerAuth>
Expand Down Expand Up @@ -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<BetterAuthOptions, 'secret' | 'baseURL'> & {
plugins?: readonly BetterAuthPlugin[]
}
export function defineServerAuth<const R>(config: (ctx: _AugmentedServerAuthContext) => R & ServerAuthConfig): (ctx: _AugmentedServerAuthContext) => R
export function defineServerAuth<const R>(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',
Expand All @@ -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',
Expand Down Expand Up @@ -326,7 +338,7 @@ declare module 'nitro/types' {
}
export {}
`,
}, { nuxt: true, nitro: true, node: true })
}, { nitro: true, node: true })
}

interface RegisterSharedTypeTemplatesInput {
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/config.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 15 additions & 11 deletions test/cases/layer-server-auth-typecheck-base/server/auth.config.ts
Original file line number Diff line number Diff line change
@@ -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()
},
},
},
},
},
}))
}
})
25 changes: 21 additions & 4 deletions test/cases/plugins-type-inference/tsconfig.type-check.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
26 changes: 15 additions & 11 deletions test/cases/server-auth-alias-typecheck/server/auth.config.ts
Original file line number Diff line number Diff line change
@@ -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()
},
},
},
},
},
}))
}
})
66 changes: 63 additions & 3 deletions test/server-auth-project-references-typecheck.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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)
})
Loading