Skip to content

Commit 6e52667

Browse files
authored
(fix) more robust preprocess source maps (#1004)
In recent Svelte versions, preprocessing also returns a source map. If the user's version is high enough, use that instead of our own (now deprecated) logic. This provides more robust source mapping. #934
1 parent c9c55a9 commit 6e52667

File tree

6 files changed

+296
-41
lines changed

6 files changed

+296
-41
lines changed

packages/language-server/src/importPackage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ export function getPackageInfo(packageName: string, fromPath: string) {
2626
path: dirname(packageJSONPath),
2727
version: {
2828
full: version,
29-
major,
30-
minor,
31-
patch
29+
major: Number(major),
30+
minor: Number(minor),
31+
patch: Number(patch)
3232
}
3333
};
3434
}

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

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { SourceMapConsumer } from 'source-map';
2-
import { PreprocessorGroup, Processed } from 'svelte/types/compiler/preprocess/types';
32
import type { compile } from 'svelte/compiler';
43
import { CompileOptions } from 'svelte/types/compiler/interfaces';
5-
4+
import { PreprocessorGroup, Processed } from 'svelte/types/compiler/preprocess/types';
65
import { Position } from 'vscode-languageserver';
6+
import { getPackageInfo, importSvelte } from '../../importPackage';
77
import {
88
Document,
99
DocumentMapper,
@@ -17,7 +17,7 @@ import {
1717
SourceMapDocumentMapper,
1818
TagInformation
1919
} from '../../lib/documents';
20-
import { importSvelte } from '../../importPackage';
20+
import { SvelteConfig } from '../../lib/documents/configLoader';
2121
import { isNotNullOrUndefined } from '../../utils';
2222

2323
export type SvelteCompileResult = ReturnType<typeof compile>;
@@ -33,7 +33,7 @@ type PositionMapper = Pick<DocumentMapper, 'getGeneratedPosition' | 'getOriginal
3333
* Represents a text document that contains a svelte component.
3434
*/
3535
export class SvelteDocument {
36-
private transpiledDoc: TranspiledSvelteDocument | undefined;
36+
private transpiledDoc: ITranspiledSvelteDocument | undefined;
3737
private compileResult: SvelteCompileResult | undefined;
3838

3939
public script: TagInformation | null;
@@ -65,14 +65,25 @@ export class SvelteDocument {
6565
return this.parent.offsetAt(position);
6666
}
6767

68-
async getTranspiled(): Promise<TranspiledSvelteDocument> {
68+
async getTranspiled(): Promise<ITranspiledSvelteDocument> {
6969
if (!this.transpiledDoc) {
70-
this.transpiledDoc = await TranspiledSvelteDocument.create(
71-
this.parent,
72-
(
70+
const {
71+
version: { major, minor }
72+
} = getPackageInfo('svelte', this.getFilePath());
73+
74+
if (major > 3 || (major === 3 && minor >= 32)) {
75+
this.transpiledDoc = await TranspiledSvelteDocument.create(
76+
this.parent,
7377
await this.config
74-
)?.preprocess
75-
);
78+
);
79+
} else {
80+
this.transpiledDoc = await FallbackTranspiledSvelteDocument.create(
81+
this.parent,
82+
(
83+
await this.config
84+
)?.preprocess
85+
);
86+
}
7687
}
7788
return this.transpiledDoc;
7889
}
@@ -101,7 +112,67 @@ export class SvelteDocument {
101112
}
102113
}
103114

104-
export class TranspiledSvelteDocument implements PositionMapper {
115+
export interface ITranspiledSvelteDocument extends PositionMapper {
116+
getText(): string;
117+
destroy(): void;
118+
}
119+
120+
export class TranspiledSvelteDocument implements ITranspiledSvelteDocument {
121+
static async create(document: Document, config: SvelteConfig | undefined) {
122+
if (!config?.preprocess) {
123+
return new TranspiledSvelteDocument(document.getText());
124+
}
125+
126+
const filename = document.getFilePath() || '';
127+
const svelte = importSvelte(filename);
128+
const preprocessed = await svelte.preprocess(document.getText(), config?.preprocess || [], {
129+
filename
130+
});
131+
132+
if (preprocessed.code === document.getText()) {
133+
return new TranspiledSvelteDocument(document.getText());
134+
}
135+
136+
return new TranspiledSvelteDocument(
137+
preprocessed.code,
138+
preprocessed.map
139+
? new SourceMapDocumentMapper(
140+
await createSourceMapConsumer(preprocessed.map),
141+
// The "sources" array only contains the Svelte filename, not its path.
142+
// For getting generated positions, the sourcemap consumer wants an exact match
143+
// of the source filepath. Therefore only pass in the filename here.
144+
filename.replace(/\\/g, '/').split('/').pop() || ''
145+
)
146+
: undefined
147+
);
148+
}
149+
150+
constructor(private code: string, private mapper?: SourceMapDocumentMapper) {}
151+
152+
getOriginalPosition(generatedPosition: Position): Position {
153+
return this.mapper?.getOriginalPosition(generatedPosition) || generatedPosition;
154+
}
155+
156+
getText() {
157+
return this.code;
158+
}
159+
160+
getGeneratedPosition(originalPosition: Position): Position {
161+
return this.mapper?.getGeneratedPosition(originalPosition) || originalPosition;
162+
}
163+
164+
destroy() {
165+
this.mapper?.destroy();
166+
}
167+
}
168+
169+
/**
170+
* Only used when the user has an old Svelte version installed where source map support
171+
* for preprocessors is not built in yet.
172+
* This fallback version does not map correctly when there's both a module and instance script.
173+
* It isn't worth fixing these cases though now that Svelte ships a preprocessor with source maps.
174+
*/
175+
export class FallbackTranspiledSvelteDocument implements ITranspiledSvelteDocument {
105176
static async create(
106177
document: Document,
107178
preprocessors: PreprocessorGroup | PreprocessorGroup[] = []
@@ -121,7 +192,12 @@ export class TranspiledSvelteDocument implements PositionMapper {
121192
processedStyles
122193
);
123194

124-
return new TranspiledSvelteDocument(document, transpiled, scriptMapper, styleMapper);
195+
return new FallbackTranspiledSvelteDocument(
196+
document,
197+
transpiled,
198+
scriptMapper,
199+
styleMapper
200+
);
125201
}
126202

127203
private fragmentInfos = [this.scriptMapper?.fragmentInfo, this.styleMapper?.fragmentInfo]
@@ -252,23 +328,13 @@ export class SvelteFragmentMapper implements PositionMapper {
252328
async (parent, processedSingle) =>
253329
processedSingle?.map
254330
? new SourceMapDocumentMapper(
255-
await new SourceMapConsumer(normalizeMap(processedSingle.map)),
331+
await createSourceMapConsumer(processedSingle.map),
256332
originalDoc.uri,
257333
await parent
258334
)
259335
: new IdentityMapper(originalDoc.uri, await parent),
260336
Promise.resolve<DocumentMapper>(<any>undefined)
261337
);
262-
263-
function normalizeMap(map: any) {
264-
// We don't know what we get, could be a stringified sourcemap,
265-
// or a class which has the required properties on it, or a class
266-
// which we need to call toString() on to get the correct format.
267-
if (typeof map === 'string' || map.version) {
268-
return map;
269-
}
270-
return map.toString();
271-
}
272338
}
273339

274340
private constructor(
@@ -380,3 +446,17 @@ async function transpile(
380446

381447
return { transpiled, processedScripts, processedStyles };
382448
}
449+
450+
async function createSourceMapConsumer(map: any): Promise<SourceMapConsumer> {
451+
return new SourceMapConsumer(normalizeMap(map));
452+
453+
function normalizeMap(map: any) {
454+
// We don't know what we get, could be a stringified sourcemap,
455+
// or a class which has the required properties on it, or a class
456+
// which we need to call toString() on to get the correct format.
457+
if (typeof map === 'string' || map.version) {
458+
return map;
459+
}
460+
return map.toString();
461+
}
462+
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Document } from '../../../src/lib/documents';
55
import * as importPackage from '../../../src/importPackage';
66
import {
77
SvelteDocument,
8-
TranspiledSvelteDocument
8+
ITranspiledSvelteDocument
99
} from '../../../src/plugins/svelte/SvelteDocument';
1010
import { configLoader, SvelteConfig } from '../../../src/lib/documents/configLoader';
1111
import { Preprocessor } from 'svelte/types/compiler/preprocess/types';
@@ -33,7 +33,7 @@ describe('Svelte Document', () => {
3333
assert.strictEqual(svelteDoc.getText(), parent.getText());
3434
});
3535

36-
describe('#transpiled', () => {
36+
describe('#transpiled (fallback)', () => {
3737
async function setupTranspiledWithStringSourceMap() {
3838
const stringSourceMapScript = () => ({
3939
code: '',
@@ -93,7 +93,10 @@ describe('Svelte Document', () => {
9393
});
9494

9595
// stub svelte preprocess and getOriginalPosition
96-
// to fake a source mapping process
96+
// to fake a source mapping process with the fallback version
97+
sinon
98+
.stub(importPackage, 'getPackageInfo')
99+
.returns({ path: '', version: { full: '', major: 3, minor: 31, patch: 0 } });
97100
sinon.stub(importPackage, 'importSvelte').returns({
98101
preprocess: (text, preprocessor) => {
99102
preprocessor = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
@@ -111,7 +114,7 @@ describe('Svelte Document', () => {
111114
parse: <any>null
112115
});
113116
const transpiled = await svelteDoc.getTranspiled();
114-
const scriptSourceMapper = (<any>transpiled.scriptMapper).sourceMapper;
117+
const scriptSourceMapper = (<any>transpiled).scriptMapper.sourceMapper;
115118
// hacky reset of method because mocking the SourceMap constructor is an impossible task
116119
scriptSourceMapper.getOriginalPosition = ({ line, character }: Position) => ({
117120
line: line - 1,
@@ -127,7 +130,7 @@ describe('Svelte Document', () => {
127130
}
128131

129132
function assertCanMapBackAndForth(
130-
transpiled: TranspiledSvelteDocument,
133+
transpiled: ITranspiledSvelteDocument,
131134
generatedPosition: Position,
132135
originalPosition: Position
133136
) {

0 commit comments

Comments
 (0)