Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/plenty-deserts-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

feat(eslint): add `recommendedTypeChecked` config
31 changes: 19 additions & 12 deletions packages/sv/src/addons/better-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {}');

Expand All @@ -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));
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand All @@ -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: {
Expand All @@ -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');
}
Expand Down Expand Up @@ -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');
}
Expand Down
10 changes: 10 additions & 0 deletions packages/sv/src/addons/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
14 changes: 13 additions & 1 deletion packages/sv/src/addons/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down Expand Up @@ -77,6 +77,7 @@ export default defineAddon({

const globalsConfig = js.object.create({
languageOptions: {
parserOptions: typescript ? { projectService: true } : undefined,
globals: globalsObjLiteral
},
rules: typescript ? rules : undefined
Expand Down Expand Up @@ -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();
});
}
}
});
9 changes: 6 additions & 3 deletions packages/sv/src/addons/paraglide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
`<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}>{locale}</a>
{#each locales as locale (locale)}
<a href={resolve(localizeHref(page.url.pathname, { locale }))}>{locale}</a>
{/each}
</div>`
);
Expand Down
11 changes: 11 additions & 0 deletions packages/sv/src/addons/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({})');
Expand Down
39 changes: 19 additions & 20 deletions packages/sv/src/cli/tests/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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,
Expand Down Expand Up @@ -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
);
}
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import favicon from '$lib/assets/favicon.svg';

let { children } = $props();
let { children }: { children: Snippet } = $props();
</script>

<svelte:head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -14,3 +14,5 @@ declare global {
}

export {};

interface UserInfo extends User { id: string; name: string }
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { locales, localizeHref } from '$lib/paraglide/runtime';
import './layout.css';
import type { Snippet } from 'svelte';
import favicon from '$lib/assets/favicon.svg';

let { children } = $props();
let { children }: { children: Snippet } = $props();
</script>

<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{@render children()}

<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}>{locale}</a>
{#each locales as locale (locale)}
<a
href={resolve(localizeHref(page.url.pathname, { locale }))}
>{locale}</a>
{/each}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
Loading
Loading