From b3ca53962f9149e6f65b6f3e9d9a2b922eeacbad Mon Sep 17 00:00:00 2001 From: wangcch Date: Sat, 1 Mar 2025 14:04:20 +0800 Subject: [PATCH 1/6] refactor(deps): @volar/jsdelivr -> local # Conflicts: # package.json # pnpm-lock.yaml # src/monaco/vue.worker.ts --- package.json | 1 + pnpm-lock.yaml | 22 ++- src/monaco/resource.ts | 318 +++++++++++++++++++++++++++++++++++++++ src/monaco/vue.worker.ts | 2 +- 4 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 src/monaco/resource.ts diff --git a/package.json b/package.json index aac92c71..a8cc8ada 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/node": "^24.2.0", "@vitejs/plugin-vue": "^6.0.1", "@volar/jsdelivr": "2.4.23", + "@volar/language-service": "~2.4.11", "@volar/monaco": "2.4.23", "@volar/typescript": "2.4.23", "@vue/babel-plugin-jsx": "^1.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 299b5148..228a2c97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@volar/jsdelivr': specifier: 2.4.23 version: 2.4.23 + '@volar/language-service': + specifier: ~2.4.11 + version: 2.4.23 '@volar/monaco': specifier: 2.4.23 version: 2.4.23 @@ -240,8 +243,8 @@ packages: '@emmetio/css-abbreviation@2.1.8': resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} - '@emmetio/css-parser@https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660': - resolution: {tarball: https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660} + '@emmetio/css-parser@git+https://git@github.com:ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660': + resolution: {commit: 370c480ac103bd17c7bcfb34bf5d577dc40d3660, repo: git@github.com:ramya-rao-a/css-parser.git, type: git} version: 0.4.0 '@emmetio/html-matcher@1.3.0': @@ -586,56 +589,67 @@ packages: resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.46.2': resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.46.2': resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.46.2': resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.46.2': resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.46.2': resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.46.2': resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.46.2': resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.46.2': resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.46.2': resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.46.2': resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.46.2': resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} @@ -2645,7 +2659,7 @@ snapshots: dependencies: '@emmetio/scanner': 1.0.4 - '@emmetio/css-parser@https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660': + '@emmetio/css-parser@git+https://git@github.com:ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660': dependencies: '@emmetio/stream-reader': 2.2.0 '@emmetio/stream-reader-utils': 0.1.0 @@ -4898,7 +4912,7 @@ snapshots: volar-service-emmet@0.0.65(@volar/language-service@2.4.23): dependencies: - '@emmetio/css-parser': https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660 + '@emmetio/css-parser': git+https://git@github.com:ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660 '@emmetio/html-matcher': 1.3.0 '@vscode/emmet-helper': 2.11.0 vscode-uri: 3.1.0 diff --git a/src/monaco/resource.ts b/src/monaco/resource.ts new file mode 100644 index 00000000..f8c64a69 --- /dev/null +++ b/src/monaco/resource.ts @@ -0,0 +1,318 @@ +/** + * base on @volar/jsdelivr + * MIT License https://github.com/volarjs/volar.js/blob/master/packages/jsdelivr/LICENSE + */ +import type { FileSystem, FileType } from '@volar/language-service' +import type { URI } from 'vscode-uri' + +const textCache = new Map>() +const jsonCache = new Map>() + +export function createNpmFileSystem( + getCdnPath = (uri: URI): string | undefined => { + if (uri.path === '/node_modules') { + return '' + } else if (uri.path.startsWith('/node_modules/')) { + return uri.path.slice('/node_modules/'.length) + } + }, + getPackageVersion?: (pkgName: string) => string | undefined, + onFetch?: (path: string, content: string) => void, +): FileSystem { + const fetchResults = new Map>() + const flatResults = new Map< + string, + Promise< + { + name: string + size: number + time: string + hash: string + }[] + > + >() + + return { + async stat(uri) { + const path = getCdnPath(uri) + if (path === undefined) { + return + } + if (path === '') { + return { + type: 2 satisfies FileType.Directory, + size: -1, + ctime: -1, + mtime: -1, + } + } + return await _stat(path) + }, + async readFile(uri) { + const path = getCdnPath(uri) + if (path === undefined) { + return + } + return await _readFile(path) + }, + readDirectory(uri) { + const path = getCdnPath(uri) + if (path === undefined) { + return [] + } + return _readDirectory(path) + }, + } + + async function _stat(path: string) { + const [modName, pkgName, pkgVersion, pkgFilePath] = resolvePackageName(path) + if (!pkgName) { + if (modName.startsWith('@')) { + return { + type: 2 satisfies FileType.Directory, + ctime: -1, + mtime: -1, + size: -1, + } + } else { + return + } + } + if (!(await isValidPackageName(pkgName))) { + return + } + + if (!pkgFilePath) { + // perf: skip flat request + return { + type: 2 satisfies FileType.Directory, + ctime: -1, + mtime: -1, + size: -1, + } + } + + if (!flatResults.has(modName)) { + flatResults.set(modName, flat(pkgName, pkgVersion)) + } + + const flatResult = await flatResults.get(modName)! + const filePath = path.slice(modName.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(path: string): Promise<[string, FileType][]> { + const [modName, pkgName, pkgVersion] = resolvePackageName(path) + if (!pkgName || !(await isValidPackageName(pkgName))) { + return [] + } + + if (!flatResults.has(modName)) { + flatResults.set(modName, flat(pkgName, pkgVersion)) + } + + const flatResult = await flatResults.get(modName)! + const dirPath = path.slice(modName.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, + ]), + ] + } + + async function _readFile(path: string): Promise { + const [_modName, pkgName, _version, pkgFilePath] = resolvePackageName(path) + if (!pkgName || !pkgFilePath || !(await isValidPackageName(pkgName))) { + return + } + + if (!fetchResults.has(path)) { + fetchResults.set( + path, + (async () => { + if ((await _stat(path))?.type !== (1 satisfies FileType.File)) { + return + } + const text = await fetchText(`https://cdn.jsdelivr.net/npm/${path}`) + if (text !== undefined) { + onFetch?.(path, text) + } + return text + })(), + ) + } + + return await fetchResults.get(path)! + } + + async function flat(pkgName: string, version: string | undefined) { + version ??= 'latest' + + // resolve latest tag + if (version === 'latest') { + const data = await fetchJson<{ version: string | null }>( + `https://data.jsdelivr.com/v1/package/resolve/npm/${pkgName}@${version}`, + ) + 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) { + // ignore @aaa/node_modules + if (pkgName.endsWith('/node_modules')) { + return false + } + // hard code to skip known invalid package + if ( + pkgName.endsWith('.d.ts') || + 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(`${originalPkgName}/package.json`) + if (!packageJson) { + return false + } + const packageJsonObj = JSON.parse(packageJson) + if (packageJsonObj.types || packageJsonObj.typings) { + return false + } + const indexDts = await _stat(`${originalPkgName}/index.d.ts`) + if (indexDts?.type === (1 satisfies FileType.File)) { + return false + } + } + return true + } + + /** + * @example + * "a/b/c" -> ["a", "a", undefined, "b/c"] + * "@a" -> ["@a", undefined, undefined, ""] + * "@a/b/c" -> ["@a/b", "@a/b", undefined, "c"] + * "@a/b@1.2.3/c" -> ["@a/b@1.2.3", "@a/b", "1.2.3", "c"] + */ + function resolvePackageName( + input: string, + ): [ + modName: string, + pkgName: string | undefined, + version: string | undefined, + path: string, + ] { + const parts = input.split('/') + let modName = parts[0] + let path: string + if (modName.startsWith('@')) { + if (!parts[1]) { + return [modName, undefined, undefined, ''] + } + modName += '/' + parts[1] + path = parts.slice(2).join('/') + } else { + path = parts.slice(1).join('/') + } + let pkgName = modName + let version: string | undefined + if (modName.lastIndexOf('@') >= 1) { + pkgName = modName.substring(0, modName.lastIndexOf('@')) + version = modName.substring(modName.lastIndexOf('@') + 1) + } + if (!version && getPackageVersion) { + getPackageVersion?.(pkgName) + } + return [modName, pkgName, version, path] + } +} + +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)! +} + +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/src/monaco/vue.worker.ts b/src/monaco/vue.worker.ts index 46024a2d..ea3c1dfb 100644 --- a/src/monaco/vue.worker.ts +++ b/src/monaco/vue.worker.ts @@ -1,9 +1,9 @@ -import { createNpmFileSystem } from '@volar/jsdelivr' import { type LanguageServiceEnvironment, createTypeScriptWorkerLanguageService, Language, } from '@volar/monaco/worker' +import { createNpmFileSystem } from './resource' import { type VueCompilerOptions, VueVirtualCode, From e584cb3df3223a06db948d3b58c0c8ea1f3804d2 Mon Sep 17 00:00:00 2001 From: wangcch Date: Tue, 4 Mar 2025 01:04:51 +0800 Subject: [PATCH 2/6] feat(monaco): replace with unpkg and load on demand (deps) --- src/monaco/resource.ts | 198 ++++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 94 deletions(-) diff --git a/src/monaco/resource.ts b/src/monaco/resource.ts index f8c64a69..6d101775 100644 --- a/src/monaco/resource.ts +++ b/src/monaco/resource.ts @@ -2,7 +2,7 @@ * base on @volar/jsdelivr * MIT License https://github.com/volarjs/volar.js/blob/master/packages/jsdelivr/LICENSE */ -import type { FileSystem, FileType } from '@volar/language-service' +import type { FileStat, FileSystem, FileType } from '@volar/language-service' import type { URI } from 'vscode-uri' const textCache = new Map>() @@ -20,17 +20,8 @@ export function createNpmFileSystem( onFetch?: (path: string, content: string) => void, ): FileSystem { const fetchResults = new Map>() - const flatResults = new Map< - string, - Promise< - { - name: string - size: number - time: string - hash: string - }[] - > - >() + const statCache = new Map() + const dirCache = new Map() return { async stat(uri) { @@ -65,7 +56,16 @@ export function createNpmFileSystem( } async function _stat(path: string) { - const [modName, pkgName, pkgVersion, pkgFilePath] = resolvePackageName(path) + if (statCache.has(path)) { + return { + ...statCache.get(path), + ctime: -1, + mtime: -1, + size: -1, + } as FileStat + } + + const [modName, pkgName, , pkgFilePath] = resolvePackageName(path) if (!pkgName) { if (modName.startsWith('@')) { return { @@ -82,72 +82,111 @@ export function createNpmFileSystem( return } - if (!pkgFilePath) { - // perf: skip flat request - return { - type: 2 satisfies FileType.Directory, - ctime: -1, - mtime: -1, - size: -1, + if (!pkgFilePath || pkgFilePath === '/') { + const result = { + type: 2 as FileType.Directory, } + statCache.set(path, result) + return { ...result, ctime: -1, mtime: -1, size: -1 } } - if (!flatResults.has(modName)) { - flatResults.set(modName, flat(pkgName, pkgVersion)) - } + try { + const parentDir = path.substring(0, path.lastIndexOf('/')) + const fileName = path.substring(path.lastIndexOf('/') + 1) - const flatResult = await flatResults.get(modName)! - const filePath = path.slice(modName.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, + const dirContent = await _readDirectory(parentDir) + const fileEntry = dirContent.find(([name]) => name === fileName) + + if (fileEntry) { + const result = { + type: fileEntry[1] as FileType, + } + statCache.set(path, result) + return { ...result, ctime: -1, mtime: -1, size: -1 } } + + return + } catch { + return } } async function _readDirectory(path: string): Promise<[string, FileType][]> { - const [modName, pkgName, pkgVersion] = resolvePackageName(path) + if (dirCache.has(path)) { + return dirCache.get(path)! + } + + const [, pkgName, pkgVersion, pkgPath] = resolvePackageName(path) + if (!pkgName || !(await isValidPackageName(pkgName))) { return [] } - if (!flatResults.has(modName)) { - flatResults.set(modName, flat(pkgName, pkgVersion)) + const resolvedVersion = pkgVersion || 'latest' + + let actualVersion = resolvedVersion + if (resolvedVersion === 'latest') { + try { + const data = await fetchJson<{ version: string }>( + `https://unpkg.com/${pkgName}@${resolvedVersion}/package.json`, + ) + if (data?.version) { + actualVersion = data.version + } + } catch { + // ignore + } } - const flatResult = await flatResults.get(modName)! - const dirPath = path.slice(modName.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, - ]), - ] + const endpoint = `https://unpkg.com/${pkgName}@${actualVersion}/${pkgPath}/?meta` + + try { + const data = await fetchJson<{ + files: { + path: string + type: 'file' | 'directory' + size?: number + }[] + }>(endpoint) + + if (!data?.files) { + return [] + } + + const result: [string, FileType][] = data.files.map((file) => { + const type = + file.type === 'directory' + ? (2 as FileType.Directory) + : (1 as FileType.File) + + const fullPath = file.path + statCache.set(fullPath, { type }) + + return [_getNameFromPath(file.path), type] + }) + + dirCache.set(path, result) + return result + } catch { + return [] + } + } + + function _getNameFromPath(path: string): string { + if (!path) return '' + + const trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path + + const lastSlashIndex = trimmedPath.lastIndexOf('/') + + if ( + lastSlashIndex === -1 || + (lastSlashIndex === 0 && trimmedPath.length === 1) + ) { + return trimmedPath + } + + return trimmedPath.slice(lastSlashIndex + 1) } async function _readFile(path: string): Promise { @@ -163,7 +202,7 @@ export function createNpmFileSystem( if ((await _stat(path))?.type !== (1 satisfies FileType.File)) { return } - const text = await fetchText(`https://cdn.jsdelivr.net/npm/${path}`) + const text = await fetchText(`https://unpkg.com/${path}`) if (text !== undefined) { onFetch?.(path, text) } @@ -175,35 +214,6 @@ export function createNpmFileSystem( return await fetchResults.get(path)! } - async function flat(pkgName: string, version: string | undefined) { - version ??= 'latest' - - // resolve latest tag - if (version === 'latest') { - const data = await fetchJson<{ version: string | null }>( - `https://data.jsdelivr.com/v1/package/resolve/npm/${pkgName}@${version}`, - ) - 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) { // ignore @aaa/node_modules if (pkgName.endsWith('/node_modules')) { From 7bfb1a94e4cc1e116bd6900e4c798e5406cf45d8 Mon Sep 17 00:00:00 2001 From: wangcch Date: Tue, 4 Mar 2025 01:47:24 +0800 Subject: [PATCH 3/6] feat(monaco): custom online urls(createNpmFileSystem) --- src/monaco/resource.ts | 42 ++++++++++++++++++++++++++++++++++++---- src/monaco/vue.worker.ts | 3 ++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/monaco/resource.ts b/src/monaco/resource.ts index 6d101775..8f78d279 100644 --- a/src/monaco/resource.ts +++ b/src/monaco/resource.ts @@ -8,6 +8,32 @@ import type { URI } from 'vscode-uri' const textCache = new Map>() const jsonCache = new Map>() +export type CreateNpmFileSystemOptions = { + getPackageLatestVersionUrl?: (pkgName: string) => string + getPackageDirectoryUrl?: ( + pkgName: string, + pkgVersion: string, + pkgPath: string, + ) => string + getPackageFileTextUrl?: ( + path: string, + pkgName: string, + pkgVersion: string | undefined, + pkgPath: string, + ) => string +} + +const defaultUnpkgOptions: Required = { + getPackageLatestVersionUrl: (pkgName: string) => + `https://unpkg.com/${pkgName}@latest/package.json`, + getPackageDirectoryUrl: ( + pkgName: string, + pkgVersion: string, + pkgPath: string, + ) => `https://unpkg.com/${pkgName}@${pkgVersion}/${pkgPath}/?meta`, + getPackageFileTextUrl: (path: string) => `https://unpkg.com/${path}`, +} + export function createNpmFileSystem( getCdnPath = (uri: URI): string | undefined => { if (uri.path === '/node_modules') { @@ -18,7 +44,14 @@ export function createNpmFileSystem( }, getPackageVersion?: (pkgName: string) => string | undefined, onFetch?: (path: string, content: string) => void, + options?: CreateNpmFileSystemOptions, ): FileSystem { + const { + getPackageDirectoryUrl = defaultUnpkgOptions.getPackageDirectoryUrl, + getPackageFileTextUrl = defaultUnpkgOptions.getPackageFileTextUrl, + getPackageLatestVersionUrl = defaultUnpkgOptions.getPackageLatestVersionUrl, + } = options || {} + const fetchResults = new Map>() const statCache = new Map() const dirCache = new Map() @@ -128,7 +161,7 @@ export function createNpmFileSystem( if (resolvedVersion === 'latest') { try { const data = await fetchJson<{ version: string }>( - `https://unpkg.com/${pkgName}@${resolvedVersion}/package.json`, + getPackageLatestVersionUrl(pkgName), ) if (data?.version) { actualVersion = data.version @@ -138,8 +171,7 @@ export function createNpmFileSystem( } } - const endpoint = `https://unpkg.com/${pkgName}@${actualVersion}/${pkgPath}/?meta` - + const endpoint = getPackageDirectoryUrl(pkgName, actualVersion, pkgPath) try { const data = await fetchJson<{ files: { @@ -202,7 +234,9 @@ export function createNpmFileSystem( if ((await _stat(path))?.type !== (1 satisfies FileType.File)) { return } - const text = await fetchText(`https://unpkg.com/${path}`) + const text = await fetchText( + getPackageFileTextUrl(path, pkgName, _version, pkgFilePath), + ) if (text !== undefined) { onFetch?.(path, text) } diff --git a/src/monaco/vue.worker.ts b/src/monaco/vue.worker.ts index ea3c1dfb..2e6e301d 100644 --- a/src/monaco/vue.worker.ts +++ b/src/monaco/vue.worker.ts @@ -3,7 +3,6 @@ import { createTypeScriptWorkerLanguageService, Language, } from '@volar/monaco/worker' -import { createNpmFileSystem } from './resource' import { type VueCompilerOptions, VueVirtualCode, @@ -34,6 +33,8 @@ import { getElementAttrs } from '@vue/typescript-plugin/lib/requests/getElementA import { getElementNames } from '@vue/typescript-plugin/lib/requests/getElementNames' import { getPropertiesAtLocation } from '@vue/typescript-plugin/lib/requests/getPropertiesAtLocation' +import { createNpmFileSystem } from './resource' + export interface CreateData { tsconfig: { compilerOptions?: import('typescript').CompilerOptions From 9f58e422322d67f889b015724adeae51fb715396 Mon Sep 17 00:00:00 2001 From: wangcch Date: Tue, 4 Mar 2025 23:14:19 +0800 Subject: [PATCH 4/6] feat(store): resourceLinks to custom cdn --- src/monaco/env.ts | 24 ++++++++++++++++++++++-- src/monaco/resource.ts | 17 +++++++---------- src/monaco/vue.worker.ts | 32 +++++++++++++++++++++++++++++--- src/output/Sandbox.vue | 5 +++++ src/output/srcdoc.html | 10 ++++------ src/store.ts | 27 ++++++++++++++++++++++++++- 6 files changed, 93 insertions(+), 22 deletions(-) diff --git a/src/monaco/env.ts b/src/monaco/env.ts index 8074bba9..2fc02630 100644 --- a/src/monaco/env.ts +++ b/src/monaco/env.ts @@ -122,6 +122,10 @@ export interface WorkerMessage { event: 'init' tsVersion: string tsLocale?: string + pkgDirUrl?: string + pkgFileTextUrl?: string + pkgLatestVersionUrl?: string + typescriptLib?: string } export function loadMonacoEnv(store: Store) { @@ -135,11 +139,27 @@ export function loadMonacoEnv(store: Store) { resolve() } }) - worker.postMessage({ + + const { + pkgDirUrl, + pkgFileTextUrl, + pkgLatestVersionUrl, + typescriptLib, + } = store.resourceLinks || {} + + const message: WorkerMessage = { event: 'init', tsVersion: store.typescriptVersion, tsLocale: store.locale, - } satisfies WorkerMessage) + pkgDirUrl: pkgDirUrl ? String(pkgDirUrl) : undefined, + pkgFileTextUrl: pkgFileTextUrl ? String(pkgFileTextUrl) : undefined, + pkgLatestVersionUrl: pkgLatestVersionUrl + ? String(pkgLatestVersionUrl) + : undefined, + typescriptLib: typescriptLib ? String(typescriptLib) : undefined, + } + + worker.postMessage(message) }) await init return worker diff --git a/src/monaco/resource.ts b/src/monaco/resource.ts index 8f78d279..da94d290 100644 --- a/src/monaco/resource.ts +++ b/src/monaco/resource.ts @@ -16,7 +16,6 @@ export type CreateNpmFileSystemOptions = { pkgPath: string, ) => string getPackageFileTextUrl?: ( - path: string, pkgName: string, pkgVersion: string | undefined, pkgPath: string, @@ -24,14 +23,12 @@ export type CreateNpmFileSystemOptions = { } const defaultUnpkgOptions: Required = { - getPackageLatestVersionUrl: (pkgName: string) => + getPackageLatestVersionUrl: (pkgName) => `https://unpkg.com/${pkgName}@latest/package.json`, - getPackageDirectoryUrl: ( - pkgName: string, - pkgVersion: string, - pkgPath: string, - ) => `https://unpkg.com/${pkgName}@${pkgVersion}/${pkgPath}/?meta`, - getPackageFileTextUrl: (path: string) => `https://unpkg.com/${path}`, + getPackageDirectoryUrl: (pkgName, pkgVersion, pkgPath) => + `https://unpkg.com/${pkgName}@${pkgVersion}/${pkgPath}/?meta`, + getPackageFileTextUrl: (pkgName, pkgVersion, pkgPath) => + `https://unpkg.com/${pkgName}@${pkgVersion || 'latest'}/${pkgPath}`, } export function createNpmFileSystem( @@ -235,7 +232,7 @@ export function createNpmFileSystem( return } const text = await fetchText( - getPackageFileTextUrl(path, pkgName, _version, pkgFilePath), + getPackageFileTextUrl(pkgName, _version, pkgFilePath), ) if (text !== undefined) { onFetch?.(path, text) @@ -317,7 +314,7 @@ export function createNpmFileSystem( version = modName.substring(modName.lastIndexOf('@') + 1) } if (!version && getPackageVersion) { - getPackageVersion?.(pkgName) + version = getPackageVersion?.(pkgName) } return [modName, pkgName, version, path] } diff --git a/src/monaco/vue.worker.ts b/src/monaco/vue.worker.ts index 2e6e301d..12498a54 100644 --- a/src/monaco/vue.worker.ts +++ b/src/monaco/vue.worker.ts @@ -45,14 +45,30 @@ export interface CreateData { const asFileName = (uri: URI) => uri.path const asUri = (fileName: string): URI => URI.file(fileName) +const createFunc = (func?: string) => (func && typeof func === 'string') ? Function(`return ${func}`)() : undefined let ts: typeof import('typescript') let locale: string | undefined +let resourceLinks: Record< + keyof Pick< + WorkerMessage, + 'pkgDirUrl' | 'pkgFileTextUrl' | 'pkgLatestVersionUrl' + >, + ((...args: any[]) => string) | undefined +> self.onmessage = async (msg: MessageEvent) => { if (msg.data?.event === 'init') { locale = msg.data.tsLocale - ts = await importTsFromCdn(msg.data.tsVersion) + ts = await importTsFromCdn( + msg.data.tsVersion, + createFunc(msg.data.typescriptLib), + ) + resourceLinks = { + pkgDirUrl: createFunc(msg.data.pkgDirUrl), + pkgFileTextUrl: createFunc(msg.data.pkgFileTextUrl), + pkgLatestVersionUrl: createFunc(msg.data.pkgLatestVersionUrl), + } self.postMessage('inited') return } @@ -82,6 +98,11 @@ self.onmessage = async (msg: MessageEvent) => { content, ) }, + { + getPackageDirectoryUrl: resourceLinks.pkgDirUrl, + getPackageFileTextUrl: resourceLinks.pkgFileTextUrl, + getPackageLatestVersionUrl: resourceLinks.pkgLatestVersionUrl, + }, ), } @@ -303,10 +324,15 @@ self.onmessage = async (msg: MessageEvent) => { ) } -async function importTsFromCdn(tsVersion: string) { +async function importTsFromCdn( + tsVersion: string, + getTsCdn?: (version?: string) => string, +) { const _module = globalThis.module ;(globalThis as any).module = { exports: {} } - const tsUrl = `https://cdn.jsdelivr.net/npm/typescript@${tsVersion}/lib/typescript.js` + const tsUrl = + getTsCdn?.(tsVersion) || + `https://cdn.jsdelivr.net/npm/typescript@${tsVersion}/lib/typescript.js` await import(/* @vite-ignore */ tsUrl) const ts = globalThis.module.exports globalThis.module = _module diff --git a/src/output/Sandbox.vue b/src/output/Sandbox.vue index c62f480c..a5bafa20 100644 --- a/src/output/Sandbox.vue +++ b/src/output/Sandbox.vue @@ -129,6 +129,11 @@ function createSandbox() { //, previewOptions.value?.placeholderHTML || '', ) + .replace( + //, + store.value.resourceLinks?.esModuleShims || + 'https://cdn.jsdelivr.net/npm/es-module-shims@1.5.18/dist/es-module-shims.wasm.js', + ) sandbox.srcdoc = sandboxSrc containerRef.value?.appendChild(sandbox) diff --git a/src/output/srcdoc.html b/src/output/srcdoc.html index 75fa51b3..03501323 100644 --- a/src/output/srcdoc.html +++ b/src/output/srcdoc.html @@ -6,8 +6,9 @@ color-scheme: dark; } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } @@ -366,10 +367,7 @@ - + diff --git a/src/store.ts b/src/store.ts index 3af17d4e..5667fadc 100644 --- a/src/store.ts +++ b/src/store.ts @@ -48,6 +48,7 @@ export function useStore( typescriptVersion = ref('latest'), dependencyVersion = ref(Object.create(null)), reloadLanguageTools = ref(), + resourceLinks = undefined, }: Partial = {}, serializedState?: string, ): ReplStore { @@ -92,7 +93,9 @@ export function useStore( vueVersion, async (version) => { if (version) { - const compilerUrl = `https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js` + const compilerUrl = + resourceLinks?.value?.vueCompilerUrl?.(version) || + `https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js` loading.value = true compiler.value = await import(/* @vite-ignore */ compilerUrl).finally( () => (loading.value = false), @@ -390,6 +393,8 @@ export function useStore( deserialize, getFiles, setFiles, + + resourceLinks, }) return store } @@ -415,6 +420,20 @@ export interface SFCOptions { template?: Partial } +export type ResourceLinkConfigs = { + esModuleShims?: string + vueCompilerUrl?: (version: string) => string + typescriptLib?: (version: string) => string + // for monaco + pkgLatestVersionUrl?: (pkgName: string) => string + pkgDirUrl?: (pkgName: string, pkgVersion: string, pkgPath: string) => string + pkgFileTextUrl?: ( + pkgName: string, + pkgVersion: string | undefined, + pkgPath: string, + ) => string +} + export type StoreState = ToRefs<{ files: Record activeFilename: string @@ -445,6 +464,9 @@ export type StoreState = ToRefs<{ /** \{ dependencyName: version \} */ dependencyVersion: Record reloadLanguageTools?: (() => void) | undefined + + /** Custom online resources */ + resourceLinks?: ResourceLinkConfigs }> export interface ReplStore extends UnwrapRef { @@ -468,6 +490,8 @@ export interface ReplStore extends UnwrapRef { deserialize(serializedState: string, checkBuiltinImportMap?: boolean): void getFiles(): Record setFiles(newFiles: Record, mainFile?: string): Promise + /** Custom online resources */ + resourceLinks?: ResourceLinkConfigs } export type Store = Pick< @@ -493,6 +517,7 @@ export type Store = Pick< | 'renameFile' | 'getImportMap' | 'getTsConfig' + | 'resourceLinks' > export class File { From 4415c70549ede39852fc949509100f9e5d4f4b69 Mon Sep 17 00:00:00 2001 From: wangcch Date: Mon, 10 Mar 2025 21:53:54 +0800 Subject: [PATCH 5/6] docs: resource links config --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index b529b203..1aa5ff5a 100644 --- a/README.md +++ b/README.md @@ -132,3 +132,73 @@ const store = useStore( ``` + +
+Configuration options for resource links. (replace CDN resources) + +```ts +export type ResourceLinkConfigs = { + /** URL for ES Module Shims. */ + esModuleShims?: string + /** Function that generates the Vue compiler URL based on the version. */ + vueCompilerUrl?: (version: string) => string + /** Function that generates the TypeScript library URL based on the version. */ + typescriptLib?: (version: string) => string + + /** [monaco] Function that generates a URL to fetch the latest version of a package. */ + pkgLatestVersionUrl?: (pkgName: string) => string + /** [monaco] Function that generates a URL to browse a package directory. */ + pkgDirUrl?: (pkgName: string, pkgVersion: string, pkgPath: string) => string + /** [monaco] Function that generates a URL to fetch the content of a file from a package. */ + pkgFileTextUrl?: ( + pkgName: string, + pkgVersion: string | undefined, + pkgPath: string, + ) => string +} +``` + +**unpkg** + +```ts +const store = useStore({ + resourceLinks: ref({ + esModuleShims: + 'https://unpkg.com/es-module-shims@1.5.18/dist/es-module-shims.wasm.js', + vueCompilerUrl: (version) => + `https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js`, + typescriptLib: (version) => + `https://unpkg.com/typescript@${version}/lib/typescript.js`, + pkgLatestVersionUrl: (pkgName) => + `https://unpkg.com/${pkgName}@latest/package.json`, + pkgDirUrl: (pkgName, pkgVersion, pkgPath) => + `https://unpkg.com/${pkgName}@${pkgVersion}/${pkgPath}/?meta`, + pkgFileTextUrl: (pkgName, pkgVersion, pkgPath) => + `https://unpkg.com/${pkgName}@${pkgVersion || 'latest'}/${pkgPath}`, + }), +}) +``` + +**npmmirror** + +```ts +const store = useStore({ + resourceLinks: ref({ + esModuleShims: + 'https://registry.npmmirror.com/es-module-shims/1.5.18/files/dist/es-module-shims.wasm.js', + vueCompilerUrl: (version) => + `https://registry.npmmirror.com/@vue/compiler-sfc/${version}/files/dist/compiler-sfc.esm-browser.js`, + typescriptLib: (version) => + `https://registry.npmmirror.com/typescript/${version}/files/lib/typescript.js`, + + pkgLatestVersionUrl: (pkgName) => + `https://registry.npmmirror.com/${pkgName}/latest/files/package.json`, + pkgDirUrl: (pkgName, pkgVersion, pkgPath) => + `https://registry.npmmirror.com/${pkgName}/${pkgVersion}/files/${pkgPath}/?meta`, + pkgFileTextUrl: (pkgName, pkgVersion, pkgPath) => + `https://registry.npmmirror.com/${pkgName}/${pkgVersion || 'latest'}/files/${pkgPath}`, + }), +}) +``` + +
From 96069b12f7b21073fc8988dedb6c7e5dd238ab97 Mon Sep 17 00:00:00 2001 From: wangcch Date: Tue, 9 Sep 2025 15:58:19 +0800 Subject: [PATCH 6/6] revert(lock): @emmetio/css-parser --- pnpm-lock.yaml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 228a2c97..302a1ac6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,8 +243,8 @@ packages: '@emmetio/css-abbreviation@2.1.8': resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} - '@emmetio/css-parser@git+https://git@github.com:ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660': - resolution: {commit: 370c480ac103bd17c7bcfb34bf5d577dc40d3660, repo: git@github.com:ramya-rao-a/css-parser.git, type: git} + '@emmetio/css-parser@https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660': + resolution: {tarball: https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660} version: 0.4.0 '@emmetio/html-matcher@1.3.0': @@ -589,67 +589,56 @@ packages: resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.46.2': resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.46.2': resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.46.2': resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.46.2': resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.46.2': resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.46.2': resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.46.2': resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.46.2': resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.46.2': resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.46.2': resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.46.2': resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} @@ -2659,7 +2648,7 @@ snapshots: dependencies: '@emmetio/scanner': 1.0.4 - '@emmetio/css-parser@git+https://git@github.com:ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660': + '@emmetio/css-parser@https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660': dependencies: '@emmetio/stream-reader': 2.2.0 '@emmetio/stream-reader-utils': 0.1.0 @@ -4912,7 +4901,7 @@ snapshots: volar-service-emmet@0.0.65(@volar/language-service@2.4.23): dependencies: - '@emmetio/css-parser': git+https://git@github.com:ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660 + '@emmetio/css-parser': https://codeload.github.com/ramya-rao-a/css-parser/tar.gz/370c480ac103bd17c7bcfb34bf5d577dc40d3660 '@emmetio/html-matcher': 1.3.0 '@vscode/emmet-helper': 2.11.0 vscode-uri: 3.1.0