Skip to content

Commit 08f2e69

Browse files
authored
(feat) support default language of svelte-preprocess (#291)
* (feat) support default language of svelte-preprocess `svelte-preprocess` has a feature for setting default languages so you don't have to write `lang="x"` each time. This adds support for this in style/script tags. To accomplish this, parsing of the `svelte.config.js` was moved up to `Document`. Inside it, the `lang` attribute is set to the default language if there exists no `lang`/`type` attribute.
1 parent a6ea6b9 commit 08f2e69

File tree

12 files changed

+139
-109
lines changed

12 files changed

+139
-109
lines changed

packages/language-server/src/plugins/importPackage.ts renamed to packages/language-server/src/importPackage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { dirname, resolve } from 'path';
22
import * as prettier from 'prettier';
33
import * as svelte from 'svelte/compiler';
44
import sveltePreprocess from 'svelte-preprocess';
5-
import { Logger } from '../logger';
5+
import { Logger } from './logger';
66

77
export function getPackageInfo(packageName: string, fromPath: string) {
88
const packageJSONPath = require.resolve(`${packageName}/package.json`, {

packages/language-server/src/lib/documents/Document.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { urlToPath } from '../../utils';
22
import { WritableDocument } from './DocumentBase';
3-
import { TagInformation, extractStyleTag, extractScriptTags } from './utils';
3+
import { extractScriptTags, extractStyleTag, TagInformation } from './utils';
4+
import { SvelteConfig, loadConfig } from './configLoader';
45

56
/**
67
* Represents a text document contains a svelte component.
@@ -10,17 +11,21 @@ export class Document extends WritableDocument {
1011
scriptInfo: TagInformation | null = null;
1112
moduleScriptInfo: TagInformation | null = null;
1213
styleInfo: TagInformation | null = null;
14+
config!: SvelteConfig;
1315

1416
constructor(public url: string, public content: string) {
1517
super();
16-
this.updateTagInfo();
18+
this.updateDocInfo();
1719
}
1820

19-
private updateTagInfo() {
21+
private updateDocInfo() {
22+
if (!this.config || this.config.loadConfigError) {
23+
this.config = loadConfig(this.getFilePath() || '');
24+
}
2025
const scriptTags = extractScriptTags(this.content);
21-
this.scriptInfo = scriptTags?.script || null;
22-
this.moduleScriptInfo = scriptTags?.moduleScript || null;
23-
this.styleInfo = extractStyleTag(this.content);
26+
this.scriptInfo = this.addDefaultLanguage(scriptTags?.script || null, 'script');
27+
this.moduleScriptInfo = this.addDefaultLanguage(scriptTags?.moduleScript || null, 'script');
28+
this.styleInfo = this.addDefaultLanguage(extractStyleTag(this.content), 'style');
2429
}
2530

2631
/**
@@ -36,7 +41,7 @@ export class Document extends WritableDocument {
3641
setText(text: string) {
3742
this.content = text;
3843
this.version++;
39-
this.updateTagInfo();
44+
this.updateDocInfo();
4045
}
4146

4247
/**
@@ -52,4 +57,20 @@ export class Document extends WritableDocument {
5257
getURL() {
5358
return this.url;
5459
}
60+
61+
private addDefaultLanguage(
62+
tagInfo: TagInformation | null,
63+
tag: 'style' | 'script',
64+
): TagInformation | null {
65+
if (!tagInfo) {
66+
return null;
67+
}
68+
69+
const defaultLang = this.config.preprocess?.defaultLanguages?.[tag];
70+
if (!tagInfo.attributes.lang && !tagInfo.attributes.type && defaultLang) {
71+
tagInfo.attributes.lang = defaultLang;
72+
}
73+
74+
return tagInfo;
75+
}
5576
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Logger } from '../../logger';
2+
import { cosmiconfigSync } from 'cosmiconfig';
3+
import { CompileOptions } from 'svelte/types/compiler/interfaces';
4+
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
5+
import { importSveltePreprocess } from '../../importPackage';
6+
7+
export interface SvelteConfig {
8+
compilerOptions?: CompileOptions;
9+
preprocess?: PreprocessorGroup & {
10+
/**
11+
* svelte-preprocess has this since 4.x
12+
*/
13+
defaultLanguages?: { markup?: string; script?: string; style?: string };
14+
};
15+
loadConfigError?: any;
16+
}
17+
18+
const DEFAULT_OPTIONS: CompileOptions = {
19+
dev: true,
20+
};
21+
22+
const NO_GENERATE: CompileOptions = {
23+
generate: false,
24+
};
25+
26+
const svelteConfigExplorer = cosmiconfigSync('svelte', {
27+
packageProp: 'svelte-ls',
28+
cache: true,
29+
});
30+
31+
/**
32+
* Tries to load `svelte.config.js`
33+
*
34+
* @param path File path of the document to load the config for
35+
*/
36+
export function loadConfig(path: string): SvelteConfig {
37+
Logger.log('Trying to load config for', path);
38+
try {
39+
const result = svelteConfigExplorer.search(path);
40+
const config: SvelteConfig = result?.config ?? useFallbackPreprocessor(path, false);
41+
if (result) {
42+
Logger.log('Found config at ', result.filepath);
43+
}
44+
return {
45+
...config,
46+
compilerOptions: { ...DEFAULT_OPTIONS, ...config.compilerOptions, ...NO_GENERATE },
47+
};
48+
} catch (err) {
49+
Logger.error('Error while loading config');
50+
Logger.error(err);
51+
return {
52+
...useFallbackPreprocessor(path, true),
53+
compilerOptions: {
54+
...DEFAULT_OPTIONS,
55+
...NO_GENERATE,
56+
},
57+
loadConfigError: err,
58+
};
59+
}
60+
}
61+
62+
function useFallbackPreprocessor(path: string, foundConfig: boolean): SvelteConfig {
63+
Logger.log(
64+
(foundConfig
65+
? 'Found svelte.config.js but there was an error loading it. '
66+
: 'No svelte.config.js found. ') +
67+
'Using https://github.com/sveltejs/svelte-preprocess as fallback',
68+
);
69+
return {
70+
preprocess: importSveltePreprocess(path)({
71+
typescript: { transpileOnly: true },
72+
}),
73+
};
74+
}

packages/language-server/src/plugins/svelte/SvelteDocument.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
11
import { SourceMapConsumer } from 'source-map';
2-
import { PreprocessorGroup } from 'svelte-preprocess/dist/types';
2+
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
33
import type { compile } from 'svelte/compiler';
4+
import { CompileOptions } from 'svelte/types/compiler/interfaces';
45
import { Processed } from 'svelte/types/compiler/preprocess';
56
import { Position } from 'vscode-languageserver';
67
import {
78
Document,
89
DocumentMapper,
10+
extractScriptTags,
11+
extractStyleTag,
912
FragmentMapper,
1013
IdentityMapper,
14+
offsetAt,
1115
SourceMapDocumentMapper,
1216
TagInformation,
13-
offsetAt,
14-
extractStyleTag,
15-
extractScriptTags,
1617
} from '../../lib/documents';
17-
import { importSvelte } from '../importPackage';
18-
import { CompileOptions } from 'svelte/types/compiler/interfaces';
18+
import { importSvelte } from '../../importPackage';
1919

2020
export type SvelteCompileResult = ReturnType<typeof compile>;
2121

22-
export interface SvelteConfig {
23-
compilerOptions?: CompileOptions;
24-
preprocess?: PreprocessorGroup;
25-
loadConfigError?: any;
26-
}
27-
2822
export enum TranspileErrorSource {
2923
Script = 'Script',
3024
Style = 'Style',
@@ -43,8 +37,9 @@ export class SvelteDocument {
4337
public languageId = 'svelte';
4438
public version = 0;
4539
public uri = this.parent.uri;
40+
public config = this.parent.config;
4641

47-
constructor(private parent: Document, public config: SvelteConfig) {
42+
constructor(private parent: Document) {
4843
this.script = this.parent.scriptInfo;
4944
this.moduleScript = this.parent.moduleScriptInfo;
5045
this.style = this.parent.styleInfo;
@@ -67,15 +62,15 @@ export class SvelteDocument {
6762
if (!this.transpiledDoc) {
6863
this.transpiledDoc = await TranspiledSvelteDocument.create(
6964
this.parent,
70-
this.config.preprocess,
65+
this.parent.config.preprocess,
7166
);
7267
}
7368
return this.transpiledDoc;
7469
}
7570

7671
async getCompiled(): Promise<SvelteCompileResult> {
7772
if (!this.compileResult) {
78-
this.compileResult = await this.getCompiledWith(this.config.compilerOptions);
73+
this.compileResult = await this.getCompiledWith(this.parent.config.compilerOptions);
7974
}
8075

8176
return this.compileResult;

packages/language-server/src/plugins/svelte/SveltePlugin.ts

Lines changed: 4 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { cosmiconfig } from 'cosmiconfig';
2-
import { CompileOptions } from 'svelte/types/compiler/interfaces';
32
import {
43
CodeAction,
54
CodeActionContext,
@@ -12,29 +11,21 @@ import {
1211
WorkspaceEdit,
1312
} from 'vscode-languageserver';
1413
import { Document } from '../../lib/documents';
15-
import { Logger } from '../../logger';
1614
import { LSConfigManager, LSSvelteConfig } from '../../ls-config';
17-
import { importPrettier, importSveltePreprocess } from '../importPackage';
15+
import { importPrettier } from '../../importPackage';
1816
import {
1917
CodeActionsProvider,
2018
CompletionsProvider,
2119
DiagnosticsProvider,
2220
FormattingProvider,
2321
HoverProvider,
2422
} from '../interfaces';
25-
import { getCodeActions, executeCommand } from './features/getCodeActions';
23+
import { executeCommand, getCodeActions } from './features/getCodeActions';
2624
import { getCompletions } from './features/getCompletions';
2725
import { getDiagnostics } from './features/getDiagnostics';
2826
import { getHoverInfo } from './features/getHoverInfo';
29-
import { SvelteCompileResult, SvelteConfig, SvelteDocument } from './SvelteDocument';
27+
import { SvelteCompileResult, SvelteDocument } from './SvelteDocument';
3028

31-
const DEFAULT_OPTIONS: CompileOptions = {
32-
dev: true,
33-
};
34-
35-
const NO_GENERATE: CompileOptions = {
36-
generate: false,
37-
};
3829
export class SveltePlugin
3930
implements
4031
DiagnosticsProvider,
@@ -152,60 +143,9 @@ export class SveltePlugin
152143
let svelteDoc = this.docManager.get(document);
153144
if (!svelteDoc || svelteDoc.version !== document.version) {
154145
svelteDoc?.destroyTranspiled();
155-
// Reuse previous config. Assumption: Config does not change often (if at all).
156-
const config =
157-
svelteDoc?.config && !svelteDoc.config.loadConfigError
158-
? svelteDoc.config
159-
: await this.loadConfig(document);
160-
svelteDoc = new SvelteDocument(document, config);
146+
svelteDoc = new SvelteDocument(document);
161147
this.docManager.set(document, svelteDoc);
162148
}
163149
return svelteDoc;
164150
}
165-
166-
private async loadConfig(document: Document): Promise<SvelteConfig> {
167-
Logger.log('Trying to load config for', document.getFilePath());
168-
try {
169-
const result = await this.cosmiConfigExplorer.search(document.getFilePath() || '');
170-
const config: SvelteConfig =
171-
result?.config ?? this.useFallbackPreprocessor(document, false);
172-
if (result) {
173-
Logger.log('Found config at ', result.filepath);
174-
}
175-
return {
176-
...config,
177-
compilerOptions: { ...DEFAULT_OPTIONS, ...config.compilerOptions, ...NO_GENERATE },
178-
};
179-
} catch (err) {
180-
Logger.error('Error while loading config');
181-
Logger.error(err);
182-
return {
183-
...this.useFallbackPreprocessor(document, true),
184-
compilerOptions: {
185-
...DEFAULT_OPTIONS,
186-
...NO_GENERATE,
187-
},
188-
loadConfigError: err,
189-
};
190-
}
191-
}
192-
193-
private useFallbackPreprocessor(document: Document, foundConfig: boolean): SvelteConfig {
194-
const needsConfig =
195-
document.styleInfo?.attributes.lang ||
196-
document.styleInfo?.attributes.type ||
197-
document.scriptInfo?.attributes.lang ||
198-
document.scriptInfo?.attributes.type;
199-
Logger.log(
200-
(foundConfig
201-
? 'Found svelte.config.js but there was an error loading it. '
202-
: 'No svelte.config.js found' + (needsConfig ? ' but one is needed. ' : '. ')) +
203-
'Using https://github.com/sveltejs/svelte-preprocess as fallback',
204-
);
205-
return {
206-
preprocess: importSveltePreprocess(document.getFilePath() || '')({
207-
typescript: { transpileOnly: true },
208-
}),
209-
};
210-
}
211151
}

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
positionAt,
1212
TagInformation,
1313
isInTag,
14-
extractScriptTags,
1514
} from '../../lib/documents';
1615
import { pathToUrl } from '../../utils';
1716
import { ConsumerDocumentMapper } from './DocumentMapper';
@@ -112,13 +111,10 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
112111
let text = document.getText();
113112

114113
try {
115-
const tsx = svelte2tsx(
116-
text,
117-
{
118-
strictMode: options.strictMode,
119-
filename: document.getFilePath() ?? undefined,
120-
}
121-
);
114+
const tsx = svelte2tsx(text, {
115+
strictMode: options.strictMode,
116+
filename: document.getFilePath() ?? undefined,
117+
});
122118
text = tsx.code;
123119
tsxMap = tsx.map;
124120
if (tsxMap) {
@@ -175,10 +171,11 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
175171

176172
get scriptKind() {
177173
if (!this._scriptKind) {
178-
const scriptTags = extractScriptTags(this.parent.getText());
179-
const scriptKind = getScriptKindFromAttributes(scriptTags?.script?.attributes ?? {});
174+
const scriptKind = getScriptKindFromAttributes(
175+
this.parent.scriptInfo?.attributes ?? {},
176+
);
180177
const moduleScriptKind = getScriptKindFromAttributes(
181-
scriptTags?.moduleScript?.attributes ?? {},
178+
this.parent.moduleScriptInfo?.attributes ?? {},
182179
);
183180
this._scriptKind = [scriptKind, moduleScriptKind].includes(ts.ScriptKind.TSX)
184181
? ts.ScriptKind.TSX

packages/language-server/src/plugins/typescript/service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { dirname, resolve } from 'path';
22
import ts from 'typescript';
33
import { Document } from '../../lib/documents';
44
import { Logger } from '../../logger';
5-
import { getPackageInfo } from '../importPackage';
5+
import { getPackageInfo } from '../../importPackage';
66
import { DocumentSnapshot } from './DocumentSnapshot';
77
import { createSvelteModuleLoader } from './module-loader';
88
import { SnapshotManager } from './SnapshotManager';

packages/language-server/test/plugins/svelte/SvelteDocument.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import * as assert from 'assert';
22
import sinon from 'sinon';
33
import { Position } from 'vscode-languageserver';
44
import { Document } from '../../../src/lib/documents';
5-
import * as importPackage from '../../../src/plugins/importPackage';
6-
import { SvelteDocument, SvelteConfig } from '../../../src/plugins/svelte/SvelteDocument';
5+
import * as importPackage from '../../../src/importPackage';
6+
import { SvelteDocument } from '../../../src/plugins/svelte/SvelteDocument';
7+
import * as configLoader from '../../../src/lib/documents/configLoader';
78

89
describe('Svelte Document', () => {
910
function getSourceCode(transpiled: boolean): string {
@@ -15,9 +16,11 @@ describe('Svelte Document', () => {
1516
`;
1617
}
1718

18-
function setup(config: SvelteConfig = {}) {
19+
function setup(config: configLoader.SvelteConfig = {}) {
20+
sinon.stub(configLoader, 'loadConfig').returns(config);
1921
const parent = new Document('file:///hello.svelte', getSourceCode(false));
20-
const svelteDoc = new SvelteDocument(parent, config);
22+
sinon.restore();
23+
const svelteDoc = new SvelteDocument(parent);
2124
return { parent, svelteDoc };
2225
}
2326

0 commit comments

Comments
 (0)