UNPKG

svelte-language-server

Version:
583 lines 23.7 kB
"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