From 082f6d51e83d97932a686bd4192b3dc3afabaa27 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 4 Jul 2023 02:50:02 +0800 Subject: [PATCH] Refactor dtsHost to `@volar/cdn` (#50) --- packages/cdn/LICENSE | 21 ++ packages/cdn/package.json | 18 ++ packages/cdn/src/cdns/github.ts | 139 ++++++++++ packages/cdn/src/cdns/jsdelivr.ts | 251 ++++++++++++++++++ packages/cdn/src/index.ts | 42 +++ packages/cdn/src/types.ts | 4 + packages/cdn/src/utils.ts | 34 +++ packages/cdn/tsconfig.build.json | 16 ++ packages/kit/src/createFormatter.ts | 2 +- packages/kit/src/createProject.ts | 2 +- packages/language-core/src/types.ts | 2 +- .../language-server/src/common/project.ts | 2 +- packages/monaco/src/worker.ts | 146 +++++----- packages/typescript/src/dtsHost.ts | 225 ---------------- packages/typescript/src/index.ts | 1 - packages/typescript/src/sys.ts | 26 +- pnpm-lock.yaml | 34 ++- tsconfig.build.json | 3 + 18 files changed, 634 insertions(+), 334 deletions(-) create mode 100644 packages/cdn/LICENSE create mode 100644 packages/cdn/package.json create mode 100644 packages/cdn/src/cdns/github.ts create mode 100644 packages/cdn/src/cdns/jsdelivr.ts create mode 100644 packages/cdn/src/index.ts create mode 100644 packages/cdn/src/types.ts create mode 100644 packages/cdn/src/utils.ts create mode 100644 packages/cdn/tsconfig.build.json delete mode 100644 packages/typescript/src/dtsHost.ts diff --git a/packages/cdn/LICENSE b/packages/cdn/LICENSE new file mode 100644 index 00000000..cb8d02b6 --- /dev/null +++ b/packages/cdn/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present Johnson Chu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cdn/package.json b/packages/cdn/package.json new file mode 100644 index 00000000..fceb94ef --- /dev/null +++ b/packages/cdn/package.json @@ -0,0 +1,18 @@ +{ + "name": "@volar/cdn", + "version": "1.7.10", + "main": "out/index.js", + "license": "MIT", + "files": [ + "out/**/*.js", + "out/**/*.d.ts" + ], + "repository": { + "type": "git", + "url": "https://github.com/volarjs/volar.js.git", + "directory": "packages/cdn" + }, + "dependencies": { + "@volar/language-service": "1.7.10" + } +} diff --git a/packages/cdn/src/cdns/github.ts b/packages/cdn/src/cdns/github.ts new file mode 100644 index 00000000..eb8bfa18 --- /dev/null +++ b/packages/cdn/src/cdns/github.ts @@ -0,0 +1,139 @@ +import type { FileType, FileSystem, FileStat } from '@volar/language-service'; +import { UriResolver } from '../types'; +import { fetchJson, fetchText } from '../utils'; + +export function createGitHubUriResolver(fileNameBase: string, owner: string, repo: string, branch: string): UriResolver { + + const gitHubUriBase = getGitHubUriBase(owner, repo, branch); + + return { + uriToFileName, + fileNameToUri, + }; + + function uriToFileName(uri: string) { + if (uri === gitHubUriBase) { + return fileNameBase; + } + if (uri.startsWith(gitHubUriBase + '/')) { + const path = uri.substring(gitHubUriBase.length); + return `${fileNameBase}${path}`; + } + } + + function fileNameToUri(fileName: string) { + if (fileName === fileNameBase) { + return gitHubUriBase; + } + if (fileName.startsWith(fileNameBase + '/')) { + const path = fileName.substring(fileNameBase.length); + return `${gitHubUriBase}${path}`; + } + } +} + +export function createGitHubFs(owner: string, repo: string, branch: string, onReadFile?: (uri: string, content: string) => void): FileSystem { + + const gitHubUriBase = getGitHubUriBase(owner, repo, branch); + + return { + stat, + readDirectory, + readFile, + }; + + async function stat(uri: string): Promise { + + if (uri === gitHubUriBase) { + return { + type: 2 satisfies FileType.Directory, + size: -1, + ctime: -1, + mtime: -1, + }; + } + + if (uri.startsWith(gitHubUriBase + '/')) { + + if (uri.endsWith('/')) { + return { + type: 2 satisfies FileType.Directory, + size: -1, + ctime: -1, + mtime: -1, + }; + } + + const path = uri.substring(gitHubUriBase.length); + const dirName = path.substring(0, path.lastIndexOf('/')); + const baseName = path.substring(path.lastIndexOf('/') + 1); + const dirData = await fetchContents(dirName); + const file = dirData.find(entry => entry.name === baseName && entry.type === 'file'); + const dir = dirData.find(entry => entry.name === baseName && entry.type === 'dir'); + if (file) { + return { + type: 1 satisfies FileType.File, + size: file.size, + ctime: -1, + mtime: -1, + }; + } + if (dir) { + return { + type: 2 satisfies FileType.Directory, + size: dir.size, + ctime: -1, + mtime: -1, + }; + } + } + } + + async function readDirectory(uri: string): Promise<[string, FileType][]> { + + if (uri === gitHubUriBase || uri.startsWith(gitHubUriBase + '/')) { + + const path = uri.substring(gitHubUriBase.length); + const dirData = await fetchContents(path); + const result: [string, FileType][] = dirData.map(entry => [ + entry.name, + entry.type === 'file' ? 1 satisfies FileType.File + : entry.type === 'dir' ? 2 satisfies FileType.Directory + : 0 satisfies FileType.Unknown, + ]); + return result; + } + + return []; + } + + async function readFile(uri: string): Promise { + + if (uri.startsWith(gitHubUriBase + '/')) { + + const text = await fetchText(uri); + if (text !== undefined) { + onReadFile?.(uri, text); + } + return text; + } + } + + async function fetchContents(dirName: string) { + return await fetchJson<{ + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: null | string; + type: 'file' | 'dir', + }[]>(`https://api.github.com/repos/${owner}/${repo}/contents${dirName}?ref=${branch}`) ?? []; + } +} + +function getGitHubUriBase(owner: string, repo: string, branch: string) { + return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`; +} diff --git a/packages/cdn/src/cdns/jsdelivr.ts b/packages/cdn/src/cdns/jsdelivr.ts new file mode 100644 index 00000000..c39dfd89 --- /dev/null +++ b/packages/cdn/src/cdns/jsdelivr.ts @@ -0,0 +1,251 @@ +import type { FileType, FileSystem, FileStat } from '@volar/language-service'; +import { UriResolver } from '../types'; +import { fetchJson, fetchText } from '../utils'; + +export const jsDelivrUriBase = 'https://cdn.jsdelivr.net/npm'; + +export function createJsDelivrUriResolver( + fileNameBase: string, + versions: Record = {}, +): UriResolver { + + return { + uriToFileName, + fileNameToUri, + }; + + function uriToFileName(uri: string) { + if (uri === jsDelivrUriBase) { + return fileNameBase; + } + if (uri.startsWith(jsDelivrUriBase + '/')) { + const path = uri.substring(jsDelivrUriBase.length); + const pkgName = getPackageName(path); + if (pkgName?.substring(1).includes('@')) { + const trimedVersion = pkgName.substring(0, pkgName.lastIndexOf('@')); + return `${fileNameBase}${path.replace(pkgName, trimedVersion)}`; + } + return `${fileNameBase}${path}`; + } + } + + function fileNameToUri(fileName: string) { + if (fileName === fileNameBase) { + return jsDelivrUriBase; + } + if (fileName.startsWith(fileNameBase + '/')) { + const path = fileName.substring(fileNameBase.length); + const pkgName = getPackageName(path); + if (pkgName) { + const version = versions[pkgName] ?? 'latest'; + return `${jsDelivrUriBase}/${pkgName}@${version}${path.substring(1 + pkgName.length)}`; + } + return `${jsDelivrUriBase}${path}`; + } + } +} + +export function createJsDelivrFs(onReadFile?: (uri: string, content: string) => void): FileSystem { + + const fetchResults = new Map>(); + const flatResults = new Map>(); + + return { + stat, + readDirectory, + readFile, + }; + + async function stat(uri: string): Promise { + + if (uri === jsDelivrUriBase) { + return { + type: 2 satisfies FileType.Directory, + size: -1, + ctime: -1, + mtime: -1, + }; + } + + if (uri.startsWith(jsDelivrUriBase + '/')) { + + const path = uri.substring(jsDelivrUriBase.length); + const pkgName = getPackageName(path); + if (!pkgName || !await isValidPackageName(pkgName)) { + return; + } + + if (!flatResults.has(pkgName)) { + flatResults.set(pkgName, flat(pkgName)); + } + + const flatResult = await flatResults.get(pkgName)!; + const filePath = path.slice(`/${pkgName}`.length); + const file = flatResult.find(file => file.name === filePath); + if (file) { + return { + type: 1 satisfies FileType.File, + ctime: new Date(file.time).valueOf(), + mtime: new Date(file.time).valueOf(), + size: file.size, + }; + } + else if (flatResult.some(file => file.name.startsWith(filePath + '/'))) { + return { + type: 2 satisfies FileType.Directory, + ctime: -1, + mtime: -1, + size: -1, + }; + } + } + } + + async function readDirectory(uri: string): Promise<[string, FileType][]> { + + if (uri.startsWith(jsDelivrUriBase + '/')) { + + const path = uri.substring(jsDelivrUriBase.length); + const pkgName = getPackageName(path); + if (!pkgName || !await isValidPackageName(pkgName)) { + return []; + } + + if (!flatResults.has(pkgName)) { + flatResults.set(pkgName, flat(pkgName)); + } + + const flatResult = await flatResults.get(pkgName)!; + const dirPath = path.slice(`/${pkgName}`.length); + const files = flatResult + .filter(f => f.name.substring(0, f.name.lastIndexOf('/')) === dirPath) + .map(f => f.name.slice(dirPath.length + 1)); + const dirs = flatResult + .filter(f => f.name.startsWith(dirPath + '/') && f.name.substring(dirPath.length + 1).split('/').length >= 2) + .map(f => f.name.slice(dirPath.length + 1).split('/')[0]); + + return [ + ...files.map<[string, FileType]>(f => [f, 1 satisfies FileType.File]), + ...[...new Set(dirs)].map<[string, FileType]>(f => [f, 2 satisfies FileType.Directory]), + ]; + } + + return []; + } + + async function readFile(uri: string): Promise { + + if (uri.startsWith(jsDelivrUriBase + '/')) { + + const path = uri.substring(jsDelivrUriBase.length); + const pkgName = getPackageName(path); + if (!pkgName || !await isValidPackageName(pkgName)) { + return; + } + + if (!fetchResults.has(path)) { + fetchResults.set(path, (async () => { + if ((await stat(uri))?.type !== 1 satisfies FileType.File) { + return; + } + const text = await fetchText(uri); + if (text !== undefined) { + onReadFile?.(uri, text); + } + return text; + })()); + } + + return await fetchResults.get(path)!; + } + } + + async function flat(pkgNameWithVersion: string) { + + let pkgName = pkgNameWithVersion; + let version = 'latest'; + + if (pkgNameWithVersion.substring(1).includes('@')) { + pkgName = pkgNameWithVersion.substring(0, pkgNameWithVersion.lastIndexOf('@')); + version = pkgNameWithVersion.substring(pkgNameWithVersion.lastIndexOf('@') + 1); + } + + // resolve tag version + if (version === 'latest') { + const data = await fetchJson<{ version: string | null; }>(`https://data.jsdelivr.com/v1/package/resolve/npm/${pkgName}@latest`); + if (!data?.version) { + return []; + } + version = data.version; + } + + const flat = await fetchJson<{ + files: { + name: string; + size: number; + time: string; + hash: string; + }[]; + }>(`https://data.jsdelivr.com/v1/package/npm/${pkgName}@${version}/flat`); + if (!flat) { + return []; + } + + return flat.files; + } + + async function isValidPackageName(pkgName: string) { + if (pkgName.substring(1).includes('@')) { + pkgName = pkgName.substring(0, pkgName.lastIndexOf('@')); + } + if (pkgName.indexOf('.') >= 0 || pkgName.endsWith('/node_modules')) { + return false; + } + // hard code for known invalid package + if (pkgName.startsWith('@typescript/') || pkgName.startsWith('@types/typescript__')) { + return false; + } + // don't check @types if original package already having types + if (pkgName.startsWith('@types/')) { + let originalPkgName = pkgName.slice('@types/'.length); + if (originalPkgName.indexOf('__') >= 0) { + originalPkgName = '@' + originalPkgName.replace('__', '/'); + } + const packageJson = await readFile(`${jsDelivrUriBase}/${originalPkgName}/package.json`); + if (packageJson) { + const packageJsonObj = JSON.parse(packageJson); + if (packageJsonObj.types || packageJsonObj.typings) { + return false; + } + const indexDts = await stat(`${jsDelivrUriBase}/${originalPkgName}/index.d.ts`); + if (indexDts?.type === 1 satisfies FileType.File) { + return false; + } + } + } + return true; + } +} + +/** + * @example + * "/a/b/c" -> "a" + * "/@a/b/c" -> "@a/b" + * "/@a/b@1.2.3/c" -> "@a/b@1.2.3" + */ +export function getPackageName(path: string) { + const parts = path.split('/'); + let pkgName = parts[1]; + if (pkgName.startsWith('@')) { + if (parts.length < 3 || !parts[2]) { + return undefined; + } + pkgName += '/' + parts[2]; + } + return pkgName; +} diff --git a/packages/cdn/src/index.ts b/packages/cdn/src/index.ts new file mode 100644 index 00000000..b3aaa35d --- /dev/null +++ b/packages/cdn/src/index.ts @@ -0,0 +1,42 @@ +import type { FileSystem, ServiceEnvironment } from '@volar/language-service'; +import type { UriResolver } from './types'; + +export * from './types'; +export * from './cdns/jsdelivr'; +export * from './cdns/github'; + +export function decorateServiceEnvironment( + env: ServiceEnvironment, + uriResolver: UriResolver, + fs: FileSystem +) { + const _fileNameToUri = env.fileNameToUri; + const _uriToFileName = env.uriToFileName; + const _fs = env.fs; + env.fileNameToUri = fileName => { + return uriResolver.fileNameToUri(fileName) ?? _fileNameToUri(fileName); + }; + env.uriToFileName = fileName => { + return uriResolver.uriToFileName(fileName) ?? _uriToFileName(fileName); + }; + env.fs = { + stat(uri) { + if (uriResolver.uriToFileName(uri)) { + return fs.stat(uri); + } + return _fs?.stat(uri); + }, + readDirectory(uri) { + if (uriResolver.uriToFileName(uri)) { + return fs.readDirectory(uri); + } + return _fs?.readDirectory(uri) ?? []; + }, + readFile(uri) { + if (uriResolver.uriToFileName(uri)) { + return fs.readFile(uri); + } + return _fs?.readFile(uri); + }, + }; +} diff --git a/packages/cdn/src/types.ts b/packages/cdn/src/types.ts new file mode 100644 index 00000000..58da39aa --- /dev/null +++ b/packages/cdn/src/types.ts @@ -0,0 +1,4 @@ +export interface UriResolver { + uriToFileName(uri: string): string | undefined; + fileNameToUri(fileName: string): string | undefined; +} diff --git a/packages/cdn/src/utils.ts b/packages/cdn/src/utils.ts new file mode 100644 index 00000000..2f9c8fa0 --- /dev/null +++ b/packages/cdn/src/utils.ts @@ -0,0 +1,34 @@ +const textCache = new Map>(); +const jsonCache = new Map>(); + +export async function fetchText(url: string) { + if (!textCache.has(url)) { + textCache.set(url, (async () => { + try { + const res = await fetch(url); + if (res.status === 200) { + return await res.text(); + } + } catch { + // ignore + } + })()); + } + return await textCache.get(url)!; +} + +export async function fetchJson(url: string) { + if (!jsonCache.has(url)) { + jsonCache.set(url, (async () => { + try { + const res = await fetch(url); + if (res.status === 200) { + return await res.json(); + } + } catch { + // ignore + } + })()); + } + return await jsonCache.get(url)! as T; +} diff --git a/packages/cdn/tsconfig.build.json b/packages/cdn/tsconfig.build.json new file mode 100644 index 00000000..073d3906 --- /dev/null +++ b/packages/cdn/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../language-service/tsconfig.build.json" + }, + ], +} \ No newline at end of file diff --git a/packages/kit/src/createFormatter.ts b/packages/kit/src/createFormatter.ts index f491ae5f..84884fce 100644 --- a/packages/kit/src/createFormatter.ts +++ b/packages/kit/src/createFormatter.ts @@ -78,7 +78,7 @@ export function createFormatter( const host: TypeScriptLanguageHost = { getCurrentDirectory: () => '/', getCompilationSettings: () => compilerOptions, - getProjectVersion: () => projectVersion++, + getProjectVersion: () => (projectVersion++).toString(), getScriptFileNames: () => fakeScriptSnapshot ? [fakeScriptFileName] : [], getScriptSnapshot: (fileName) => { if (fileName === fakeScriptFileName) { diff --git a/packages/kit/src/createProject.ts b/packages/kit/src/createProject.ts index 0b3aab5c..83612a86 100644 --- a/packages/kit/src/createProject.ts +++ b/packages/kit/src/createProject.ts @@ -54,7 +54,7 @@ function createProjectBase(rootPath: string, createParsedCommandLine: () => Pick }, getProjectVersion: () => { checkRootFilesUpdate(); - return projectVersion; + return projectVersion.toString(); }, getScriptFileNames: () => { checkRootFilesUpdate(); diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index 08fd7aa7..ce1c98a8 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -93,7 +93,7 @@ export interface Language { } interface LanguageHost { - getProjectVersion(): number | string; + getProjectVersion(): string; getScriptFileNames(): string[]; getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined; getLanguageId?(fileName: string): string | undefined; diff --git a/packages/language-server/src/common/project.ts b/packages/language-server/src/common/project.ts index 4738af8f..a512d922 100644 --- a/packages/language-server/src/common/project.ts +++ b/packages/language-server/src/common/project.ts @@ -45,7 +45,7 @@ export async function createProject(context: ProjectContext) { const fsScriptsCache = createUriMap(fileNameToUri); const askedFiles = createUriMap(fileNameToUri); const languageHost: TypeScriptLanguageHost = { - getProjectVersion: () => projectVersion, + getProjectVersion: () => projectVersion.toString(), getScriptFileNames: () => parsedCommandLine.fileNames, getScriptSnapshot: (fileName) => { askedFiles.pathSet(fileName, true); diff --git a/packages/monaco/src/worker.ts b/packages/monaco/src/worker.ts index 663ece47..b8595124 100644 --- a/packages/monaco/src/worker.ts +++ b/packages/monaco/src/worker.ts @@ -2,30 +2,90 @@ import { createLanguageService as _createLanguageService, type TypeScriptLanguageHost, type Config, + type ServiceEnvironment, + type SharedModules, + type LanguageService, } from '@volar/language-service'; import type * as monaco from 'monaco-editor-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { URI } from 'vscode-uri'; -export function createLanguageService(options: { - workerContext: monaco.worker.IWorkerContext, +export function createServiceEnvironment(): ServiceEnvironment { + return { + uriToFileName: uri => URI.parse(uri).fsPath.replace(/\\/g, '/'), + fileNameToUri: fileName => URI.file(fileName).toString(), + rootUri: URI.file('/'), + }; +} + +export function createLanguageHost( + getMirrorModels: monaco.worker.IWorkerContext['getMirrorModels'], + env: ServiceEnvironment, + rootPath: string, + compilerOptions: ts.CompilerOptions = {} +): TypeScriptLanguageHost { + + let projectVersion = 0; + + const modelSnapshot = new WeakMap(); + const modelVersions = new Map(); + const host: TypeScriptLanguageHost = { + getProjectVersion() { + const models = getMirrorModels(); + if (modelVersions.size === getMirrorModels().length) { + if (models.every(model => modelVersions.get(model) === model.version)) { + return projectVersion.toString(); + } + } + modelVersions.clear(); + for (const model of getMirrorModels()) { + modelVersions.set(model, model.version); + } + projectVersion++; + return projectVersion.toString(); + }, + getScriptFileNames() { + const models = getMirrorModels(); + return models.map(model => env.uriToFileName(model.uri.toString(true))); + }, + getScriptSnapshot(fileName) { + const uri = env.fileNameToUri(fileName); + const model = getMirrorModels().find(model => model.uri.toString(true) === uri); + if (model) { + const cache = modelSnapshot.get(model); + if (cache && cache[0] === model.version) { + return cache[1]; + } + const text = model.getValue(); + modelSnapshot.set(model, [model.version, { + getText: (start, end) => text.substring(start, end), + getLength: () => text.length, + getChangeRange: () => undefined, + }]); + return modelSnapshot.get(model)?.[1]; + } + }, + getCompilationSettings() { + return compilerOptions; + }, + getCurrentDirectory() { + return rootPath; + }, + }; + + return host; +} + +export function createLanguageService( + modules: SharedModules, + env: ServiceEnvironment, config: Config, - typescript?: { - module: typeof import('typescript/lib/tsserverlibrary'), - compilerOptions: ts.CompilerOptions, - }, -}) { + host: TypeScriptLanguageHost, +) { - const ts = options.typescript?.module; - const config = options.config ?? {}; - const host = createLanguageServiceHost(); const languageService = _createLanguageService( - { typescript: ts }, - { - uriToFileName: uri => URI.parse(uri).fsPath.replace(/\\/g, '/'), - fileNameToUri: fileName => URI.file(fileName).toString(), - rootUri: URI.file('/'), - }, + modules, + env, config, host, ); @@ -39,57 +99,5 @@ export function createLanguageService(options: { } } - return new InnocentRabbit(); - - function createLanguageServiceHost() { - - let projectVersion = 0; - - const modelSnapshot = new WeakMap(); - const modelVersions = new Map(); - const host: TypeScriptLanguageHost = { - getProjectVersion() { - const models = options.workerContext.getMirrorModels(); - if (modelVersions.size === options.workerContext.getMirrorModels().length) { - if (models.every(model => modelVersions.get(model) === model.version)) { - return projectVersion; - } - } - modelVersions.clear(); - for (const model of options.workerContext.getMirrorModels()) { - modelVersions.set(model, model.version); - } - projectVersion++; - return projectVersion; - }, - getScriptFileNames() { - const models = options.workerContext.getMirrorModels(); - return models.map(model => model.uri.fsPath); - }, - getScriptSnapshot(fileName) { - const model = options.workerContext.getMirrorModels().find(model => model.uri.fsPath === fileName); - if (model) { - const cache = modelSnapshot.get(model); - if (cache && cache[0] === model.version) { - return cache[1]; - } - const text = model.getValue(); - modelSnapshot.set(model, [model.version, { - getText: (start, end) => text.substring(start, end), - getLength: () => text.length, - getChangeRange: () => undefined, - }]); - return modelSnapshot.get(model)?.[1]; - } - }, - getCompilationSettings() { - return options.typescript?.compilerOptions ?? {}; - }, - getCurrentDirectory() { - return '/'; - }, - }; - - return host; - } + return new InnocentRabbit() as LanguageService; } diff --git a/packages/typescript/src/dtsHost.ts b/packages/typescript/src/dtsHost.ts deleted file mode 100644 index add56368..00000000 --- a/packages/typescript/src/dtsHost.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { FileStat, FileType } from '@volar/language-service'; - -export interface IDtsHost { - stat(uri: string): Promise; - readFile(fileName: string): Promise; - readDirectory(dirName: string): Promise<[string, FileType][]>; -} - -export function createJsDelivrDtsHost( - versions: Record = {}, - onFetch?: (fileName: string, text: string) => void, -): IDtsHost { - return new DtsHost( - async fileName => { - const requestFileName = resolveRequestFileName(fileName); - const url = 'https://cdn.jsdelivr.net/npm/' + requestFileName.slice('/node_modules/'.length); - const text = await fetchText(url); - if (text !== undefined) { - onFetch?.(fileName, text); - } - return text; - }, - async (pkg) => { - - let version = versions[pkg]; - if (!version) { - const data = await fetchJson<{ version: string | null; }>(`https://data.jsdelivr.com/v1/package/resolve/npm/${pkg}@latest`); - if (data?.version) { - version = data.version; - } - } - if (!version) { - return []; - } - - const flat = await fetchJson<{ files: { name: string; }[]; }>(`https://data.jsdelivr.com/v1/package/npm/${pkg}@${version}/flat`); - if (!flat) { - return []; - } - - return flat.files.map(file => file.name); - }, - ); - - function resolveRequestFileName(fileName: string) { - for (const [key, version] of Object.entries(versions)) { - if (fileName.startsWith(`/node_modules/${key}/`)) { - fileName = fileName.replace(`/node_modules/${key}/`, `/node_modules/${key}@${version}/`); - return fileName; - } - } - return fileName; - } -} - -class DtsHost implements IDtsHost { - - fetchResults = new Map>(); - flatResults = new Map>(); - - constructor( - private fetchText: (path: string) => Promise, - private flat: (pkg: string) => Promise, - ) { } - - async stat(fileName: string) { - - if (!await this.valid(fileName)) { - return; - } - - const pkgName = getPackageNameOfDtsPath(fileName); - if (!pkgName) { - return; - } - - if (!this.flatResults.has(pkgName)) { - this.flatResults.set(pkgName, this.flat(pkgName)); - } - - const flat = await this.flatResults.get(pkgName)!; - const filePath = fileName.slice(`/node_modules/${pkgName}`.length); - if (flat.includes(filePath)) { - return { - type: 1 satisfies FileType.File, - ctime: -1, - mtime: -1, - size: -1, - }; - } - else if (flat.some(f => f.startsWith(filePath + '/'))) { - return { - type: 2 satisfies FileType.Directory, - ctime: -1, - mtime: -1, - size: -1, - }; - } - } - - async readDirectory(dirName: string) { - - if (!await this.valid(dirName)) { - return []; - } - - const pkgName = getPackageNameOfDtsPath(dirName); - if (!pkgName) { - return []; - } - - if (!this.flatResults.has(pkgName)) { - this.flatResults.set(pkgName, this.flat(pkgName)); - } - - const flat = await this.flatResults.get(pkgName)!; - const dirPath = dirName.slice(`/node_modules/${pkgName}`.length); - const files = flat - .filter(f => f.substring(0, f.lastIndexOf('/')) === dirPath) - .map(f => f.slice(dirPath.length + 1)); - const dirs = flat - .filter(f => f.startsWith(dirPath + '/') && f.substring(dirPath.length + 1).split('/').length >= 2) - .map(f => f.slice(dirPath.length + 1).split('/')[0]); - - return [ - ...files.map<[string, FileType]>(f => [f, 1 satisfies FileType.File]), - ...[...new Set(dirs)].map<[string, FileType]>(f => [f, 2 satisfies FileType.Directory]), - ]; - } - - async readFile(fileName: string) { - - if (!await this.valid(fileName)) { - return; - } - - if (!this.fetchResults.has(fileName)) { - this.fetchResults.set(fileName, this.fetchFile(fileName)); - } - return await this.fetchResults.get(fileName); - } - - async fetchFile(fileName: string) { - - const pkgName = getPackageNameOfDtsPath(fileName); - if (!pkgName) { - return undefined; - } - - if ((await this.stat(fileName))?.type !== 1 satisfies FileType.File) { - return undefined; - } - - return await this.fetchText(fileName); - } - - async valid(fileName: string) { - const pkgName = getPackageNameOfDtsPath(fileName); - if (!pkgName) { - return false; - } - if (pkgName.indexOf('.') >= 0 || pkgName.endsWith('/node_modules')) { - return false; - } - // hard code for known invalid package - if (pkgName.startsWith('@typescript/') || pkgName.startsWith('@types/typescript__')) { - return false; - } - // don't check @types if original package already having types - if (pkgName.startsWith('@types/')) { - let originalPkgName = pkgName.slice('@types/'.length); - if (originalPkgName.indexOf('__') >= 0) { - originalPkgName = '@' + originalPkgName.replace('__', '/'); - } - const packageJson = await this.readFile(`/node_modules/${originalPkgName}/package.json`); - if (packageJson) { - const packageJsonObj = JSON.parse(packageJson); - if (packageJsonObj.types || packageJsonObj.typings) { - return false; - } - const indexDts = await this.stat(`/node_modules/${originalPkgName}/index.d.ts`); - if (indexDts?.type === 1 satisfies FileType.File) { - return false; - } - } - } - return true; - } -} - -async function fetchText(url: string) { - try { - const res = await fetch(url); - if (res.status === 200) { - return await res.text(); - } - } catch { - // ignore - } -} - -async function fetchJson(url: string) { - try { - const res = await fetch(url); - if (res.status === 200) { - return await res.json() as T; - } - } catch { - // ignore - } -} - -export function getPackageNameOfDtsPath(path: string) { - if (!path.startsWith('/node_modules/')) { - return undefined; - } - let pkgName = path.split('/')[2]; - if (pkgName.startsWith('@')) { - if (path.split('/').length < 4) { - return undefined; - } - pkgName += '/' + path.split('/')[3]; - } - return pkgName; -} diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index 45d71475..495d50dd 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -1,5 +1,4 @@ export * from './documentRegistry'; -export * from './dtsHost'; export * from './languageService'; export * from './languageServiceHost'; export * from './sys'; diff --git a/packages/typescript/src/sys.ts b/packages/typescript/src/sys.ts index 5a3e57fe..30e9745e 100644 --- a/packages/typescript/src/sys.ts +++ b/packages/typescript/src/sys.ts @@ -2,7 +2,6 @@ import type { FileChangeType, FileType, ServiceEnvironment, Disposable } from '@ import type * as ts from 'typescript/lib/tsserverlibrary'; import { posix as path } from 'path'; import { matchFiles } from './typescript/utilities'; -import { IDtsHost, getPackageNameOfDtsPath } from './dtsHost'; interface File { text?: string; @@ -23,7 +22,6 @@ let currentCwd = ''; export function createSys( ts: typeof import('typescript/lib/tsserverlibrary'), env: ServiceEnvironment, - dtsHost?: IDtsHost, ): ts.System & { version: number; sync(): Promise; @@ -138,17 +136,9 @@ export function createSys( dirName = resolvePath(dirName); const dir = getDir(dirName); - if (dirName === '/node_modules' && dtsHost) { - dir.exists = true; - } - else if (dirName.startsWith('/node_modules/') && dtsHost && !getPackageNameOfDtsPath(dirName)) { - dir.exists = true; - } - else if (dir.exists === undefined) { + if (dir.exists === undefined) { dir.exists = false; - const result = dirName.startsWith('/node_modules/') && dtsHost - ? dtsHost.stat(dirName) - : env.fs?.stat(env.fileNameToUri(dirName)); + const result = env.fs?.stat(env.fileNameToUri(dirName)); if (typeof result === 'object' && 'then' in result) { const promise = result; promises.add(promise); @@ -177,9 +167,7 @@ export function createSys( const file = dir.files[baseName] ??= {}; if (file.exists === undefined) { file.exists = false; - const result = fileName.startsWith('/node_modules/') && dtsHost - ? dtsHost.stat(fileName) - : env.fs?.stat(env.fileNameToUri(fileName)); + const result = env.fs?.stat(env.fileNameToUri(fileName)); if (typeof result === 'object' && 'then' in result) { const promise = result; promises.add(promise); @@ -252,9 +240,7 @@ export function createSys( file.requested = true; const uri = env.fileNameToUri(fileName); - const result = fileName.startsWith('/node_modules/') && dtsHost - ? dtsHost.readFile(fileName) - : env.fs?.readFile(uri, encoding); + const result = env.fs?.readFile(uri, encoding); if (typeof result === 'object' && 'then' in result) { const promise = result; @@ -292,9 +278,7 @@ export function createSys( } dir.requested = true; - const result = dirName.startsWith('/node_modules/') && dtsHost - ? dtsHost.readDirectory(dirName) - : env.fs?.readDirectory(env.fileNameToUri(dirName || '.')); + const result = env.fs?.readDirectory(env.fileNameToUri(dirName || '.')); if (typeof result === 'object' && 'then' in result) { const promise = result; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8300412f..72fc78fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,17 +17,23 @@ importers: devDependencies: '@types/node': specifier: latest - version: 20.3.2 + version: 20.3.3 typescript: specifier: latest - version: 5.1.5 + version: 5.1.6 vite: specifier: latest - version: 4.3.9(@types/node@20.3.2) + version: 4.3.9(@types/node@20.3.3) vitest: specifier: latest version: 0.32.2 + packages/cdn: + dependencies: + '@volar/language-service': + specifier: 1.7.10 + version: link:../language-service + packages/kit: dependencies: '@volar/language-service': @@ -1024,8 +1030,8 @@ packages: dev: false optional: true - /@types/node@20.3.2: - resolution: {integrity: sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==} + /@types/node@20.3.3: + resolution: {integrity: sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==} dev: true /@types/normalize-package-data@2.4.1: @@ -5132,8 +5138,8 @@ packages: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} dev: false - /typescript@5.1.5: - resolution: {integrity: sha512-FOH+WN/DQjUvN6WgW+c4Ml3yi0PH+a/8q+kNIfRehv1wLhWONedw85iu+vQ39Wp49IzTJEsZ2lyLXpBF7mkF1g==} + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -5230,7 +5236,7 @@ packages: dev: false optional: true - /vite-node@0.32.2(@types/node@20.3.2): + /vite-node@0.32.2(@types/node@20.3.3): resolution: {integrity: sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -5240,7 +5246,7 @@ packages: mlly: 1.3.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.3.9(@types/node@20.3.2) + vite: 4.3.9(@types/node@20.3.3) transitivePeerDependencies: - '@types/node' - less @@ -5251,7 +5257,7 @@ packages: - terser dev: true - /vite@4.3.9(@types/node@20.3.2): + /vite@4.3.9(@types/node@20.3.3): resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -5276,7 +5282,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.3.2 + '@types/node': 20.3.3 esbuild: 0.17.19 postcss: 8.4.24 rollup: 3.23.0 @@ -5317,7 +5323,7 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.3.2 + '@types/node': 20.3.3 '@vitest/expect': 0.32.2 '@vitest/runner': 0.32.2 '@vitest/snapshot': 0.32.2 @@ -5337,8 +5343,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.5.0 - vite: 4.3.9(@types/node@20.3.2) - vite-node: 0.32.2(@types/node@20.3.2) + vite: 4.3.9(@types/node@20.3.3) + vite-node: 0.32.2(@types/node@20.3.3) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/tsconfig.build.json b/tsconfig.build.json index da693821..a5b7bd2b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,6 +3,9 @@ "include": [], "references": [ // Pkgs + { + "path": "./packages/cdn/tsconfig.build.json" + }, { "path": "./packages/kit/tsconfig.build.json" },