diff --git a/.changeset/plenty-deserts-tan.md b/.changeset/plenty-deserts-tan.md new file mode 100644 index 000000000..9a81c6cb6 --- /dev/null +++ b/.changeset/plenty-deserts-tan.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat(eslint): add `recommendedTypeChecked` config diff --git a/packages/sv/src/addons/better-auth.ts b/packages/sv/src/addons/better-auth.ts index 1670c4d37..2afbed850 100644 --- a/packages/sv/src/addons/better-auth.ts +++ b/packages/sv/src/addons/better-auth.ts @@ -50,8 +50,8 @@ export default defineAddon({ let drizzleDialect: Dialect; - sv.devDependency('better-auth', '^1.4.18'); - sv.devDependency('@better-auth/cli', '^1.4.18'); + sv.devDependency('better-auth', '~1.4.18'); + sv.devDependency('@better-auth/cli', '~1.4.18'); sv.file(`drizzle.config.${language}`, (content) => { const { ast, generateCode } = parse.script(content); @@ -169,6 +169,13 @@ export default defineAddon({ throw new Error('Failed detecting `locals` interface in `src/app.d.ts`'); } + // Add UserInfo interface with explicit properties + // as better-auth/minimal does not export User type + js.common.appendFromString(ast, { + code: 'interface UserInfo extends User { id: string; name: string; }', + comments + }); + // remove the commented out placeholder since we're adding the real one comments.remove((c) => c.type === 'Line' && c.value.trim() === 'interface Locals {}'); @@ -180,7 +187,7 @@ export default defineAddon({ ); if (!user) { - locals.body.body.push(js.common.createTypeProperty('user', 'User', true)); + locals.body.body.push(js.common.createTypeProperty('user', 'UserInfo', true)); } if (!session) { locals.body.body.push(js.common.createTypeProperty('session', 'Session', true)); @@ -241,8 +248,8 @@ export default defineAddon({ ? ` signInEmail: async (event) => { const formData = await event.request.formData(); - const email = formData.get('email')?.toString() ?? ''; - const password = formData.get('password')?.toString() ?? ''; + const email = (formData.get('email') ?? '') as string; + const password = (formData.get('password') ?? '') as string; try { await auth.api.signInEmail({ @@ -263,9 +270,9 @@ export default defineAddon({ }, signUpEmail: async (event) => { const formData = await event.request.formData(); - const email = formData.get('email')?.toString() ?? ''; - const password = formData.get('password')?.toString() ?? ''; - const name = formData.get('name')?.toString() ?? ''; + const email = (formData.get('email') ?? '') as string; + const password = (formData.get('password') ?? '') as string; + const name = (formData.get('name') ?? '') as string; try { await auth.api.signUpEmail({ @@ -291,8 +298,8 @@ export default defineAddon({ ? ` signInSocial: async (event) => { const formData = await event.request.formData(); - const provider = formData.get('provider')?.toString() ?? 'github'; - const callbackURL = formData.get('callbackURL')?.toString() ?? '/demo/better-auth'; + const provider = (formData.get('provider') ?? 'github') as string; + const callbackURL = (formData.get('callbackURL') ?? '/demo/better-auth') as string; const result = await auth.api.signInSocial({ body: { @@ -317,7 +324,7 @@ export default defineAddon({ import { auth } from '$lib/server/auth'; ${needsAPIError ? "import { APIError } from 'better-auth/api';" : ''} - export const load${ts(': PageServerLoad')} = async (event) => { + export const load${ts(': PageServerLoad')} = (event) => { if (event.locals.user) { return redirect(302, '/demo/better-auth'); } @@ -406,7 +413,7 @@ export default defineAddon({ ${ts("import type { PageServerLoad } from './$types';")} import { auth } from '$lib/server/auth'; - export const load${ts(': PageServerLoad')} = async (event) => { + export const load${ts(': PageServerLoad')} = (event) => { if (!event.locals.user) { return redirect(302, '/demo/better-auth/login'); } diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index 18bb98d43..865b4f58b 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -411,6 +411,16 @@ export default defineAddon({ return generateCode(); }); + + if (typescript) { + sv.file('tsconfig.json', (content) => { + const { data, generateCode } = parse.json(content); + const file = `drizzle.config.${language}`; + if (!data.files) data.files = []; + if (!data.files.includes(file)) data.files.push(file); + return generateCode(); + }); + } }, nextSteps: ({ options, packageManager }) => { const steps: string[] = []; diff --git a/packages/sv/src/addons/eslint.ts b/packages/sv/src/addons/eslint.ts index 120c7d35a..59d7be63e 100644 --- a/packages/sv/src/addons/eslint.ts +++ b/packages/sv/src/addons/eslint.ts @@ -48,7 +48,7 @@ export default defineAddon({ eslintConfigs.push(jsConfig); if (typescript) { - const tsConfig = js.common.parseExpression('ts.configs.recommended'); + const tsConfig = js.common.parseExpression('ts.configs.recommendedTypeChecked'); eslintConfigs.push(js.common.createSpread(tsConfig)); } @@ -77,6 +77,7 @@ export default defineAddon({ const globalsConfig = js.object.create({ languageOptions: { + parserOptions: typescript ? { projectService: true } : undefined, globals: globalsObjLiteral }, rules: typescript ? rules : undefined @@ -158,5 +159,16 @@ export default defineAddon({ if (prettierInstalled) { sv.file(files.eslintConfig, addEslintConfigPrettier); } + + if (typescript) { + sv.file('tsconfig.json', (content) => { + const { data, generateCode } = parse.json(content); + if (!data.files) data.files = []; + for (const file of ['svelte.config.js', files.eslintConfig]) { + if (!data.files.includes(file)) data.files.push(file); + } + return generateCode(); + }); + } } }); diff --git a/packages/sv/src/addons/paraglide.ts b/packages/sv/src/addons/paraglide.ts index 6bcde3224..15ca1201d 100644 --- a/packages/sv/src/addons/paraglide.ts +++ b/packages/sv/src/addons/paraglide.ts @@ -85,7 +85,9 @@ export default defineAddon({ }); const expression = js.common.parseExpression( - '(request) => deLocalizeUrl(request.url).pathname' + language === 'ts' + ? '(request: { url: URL }) => deLocalizeUrl(request.url).pathname' + : '(request) => deLocalizeUrl(request.url).pathname' ); const rerouteIdentifier = js.variables.declaration(ast, { kind: 'const', @@ -186,11 +188,12 @@ export default defineAddon({ from: '$lib/paraglide/runtime' }); js.imports.addNamed(ast.instance.content, { imports: ['page'], from: '$app/state' }); + js.imports.addNamed(ast.instance.content, { imports: ['resolve'], from: '$app/paths' }); svelte.addFragment( ast, `
- {#each locales as locale} - {locale} + {#each locales as locale (locale)} + {locale} {/each}
` ); diff --git a/packages/sv/src/addons/playwright.ts b/packages/sv/src/addons/playwright.ts index d886ec498..2382d0c92 100644 --- a/packages/sv/src/addons/playwright.ts +++ b/packages/sv/src/addons/playwright.ts @@ -37,6 +37,17 @@ export default defineAddon({ `; }); + if (language === 'ts') { + sv.file('tsconfig.json', (content) => { + const { data, generateCode } = parse.json(content); + if (!data.files) data.files = []; + for (const file of [`playwright.config.ts`, `e2e/demo.test.ts`]) { + if (!data.files.includes(file)) data.files.push(file); + } + return generateCode(); + }); + } + sv.file(`playwright.config.${language}`, (content) => { const { ast, generateCode } = parse.script(content); const defineConfig = js.common.parseExpression('defineConfig({})'); diff --git a/packages/sv/src/cli/tests/cli.ts b/packages/sv/src/cli/tests/cli.ts index 61fa5e05c..9d98131f0 100644 --- a/packages/sv/src/cli/tests/cli.ts +++ b/packages/sv/src/cli/tests/cli.ts @@ -16,7 +16,7 @@ beforeAll(() => { describe('cli', () => { const testCases = [ - { projectName: 'create-only', args: ['--no-add-ons'] }, + { projectName: 'create-only', args: ['--no-add-ons'], cmds: [] }, { projectName: 'create-with-all-addons', args: [ @@ -34,20 +34,27 @@ describe('cli', () => { 'paraglide=languageTags:en,es+demo:yes', 'mcp=ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local' // 'storybook' // No storybook addon during tests! + ], + cmds: [ + ['i'], + ['run', 'auth:schema'], + ['run', 'build'], // needed for paraglide addon + ['exec', 'eslint', '--', '.'] ] }, { projectName: '@my-org/sv', template: 'addon', - args: [] + args: [], + cmds: [['i'], ['run', 'demo-create'], ['run', 'demo-add:ci'], ['run', 'test']] } ]; it.for(testCases)( 'should create a new project with name $projectName', - { timeout: 51_000 }, + { timeout: 101_000 }, async (testCase) => { - const { projectName, args, template = 'minimal' } = testCase; + const { projectName, args, template = 'minimal', cmds } = testCase; const testOutputPath = path.relative( monoRepoPath, @@ -129,23 +136,15 @@ describe('cli', () => { packageJsonPath, JSON.stringify(packageJson, null, 3).replaceAll(' ', '\t') ); + } - const cmds = [ - // list of cmds to test - ['i'], - ['run', 'demo-create'], - ['run', 'demo-add:ci'], - ['run', 'test'] - ]; - for (const cmd of cmds) { - const res = await exec('npm', cmd, { - nodeOptions: { stdio: 'pipe', cwd: testOutputPath } - }); - expect( - res.exitCode, - `Error addon test: '${cmd}' -> ${JSON.stringify(res, null, 2)}` - ).toBe(0); - } + for (const cmd of cmds) { + const res = await exec('npm', cmd, { + nodeOptions: { stdio: 'pipe', cwd: testOutputPath } + }); + expect(res.exitCode, `Error addon test: '${cmd}' -> ${JSON.stringify(res, null, 2)}`).toBe( + 0 + ); } } ); diff --git a/packages/sv/src/cli/tests/snapshots/create-only/src/routes/+layout.svelte b/packages/sv/src/cli/tests/snapshots/create-only/src/routes/+layout.svelte index 9cebde545..671636796 100644 --- a/packages/sv/src/cli/tests/snapshots/create-only/src/routes/+layout.svelte +++ b/packages/sv/src/cli/tests/snapshots/create-only/src/routes/+layout.svelte @@ -1,7 +1,8 @@ diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js index ef3e6616b..4c63b101a 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/eslint.config.js @@ -13,12 +13,15 @@ const gitignorePath = path.resolve(import.meta.dirname, '.gitignore'); export default defineConfig( includeIgnoreFile(gitignorePath), js.configs.recommended, - ...ts.configs.recommended, + ...ts.configs.recommendedTypeChecked, ...svelte.configs.recommended, prettier, ...svelte.configs.prettier, { - languageOptions: { globals: { ...globals.browser, ...globals.node } }, + languageOptions: { + parserOptions: { projectService: true }, + globals: { ...globals.browser, ...globals.node } + }, rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json index 5cd80f37d..c8a3d3e3a 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/package.json @@ -22,7 +22,7 @@ "auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes" }, "devDependencies": { - "@better-auth/cli": "^1.4.18", + "@better-auth/cli": "~1.4.18", "@eslint/compat": "^2.0.2", "@eslint/js": "^9.39.2", "@inlang/paraglide-js": "^2.10.0", @@ -35,7 +35,7 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", "@vitest/browser-playwright": "^4.0.18", - "better-auth": "^1.4.18", + "better-auth": "~1.4.18", "drizzle-kit": "^0.31.8", "drizzle-orm": "^0.45.1", "eslint": "^9.39.2", diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts index 87f8dbd97..8c917b12e 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/app.d.ts @@ -4,7 +4,7 @@ import type { User, Session } from 'better-auth/minimal'; // for information about these interfaces declare global { namespace App { - interface Locals { user?: User; session?: Session } + interface Locals { user?: UserInfo; session?: Session } // interface Error {} // interface PageData {} @@ -14,3 +14,5 @@ declare global { } export {}; + +interface UserInfo extends User { id: string; name: string } diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.ts index e75600b3e..0988131ea 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/hooks.ts @@ -1,3 +1,3 @@ import { deLocalizeUrl } from '$lib/paraglide/runtime'; -export const reroute = (request) => deLocalizeUrl(request.url).pathname; +export const reroute = (request: { url: URL }) => deLocalizeUrl(request.url).pathname; diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte index b22ec360f..78dc893ec 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte @@ -1,17 +1,21 @@ {@render children()}
- {#each locales as locale} - {locale} + {#each locales as locale (locale)} + {locale} {/each}
diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/+page.server.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/+page.server.ts index 7c3083543..92a1f2ec3 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/+page.server.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/+page.server.ts @@ -3,7 +3,7 @@ import type { Actions } from './$types'; import type { PageServerLoad } from './$types'; import { auth } from '$lib/server/auth'; -export const load: PageServerLoad = async (event) => { +export const load: PageServerLoad = (event) => { if (!event.locals.user) { return redirect(302, '/demo/better-auth/login'); } diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/login/+page.server.ts b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/login/+page.server.ts index 2ddbc0e7c..e5f3c649d 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/login/+page.server.ts +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/src/routes/demo/better-auth/login/+page.server.ts @@ -4,7 +4,7 @@ import type { PageServerLoad } from './$types'; import { auth } from '$lib/server/auth'; import { APIError } from 'better-auth/api'; -export const load: PageServerLoad = async (event) => { +export const load: PageServerLoad = (event) => { if (event.locals.user) { return redirect(302, '/demo/better-auth'); } @@ -14,8 +14,8 @@ export const load: PageServerLoad = async (event) => { export const actions: Actions = { signInEmail: async (event) => { const formData = await event.request.formData(); - const email = formData.get('email')?.toString() ?? ''; - const password = formData.get('password')?.toString() ?? ''; + const email = (formData.get('email') ?? '') as string; + const password = (formData.get('password') ?? '') as string; try { await auth.api.signInEmail({ @@ -36,9 +36,9 @@ export const actions: Actions = { }, signUpEmail: async (event) => { const formData = await event.request.formData(); - const email = formData.get('email')?.toString() ?? ''; - const password = formData.get('password')?.toString() ?? ''; - const name = formData.get('name')?.toString() ?? ''; + const email = (formData.get('email') ?? '') as string; + const password = (formData.get('password') ?? '') as string; + const name = (formData.get('name') ?? '') as string; try { await auth.api.signUpEmail({ @@ -60,8 +60,8 @@ export const actions: Actions = { }, signInSocial: async (event) => { const formData = await event.request.formData(); - const provider = formData.get('provider')?.toString() ?? 'github'; - const callbackURL = formData.get('callbackURL')?.toString() ?? '/demo/better-auth'; + const provider = (formData.get('provider') ?? 'github') as string; + const callbackURL = (formData.get('callbackURL') ?? '/demo/better-auth') as string; const result = await auth.api.signInSocial({ body: { diff --git a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/tsconfig.json b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/tsconfig.json index 2c2ed3c4d..d61687cf7 100644 --- a/packages/sv/src/cli/tests/snapshots/create-with-all-addons/tsconfig.json +++ b/packages/sv/src/cli/tests/snapshots/create-with-all-addons/tsconfig.json @@ -11,10 +11,12 @@ "sourceMap": true, "strict": true, "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript + }, + "files": [ + "svelte.config.js", + "eslint.config.js", + "playwright.config.ts", + "e2e/demo.test.ts", + "drizzle.config.ts" + ] } diff --git a/packages/sv/src/create/templates/minimal/src/routes/+layout.svelte b/packages/sv/src/create/templates/minimal/src/routes/+layout.svelte index 9cebde545..671636796 100644 --- a/packages/sv/src/create/templates/minimal/src/routes/+layout.svelte +++ b/packages/sv/src/create/templates/minimal/src/routes/+layout.svelte @@ -1,7 +1,8 @@