diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..cc96577a0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +packages/cli/tests/snapshots/* \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 8beb18951..c3b2f91ec 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,6 +31,7 @@ export default [ '**/.test-output/*', '**/dist/*', 'packages/**/tests/**/{output,input}.ts', + 'packages/cli/tests/snapshots/*', 'rolldown.config.js', 'community-addon-template/tests/*' ] diff --git a/packages/addons/_tests/_setup/global.ts b/packages/addons/_tests/_setup/global.ts index c078a3c67..734f0c0d1 100644 --- a/packages/addons/_tests/_setup/global.ts +++ b/packages/addons/_tests/_setup/global.ts @@ -1,11 +1,26 @@ import { fileURLToPath } from 'node:url'; import { setup, type ProjectVariant } from 'sv/testing'; import type { TestProject } from 'vitest/node'; +import process from 'node:process'; +import { exec } from 'tinyexec'; + +import { STORYBOOK_VERSION } from '../../storybook/index.ts'; const TEST_DIR = fileURLToPath(new URL('../../../../.test-output/addons/', import.meta.url)); const variants: ProjectVariant[] = ['kit-js', 'kit-ts', 'vite-js', 'vite-ts']; +const CI = Boolean(process.env.CI); export default async function ({ provide }: TestProject) { + if (CI) { + // prefetch the storybook cli during ci to reduce fetching errors in tests + const { stdout } = await exec('pnpm', [ + 'dlx', + `create-storybook@${STORYBOOK_VERSION}`, + '--version' + ]); + console.info('storybook version:', stdout); + } + // downloads different project configurations (sveltekit, js/ts, vite-only, etc) const { templatesDir } = await setup({ cwd: TEST_DIR, variants }); diff --git a/packages/addons/_tests/storybook/test.ts b/packages/addons/_tests/storybook/test.ts index 5ca86e022..a651fadb6 100644 --- a/packages/addons/_tests/storybook/test.ts +++ b/packages/addons/_tests/storybook/test.ts @@ -1,7 +1,4 @@ -import process from 'node:process'; -import { execSync } from 'node:child_process'; import { expect } from '@playwright/test'; -import { beforeAll } from 'vitest'; import { setupTest } from '../_setup/suite.ts'; import storybook from '../../storybook/index.ts'; import eslint from '../../eslint/index.ts'; @@ -13,31 +10,19 @@ const { test, testCases, prepareServer } = setupTest( ); let port = 6006; -const CI = Boolean(process.env.CI); -beforeAll(() => { - if (CI) { - // prefetch the storybook cli during ci to reduce fetching errors in tests - execSync('pnpm dlx create-storybook@latest --version'); - } -}); - -test.for(testCases)( - 'storybook $variant', - { concurrent: !CI }, - async (testCase, { page, ...ctx }) => { - const cwd = ctx.cwd(testCase); +test.concurrent.for(testCases)('storybook $variant', async (testCase, { page, ...ctx }) => { + const cwd = ctx.cwd(testCase); - const { close } = await prepareServer({ - cwd, - page, - previewCommand: `pnpm storybook -p ${++port} --ci`, - buildCommand: '' - }); - // kill server process when we're done - ctx.onTestFinished(async () => await close()); + const { close } = await prepareServer({ + cwd, + page, + previewCommand: `pnpm storybook -p ${++port} --ci`, + buildCommand: '' + }); + // kill server process when we're done + ctx.onTestFinished(async () => await close()); - expect(page.locator('main .sb-bar')).toBeTruthy(); - expect(page.locator('#storybook-preview-wrapper')).toBeTruthy(); - } -); + expect(page.locator('main .sb-bar')).toBeTruthy(); + expect(page.locator('#storybook-preview-wrapper')).toBeTruthy(); +}); diff --git a/packages/addons/package.json b/packages/addons/package.json index 085d39d3b..2046ab500 100644 --- a/packages/addons/package.json +++ b/packages/addons/package.json @@ -15,6 +15,7 @@ "@sveltejs/cli-core": "workspace:*" }, "devDependencies": { - "package-manager-detector": "^0.2.11" + "package-manager-detector": "^0.2.11", + "tinyexec": "^0.3.2" } } diff --git a/packages/addons/prettier/index.ts b/packages/addons/prettier/index.ts index 39de21190..7bf185f10 100644 --- a/packages/addons/prettier/index.ts +++ b/packages/addons/prettier/index.ts @@ -56,7 +56,7 @@ export default defineAddon({ data.tailwindStylesheet ??= files.getRelative({ to: files.stylesheet }); } if (!plugins.includes('prettier-plugin-svelte')) { - data.plugins.unshift('prettier-plugin-svelte'); + data.plugins.push('prettier-plugin-svelte'); } data.overrides ??= []; diff --git a/packages/addons/storybook/index.ts b/packages/addons/storybook/index.ts index dd3b76393..d683c3e14 100644 --- a/packages/addons/storybook/index.ts +++ b/packages/addons/storybook/index.ts @@ -2,6 +2,8 @@ import process from 'node:process'; import { defineAddon } from '@sveltejs/cli-core'; import { getNodeTypesVersion } from '../common.ts'; +export const STORYBOOK_VERSION = '10.1.0'; + export default defineAddon({ id: 'storybook', shortDescription: 'frontend workshop', @@ -12,7 +14,12 @@ export default defineAddon({ runsAfter('eslint'); }, run: async ({ sv }) => { - const args = ['create-storybook@latest', '--skip-install', '--no-dev']; + const args = [ + `create-storybook@${STORYBOOK_VERSION}`, + '--skip-install', + '--no-dev', + '--no-features' + ]; // skips the onboarding prompt during tests if (process.env.NODE_ENV?.toLowerCase() === 'test') args.push('--yes'); diff --git a/packages/addons/tailwindcss/index.ts b/packages/addons/tailwindcss/index.ts index 55b39aab5..8c56eb72e 100644 --- a/packages/addons/tailwindcss/index.ts +++ b/packages/addons/tailwindcss/index.ts @@ -141,7 +141,7 @@ export default defineAddon({ data.plugins ??= []; const plugins: string[] = data.plugins; - if (!plugins.includes(PLUGIN_NAME)) plugins.push(PLUGIN_NAME); + if (!plugins.includes(PLUGIN_NAME)) plugins.unshift(PLUGIN_NAME); data.tailwindStylesheet ??= files.getRelative({ to: files.stylesheet }); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 759a962c7..601ea7164 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # sv +## 0.10.6 +### Patch Changes + + +- fix(cli): files will be formatted after create ([#827](https://github.com/sveltejs/cli/pull/827)) + ## 0.10.5 ### Patch Changes diff --git a/packages/cli/commands/add/index.ts b/packages/cli/commands/add/index.ts index e2bbb96ee..bee6e5e68 100644 --- a/packages/cli/commands/add/index.ts +++ b/packages/cli/commands/add/index.ts @@ -556,7 +556,7 @@ export async function runAddonsApply({ addonSetupResults?: Record; workspace: Workspace; fromCommand: 'create' | 'add'; -}): Promise<{ nextSteps: string[]; argsFormattedAddons: string[] }> { +}): Promise<{ nextSteps: string[]; argsFormattedAddons: string[]; filesToFormat: string[] }> { if (!addonSetupResults) { const setups = selectedAddons.length ? selectedAddons.map(({ addon }) => addon) @@ -565,7 +565,8 @@ export async function runAddonsApply({ } // we'll return early when no addons are selected, // indicating that installing deps was skipped and no PM was selected - if (selectedAddons.length === 0) return { nextSteps: [], argsFormattedAddons: [] }; + if (selectedAddons.length === 0) + return { nextSteps: [], argsFormattedAddons: [], filesToFormat: [] }; // apply addons const officialDetails = Object.keys(answersOfficial).map((id) => getAddonDetails(id)); @@ -662,19 +663,7 @@ export async function runAddonsApply({ if (packageManager) { workspace.packageManager = packageManager; await installDependencies(packageManager, options.cwd); - } - - // format modified/created files with prettier (if available) - if (filesToFormat.length > 0 && packageManager && !!workspace.dependencyVersion('prettier')) { - const { start, stop } = p.spinner(); - start('Formatting modified files'); - try { - await formatFiles({ packageManager, cwd: options.cwd, paths: filesToFormat }); - stop('Successfully formatted modified files'); - } catch (e) { - stop('Failed to format files'); - if (e instanceof Error) p.log.error(e.message); - } + await formatFiles({ packageManager, cwd: options.cwd, filesToFormat }); } const highlighter = getHighlighter(); @@ -693,7 +682,7 @@ export async function runAddonsApply({ }) .filter((msg) => msg !== undefined); - return { nextSteps, argsFormattedAddons }; + return { nextSteps, argsFormattedAddons, filesToFormat }; } /** diff --git a/packages/cli/commands/add/utils.ts b/packages/cli/commands/add/utils.ts index 8e07c89a1..5eab948e6 100644 --- a/packages/cli/commands/add/utils.ts +++ b/packages/cli/commands/add/utils.ts @@ -5,6 +5,7 @@ import { exec } from 'tinyexec'; import { parseJson } from '@sveltejs/cli-core/parsers'; import { resolveCommand, type AgentName } from 'package-manager-detector'; import type { Highlighter, Workspace } from '@sveltejs/cli-core'; +import * as p from '@clack/prompts'; export type Package = { name: string; @@ -35,14 +36,32 @@ export function getPackageJson(cwd: string): { export async function formatFiles(options: { packageManager: AgentName; cwd: string; - paths: string[]; + filesToFormat: string[]; }): Promise { - const args = ['prettier', '--write', '--ignore-unknown', ...options.paths]; + if (options.filesToFormat.length === 0) return; + const { start, stop } = p.spinner(); + start('Formatting modified files'); + + const args = ['prettier', '--write', '--ignore-unknown', ...options.filesToFormat]; const cmd = resolveCommand(options.packageManager, 'execute-local', args)!; - await exec(cmd.command, cmd.args, { - nodeOptions: { cwd: options.cwd, stdio: 'pipe' }, - throwOnError: true - }); + + try { + const result = await exec(cmd.command, cmd.args, { + nodeOptions: { cwd: options.cwd, stdio: 'pipe' }, + throwOnError: true + }); + if (result.exitCode !== 0) { + stop('Failed to format files'); + p.log.error(result.stderr); + return; + } + } catch (e) { + stop('Failed to format files'); + // @ts-expect-error + p.log.error(e?.output?.stderr || 'unknown error'); + return; + } + stop('Successfully formatted modified files'); } export function readFile(cwd: string, filePath: string): string { diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index b75e39a9c..35addd565 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -37,7 +37,7 @@ import { sanitizeAddons, type SelectedAddon } from './add/index.ts'; -import { commonFilePaths, getPackageJson } from './add/utils.ts'; +import { commonFilePaths, formatFiles, getPackageJson } from './add/utils.ts'; import { createWorkspace } from './add/workspace.ts'; import { dist } from '../../create/utils.ts'; @@ -266,12 +266,18 @@ async function createProject(cwd: ProjectPath, options: Options) { let addOnNextSteps: string[] = []; let argsFormattedAddons: string[] = []; + let addOnFilesToFormat: string[] = []; if (options.addOns || options.add.length > 0) { - const { nextSteps, argsFormattedAddons: argsFormatted } = await runAddonsApply({ + const { + nextSteps, + argsFormattedAddons: argsFormatted, + filesToFormat + } = await runAddonsApply({ answersOfficial, answersCommunity, options: { cwd: projectPath, + // in the create command, we don't want to install dependencies, we want to do it after the project is created install: false, gitCheck: false, community: [], @@ -283,7 +289,7 @@ async function createProject(cwd: ProjectPath, options: Options) { fromCommand: 'create' }); argsFormattedAddons = argsFormatted; - + addOnFilesToFormat = filesToFormat; addOnNextSteps = nextSteps; } @@ -308,7 +314,10 @@ async function createProject(cwd: ProjectPath, options: Options) { common.logArgs(packageManager, 'create', argsFormatted, [directory]); await addPnpmBuildDependencies(projectPath, packageManager, ['esbuild']); - if (packageManager) await installDependencies(packageManager, projectPath); + if (packageManager) { + await installDependencies(packageManager, projectPath); + await formatFiles({ packageManager, cwd: projectPath, filesToFormat: addOnFilesToFormat }); + } return { directory: projectPath, addOnNextSteps, packageManager }; } diff --git a/packages/cli/lib/install.ts b/packages/cli/lib/install.ts index 80fed8ea6..fb9b0b692 100644 --- a/packages/cli/lib/install.ts +++ b/packages/cli/lib/install.ts @@ -63,6 +63,8 @@ export async function applyAddons({ const mapped = Object.entries(addons).map(([, addon]) => addon); const ordered = orderAddons(mapped, addonSetupResults); + let hasFormatter = false; + for (const addon of ordered) { const workspaceOptions = options[addon.id] || {}; @@ -71,6 +73,8 @@ export async function applyAddons({ cwd: workspace.cwd, packageManager: workspace.packageManager }); + // If we don't have a formatter yet, check if the addon adds one + if (!hasFormatter) hasFormatter = !!addonWorkspace.dependencyVersion('prettier'); const { files, pnpmBuildDependencies, cancels } = await runAddon({ workspace: addonWorkspace, @@ -89,7 +93,7 @@ export async function applyAddons({ } return { - filesToFormat: Array.from(filesToFormat), + filesToFormat: hasFormatter ? Array.from(filesToFormat) : [], pnpmBuildDependencies: allPnpmBuildDependencies, status }; diff --git a/packages/cli/package.json b/packages/cli/package.json index d513a354c..60301cf30 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "sv", - "version": "0.10.5", + "version": "0.10.6", "type": "module", "description": "A CLI for creating and updating SvelteKit projects", "license": "MIT", diff --git a/packages/cli/tests/cli.ts b/packages/cli/tests/cli.ts index 156674dec..4dfa7b1ac 100644 --- a/packages/cli/tests/cli.ts +++ b/packages/cli/tests/cli.ts @@ -34,40 +34,70 @@ describe('cli', () => { 'mdsvex', 'paraglide=languageTags:en,es+demo:yes', 'mcp=ide:claude-code,cursor,gemini,opencode,vscode,other+setup:local' + // 'storybook' // No storybook addon during tests! ] } ]; - it.for(testCases)('should create a new project with name $projectName', async (testCase) => { - const { projectName, args } = testCase; - const svBinPath = path.resolve(monoRepoPath, 'packages', 'cli', 'dist', 'bin.js'); - const testOutputPath = path.resolve(monoRepoPath, '.test-output', 'cli', projectName); + it.for(testCases)( + 'should create a new project with name $projectName', + { timeout: 10_000 }, + async (testCase) => { + const { projectName, args } = testCase; + const svBinPath = path.resolve(monoRepoPath, 'packages', 'cli', 'dist', 'bin.js'); + const testOutputPath = path.resolve(monoRepoPath, '.test-output', 'cli', projectName); - const result = await exec( - 'node', - [ - svBinPath, - 'create', - testOutputPath, - '--template', - 'minimal', - '--types', - 'ts', - '--no-install', - ...args - ], - { nodeOptions: { stdio: 'ignore' } } - ); + const result = await exec( + 'node', + [ + svBinPath, + 'create', + testOutputPath, + '--template', + 'minimal', + '--types', + 'ts', + '--no-install', + ...args + ], + { nodeOptions: { stdio: 'pipe' } } + ); - // cli finished well - expect(result.exitCode).toBe(0); + // cli finished well + expect(result.exitCode).toBe(0); - // test output path exists - expect(fs.existsSync(testOutputPath)).toBe(true); + // test output path exists + expect(fs.existsSync(testOutputPath)).toBe(true); - // package.json has a name - const packageJsonPath = path.resolve(testOutputPath, 'package.json'); - const packageJson = parseJson(fs.readFileSync(packageJsonPath, 'utf-8')); - expect(packageJson.name).toBe(projectName); - }); + // package.json has a name + const packageJsonPath = path.resolve(testOutputPath, 'package.json'); + const packageJson = parseJson(fs.readFileSync(packageJsonPath, 'utf-8')); + expect(packageJson.name).toBe(projectName); + + const snapPath = path.resolve( + monoRepoPath, + 'packages', + 'cli', + 'tests', + 'snapshots', + projectName + ); + const relativeFiles = fs.readdirSync(testOutputPath, { recursive: true }) as string[]; + for (const relativeFile of relativeFiles) { + if (!fs.statSync(path.resolve(testOutputPath, relativeFile)).isFile()) continue; + if (['.svg', '.env'].some((ext) => relativeFile.endsWith(ext))) continue; + let generated = fs.readFileSync(path.resolve(testOutputPath, relativeFile), 'utf-8'); + if (relativeFile === 'package.json') { + const generatedPackageJson = parseJson(generated); + // remove @types/node from generated package.json as we test on different node versions + delete generatedPackageJson.devDependencies['@types/node']; + generated = JSON.stringify(generatedPackageJson, null, 3).replaceAll(' ', '\t'); + } + await expect(generated.replaceAll('\r\n', '\n')).toMatchFileSnapshot( + path.resolve(snapPath, relativeFile), + `file "${relativeFile}" does not match snapshot` + ); + } + } + ); }); diff --git a/packages/cli/tests/snapshots/create-only/.gitignore b/packages/cli/tests/snapshots/create-only/.gitignore new file mode 100644 index 000000000..3b462cb0c --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/packages/cli/tests/snapshots/create-only/.npmrc b/packages/cli/tests/snapshots/create-only/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/packages/cli/tests/snapshots/create-only/README.md b/packages/cli/tests/snapshots/create-only/README.md new file mode 100644 index 000000000..75842c404 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/packages/cli/tests/snapshots/create-only/package.json b/packages/cli/tests/snapshots/create-only/package.json new file mode 100644 index 000000000..93b251454 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/package.json @@ -0,0 +1,23 @@ +{ + "name": "create-only", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.43.8", + "svelte-check": "^4.3.4", + "typescript": "^5.9.3", + "vite": "^7.2.2" + } +} \ No newline at end of file diff --git a/packages/cli/tests/snapshots/create-only/src/app.d.ts b/packages/cli/tests/snapshots/create-only/src/app.d.ts new file mode 100644 index 000000000..da08e6da5 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/cli/tests/snapshots/create-only/src/app.html b/packages/cli/tests/snapshots/create-only/src/app.html new file mode 100644 index 000000000..f273cc58f --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/cli/tests/snapshots/create-only/src/lib/index.ts b/packages/cli/tests/snapshots/create-only/src/lib/index.ts new file mode 100644 index 000000000..856f2b6c3 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/packages/cli/tests/snapshots/create-only/src/routes/+layout.svelte b/packages/cli/tests/snapshots/create-only/src/routes/+layout.svelte new file mode 100644 index 000000000..9cebde545 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/src/routes/+layout.svelte @@ -0,0 +1,11 @@ + + + + + + +{@render children()} diff --git a/packages/cli/tests/snapshots/create-only/src/routes/+page.svelte b/packages/cli/tests/snapshots/create-only/src/routes/+page.svelte new file mode 100644 index 000000000..cc88df0ea --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/packages/cli/tests/snapshots/create-only/static/robots.txt b/packages/cli/tests/snapshots/create-only/static/robots.txt new file mode 100644 index 000000000..b6dd6670c --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/packages/cli/tests/snapshots/create-only/svelte.config.js b/packages/cli/tests/snapshots/create-only/svelte.config.js new file mode 100644 index 000000000..1295460d1 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/packages/cli/tests/snapshots/create-only/tsconfig.json b/packages/cli/tests/snapshots/create-only/tsconfig.json new file mode 100644 index 000000000..2c2ed3c4d --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "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 +} diff --git a/packages/cli/tests/snapshots/create-only/vite.config.ts b/packages/cli/tests/snapshots/create-only/vite.config.ts new file mode 100644 index 000000000..bbf8c7da4 --- /dev/null +++ b/packages/cli/tests/snapshots/create-only/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.cursor/mcp.json b/packages/cli/tests/snapshots/create-with-all-addons/.cursor/mcp.json new file mode 100644 index 000000000..801dd2ed9 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.cursor/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "svelte": { + "command": "npx", + "args": [ + "-y", + "@sveltejs/mcp" + ] + } + } +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.env.example b/packages/cli/tests/snapshots/create-with-all-addons/.env.example new file mode 100644 index 000000000..317118dec --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.env.example @@ -0,0 +1 @@ +DATABASE_URL=file:local.db diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.gemini/settings.json b/packages/cli/tests/snapshots/create-with-all-addons/.gemini/settings.json new file mode 100644 index 000000000..3cc6af9b0 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.gemini/settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json", + "mcpServers": { + "svelte": { + "command": "npx", + "args": [ + "-y", + "@sveltejs/mcp" + ] + } + } +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.gitignore b/packages/cli/tests/snapshots/create-with-all-addons/.gitignore new file mode 100644 index 000000000..4a4252abd --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.gitignore @@ -0,0 +1,30 @@ +test-results +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Paraglide +src/lib/paraglide + +# SQLite +*.db diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.mcp.json b/packages/cli/tests/snapshots/create-with-all-addons/.mcp.json new file mode 100644 index 000000000..1d41a9f09 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.mcp.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "svelte": { + "type": "stdio", + "command": "npx", + "env": { + + }, + "args": [ + "-y", + "@sveltejs/mcp" + ] + } + } +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.npmrc b/packages/cli/tests/snapshots/create-with-all-addons/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.prettierignore b/packages/cli/tests/snapshots/create-with-all-addons/.prettierignore new file mode 100644 index 000000000..024357648 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.prettierignore @@ -0,0 +1,10 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ +/drizzle/ diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.prettierrc b/packages/cli/tests/snapshots/create-with-all-addons/.prettierrc new file mode 100644 index 000000000..a30904c18 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.prettierrc @@ -0,0 +1,19 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": [ + "prettier-plugin-tailwindcss", + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/routes/layout.css" +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.vscode/mcp.json b/packages/cli/tests/snapshots/create-with-all-addons/.vscode/mcp.json new file mode 100644 index 000000000..825801097 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.vscode/mcp.json @@ -0,0 +1,11 @@ +{ + "servers": { + "svelte": { + "command": "npx", + "args": [ + "-y", + "@sveltejs/mcp" + ] + } + } +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/.vscode/settings.json b/packages/cli/tests/snapshots/create-with-all-addons/.vscode/settings.json new file mode 100644 index 000000000..bc31e1553 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/AGENTS.md b/packages/cli/tests/snapshots/create-with-all-addons/AGENTS.md new file mode 100644 index 000000000..a6e66ff15 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/AGENTS.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/packages/cli/tests/snapshots/create-with-all-addons/CLAUDE.md b/packages/cli/tests/snapshots/create-with-all-addons/CLAUDE.md new file mode 100644 index 000000000..a6e66ff15 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/CLAUDE.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/packages/cli/tests/snapshots/create-with-all-addons/GEMINI.md b/packages/cli/tests/snapshots/create-with-all-addons/GEMINI.md new file mode 100644 index 000000000..a6e66ff15 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/GEMINI.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/packages/cli/tests/snapshots/create-with-all-addons/README.md b/packages/cli/tests/snapshots/create-with-all-addons/README.md new file mode 100644 index 000000000..75842c404 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/packages/cli/tests/snapshots/create-with-all-addons/drizzle.config.ts b/packages/cli/tests/snapshots/create-with-all-addons/drizzle.config.ts new file mode 100644 index 000000000..317f310ed --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/drizzle.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'drizzle-kit'; + +if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); + +export default defineConfig({ + schema: './src/lib/server/db/schema.ts', + dialect: 'sqlite', + dbCredentials: { url: process.env.DATABASE_URL }, + verbose: true, + strict: true +}); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts b/packages/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts new file mode 100644 index 000000000..9985ce113 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/e2e/demo.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test'; + +test('home page has expected h1', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toBeVisible(); +}); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/eslint.config.js b/packages/cli/tests/snapshots/create-with-all-addons/eslint.config.js new file mode 100644 index 000000000..e02473646 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/eslint.config.js @@ -0,0 +1,4 @@ +import prettier from 'eslint-config-prettier'; +import svelte from 'eslint-plugin-svelte'; + +export default [prettier, ...svelte.configs.prettier]; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/messages/en.json b/packages/cli/tests/snapshots/create-with-all-addons/messages/en.json new file mode 100644 index 000000000..37a989440 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/messages/en.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from en!" +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/messages/es.json b/packages/cli/tests/snapshots/create-with-all-addons/messages/es.json new file mode 100644 index 000000000..176345c1f --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/messages/es.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from es!" +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/opencode.json b/packages/cli/tests/snapshots/create-with-all-addons/opencode.json new file mode 100644 index 000000000..300e082e3 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/opencode.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "svelte": { + "type": "local", + "command": [ + "npx", + "-y", + "@sveltejs/mcp" + ] + } + } +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/package.json b/packages/cli/tests/snapshots/create-with-all-addons/package.json new file mode 100644 index 000000000..c0195863d --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/package.json @@ -0,0 +1,62 @@ +{ + "name": "create-with-all-addons", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run && npm run test:e2e", + "test:e2e": "playwright test", + "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.39.1", + "@inlang/paraglide-js": "^2.5.0", + "@libsql/client": "^0.15.15", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "@playwright/test": "^1.56.1", + "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.17", + "@vitest/browser-playwright": "^4.0.10", + "drizzle-kit": "^0.31.7", + "drizzle-orm": "^0.44.7", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.13.0", + "globals": "^16.5.0", + "mdsvex": "^0.12.6", + "playwright": "^1.56.1", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.1", + "svelte": "^5.43.8", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "typescript-eslint": "^8.47.0", + "vite": "^7.2.2", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^4.0.10", + "vitest-browser-svelte": "^2.0.1" + }, + "dependencies": { + "@node-rs/argon2": "^2.0.2" + } +} \ No newline at end of file diff --git a/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts b/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts new file mode 100644 index 000000000..8f5062c2f --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/playwright.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + webServer: { command: 'npm run build && npm run preview', port: 4173 }, + testDir: 'e2e' +}); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/project.inlang/settings.json b/packages/cli/tests/snapshots/create-with-all-addons/project.inlang/settings.json new file mode 100644 index 000000000..5de85f9b5 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/project.inlang/settings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + }, + "baseLocale": "en", + "locales": [ + "en", + "es" + ] +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts new file mode 100644 index 000000000..bb1c423bb --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/app.d.ts @@ -0,0 +1,18 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + interface Locals { + user: import('$lib/server/auth').SessionValidationResult['user']; + session: import('$lib/server/auth').SessionValidationResult['session'] + } + + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/app.html b/packages/cli/tests/snapshots/create-with-all-addons/src/app.html new file mode 100644 index 000000000..50bd0b52c --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/demo.spec.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/demo.spec.ts new file mode 100644 index 000000000..e07cbbd72 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/demo.spec.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts new file mode 100644 index 000000000..2fab5d931 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.server.ts @@ -0,0 +1,38 @@ +import { sequence } from '@sveltejs/kit/hooks'; +import * as auth from '$lib/server/auth'; +import type { Handle } from '@sveltejs/kit'; +import { paraglideMiddleware } from '$lib/paraglide/server'; + +const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => { + event.request = request; + + return resolve(event, { + transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale) + }); +}); + +const handleAuth: Handle = async ({ event, resolve }) => { + const sessionToken = event.cookies.get(auth.sessionCookieName); + + if (!sessionToken) { + event.locals.user = null; + event.locals.session = null; + + return resolve(event); + } + + const { session, user } = await auth.validateSessionToken(sessionToken); + + if (session) { + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } else { + auth.deleteSessionTokenCookie(event); + } + + event.locals.user = user; + event.locals.session = session; + + return resolve(event); +}; + +export const handle: Handle = sequence(handleParaglide, handleAuth); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.ts new file mode 100644 index 000000000..e75600b3e --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/hooks.ts @@ -0,0 +1,3 @@ +import { deLocalizeUrl } from '$lib/paraglide/runtime'; + +export const reroute = (request) => deLocalizeUrl(request.url).pathname; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/lib/index.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/index.ts new file mode 100644 index 000000000..856f2b6c3 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/auth.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/auth.ts new file mode 100644 index 000000000..38c9930dd --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/auth.ts @@ -0,0 +1,81 @@ +import type { RequestEvent } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import { sha256 } from '@oslojs/crypto/sha2'; +import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; + +const DAY_IN_MS = 1000 * 60 * 60 * 24; + +export const sessionCookieName = 'auth-session'; + +export function generateSessionToken() { + const bytes = crypto.getRandomValues(new Uint8Array(18)); + const token = encodeBase64url(bytes); + return token; +} + +export async function createSession(token: string, userId: string) { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session: table.Session = { + id: sessionId, + userId, + expiresAt: new Date(Date.now() + DAY_IN_MS * 30) + }; + await db.insert(table.session).values(session); + return session; +} + +export async function validateSessionToken(token: string) { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const [result] = await db + .select({ + // Adjust user table here to tweak returned data + user: { id: table.user.id, username: table.user.username }, + session: table.session + }) + .from(table.session) + .innerJoin(table.user, eq(table.session.userId, table.user.id)) + .where(eq(table.session.id, sessionId)); + + if (!result) { + return { session: null, user: null }; + } + const { session, user } = result; + + const sessionExpired = Date.now() >= session.expiresAt.getTime(); + if (sessionExpired) { + await db.delete(table.session).where(eq(table.session.id, session.id)); + return { session: null, user: null }; + } + + const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; + if (renewSession) { + session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); + await db + .update(table.session) + .set({ expiresAt: session.expiresAt }) + .where(eq(table.session.id, session.id)); + } + + return { session, user }; +} + +export type SessionValidationResult = Awaited>; + +export async function invalidateSession(sessionId: string) { + await db.delete(table.session).where(eq(table.session.id, sessionId)); +} + +export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { + event.cookies.set(sessionCookieName, token, { + expires: expiresAt, + path: '/' + }); +} + +export function deleteSessionTokenCookie(event: RequestEvent) { + event.cookies.delete(sessionCookieName, { + path: '/' + }); +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/index.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/index.ts new file mode 100644 index 000000000..662fc2dd4 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/index.ts @@ -0,0 +1,10 @@ +import { drizzle } from 'drizzle-orm/libsql'; +import { createClient } from '@libsql/client'; +import * as schema from './schema'; +import { env } from '$env/dynamic/private'; + +if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); + +const client = createClient({ url: env.DATABASE_URL }); + +export const db = drizzle(client, { schema }); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts new file mode 100644 index 000000000..fe540a678 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/lib/server/db/schema.ts @@ -0,0 +1,18 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +export const user = sqliteTable('user', { + id: text('id').primaryKey(), + age: integer('age'), + username: text('username').notNull().unique(), + passwordHash: text('password_hash').notNull() +}); + +export const session = sqliteTable('session', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => user.id), + expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull() +}); + +export type Session = typeof session.$inferSelect; + +export type User = typeof user.$inferSelect; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte new file mode 100644 index 000000000..8b9bd05ca --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+layout.svelte @@ -0,0 +1,12 @@ + + + + + + +{@render children()} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+page.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+page.svelte new file mode 100644 index 000000000..cc88df0ea --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/+page.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/+page.svelte new file mode 100644 index 000000000..11d979836 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/+page.svelte @@ -0,0 +1,6 @@ + + +lucia +paraglide diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/+page.server.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/+page.server.ts new file mode 100644 index 000000000..a123e7e77 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/+page.server.ts @@ -0,0 +1,31 @@ +import * as auth from '$lib/server/auth'; +import { fail, redirect } from '@sveltejs/kit'; +import { getRequestEvent } from '$app/server'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const user = requireLogin() + return { user }; +}; + +export const actions: Actions = { + logout: async (event) => { + if (!event.locals.session) { + return fail(401); + } + await auth.invalidateSession(event.locals.session.id); + auth.deleteSessionTokenCookie(event); + + return redirect(302, '/demo/lucia/login'); + }, +}; + +function requireLogin() { + const { locals } = getRequestEvent(); + + if (!locals.user) { + return redirect(302, "/demo/lucia/login"); + } + + return locals.user; +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/+page.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/+page.svelte new file mode 100644 index 000000000..a5308cb7d --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/+page.svelte @@ -0,0 +1,12 @@ + + +

Hi, {data.user.username}!

+

Your user ID is {data.user.id}.

+
+ +
diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/login/+page.server.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/login/+page.server.ts new file mode 100644 index 000000000..d8e0ddd28 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/login/+page.server.ts @@ -0,0 +1,112 @@ +import { hash, verify } from '@node-rs/argon2'; +import { encodeBase32LowerCase } from '@oslojs/encoding'; +import { fail, redirect } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import * as auth from '$lib/server/auth'; +import { db } from '$lib/server/db'; +import * as table from '$lib/server/db/schema'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (event.locals.user) { + return redirect(302, '/demo/lucia'); + } + return {}; +}; + +export const actions: Actions = { + login: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + if (!validateUsername(username)) { + return fail(400, { message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' }); + } + if (!validatePassword(password)) { + return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); + } + + const results = await db + .select() + .from(table.user) + .where(eq(table.user.username, username)); + + const existingUser = results.at(0); + if (!existingUser) { + return fail(400, { message: 'Incorrect username or password' }); + } + + const validPassword = await verify(existingUser.passwordHash, password, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + if (!validPassword) { + return fail(400, { message: 'Incorrect username or password' }); + } + + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, existingUser.id); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + + return redirect(302, '/demo/lucia'); + }, + register: async (event) => { + const formData = await event.request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + if (!validateUsername(username)) { + return fail(400, { message: 'Invalid username' }); + } + if (!validatePassword(password)) { + return fail(400, { message: 'Invalid password' }); + } + + const userId = generateUserId(); + const passwordHash = await hash(password, { + // recommended minimum parameters + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + try { + await db.insert(table.user).values({ id: userId, username, passwordHash }); + + const sessionToken = auth.generateSessionToken(); + const session = await auth.createSession(sessionToken, userId); + auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + } catch { + return fail(500, { message: 'An error has occurred' }); + } + return redirect(302, '/demo/lucia'); + }, +}; + +function generateUserId() { + // ID with 120 bits of entropy, or about the same as UUID v4. + const bytes = crypto.getRandomValues(new Uint8Array(15)); + const id = encodeBase32LowerCase(bytes); + return id; +} + +function validateUsername(username: unknown): username is string { + return ( + typeof username === 'string' && + username.length >= 3 && + username.length <= 31 && + /^[a-z0-9_-]+$/.test(username) + ); +} + +function validatePassword(password: unknown): password is string { + return ( + typeof password === 'string' && + password.length >= 6 && + password.length <= 255 + ); +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/login/+page.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/login/+page.svelte new file mode 100644 index 000000000..b7b20ba19 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/lucia/login/+page.svelte @@ -0,0 +1,32 @@ + + +

Login/Register

+
+ + + + +
+

{form?.message ?? ''}

diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte new file mode 100644 index 000000000..04d3480cc --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/demo/paraglide/+page.svelte @@ -0,0 +1,16 @@ + + + + +

{m.hello_world({ name: 'SvelteKit User' })}

+
+ + +

+If you use VSCode, install the Sherlock i18n extension for a better i18n experience. +

diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/layout.css b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/layout.css new file mode 100644 index 000000000..cd6702375 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/layout.css @@ -0,0 +1,3 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/src/routes/page.svelte.spec.ts b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/page.svelte.spec.ts new file mode 100644 index 000000000..9b564bba4 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/src/routes/page.svelte.spec.ts @@ -0,0 +1,13 @@ +import { page } from 'vitest/browser'; +import { describe, expect, it } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import Page from './+page.svelte'; + +describe('/+page.svelte', () => { + it('should render h1', async () => { + render(Page); + + const heading = page.getByRole('heading', { level: 1 }); + await expect.element(heading).toBeInTheDocument(); + }); +}); diff --git a/packages/cli/tests/snapshots/create-with-all-addons/static/robots.txt b/packages/cli/tests/snapshots/create-with-all-addons/static/robots.txt new file mode 100644 index 000000000..b6dd6670c --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js b/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js new file mode 100644 index 000000000..f59ea15cb --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/svelte.config.js @@ -0,0 +1,15 @@ +import { mdsvex } from 'mdsvex'; +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: [vitePreprocess(), mdsvex()], + + kit: { adapter: adapter() }, + extensions: ['.svelte', '.svx'] +}; + +export default config; diff --git a/packages/cli/tests/snapshots/create-with-all-addons/tsconfig.json b/packages/cli/tests/snapshots/create-with-all-addons/tsconfig.json new file mode 100644 index 000000000..2c2ed3c4d --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "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 +} diff --git a/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts b/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts new file mode 100644 index 000000000..6f612e940 --- /dev/null +++ b/packages/cli/tests/snapshots/create-with-all-addons/vite.config.ts @@ -0,0 +1,49 @@ +import { paraglideVitePlugin } from '@inlang/paraglide-js'; +import devtoolsJson from 'vite-plugin-devtools-json'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; +import { sveltekit } from '@sveltejs/kit/vite'; + +export default defineConfig({ + plugins: [ + tailwindcss(), + sveltekit(), + devtoolsJson(), + paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' }) + ], + + test: { + expect: { requireAssertions: true }, + + projects: [ + { + extends: './vite.config.ts', + + test: { + name: 'client', + + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: 'chromium', headless: true }] + }, + + include: ['src/**/*.svelte.{test,spec}.{js,ts}'], + exclude: ['src/lib/server/**'] + } + }, + + { + extends: './vite.config.ts', + + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] + } + } + ] + } +}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index 3be07ef11..26da5644b 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { name: 'cli', include: ['./tests/**/index.ts', './tests/*.ts'], + exclude: ['./tests/snapshots/**'], expect: { requireAssertions: true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 715466c8c..aaee94169 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: package-manager-detector: specifier: ^0.2.11 version: 0.2.11 + tinyexec: + specifier: ^0.3.2 + version: 0.3.2 packages/cli: devDependencies: