svelte-language-server
Version:
A language server for Svelte
583 lines • 23.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DtsDocumentSnapshot = exports.JSOrTSDocumentSnapshot = exports.SvelteDocumentSnapshot = exports.DocumentSnapshot = exports.INITIAL_VERSION = void 0;
const trace_mapping_1 = require("@jridgewell/trace-mapping");
const svelte2tsx_1 = require("svelte2tsx");
const typescript_1 = __importDefault(require("typescript"));
const documents_1 = require("../../lib/documents");
const utils_1 = require("../../utils");
const DocumentMapper_1 = require("./DocumentMapper");
const svelte_ast_utils_1 = require("./svelte-ast-utils");
const utils_2 = require("./utils");
const logger_1 = require("../../logger");
const path_1 = require("path");
const vscode_uri_1 = require("vscode-uri");
const utils_3 = require("./features/utils");
const configLoader_1 = require("../../lib/documents/configLoader");
/**
* Initial version of snapshots.
*/
exports.INITIAL_VERSION = 0;
var DocumentSnapshot;
(function (DocumentSnapshot) {
/**
* Returns a svelte snapshot from a svelte document.
* @param document the svelte document
* @param options options that apply to the svelte document
*/
function fromDocument(document, options) {
const { tsxMap, htmlAst, text, exportedNames, parserError, nrPrependedLines, scriptKind } = preprocessSvelteFile(document, options);
return new SvelteDocumentSnapshot(document, parserError, scriptKind, options.version, text, nrPrependedLines, exportedNames, tsxMap, htmlAst);
}
DocumentSnapshot.fromDocument = fromDocument;
/**
* Returns a svelte or ts/js snapshot from a file path, depending on the file contents.
* @param filePath path to the js/ts/svelte file
* @param createDocument function that is used to create a document in case it's a Svelte file
* @param options options that apply in case it's a svelte file
*/
function fromFilePath(filePath, createDocument, options, tsSystem) {
if ((0, utils_2.isSvelteFilePath)(filePath)) {
return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options, tsSystem);
}
else {
return DocumentSnapshot.fromNonSvelteFilePath(filePath, tsSystem);
}
}
DocumentSnapshot.fromFilePath = fromFilePath;
/**
* Returns a ts/js snapshot from a file path.
* @param filePath path to the js/ts file
* @param options options that apply in case it's a svelte file
*/
function fromNonSvelteFilePath(filePath, tsSystem) {
let originalText = '';
// The following (very hacky) code makes sure that the ambient module definitions
// that tell TS "every import ending with .svelte is a valid module" are removed.
// They exist in svelte2tsx and svelte to make sure that people don't
// get errors in their TS files when importing Svelte files and not using our TS plugin.
// If someone wants to get back the behavior they can add an ambient module definition
// on their own.
const normalizedPath = filePath.replace(/\\/g, '/');
if (!normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) {
originalText = tsSystem.readFile(filePath) || '';
}
if (normalizedPath.endsWith('node_modules/svelte/types/index.d.ts')) {
const startIdx = originalText.indexOf(`declare module '*.svelte' {`);
const endIdx = originalText.indexOf(`\n}`, startIdx + 1) + 2;
originalText =
originalText.substring(0, startIdx) +
' '.repeat(endIdx - startIdx) +
originalText.substring(endIdx);
}
else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts') ||
normalizedPath.endsWith('svelte-check/dist/src/svelte-shims.d.ts')) {
// If not present, the LS uses an older version of svelte2tsx
if (originalText.includes('// -- start svelte-ls-remove --')) {
originalText =
originalText.substring(0, originalText.indexOf('// -- start svelte-ls-remove --')) +
originalText.substring(originalText.indexOf('// -- end svelte-ls-remove --'));
}
}
const declarationExtensions = [typescript_1.default.Extension.Dcts, typescript_1.default.Extension.Dts, typescript_1.default.Extension.Dmts];
if (declarationExtensions.some((ext) => normalizedPath.endsWith(ext))) {
return new DtsDocumentSnapshot(exports.INITIAL_VERSION, filePath, originalText, tsSystem);
}
return new JSOrTSDocumentSnapshot(exports.INITIAL_VERSION, filePath, originalText);
}
DocumentSnapshot.fromNonSvelteFilePath = fromNonSvelteFilePath;
/**
* Returns a svelte snapshot from a file path.
* @param filePath path to the svelte file
* @param createDocument function that is used to create a document
* @param options options that apply in case it's a svelte file
*/
function fromSvelteFilePath(filePath, createDocument, options, tsSystem) {
const originalText = tsSystem.readFile(filePath) ?? '';
return fromDocument(createDocument(filePath, originalText), options);
}
DocumentSnapshot.fromSvelteFilePath = fromSvelteFilePath;
})(DocumentSnapshot || (exports.DocumentSnapshot = DocumentSnapshot = {}));
/**
* Tries to preprocess the svelte document and convert the contents into better analyzable js/ts(x) content.
*/
function preprocessSvelteFile(document, options) {
let tsxMap;
let parserError = null;
let nrPrependedLines = 0;
let text = document.getText();
let exportedNames = { has: () => false };
let htmlAst;
const scriptKind = [
(0, utils_2.getScriptKindFromAttributes)(document.scriptInfo?.attributes ?? {}),
(0, utils_2.getScriptKindFromAttributes)(document.moduleScriptInfo?.attributes ?? {})
].includes(typescript_1.default.ScriptKind.TSX)
? typescript_1.default.ScriptKind.TS
: typescript_1.default.ScriptKind.JS;
try {
const tsx = (0, svelte2tsx_1.svelte2tsx)(text, {
parse: options.parse,
version: options.version,
filename: document.getFilePath() ?? undefined,
isTsFile: scriptKind === typescript_1.default.ScriptKind.TS,
mode: 'ts',
typingsNamespace: options.typingsNamespace,
emitOnTemplateError: options.transformOnTemplateError,
namespace: document.config?.compilerOptions?.namespace,
accessors: document.config?.compilerOptions?.accessors ??
document.config?.compilerOptions?.customElement
});
text = tsx.code;
tsxMap = tsx.map;
exportedNames = tsx.exportedNames;
// We know it's there, it's not part of the public API so people don't start using it
htmlAst = tsx.htmlAst;
if (tsxMap) {
tsxMap.sources = [document.uri];
const scriptInfo = document.scriptInfo || document.moduleScriptInfo;
const tsCheck = (0, utils_2.getTsCheckComment)(scriptInfo?.content);
if (tsCheck) {
text = tsCheck + text;
nrPrependedLines = 1;
}
}
}
catch (e) {
// Error start/end logic is different and has different offsets for line, so we need to convert that
const start = {
line: (e.start?.line ?? 1) - 1,
character: e.start?.column ?? 0
};
const end = e.end ? { line: e.end.line - 1, character: e.end.column } : start;
parserError = {
range: { start, end },
message: e.message,
code: -1
};
// fall back to extracted script, if any
const scriptInfo = document.scriptInfo || document.moduleScriptInfo;
text = scriptInfo ? scriptInfo.content : '';
}
return {
tsxMap,
text,
exportedNames,
htmlAst,
parserError,
nrPrependedLines,
scriptKind
};
}
/**
* A svelte document snapshot suitable for the TS language service and the plugin.
* It contains the generated code (Svelte->TS/JS) so the TS language service can understand it.
*/
class SvelteDocumentSnapshot {
constructor(parent, parserError, scriptKind, svelteVersion, text, nrPrependedLines, exportedNames, tsxMap, htmlAst) {
this.parent = parent;
this.parserError = parserError;
this.scriptKind = scriptKind;
this.svelteVersion = svelteVersion;
this.text = text;
this.nrPrependedLines = nrPrependedLines;
this.exportedNames = exportedNames;
this.tsxMap = tsxMap;
this.htmlAst = htmlAst;
this.url = (0, utils_1.pathToUrl)(this.filePath);
this.version = this.parent.version;
this.isSvelte5Plus = Number(this.svelteVersion?.split('.')[0]) >= 5;
}
get filePath() {
return this.parent.getFilePath() || '';
}
get scriptInfo() {
return this.parent.scriptInfo;
}
get moduleScriptInfo() {
return this.parent.moduleScriptInfo;
}
getOriginalText(range) {
return this.parent.getText(range);
}
getText(start, end) {
return this.text.substring(start, end);
}
getLength() {
return this.text.length;
}
getFullText() {
return this.text;
}
getChangeRange() {
return undefined;
}
positionAt(offset) {
return (0, documents_1.positionAt)(offset, this.text, this.getLineOffsets());
}
offsetAt(position) {
return (0, documents_1.offsetAt)(position, this.text, this.getLineOffsets());
}
getLineContainingOffset(offset) {
const chunks = this.getText(0, offset).split('\n');
return chunks[chunks.length - 1];
}
hasProp(name) {
return this.exportedNames.has(name);
}
svelteNodeAt(positionOrOffset) {
if (!this.htmlAst) {
return null;
}
const offset = typeof positionOrOffset === 'number'
? positionOrOffset
: this.parent.offsetAt(positionOrOffset);
let foundNode = null;
this.walkSvelteAst({
enter(node) {
// In case the offset is at a point where a node ends and a new one begins,
// the node where the code ends is used. If this introduces problems, introduce
// an affinity parameter to prefer the node where it ends/starts.
if (node.start > offset || node.end < offset) {
this.skip();
return;
}
const parent = foundNode;
// Spread so the "parent" property isn't added to the original ast,
// causing an infinite loop
foundNode = { ...node };
if (parent) {
foundNode.parent = parent;
}
}
});
return foundNode;
}
walkSvelteAst(walker) {
if (!this.htmlAst) {
return;
}
(0, svelte_ast_utils_1.walkSvelteAst)(this.htmlAst, walker);
}
getOriginalPosition(pos) {
return this.getMapper().getOriginalPosition(pos);
}
getGeneratedPosition(pos) {
return this.getMapper().getGeneratedPosition(pos);
}
isInGenerated(pos) {
return !(0, documents_1.isInTag)(pos, this.parent.styleInfo);
}
getURL() {
return this.url;
}
isOpenedInClient() {
return this.parent.openedByClient;
}
getLineOffsets() {
if (!this.lineOffsets) {
this.lineOffsets = (0, documents_1.getLineOffsets)(this.text);
}
return this.lineOffsets;
}
getMapper() {
if (!this.mapper) {
this.mapper = this.initMapper();
}
return this.mapper;
}
initMapper() {
const scriptInfo = this.parent.scriptInfo || this.parent.moduleScriptInfo;
if (!this.tsxMap) {
if (!scriptInfo) {
return new documents_1.IdentityMapper(this.url);
}
return new documents_1.FragmentMapper(this.parent.getText(), scriptInfo, this.url);
}
return new DocumentMapper_1.ConsumerDocumentMapper(new trace_mapping_1.TraceMap(this.tsxMap), this.url, this.nrPrependedLines);
}
}
exports.SvelteDocumentSnapshot = SvelteDocumentSnapshot;
/**
* 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.
* If it's a SvelteKit file (e.g. +page.ts), types will be auto-added if not explicitly typed.
*/
class JSOrTSDocumentSnapshot extends documents_1.IdentityMapper {
isOpenedInClient() {
return this.openedByClient;
}
constructor(version, filePath, text) {
super((0, utils_1.pathToUrl)(filePath));
this.version = version;
this.filePath = filePath;
this.text = text;
this.scriptKind = (0, utils_2.getScriptKindFromFileName)(this.filePath);
this.scriptInfo = null;
this.originalText = this.text;
this.kitFile = false;
this.addedCode = [];
this.paramsPath = 'src/params';
this.serverHooksPath = 'src/hooks.server';
this.clientHooksPath = 'src/hooks.client';
this.universalHooksPath = 'src/hooks';
this.openedByClient = false;
this.adjustText();
}
getText(start, end) {
return this.text.substring(start, end);
}
getLength() {
return this.text.length;
}
getFullText() {
return this.text;
}
getChangeRange() {
return undefined;
}
positionAt(offset) {
return (0, documents_1.positionAt)(offset, this.text, this.getLineOffsets());
}
offsetAt(position) {
return (0, documents_1.offsetAt)(position, this.text, this.getLineOffsets());
}
getGeneratedPosition(originalPosition) {
if (!this.kitFile || this.addedCode.length === 0) {
return super.getGeneratedPosition(originalPosition);
}
const pos = this.originalOffsetAt(originalPosition);
let total = 0;
for (const added of this.addedCode) {
if (pos < added.generatedPos)
break;
total += added.length;
}
return this.positionAt(pos + total);
}
getOriginalPosition(generatedPosition) {
if (!this.kitFile || this.addedCode.length === 0) {
return super.getOriginalPosition(generatedPosition);
}
const pos = this.offsetAt(generatedPosition);
let total = 0;
let idx = 0;
for (; idx < this.addedCode.length; idx++) {
const added = this.addedCode[idx];
if (pos < added.generatedPos)
break;
total += added.length;
}
if (idx > 0) {
const prev = this.addedCode[idx - 1];
// Special case: pos is in the middle of an added range
if (pos > prev.generatedPos && pos < prev.generatedPos + prev.length) {
return this.originalPositionAt(prev.originalPos);
}
}
return this.originalPositionAt(pos - total);
}
update(changes) {
for (const change of changes) {
let start = 0;
let end = 0;
if ('range' in change) {
start = this.originalOffsetAt(change.range.start);
end = this.originalOffsetAt(change.range.end);
}
else {
end = this.originalText.length;
}
this.originalText =
this.originalText.slice(0, start) + change.text + this.originalText.slice(end);
}
this.adjustText();
this.version++;
this.lineOffsets = undefined;
this.internalLineOffsets = undefined;
// only client can have incremental updates
this.openedByClient = true;
}
getLineOffsets() {
if (!this.lineOffsets) {
this.lineOffsets = (0, documents_1.getLineOffsets)(this.text);
}
return this.lineOffsets;
}
originalOffsetAt(position) {
return (0, documents_1.offsetAt)(position, this.originalText, this.getOriginalLineOffsets());
}
originalPositionAt(offset) {
return (0, documents_1.positionAt)(offset, this.originalText, this.getOriginalLineOffsets());
}
getOriginalLineOffsets() {
if (!this.kitFile) {
return this.getLineOffsets();
}
if (!this.internalLineOffsets) {
this.internalLineOffsets = (0, documents_1.getLineOffsets)(this.originalText);
}
return this.internalLineOffsets;
}
adjustText() {
const result = svelte2tsx_1.internalHelpers.upsertKitFile(typescript_1.default, this.filePath, {
clientHooksPath: this.clientHooksPath,
paramsPath: this.paramsPath,
serverHooksPath: this.serverHooksPath,
universalHooksPath: this.universalHooksPath
}, () => this.createSource(), utils_3.surroundWithIgnoreComments);
if (!result) {
this.kitFile = false;
this.addedCode = [];
this.text = this.originalText;
return;
}
if (!this.kitFile) {
const files = configLoader_1.configLoader.getConfig(this.filePath)?.kit?.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;
this.addedCode = addedCode;
this.text = text;
}
createSource() {
return typescript_1.default.createSourceFile(this.filePath, this.originalText, typescript_1.default.ScriptTarget.Latest, true, this.scriptKind);
}
}
exports.JSOrTSDocumentSnapshot = JSOrTSDocumentSnapshot;
const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\r?\n?$/;
const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/;
const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/;
class DtsDocumentSnapshot extends JSOrTSDocumentSnapshot {
constructor(version, filePath, text, tsSys) {
super(version, filePath, text);
this.tsSys = tsSys;
this.mapperInitialized = false;
}
getOriginalFilePosition(generatedPosition) {
if (!this.mapperInitialized) {
this.traceMap = this.initMapper();
this.mapperInitialized = true;
}
const mapped = this.traceMap
? (0, trace_mapping_1.originalPositionFor)(this.traceMap, {
line: generatedPosition.line + 1,
column: generatedPosition.character
})
: undefined;
if (!mapped || mapped.line == null || !mapped.source) {
return generatedPosition;
}
const originalFilePath = vscode_uri_1.URI.isUri(mapped.source)
? (0, utils_1.urlToPath)(mapped.source)
: this.filePath
? (0, path_1.resolve)((0, path_1.dirname)(this.filePath), mapped.source).toString()
: undefined;
// ex: library publish with declarationMap but npmignore the original files
if (!originalFilePath || !this.tsSys.fileExists(originalFilePath)) {
return generatedPosition;
}
return {
line: mapped.line - 1,
character: mapped.column,
uri: (0, utils_1.pathToUrl)(originalFilePath)
};
}
initMapper() {
const sourceMapUrl = tryGetSourceMappingURL(this.getLineOffsets(), this.getFullText());
if (!sourceMapUrl) {
return;
}
const match = sourceMapUrl.match(base64UrlRegExp);
if (match) {
const base64Json = match[1];
if (!base64Json || !this.tsSys.base64decode) {
return;
}
return this.initMapperByRawSourceMap(this.tsSys.base64decode(base64Json));
}
const tryingLocations = new Set([
(0, path_1.resolve)((0, path_1.dirname)(this.filePath), sourceMapUrl),
this.filePath + '.map'
]);
for (const mapFilePath of tryingLocations) {
if (!this.tsSys.fileExists(mapFilePath)) {
continue;
}
const mapFileContent = this.tsSys.readFile(mapFilePath);
if (mapFileContent) {
return this.initMapperByRawSourceMap(mapFileContent);
}
}
this.logFailedToResolveSourceMap("can't find valid sourcemap file");
}
initMapperByRawSourceMap(input) {
const map = tryParseRawSourceMap(input);
// don't support inline sourcemap because
// it must be a file that editor can point to
if (!map ||
!map.mappings ||
map.sourcesContent?.some((content) => typeof content === 'string')) {
this.logFailedToResolveSourceMap('invalid or unsupported sourcemap');
return;
}
return new trace_mapping_1.TraceMap(map);
}
logFailedToResolveSourceMap(...errors) {
logger_1.Logger.debug(`Resolving declaration map for ${this.filePath} failed. `, ...errors);
}
}
exports.DtsDocumentSnapshot = DtsDocumentSnapshot;
// https://github.com/microsoft/TypeScript/blob/1dc5b28b94b4a63f735a42d6497d538434d69b66/src/compiler/sourcemap.ts#L381
function tryGetSourceMappingURL(lineOffsets, text) {
for (let index = lineOffsets.length - 1; index >= 0; index--) {
const line = text.slice(lineOffsets[index], lineOffsets[index + 1]);
const comment = sourceMapCommentRegExp.exec(line);
if (comment) {
return comment[1].trimEnd();
}
// If we see a non-whitespace/map comment-like line, break, to avoid scanning up the entire file
else if (!line.match(whitespaceOrMapCommentRegExp)) {
break;
}
}
}
// https://github.com/microsoft/TypeScript/blob/1dc5b28b94b4a63f735a42d6497d538434d69b66/src/compiler/sourcemap.ts#L402
function isRawSourceMap(x) {
return (x !== null &&
typeof x === 'object' &&
x.version === 3 &&
typeof x.file === 'string' &&
typeof x.mappings === 'string' &&
Array.isArray(x.sources) &&
x.sources.every((source) => typeof source === 'string') &&
(x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === 'string') &&
(x.sourcesContent === undefined ||
x.sourcesContent === null ||
(Array.isArray(x.sourcesContent) &&
x.sourcesContent.every((content) => typeof content === 'string' || content === null))) &&
(x.names === undefined ||
x.names === null ||
(Array.isArray(x.names) && x.names.every((name) => typeof name === 'string'))));
}
function tryParseRawSourceMap(text) {
try {
const parsed = JSON.parse(text);
if (isRawSourceMap(parsed)) {
return parsed;
}
}
catch {
// empty
}
return undefined;
}
//# sourceMappingURL=DocumentSnapshot.js.map