diff --git a/playground/app/types/auth.d.ts b/playground/app/types/auth.d.ts deleted file mode 100644 index 7cf40ce..0000000 --- a/playground/app/types/auth.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Augment AuthUser with plugin fields (admin, twoFactor) -declare module '#nuxt-better-auth' { - interface AuthUser { - role?: 'user' | 'admin' - twoFactorEnabled?: boolean - } -} - -export {} diff --git a/src/module/type-templates.ts b/src/module/type-templates.ts index 8192fd2..3b01bc3 100644 --- a/src/module/type-templates.ts +++ b/src/module/type-templates.ts @@ -37,31 +37,57 @@ declare module '#auth/database' { addTypeTemplate({ filename: 'types/nuxt-better-auth-infer.d.ts', getContents: () => ` -import type { InferUser, InferSession, InferPluginTypes } from 'better-auth' +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 _Config = ReturnType +type _RawConfig = ReturnType +type _RawPlugins = _RawConfig extends { plugins: infer P } ? P : _RawConfig extends { plugins?: infer P } ? P : [] +type _NormalizedPlugins = _RawPlugins extends readonly (infer T)[] + ? Array + : _RawPlugins extends (infer T)[] + ? Array + : BetterAuthPlugin[] +type _Config = Omit & Omit<_RawConfig, 'plugins'> & { + plugins?: _NormalizedPlugins +} + +type _InferModelFieldsFromPlugins = P extends readonly (infer Plugin)[] + ? UnionToIntersection : {}> + : P extends (infer Plugin)[] + ? UnionToIntersection : {}> + : {} + +type _InferModelFieldsFromOptions = C extends { [K in M]: { additionalFields: infer F } } + ? InferFieldsOutput + : {} + +type _UserFallback = _InferModelFieldsFromPlugins<_RawPlugins, 'user'> & _InferModelFieldsFromOptions<_RawConfig, 'user'> +type _SessionFallback = _InferModelFieldsFromPlugins<_RawPlugins, 'session'> & _InferModelFieldsFromOptions<_RawConfig, 'session'> declare module '#nuxt-better-auth' { - interface AuthUser extends InferUser<_Config> {} - interface AuthSession extends InferSession<_Config> {} + interface AuthUser extends _UserFallback {} + interface AuthSession extends _SessionFallback {} interface ServerAuthContext { runtimeConfig: RuntimeConfig - ${hasHubDb ? `db: typeof import('@nuxthub/db')['db']` : ''} + db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'unknown'} } type PluginTypes = InferPluginTypes<_Config> } interface _AugmentedServerAuthContext { runtimeConfig: RuntimeConfig - ${hasHubDb ? `db: typeof import('@nuxthub/db')['db']` : 'db: unknown'} + db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'unknown'} } declare module '@onmax/nuxt-better-auth/config' { - import type { BetterAuthOptions } from 'better-auth' - type ServerAuthConfig = Omit - export function defineServerAuth(config: T | ((ctx: _AugmentedServerAuthContext) => T)): (ctx: _AugmentedServerAuthContext) => T + import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth' + type ServerAuthConfig = Omit & { + plugins?: readonly BetterAuthPlugin[] + } + export function defineServerAuth(config: R): (ctx: _AugmentedServerAuthContext) => R + export function defineServerAuth(config: (ctx: _AugmentedServerAuthContext) => R): (ctx: _AugmentedServerAuthContext) => R } `, }, { nuxt: true, nitro: true, node: true }) diff --git a/src/runtime/config.ts b/src/runtime/config.ts index 26d6e3e..67ca804 100644 --- a/src/runtime/config.ts +++ b/src/runtime/config.ts @@ -1,4 +1,4 @@ -import type { BetterAuthOptions } from 'better-auth' +import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth' import type { BetterAuthClientOptions } from 'better-auth/client' import type { DatabaseProvider } from '../database-provider' import type { CasingOption } from '../schema-generator' @@ -12,7 +12,9 @@ export interface ClientAuthContext { siteUrl: string } -export type ServerAuthConfig = Omit +export type ServerAuthConfig = Omit & { + plugins?: readonly BetterAuthPlugin[] +} export type ClientAuthConfig = Omit & { baseURL?: string } export type ServerAuthConfigFn = (ctx: ServerAuthContext) => ServerAuthConfig @@ -72,6 +74,8 @@ export interface AuthPrivateRuntimeConfig { secondaryStorage: boolean } +export function defineServerAuth(config: R): (ctx: ServerAuthContext) => R +export function defineServerAuth(config: (ctx: ServerAuthContext) => R): (ctx: ServerAuthContext) => R export function defineServerAuth(config: T | ((ctx: ServerAuthContext) => T)): (ctx: ServerAuthContext) => T { return typeof config === 'function' ? config : () => config } diff --git a/test/fixtures/basic/.nuxtrc b/test/cases/core-auth/.nuxtrc similarity index 100% rename from test/fixtures/basic/.nuxtrc rename to test/cases/core-auth/.nuxtrc diff --git a/test/fixtures/basic/app/app.vue b/test/cases/core-auth/app/app.vue similarity index 100% rename from test/fixtures/basic/app/app.vue rename to test/cases/core-auth/app/app.vue diff --git a/test/fixtures/basic/app/auth.config.ts b/test/cases/core-auth/app/auth.config.ts similarity index 100% rename from test/fixtures/basic/app/auth.config.ts rename to test/cases/core-auth/app/auth.config.ts diff --git a/test/fixtures/basic/app/pages/custom-protected.vue b/test/cases/core-auth/app/pages/custom-protected.vue similarity index 100% rename from test/fixtures/basic/app/pages/custom-protected.vue rename to test/cases/core-auth/app/pages/custom-protected.vue diff --git a/test/fixtures/basic/app/pages/dynamic/[...slug].vue b/test/cases/core-auth/app/pages/dynamic/[...slug].vue similarity index 100% rename from test/fixtures/basic/app/pages/dynamic/[...slug].vue rename to test/cases/core-auth/app/pages/dynamic/[...slug].vue diff --git a/test/fixtures/basic/app/pages/index.vue b/test/cases/core-auth/app/pages/index.vue similarity index 100% rename from test/fixtures/basic/app/pages/index.vue rename to test/cases/core-auth/app/pages/index.vue diff --git a/test/fixtures/basic/app/pages/login.vue b/test/cases/core-auth/app/pages/login.vue similarity index 100% rename from test/fixtures/basic/app/pages/login.vue rename to test/cases/core-auth/app/pages/login.vue diff --git a/test/fixtures/basic/app/pages/protected.vue b/test/cases/core-auth/app/pages/protected.vue similarity index 100% rename from test/fixtures/basic/app/pages/protected.vue rename to test/cases/core-auth/app/pages/protected.vue diff --git a/test/fixtures/basic/nuxt.config.ts b/test/cases/core-auth/nuxt.config.ts similarity index 100% rename from test/fixtures/basic/nuxt.config.ts rename to test/cases/core-auth/nuxt.config.ts diff --git a/test/fixtures/basic/server/api/test/config.get.ts b/test/cases/core-auth/server/api/test/config.get.ts similarity index 100% rename from test/fixtures/basic/server/api/test/config.get.ts rename to test/cases/core-auth/server/api/test/config.get.ts diff --git a/test/fixtures/basic/server/api/test/me.get.ts b/test/cases/core-auth/server/api/test/me.get.ts similarity index 100% rename from test/fixtures/basic/server/api/test/me.get.ts rename to test/cases/core-auth/server/api/test/me.get.ts diff --git a/test/fixtures/basic/server/auth.config.ts b/test/cases/core-auth/server/auth.config.ts similarity index 100% rename from test/fixtures/basic/server/auth.config.ts rename to test/cases/core-auth/server/auth.config.ts diff --git a/test/fixtures/basic/server/plugins/init-db.ts b/test/cases/core-auth/server/plugins/init-db.ts similarity index 100% rename from test/fixtures/basic/server/plugins/init-db.ts rename to test/cases/core-auth/server/plugins/init-db.ts diff --git a/test/fixtures/no-db/.nuxtrc b/test/cases/database-less/.nuxtrc similarity index 100% rename from test/fixtures/no-db/.nuxtrc rename to test/cases/database-less/.nuxtrc diff --git a/test/fixtures/no-db/app/app.vue b/test/cases/database-less/app/app.vue similarity index 100% rename from test/fixtures/no-db/app/app.vue rename to test/cases/database-less/app/app.vue diff --git a/test/fixtures/no-db/app/auth.config.ts b/test/cases/database-less/app/auth.config.ts similarity index 100% rename from test/fixtures/no-db/app/auth.config.ts rename to test/cases/database-less/app/auth.config.ts diff --git a/test/fixtures/no-db/app/pages/index.vue b/test/cases/database-less/app/pages/index.vue similarity index 100% rename from test/fixtures/no-db/app/pages/index.vue rename to test/cases/database-less/app/pages/index.vue diff --git a/test/fixtures/no-db/nuxt.config.ts b/test/cases/database-less/nuxt.config.ts similarity index 100% rename from test/fixtures/no-db/nuxt.config.ts rename to test/cases/database-less/nuxt.config.ts diff --git a/test/fixtures/no-db/server/api/test/config.get.ts b/test/cases/database-less/server/api/test/config.get.ts similarity index 100% rename from test/fixtures/no-db/server/api/test/config.get.ts rename to test/cases/database-less/server/api/test/config.get.ts diff --git a/test/fixtures/no-db/server/auth.config.ts b/test/cases/database-less/server/auth.config.ts similarity index 100% rename from test/fixtures/no-db/server/auth.config.ts rename to test/cases/database-less/server/auth.config.ts diff --git a/test/fixtures/no-hub/.nuxtrc b/test/cases/plugins-type-inference/.nuxtrc similarity index 100% rename from test/fixtures/no-hub/.nuxtrc rename to test/cases/plugins-type-inference/.nuxtrc diff --git a/test/cases/plugins-type-inference/app/app.vue b/test/cases/plugins-type-inference/app/app.vue new file mode 100644 index 0000000..2bb142d --- /dev/null +++ b/test/cases/plugins-type-inference/app/app.vue @@ -0,0 +1,3 @@ + diff --git a/test/cases/plugins-type-inference/app/auth.config.ts b/test/cases/plugins-type-inference/app/auth.config.ts new file mode 100644 index 0000000..ce985ee --- /dev/null +++ b/test/cases/plugins-type-inference/app/auth.config.ts @@ -0,0 +1,3 @@ +import { defineClientAuth } from '../../../../src/runtime/config' + +export default defineClientAuth({}) diff --git a/test/cases/plugins-type-inference/nuxt.config.ts b/test/cases/plugins-type-inference/nuxt.config.ts new file mode 100644 index 0000000..4d5379e --- /dev/null +++ b/test/cases/plugins-type-inference/nuxt.config.ts @@ -0,0 +1,10 @@ +export default defineNuxtConfig({ + modules: ['../../../src/module'], + runtimeConfig: { + betterAuthSecret: 'test-secret-for-testing-only-32chars!', + public: { siteUrl: 'http://localhost:3000' }, + }, + routeRules: { + '/admin/**': { auth: { user: { role: 'admin', internalCode: 'x' } } }, + }, +}) diff --git a/test/cases/plugins-type-inference/server/auth.config.ts b/test/cases/plugins-type-inference/server/auth.config.ts new file mode 100644 index 0000000..c55110d --- /dev/null +++ b/test/cases/plugins-type-inference/server/auth.config.ts @@ -0,0 +1,37 @@ +import { defineServerAuth } from '../../../../src/runtime/config' + +function customAdminLikePlugin() { + return { + id: 'custom-admin-like', + $ERROR_CODES: { + BROKEN: { + code: 'BROKEN', + message: 'Broken', + }, + }, + schema: { + user: { + fields: { + role: { + type: 'string', + required: false, + input: false, + }, + }, + }, + }, + } as const +} + +export default defineServerAuth({ + emailAndPassword: { enabled: true }, + plugins: [customAdminLikePlugin()], + user: { + additionalFields: { + internalCode: { + type: 'string', + required: false, + }, + }, + }, +}) diff --git a/test/cases/plugins-type-inference/tsconfig.json b/test/cases/plugins-type-inference/tsconfig.json new file mode 100644 index 0000000..4b34df1 --- /dev/null +++ b/test/cases/plugins-type-inference/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/test/cases/plugins-type-inference/tsconfig.type-check.json b/test/cases/plugins-type-inference/tsconfig.type-check.json new file mode 100644 index 0000000..04e57dc --- /dev/null +++ b/test/cases/plugins-type-inference/tsconfig.type-check.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "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 + }, + "files": [ + "./.nuxt/types/nuxt-better-auth-infer.d.ts", + "./typecheck-target.ts" + ] +} diff --git a/test/cases/plugins-type-inference/typecheck-target.ts b/test/cases/plugins-type-inference/typecheck-target.ts new file mode 100644 index 0000000..1937fad --- /dev/null +++ b/test/cases/plugins-type-inference/typecheck-target.ts @@ -0,0 +1,14 @@ +import type { AuthUser } from '#nuxt-better-auth' + +const user: AuthUser = { + id: '1', + createdAt: new Date(), + updatedAt: new Date(), + email: 'a@b.c', + emailVerified: false, + name: 'n', + role: 'admin', + internalCode: 'x', +} + +void user diff --git a/test/cases/without-nuxthub/.nuxtrc b/test/cases/without-nuxthub/.nuxtrc new file mode 100644 index 0000000..c85c9d1 --- /dev/null +++ b/test/cases/without-nuxthub/.nuxtrc @@ -0,0 +1 @@ +setups.@onmax/nuxt-better-auth="0.0.2-alpha.21" \ No newline at end of file diff --git a/test/fixtures/no-hub/app/app.vue b/test/cases/without-nuxthub/app/app.vue similarity index 100% rename from test/fixtures/no-hub/app/app.vue rename to test/cases/without-nuxthub/app/app.vue diff --git a/test/fixtures/no-hub/app/auth.config.ts b/test/cases/without-nuxthub/app/auth.config.ts similarity index 100% rename from test/fixtures/no-hub/app/auth.config.ts rename to test/cases/without-nuxthub/app/auth.config.ts diff --git a/test/fixtures/no-hub/app/pages/index.vue b/test/cases/without-nuxthub/app/pages/index.vue similarity index 100% rename from test/fixtures/no-hub/app/pages/index.vue rename to test/cases/without-nuxthub/app/pages/index.vue diff --git a/test/fixtures/no-hub/nuxt.config.ts b/test/cases/without-nuxthub/nuxt.config.ts similarity index 100% rename from test/fixtures/no-hub/nuxt.config.ts rename to test/cases/without-nuxthub/nuxt.config.ts diff --git a/test/fixtures/no-hub/server/auth.config.ts b/test/cases/without-nuxthub/server/auth.config.ts similarity index 100% rename from test/fixtures/no-hub/server/auth.config.ts rename to test/cases/without-nuxthub/server/auth.config.ts diff --git a/test/infer-plugins-types.test.ts b/test/infer-plugins-types.test.ts new file mode 100644 index 0000000..03ab556 --- /dev/null +++ b/test/infer-plugins-types.test.ts @@ -0,0 +1,32 @@ +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const fixtureDir = fileURLToPath(new URL('./cases/plugins-type-inference', import.meta.url)) +const env = { + ...process.env, + BETTER_AUTH_SECRET: 'test-secret-for-testing-only-32chars', +} + +describe('type inference regression #107', () => { + it('typechecks routeRules user fields inferred from plugins/additionalFields', () => { + const prepare = spawnSync('npx', ['nuxi', 'prepare'], { + cwd: fixtureDir, + env, + encoding: 'utf8', + }) + expect(prepare.status, `nuxi prepare failed:\n${prepare.stdout}\n${prepare.stderr}`).toBe(0) + + const typecheck = spawnSync('npx', ['vue-tsc', '--noEmit', '--pretty', 'false', '-p', 'tsconfig.type-check.json'], { + cwd: fixtureDir, + env, + encoding: 'utf8', + }) + expect(typecheck.status, `vue-tsc failed:\n${typecheck.stdout}\n${typecheck.stderr}`).toBe(0) + const output = `${typecheck.stdout}\n${typecheck.stderr}` + + expect(output).not.toContain(`is not assignable to type 'BetterAuthPlugin'`) + expect(output).not.toContain(`'role' does not exist in type`) + expect(output).not.toContain(`'internalCode' does not exist in type`) + }, 60_000) +}) diff --git a/test/module.test.ts b/test/module.test.ts index 28d5a50..52ed6f8 100644 --- a/test/module.test.ts +++ b/test/module.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest' describe('nuxt-better-auth module', async () => { await setup({ - rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), + rootDir: fileURLToPath(new URL('./cases/core-auth', import.meta.url)), }) describe('page rendering', () => { diff --git a/test/no-db.test.ts b/test/no-db.test.ts index 59176b2..b65774d 100644 --- a/test/no-db.test.ts +++ b/test/no-db.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest' describe('no-db mode (NuxtHub without database)', async () => { await setup({ - rootDir: fileURLToPath(new URL('./fixtures/no-db', import.meta.url)), + rootDir: fileURLToPath(new URL('./cases/database-less', import.meta.url)), }) it('renders home page without database', async () => { diff --git a/test/no-hub.test.ts b/test/no-hub.test.ts index 21c57e9..c670cd9 100644 --- a/test/no-hub.test.ts +++ b/test/no-hub.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest' describe('no-hub mode (without NuxtHub)', async () => { await setup({ - rootDir: fileURLToPath(new URL('./fixtures/no-hub', import.meta.url)), + rootDir: fileURLToPath(new URL('./cases/without-nuxthub', import.meta.url)), }) it('renders home page without NuxtHub', async () => { diff --git a/test/schema-generator.test.ts b/test/schema-generator.test.ts index eba7521..bc6e456 100644 --- a/test/schema-generator.test.ts +++ b/test/schema-generator.test.ts @@ -140,6 +140,13 @@ describe('loadUserAuthConfig', () => { const result = await loadUserAuthConfig(configPath, false) expect(result).toEqual({ appName: 'Test', plugins: [] }) }) + + it('accepts readonly plugin tuples in object syntax defineServerAuth', async () => { + const configPath = join(TEST_DIR, 'readonly-object-config.ts') + writeFileSync(configPath, `const plugin = { id: 'test-plugin', schema: { user: { fields: {} } } } as const\nexport default defineServerAuth({ appName: 'Readonly', plugins: [plugin] as const })`) + const result = await loadUserAuthConfig(configPath, false) + expect(result).toEqual({ appName: 'Readonly', plugins: [{ id: 'test-plugin', schema: { user: { fields: {} } } }] }) + }) }) describe('defineServerAuth', () => {