Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .changeset/kit-files-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'svelte2tsx': patch
'svelte-language-server': patch
'svelte-check': patch
---

fix: type SvelteKit hooks and params files at the locations set in the Svelte config's `files` option
1 change: 1 addition & 0 deletions packages/language-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './server';
export { offsetAt, positionAt, getLineOffsets } from './lib/documents';
export { normalizePath } from './utils';
export { defaultKitFilesSettings, resolveKitFilesSettings } from './lib/documents/configLoader';
export {
mapSvelteCheckDiagnostics,
SvelteCheck,
Expand Down
51 changes: 51 additions & 0 deletions packages/language-server/src/lib/documents/configLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import _fs from 'fs';
import { URL } from 'url';
import { FileMap } from './fileCollection';
import ts from 'typescript';
import { InternalHelpers } from 'svelte2tsx';

export type InternalPreprocessorGroup = PreprocessorGroup & {
/**
Expand Down Expand Up @@ -41,6 +42,13 @@ export interface ExplicitConfigScope {

type LoadConfigFromDirectoryFn = typeof loadConfigFromDirectory;

export const defaultKitFilesSettings: InternalHelpers.KitFilesSettings = {
paramsPath: 'src/params',
serverHooksPath: 'src/hooks.server',
clientHooksPath: 'src/hooks.client',
universalHooksPath: 'src/hooks'
};

const DEFAULT_OPTIONS: CompileOptions = {
dev: true
};
Expand Down Expand Up @@ -357,6 +365,17 @@ export class ConfigLoader {
}
}

/**
* Where the SvelteKit hooks and params files are for the file, from its config's `files` option
*/
getKitFilesSettings(file: string): InternalHelpers.KitFilesSettings {
const files = this.getConfig(file)?.files;
const configPath = this.filePathToConfigPath.get(file);
return files && configPath
? resolveKitFilesSettings(files, this.path.dirname(configPath))
: defaultKitFilesSettings;
}

/**
* Like `getConfig`, but will search for a config above if no config found.
*/
Expand Down Expand Up @@ -452,6 +471,38 @@ export class ConfigLoader {
}
}

/**
* Where SvelteKit's hooks and params files are, from a config's `files` option. Like SvelteKit, a
* location the config sets, directly or through `files.src`, is resolved in the config's directory.
* The option comes from user code, so anything that isn't a string is ignored.
*/
export function resolveKitFilesSettings(
files: unknown,
configDirectory: string
): InternalHelpers.KitFilesSettings {
const { src, params, hooks }: Record<string, unknown> = isRecord(files) ? files : {};
const { server, client, universal }: Record<string, unknown> = isRecord(hooks) ? hooks : {};
const locate = (location: unknown, name: string) => {
if (typeof location === 'string') {
return _path.resolve(configDirectory, location);
}
if (typeof src === 'string') {
return _path.resolve(configDirectory, src, name);
}
return `src/${name}`;
};
return {
paramsPath: locate(params, 'params'),
serverHooksPath: locate(server, 'hooks.server'),
clientHooksPath: locate(client, 'hooks.client'),
universalHooksPath: locate(universal, 'hooks')
};
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

function getSvelteConfigExtensions(loadSvelteConfigTs: boolean) {
return loadSvelteConfigTs
? [...SVELTE_CONFIG_EXTENSIONS, ...SVELTE_CONFIG_TS_EXTENSIONS]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { DiagnosticsProvider } from '../../interfaces';
import {
DocumentSnapshot,
getKitFilesSettings,
JSOrTSDocumentSnapshot,
SvelteDocumentSnapshot,
SvelteSnapshotOptions
Expand Down Expand Up @@ -338,12 +339,6 @@ export class SvelteCheckTSGoDiagnosticsProvider implements DiagnosticsProvider {
Pick<FileSystem, 'readFile' | 'fileExists' | 'getAccessibleEntries'>
> {
const service = this;
const kitFiles: Parameters<typeof internalHelpers.isKitFile>[1] = {
serverHooksPath: 'src/hooks.client',
clientHooksPath: 'src/hooks.client',
universalHooksPath: 'src/hooks',
paramsPath: 'src/params'
};

return {
getAccessibleEntries(directory: string) {
Expand Down Expand Up @@ -392,7 +387,7 @@ export class SvelteCheckTSGoDiagnosticsProvider implements DiagnosticsProvider {

if (
normalizedPath.endsWith('node_modules/svelte/types/index.d.ts') ||
internalHelpers.isKitFile(normalizedPath, kitFiles)
internalHelpers.isKitFile(normalizedPath, getKitFilesSettings(normalizedPath))
) {
const snapshot = DocumentSnapshot.fromFilePath(
normalizedPath,
Expand Down
32 changes: 10 additions & 22 deletions packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EncodedSourceMap, TraceMap, originalPositionFor } from '@jridgewell/trace-mapping';
// @ts-ignore
import { TemplateNode } from 'svelte/types/compiler/interfaces';
import { svelte2tsx, IExportedNames, internalHelpers } from 'svelte2tsx';
import { svelte2tsx, IExportedNames, internalHelpers, InternalHelpers } from 'svelte2tsx';
import ts from 'typescript';
import { Position, Range, TextDocumentContentChangeEvent } from 'vscode-languageserver';
import {
Expand Down Expand Up @@ -29,7 +29,7 @@ import { Logger } from '../../logger';
import { dirname, resolve } from 'path';
import { URI } from 'vscode-uri';
import { surroundWithIgnoreComments } from './features/utils';
import { configLoader } from '../../lib/documents/configLoader';
import { configLoader, defaultKitFilesSettings } from '../../lib/documents/configLoader';

/**
* Computes the change range between two snapshots by scanning for the first and last
Expand Down Expand Up @@ -477,6 +477,13 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
}
}

export function getKitFilesSettings(filePath: string): InternalHelpers.KitFilesSettings {
// Declaration files aren't hooks or params files, and there are many in node_modules
return filePath.endsWith('.d.ts')
? defaultKitFilesSettings
: configLoader.getKitFilesSettings(filePath);
}

/**
* A js/ts document snapshot suitable for the ts language service and the plugin.
* Since no mapping has to be done here, it also implements the mapper interface.
Expand All @@ -496,10 +503,6 @@ export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSn
inserted: string;
total: number;
}> = [];
private paramsPath = 'src/params';
private serverHooksPath = 'src/hooks.server';
private clientHooksPath = 'src/hooks.client';
private universalHooksPath = 'src/hooks';

isOpenedInClient(): boolean {
return this.openedByClient;
Expand Down Expand Up @@ -633,12 +636,7 @@ export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSn
const result = internalHelpers.upsertKitFile(
ts,
this.filePath,
{
clientHooksPath: this.clientHooksPath,
paramsPath: this.paramsPath,
serverHooksPath: this.serverHooksPath,
universalHooksPath: this.universalHooksPath
},
getKitFilesSettings(this.filePath),
() => this.createSource(),
surroundWithIgnoreComments
);
Expand All @@ -649,16 +647,6 @@ export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSn
return;
}

if (!this.kitFile) {
const files = configLoader.getConfig(this.filePath)?.files;
if (files) {
this.paramsPath ||= files.params;
this.serverHooksPath ||= files.hooks?.server;
this.clientHooksPath ||= files.hooks?.client;
this.universalHooksPath ||= files.hooks?.universal;
}
}

const { text, addedCode } = result;

this.kitFile = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as assert from 'assert';
import * as path from 'path';
import ts from 'typescript';
import {
computeChangeRange,
JSOrTSDocumentSnapshot,
SvelteDocumentSnapshot
} from '../../../src/plugins/typescript/DocumentSnapshot';
import { Document } from '../../../src/lib/documents';
import { configLoader } from '../../../src/lib/documents/configLoader';
import { pathToUrl } from '../../../src/utils';

/**
Expand Down Expand Up @@ -188,3 +190,29 @@ describe('SvelteDocumentSnapshot.getChangeRange', () => {
assert.strictEqual(range, undefined);
});
});

describe('JSOrTSDocumentSnapshot SvelteKit files', () => {
it('types hooks and params at the locations set in the Svelte config', async () => {
const project = path.join(__dirname, 'testfiles', 'kit-files');
await configLoader.loadConfigs(project);
const handle = 'export const handle = ({ event, resolve }) => resolve(event);';
const hooks = new JSOrTSDocumentSnapshot(
0,
path.join(project, 'app/server/hooks.ts'),
handle
);
assert.ok(hooks.getFullText().includes(').Handle'), 'configured server hook must be typed');
const params = new JSOrTSDocumentSnapshot(
0,
path.join(project, 'app/params/id.ts'),
'export const match = (param) => true;'
);
assert.ok(params.getFullText().includes(': string'));
const elsewhere = new JSOrTSDocumentSnapshot(
0,
path.join(project, 'vendor/app/server/hooks.ts'),
handle
);
assert.ok(!elsewhere.kitFile);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
kit: {
files: { src: './app', hooks: { server: 'app/server/hooks' } }
}
};
38 changes: 12 additions & 26 deletions packages/svelte-check/src/incremental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
mapSvelteCheckDiagnostics,
offsetAt,
positionAt,
getLineOffsets
getLineOffsets,
defaultKitFilesSettings,
resolveKitFilesSettings
} from 'svelte-language-server';
import { loadConfig } from '@sveltejs/load-config';
import { findFiles } from './utils';
Expand Down Expand Up @@ -94,28 +96,12 @@ function toRelativePosix(baseDir: string, targetPath: string) {
return toPosixPath(relative || '.');
}

/**
* Default SvelteKit file paths used when svelte.config.js doesn't specify custom paths.
*/
const defaultKitFilesSettings: InternalHelpers.KitFilesSettings = {
paramsPath: 'src/params',
serverHooksPath: 'src/hooks.server',
clientHooksPath: 'src/hooks.client',
universalHooksPath: 'src/hooks'
};

function kitFilesSettingsFromConfig(config: any): InternalHelpers.KitFilesSettings {
if (!config?.files) {
return defaultKitFilesSettings;
}

const files = config.files;
return {
paramsPath: files.params ?? defaultKitFilesSettings.paramsPath,
serverHooksPath: files.hooks?.server ?? defaultKitFilesSettings.serverHooksPath,
clientHooksPath: files.hooks?.client ?? defaultKitFilesSettings.clientHooksPath,
universalHooksPath: files.hooks?.universal ?? defaultKitFilesSettings.universalHooksPath
};
// SvelteKit 3 puts its options at the top level, SvelteKit 2 under `kit`
function getKitFilesOption(config: Record<string, unknown>): unknown {
const kitOptions = 'files' in config ? config : config.kit;
return typeof kitOptions === 'object' && kitOptions !== null && 'files' in kitOptions
? kitOptions.files
: undefined;
}

/**
Expand All @@ -134,9 +120,9 @@ async function loadKitFilesSettings(
return defaultKitFilesSettings;
}

return kitFilesSettingsFromConfig(
// SvelteKit 3 puts its options at the top level
'files' in result.config ? result.config : result.config.kit
return resolveKitFilesSettings(
getKitFilesOption(result.config),
path.dirname(result.configFilePath)
);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/svelte2tsx/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ export namespace InternalHelpers {
inserted: string;
}

/**
* Where the hooks and params files are, without extension. An absolute path names that one
* location; a relative one, like the defaults, matches in any folder.
*/
export interface KitFilesSettings {
serverHooksPath: string;
clientHooksPath: string;
Expand Down
17 changes: 14 additions & 3 deletions packages/svelte2tsx/src/helpers/sveltekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface AddedCode {
inserted: string;
}

/**
* Where the hooks and params files are, without extension. An absolute path names that one
* location; a relative one, like the defaults, matches in any folder.
*/
export interface KitFilesSettings {
serverHooksPath: string;
clientHooksPath: string;
Expand Down Expand Up @@ -67,8 +71,8 @@ export function isKitErrorFile(basename: string): boolean {
export function isHooksFile(fileName: string, basename: string, hooksPath: string): boolean {
return (
((basename === 'index.ts' || basename === 'index.js') &&
fileName.slice(0, -basename.length - 1).endsWith(hooksPath)) ||
fileName.slice(0, -path.extname(basename).length).endsWith(hooksPath)
isAtLocation(path.dirname(fileName), hooksPath)) ||
isAtLocation(fileName.slice(0, -path.extname(basename).length), hooksPath)
);
}

Expand All @@ -77,12 +81,19 @@ export function isHooksFile(fileName: string, basename: string, hooksPath: strin
*/
export function isParamsFile(fileName: string, basename: string, paramsPath: string): boolean {
return (
fileName.slice(0, -basename.length - 1).endsWith(paramsPath) &&
isAtLocation(path.dirname(fileName), paramsPath) &&
!basename.includes('.test') &&
!basename.includes('.spec')
);
}

// Absolute locations are compared as paths, so separators and the drive letter's case don't matter
function isAtLocation(filePath: string, location: string): boolean {
return path.isAbsolute(location)
? path.relative(location, filePath) === ''
: filePath.endsWith(location);
}

export function upsertKitFile(
ts: _ts,
fileName: string,
Expand Down