Skip to content
Merged
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/nervous-poems-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

fix(experimental): `@sveltejs/kit@next` projects now install, build and type-check - `#lib` imports instead of `$lib`, a `tsconfig` extending `$app/tsconfig`, and no options kit 3 removed
5 changes: 5 additions & 0 deletions .changeset/olive-geese-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/sv-utils': patch
---

feat: add SvelteKit 3 helpers - `isKit3`, `resolveLibPrefix`, `libSubpathImports`
15 changes: 15 additions & 0 deletions packages/sv-utils/api-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,16 @@ type DefineEnv = {
};

declare function defineEnv({ sv, cwd, dependencyVersion }: DefineEnvContext): DefineEnv;

declare function isKit3(kitRange: string | undefined): boolean;

declare function resolveLibPrefix(kitRange: string | undefined): '#lib' | '$lib';

declare function libSubpathImports(libDir: string): Record<string, string>;

declare const KIT3_TSCONFIG = '$app/tsconfig';

declare const KIT3_TSCONFIG_DEFAULT: Record<string, unknown>;
type ColorInput = string | string[];
declare const color: {
addon: (str: ColorInput) => string;
Expand Down Expand Up @@ -886,6 +896,8 @@ export {
COMMANDS,
type Comments,
type ConfigFileReader,
KIT3_TSCONFIG,
KIT3_TSCONFIG_DEFAULT,
type Package,
type SvelteAst,
type SvelteConfigKind,
Expand All @@ -905,16 +917,19 @@ export {
downloadJson,
fileExists,
index_d_exports$2 as html,
isKit3,
isVersionUnsupportedBelow,
index_d_exports$3 as js,
json_d_exports as json,
libSubpathImports,
loadFile,
loadPackageJson,
minVersion,
parse,
pnpm_d_exports as pnpm,
resolveCommand,
resolveCommandArray,
resolveLibPrefix,
sanitizeName,
saveFile,
splitVersion,
Expand Down
24 changes: 13 additions & 11 deletions packages/sv-utils/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fileExists } from './files.ts';
import { isKit3 } from './kit3.ts';
import { coerceVersion } from './semver.ts';
import { svelteConfig, type ConfigFileReader, type SvFileApi } from './svelte-config.ts';
import type { AstTypes } from './tooling/index.ts';
Expand All @@ -15,10 +16,8 @@ export function resolveEnvMode({
explicitEnvFlag: boolean;
}): EnvMode {
if (!kitRange) return 'legacy';
if (kitRange === 'next') return 'declared';
const { major } = coerceVersion(kitRange);
if (major !== undefined && major >= 3) return 'declared';
if (major === 2 && explicitEnvFlag) return 'declared';
if (isKit3(kitRange)) return 'declared';
if (coerceVersion(kitRange).major === 2 && explicitEnvFlag) return 'declared';
return 'legacy';
}

Expand Down Expand Up @@ -110,23 +109,23 @@ function getOrCreateVariablesObject(
* just call `define`/`reference` and never deal with the legacy-vs-declared distinction themselves.
*/
export function defineEnv({ sv, cwd, dependencyVersion }: DefineEnvContext): DefineEnv {
const mode = resolveEnvMode({
kitRange: dependencyVersion('@sveltejs/kit'),
explicitEnvFlag: readExplicitEnvFlag(cwd)
});
const kitRange = dependencyVersion('@sveltejs/kit');
const mode = resolveEnvMode({ kitRange, explicitEnvFlag: readExplicitEnvFlag(cwd) });
const language = fileExists(cwd, 'tsconfig.json') ? 'ts' : 'js';
return _bindEnv({ sv, mode, language });
return _bindEnv({ sv, mode, language, kit3: isKit3(kitRange) });
}

/** @internal The mode-resolved core, exported for filesystem-free tests. */
export function _bindEnv({
sv,
mode,
language
language,
kit3 = false
}: {
sv: SvFileApi;
mode: EnvMode;
language: 'ts' | 'js';
kit3?: boolean;
}): DefineEnv {
const declared = new Map<string, EnvVarSpec>();

Expand All @@ -138,7 +137,10 @@ export function _bindEnv({
const envPath = `src/env.${language}`;
sv.file(envPath, (content) =>
transforms.script(({ ast, js }) => {
js.imports.addNamed(ast, { from: '@sveltejs/kit/hooks', imports: ['defineEnvVars'] });
js.imports.addNamed(ast, {
from: kit3 ? '@sveltejs/kit/env' : '@sveltejs/kit/hooks',
imports: ['defineEnvVars']
});
const variables = getOrCreateVariablesObject(ast, js);
const entry = js.object.property(variables, {
name: spec.name,
Expand Down
9 changes: 9 additions & 0 deletions packages/sv-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ export {
// Env access (abstracts over legacy `$env/dynamic/*` vs declared `$app/env/*` + `src/env.ts`)
export { defineEnv } from './env.ts';

// Kit 3 specifics (version detection, `$lib` -> `#lib`, the generated tsconfig)
export {
KIT3_TSCONFIG,
KIT3_TSCONFIG_DEFAULT,
isKit3,
libSubpathImports,
resolveLibPrefix
} from './kit3.ts';

// Terminal styling
export { color } from './color.ts';

Expand Down
43 changes: 43 additions & 0 deletions packages/sv-utils/src/kit3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { coerceVersion } from './semver.ts';

/** Whether a `@sveltejs/kit` range resolves to v3+, including the `next` dist-tag. */
export function isKit3(kitRange: string | undefined): boolean {
Comment thread
jycouet marked this conversation as resolved.
if (!kitRange) return false;
if (kitRange === 'next') return true;
const { major } = coerceVersion(kitRange);
return major !== undefined && major >= 3;
}

/** The prefix for `src/lib` imports. Kit 3 dropped the built-in `$lib` alias for `#lib` subpath imports. */
export function resolveLibPrefix(kitRange: string | undefined): '#lib' | '$lib' {
Comment thread
jycouet marked this conversation as resolved.
return isKit3(kitRange) ? '#lib' : '$lib';
}

/** The `package.json#imports` entries backing `#lib`. `libDir` is workspace-relative, e.g. `src/lib`. */
export function libSubpathImports(libDir: string): Record<string, string> {
return { '#lib': `./${libDir}/index.js`, '#lib/*': `./${libDir}/*` };
}

/** The config kit 3 generates into `node_modules`, replacing `.svelte-kit/tsconfig.json`. */
export const KIT3_TSCONFIG = '$app/tsconfig';

/**
* Options `$app/tsconfig` already sets. A local copy of the same value is noise, but a different
* value is a deliberate override and must stay - so only drop keys whose value matches.
*/
export const KIT3_TSCONFIG_DEFAULT: Record<string, unknown> = {
allowImportingTsExtensions: true,
allowJs: true,
checkJs: true,
esModuleInterop: true,
forceConsistentCasingInFileNames: true,
isolatedModules: true,
module: 'esnext',
moduleDetection: 'force',
moduleResolution: 'bundler',
noEmit: true,
resolveJsonModule: true,
skipLibCheck: true,
target: 'esnext',
verbatimModuleSyntax: true
};
12 changes: 7 additions & 5 deletions packages/sv/src/addons/better-auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { log } from '@clack/prompts';
import {
resolveLibPrefix,
type AstTypes,
Walker,
color,
Expand Down Expand Up @@ -44,6 +45,7 @@ export default defineAddon({
runsAfter('experimental');
},
run: ({ sv, cwd, language, options, directory, dependencyVersion, file }) => {
const lib = resolveLibPrefix(dependencyVersion('@sveltejs/kit'));
const svelteVersion = dependencyVersion('svelte');
const svelte5 = !!svelteVersion && coerceVersion(svelteVersion).major === 5;
const [ts, s5] = createPrinter(language === 'ts', svelte5);
Expand Down Expand Up @@ -122,7 +124,7 @@ export default defineAddon({
sv.file(
`${directory.lib}/server/auth.${language}`,
transforms.script(({ ast, comments, js }) => {
js.imports.addNamed(ast, { from: '$lib/server/db', imports: [d1 ? 'getDb' : 'db'] });
js.imports.addNamed(ast, { from: `${lib}/server/db`, imports: [d1 ? 'getDb' : 'db'] });
js.imports.addNamed(ast, { from: '$app/server', imports: ['getRequestEvent'] });
js.imports.addNamed(ast, {
from: 'better-auth/svelte-kit',
Expand Down Expand Up @@ -238,7 +240,7 @@ export default defineAddon({
sv.file(
'src/app.d.ts',
transforms.script(({ ast, comments, js }) => {
if (d1) js.imports.addNamed(ast, { imports: ['createAuth'], from: '$lib/server/auth' });
if (d1) js.imports.addNamed(ast, { imports: ['createAuth'], from: `${lib}/server/auth` });
js.imports.addNamed(ast, {
imports: ['User', 'Session'],
from: 'better-auth',
Expand Down Expand Up @@ -286,7 +288,7 @@ export default defineAddon({
});
js.imports.addNamed(ast, {
imports: [d1 ? 'createAuth' : 'auth'],
from: '$lib/server/auth'
from: `${lib}/server/auth`
});
env.importEnv(ast, js, ['building']);

Expand Down Expand Up @@ -413,7 +415,7 @@ export default defineAddon({
import { fail, redirect } from '@sveltejs/kit';
${ts("import type { Actions } from './$types';")}
${ts("import type { PageServerLoad } from './$types';")}
${!d1 ? "import { auth } from '$lib/server/auth';" : ''}
${!d1 ? `import { auth } from '${lib}/server/auth';` : ''}
${needsAPIError ? "import { APIError } from 'better-auth/api';" : ''}

export const load${ts(': PageServerLoad')} = (event) => {
Expand Down Expand Up @@ -502,7 +504,7 @@ export default defineAddon({
import { redirect } from '@sveltejs/kit';
${ts("import type { Actions } from './$types';")}
${ts("import type { PageServerLoad } from './$types';")}
${!d1 ? "import { auth } from '$lib/server/auth';" : ''}
${!d1 ? `import { auth } from '${lib}/server/auth';` : ''}

export const load${ts(': PageServerLoad')} = (event) => {
if (!event.locals.user) {
Expand Down
37 changes: 28 additions & 9 deletions packages/sv/src/addons/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
fileExists,
createPrinter,
svelteConfig,
defineEnv
defineEnv,
isKit3
} from '@sveltejs/sv-utils';
import crypto from 'node:crypto';
import fs from 'node:fs';
Expand Down Expand Up @@ -300,15 +301,33 @@ export default defineAddon({
})
);

svelteConfig.edit({ sv, cwd }, ({ override, js }) => {
override({
typescript: {
config: js.common.parseExpression(
`(config) => { config.include.push('../drizzle.config.${language}')}`
)
}
// kit 3 dropped the `typescript.config` hook's `include` (and deprecates the hook itself),
// so the project's own ts/jsconfig has to cover the drizzle config
if (isKit3(dependencyVersion('@sveltejs/kit'))) {
const configFile = language === 'ts' ? 'tsconfig.json' : 'jsconfig.json';
if (fileExists(cwd, configFile)) {
sv.file(
configFile,
transforms.json(({ data }) => {
const include: string[] = (data.include ??= ['src']);
if (!include.includes(`drizzle.config.${language}`)) {
include.push(`drizzle.config.${language}`);
}
})
);
}
} else {
Comment thread
jycouet marked this conversation as resolved.
// prior to kit 3
svelteConfig.edit({ sv, cwd }, ({ override, js }) => {
override({
typescript: {
config: js.common.parseExpression(
`(config) => { config.include.push('../drizzle.config.${language}')}`
)
}
});
});
});
}

sv.file(
paths['database schema'],
Expand Down
66 changes: 63 additions & 3 deletions packages/sv/src/addons/experimental.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { isVersionUnsupportedBelow, loadPackageJson, svelteConfig } from '@sveltejs/sv-utils';
import {
KIT3_TSCONFIG,
KIT3_TSCONFIG_DEFAULT,
fileExists,
isVersionUnsupportedBelow,
libSubpathImports,
loadPackageJson,
svelteConfig,
transforms
} from '@sveltejs/sv-utils';
import fs from 'node:fs';
import path from 'node:path';
import { defineAddon, defineAddonOptions } from '../core/config.ts';

// Single source of truth, keyed by flag name. `path` defaults to `experimental.<name>`; `off` opts out
Expand All @@ -8,10 +19,13 @@ const FEATURES: Record<string, Feature> = {
async: { label: 'async', hint: 'await in components', path: 'compilerOptions.experimental.async' }, // prettier-ignore
remoteFunctions: { label: 'remote functions' },
explicitEnvironmentVariables: { label: 'explicit environment variables', hint: 'kit ^2 only', inNext: false }, // prettier-ignore
handleRenderingErrors: { label: 'rendering error boundaries' },
handleRenderingErrors: { label: 'rendering error boundaries', hint: 'kit ^2 only', inNext: false }, // prettier-ignore
forkPreloads: { label: 'forked preloading', off: true }
};

// files whose `$lib` imports are rewritten to `#lib`
const SOURCE_EXTENSIONS = ['.svelte', '.svelte.ts', '.svelte.js', '.ts', '.js', '.svx', '.md'];

// kit 3 raises these peer floors; bump only when the project is below them (never downgrade).
const KIT3_PEERS = {
vite: '^8.0.0',
Expand Down Expand Up @@ -47,7 +61,7 @@ export default defineAddon({

setup: ({ runsAfter }) => runsAfter('sveltekitAdapter'),

run: ({ sv, cwd, options, language, dependencyVersion }) => {
run: ({ sv, cwd, options, language, directory, dependencyVersion }) => {
const kitNext = options.versions.includes('kit');

if (kitNext) {
Expand All @@ -65,6 +79,38 @@ export default defineAddon({
}
}

if (kitNext) {
// kit 3 serves the generated config from `$app/tsconfig` and no longer supplies `include`
for (const name of ['tsconfig.json', 'jsconfig.json']) {
if (!fileExists(cwd, name)) continue;
sv.file(
name,
transforms.json(({ data }) => {
data.extends = KIT3_TSCONFIG;
data.include ??= [directory.src];
for (const [key, value] of Object.entries(data.compilerOptions ?? {})) {
// a differing value is a deliberate override and stays
if (KIT3_TSCONFIG_DEFAULT[key] === value) delete data.compilerOptions[key];
}
})
);
}

// `$lib` is gone in favour of `#lib` subpath imports, which Vite resolves from `package.json`
sv.file(
'package.json',
transforms.json(({ data }) => {
data.imports = { ...libSubpathImports(directory.lib), ...data.imports };
})
);
// safe here: templates are already written and every add-on emitting `$lib` runs later
for (const relative of sourceFiles(cwd, directory.src)) {
sv.file(relative, (content) =>
content.includes('$lib') ? content.replaceAll('$lib', '#lib') : false
);
}
}

const config: Record<string, any> = {};
for (const [name, f] of Object.entries(FEATURES)) {
if (!options.features.includes(name)) continue;
Expand All @@ -79,3 +125,17 @@ export default defineAddon({
svelteConfig.edit({ sv, cwd }, ({ override }) => override(config));
}
});

/** Workspace-relative source files under `src`, for the `$lib` -> `#lib` rewrite. */
function sourceFiles(cwd: string, src: string): string[] {
const root = path.resolve(cwd, src);
if (!fs.existsSync(root)) return [];
return fs
.readdirSync(root, { recursive: true })
.map((entry) => path.join(src, entry as string))
.filter(
(relative) =>
SOURCE_EXTENSIONS.some((ext) => relative.endsWith(ext)) &&
fs.statSync(path.resolve(cwd, relative)).isFile()
);
}
Loading