diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40a6d1c7..4e46de25 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,22 @@ git submodule update --init --recursive pnpm install ``` -使用`vscode`打开文件夹打开项目根目录,点击键盘上的`F5`即可启动一个扩展开发宿主进程,打开另一个`vscode`窗口,开始调试项目。 +> **💡 关于依赖安装:** +> +> 本项目使用 pnpm workspace + `nodeLinker: isolated`(严格隔离模式),每个包的依赖装在各自的 `node_modules` 下,不会向上 hoist。 +> +> 但 `vp pack` 构建时会将 `packages/language-server` 的入口打包为 `packages/vscode/dist/server.js`,运行时从这个产物 `require` 的各种依赖**必须能被 `packages/vscode` 的模块解析路径找到**。 +> +> 因此,有些 language-server 用到的依赖(如 `volar-service-typescript`)需要在 `packages/vscode/package.json` 的 `dependencies` 中**显式重复声明**,否则运行时会出现 `MODULE_NOT_FOUND` 错误。如果你新加了某个 language-server 依赖后遇到这种错误,大概率是这个原因。 + +使用`VSCode`打开文件夹打开项目根目录。 + +1. 在左侧栏找到「运行和调试」面板(`Cmd+Shift+D`) +2. 选择 "Launch Client" 配置 +3. 点击绿色的 ▶ 按钮(或使用键盘快捷键`F5`) +4. 即可启动一个扩展开发宿主进程,打开另一个`vscode`窗口,开始调试项目。 + + ## `volar labs` 插件 diff --git a/README-en.md b/README-en.md index 9b51ff1a..6286b4cb 100644 --- a/README-en.md +++ b/README-en.md @@ -106,7 +106,7 @@ PR address: [https://github.com/material-extensions/vscode-material-icon-theme/p | `ets.lspDebugMode` | Enable ETS Language Server debug logging. | `boolean` | `false` | | `ets.ignoreWorkspaceLocalPropertiesFile` | Ignore auto infer the base SDK path from `local.properties` file in the local workspace. | `boolean` | `false` | | `ets.linterVersion` | The version of the ArkTS linter to use. Set to 'off' to disable the linter. | `string` | `"1.1"` | -| `ets.resourceReferenceDiagnostic` | 未匹配到的 $r() 资源引用的诊断级别 | `string` | `"error"` | +| `ets.resourceReferenceDiagnostic` | Diagnostic level for unmatched $r() resource references | `string` | `"error"` | | `ets.localImagePath` | The path of the local image folder. The local image folder is used to store the images of the devices. In MacOS, the default path is `~/Library/Huawei/Sdk`; in Windows, the default path is `%APPDATA%\Local\Huawei\Sdk`. | `string` | `` | @@ -139,6 +139,10 @@ In MacOS, the default path is `~/Library/Huawei/Sdk`; in Windows, the default pa +## Contributing 👩‍💻 + +If you'd like to contribute to this project or build from source, please refer to [CONTRIBUTING.md](./CONTRIBUTING.md). + ## Star History 🌟 [![Star History Chart](https://api.star-history.com/svg?repos=ohosvscode/arkTS&type=Date)](https://star-history.com/#ohosvscode/arkTS&Date) @@ -149,7 +153,7 @@ In MacOS, the default path is `~/Library/Huawei/Sdk`; in Windows, the default pa - Telegram: [@GCZ_Zero](https://t.me/GCZ_Zero) - X (Twitter): [@GCZ_Zero](https://x.com/GCZ_Zero) -- QQ: 1203970284,QQ Group: 746153004 +- QQ: 1203970284, QQ Group: 746153004 - WeChat: gcz-zero ### Coffee ☕️ diff --git a/README.md b/README.md index 06912e64..f5be4b79 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,10 @@ In order to run your app, type: Your app code is in demo/entry. ``` +## 参与开发 👩‍💻 + +如果你想参与本项目的开发或从源码构建,请查阅 [CONTRIBUTING.md](./CONTRIBUTING.md)。 + ## Star History 🌟 [![Star History Chart](https://api.star-history.com/svg?repos=ohosvscode/arkTS&type=Date)](https://star-history.com/#ohosvscode/arkTS&Date) diff --git a/packages/vscode/assets/create-project.md b/packages/vscode/assets/create-project.md new file mode 100644 index 00000000..6eb363ed --- /dev/null +++ b/packages/vscode/assets/create-project.md @@ -0,0 +1,3 @@ +## Create ArkTS Project + +Create a new ArkTS project to start developing your OpenHarmony application. diff --git a/packages/vscode/assets/create-project.zh-cn.md b/packages/vscode/assets/create-project.zh-cn.md new file mode 100644 index 00000000..a4395909 --- /dev/null +++ b/packages/vscode/assets/create-project.zh-cn.md @@ -0,0 +1,3 @@ +## 创建 ArkTS 项目 + +创建一个新的 ArkTS 项目,开始编写您的鸿蒙应用。 diff --git a/packages/vscode/package.json b/packages/vscode/package.json index f178b414..bb5a7c67 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -31,7 +31,6 @@ "Linters", "Snippets", "Formatters", - "Themes", "Debuggers" ], "main": "./dist/client.js", @@ -122,17 +121,15 @@ "properties": { "projectRoot": { "type": "string", - "description": "Hvigor 项目根目录。" + "description": "Hvigor 项目根目录" }, "moduleName": { "type": "string", - "description": "模块名称。", - "default": "entry" + "description": "模块名称,必填" }, "productName": { "type": "string", - "description": "产品名称。", - "default": "default" + "description": "产品名称,默认 default" }, "requiredDeviceType": { "type": "string", @@ -591,6 +588,14 @@ "media": { "markdown": "%walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK.media.markdown%" } + }, + { + "id": "ArkTS-Walkthrough-CreateProject", + "title": "%walkthroughs.ArkTS-Walkthrough.steps.createProject%", + "description": "%walkthroughs.ArkTS-Walkthrough.steps.createProject.description%", + "media": { + "markdown": "%walkthroughs.ArkTS-Walkthrough.steps.createProject.media.markdown%" + } } ] } @@ -599,14 +604,15 @@ "scripts": { "build": "tsx scripts/build.ts", "watch": "tsx scripts/watch.ts", - "pack": "tsx scripts/pre-process.ts && pnpm run build && vsce package", + "pack": "tsx scripts/pre-process.ts && pnpm run build && vsce package --no-dependencies", "postpack": "tsx scripts/clean-process.ts", "update": "vscode-ext-gen --output src/generated/meta.ts --locale zh-cn && vscode-ext-gen --output src/generated/meta.ts --locale zh-cn --readme ../../README.md && vscode-ext-gen --output src/generated/meta.ts --locale --readme ../../README-en.md", "ovsx": "ovsx", "vsce": "vsce" }, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.7" + "@napi-rs/wasm-runtime": "^1.0.7", + "volar-service-typescript": "catalog:prod" }, "devDependencies": { "@types/vscode": "^1.22.0" diff --git a/packages/vscode/package.nls.json b/packages/vscode/package.nls.json index 1b06ceff..4c44e64c 100644 --- a/packages/vscode/package.nls.json +++ b/packages/vscode/package.nls.json @@ -55,6 +55,10 @@ "project.createProject.submit": "Create", "project.createProject.title": "Create ArkTS Project", "project.openProject": "Open Project", + "project.writePath.blockedSystemPath": "Cannot write project to a system or unsafe directory.", + "project.writePath.fileExists": "A file already exists at the target path: {0}", + "project.writePath.invalid": "Please enter a valid save path.", + "project.writePath.zipSlip": "The template archive contains an unsafe file path.", "project.templateMarket.detail": "Detail", "project.templateMarket.downloadTo": "Choose where to download the template", "project.templateMarket.downloading": "Downloading template...", @@ -185,6 +189,8 @@ "sdk.error.SDKPathNotDirectory": "OpenHarmony SDK path must be a directory, please check the 'ets.sdkPath' in the IDE settings or the 'sdk.dir' in the local.properties file in the local workspace.", "sdk.error.SDKPathNotFound": "OpenHarmony SDK path not found, please check the 'ets.sdkPath' in the IDE settings or the 'sdk.dir' in the local.properties file in the local workspace.", "sdk.error.choiceSdkPathMasually": "Manually choose a OpenHarmony SDK path", + "sdk.error.autoDetectSdkPath": "Use default (Auto-detect from DevEco Studio)", + "sdk.error.sdkStructureInvalid": "Selected folder is not a valid OpenHarmony SDK, not found: {0}", "sdk.error.downloadOrChoiceSdkPath": "Download or choose a OpenHarmony SDK path", "sdk.error.languageServerError": "ArkTS Language Server Start Failed, please try to restart the IDE or check the OpenHarmony SDK path is correct. Error detail: {0}", "sdk.error.validSdkPath": "OpenHarmony SDK path is not set in global IDE configuration and local workspace local.properties file, the ETS Language Server will not work properly.", @@ -230,6 +236,9 @@ "walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK": "Install OpenHarmony SDK", "walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK.description": "Install OpenHarmony SDK to support ArkTS development. \n[Install OpenHarmony SDK](command:ets.installSDK)", "walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK.media.markdown": "./assets/install-sdk.md", + "walkthroughs.ArkTS-Walkthrough.steps.createProject": "Create ArkTS Project", + "walkthroughs.ArkTS-Walkthrough.steps.createProject.description": "Create a new ArkTS project to start developing. \n[Create ArkTS Project](command:ets.createProject)", + "walkthroughs.ArkTS-Walkthrough.steps.createProject.media.markdown": "./assets/create-project.md", "walkthroughs.ArkTS-Walkthrough.title": "OpenHarmony ArkTS Setup", "tasks.error.hdcNotFoundInSystemEnvironmentVariables": "hdc command not found in system environment variables.", "tasks.error.hvigorNotFoundInSystemEnvironmentVariables": "hvigor or hvigorw command not found in system environment variables.", diff --git a/packages/vscode/package.nls.zh-cn.json b/packages/vscode/package.nls.zh-cn.json index 30b41e5a..41481ca5 100644 --- a/packages/vscode/package.nls.zh-cn.json +++ b/packages/vscode/package.nls.zh-cn.json @@ -122,6 +122,10 @@ "project.createProject.submit": "创建", "project.createProject.title": "创建 ArkTS 项目", "project.openProject": "打开文件夹", + "project.writePath.blockedSystemPath": "不能将工程写入系统目录或不安全的路径。", + "project.writePath.fileExists": "目标路径已存在文件: {0}", + "project.writePath.invalid": "请输入有效的保存路径。", + "project.writePath.zipSlip": "模版压缩包包含不安全的文件路径。", "project.templateMarket.detail": "详情", "project.templateMarket.downloadTo": "选择将模版下载到何处", "project.templateMarket.downloading": "模版下载中...", @@ -185,6 +189,8 @@ "sdk.error.SDKPathNotDirectory": "OpenHarmony SDK 路径必须是一个目录, 请检查IDE设置中的“ets.sdkPath”或者本地工作区下的“local.properties”里的“sdk.dir”属性是否设置正确。", "sdk.error.SDKPathNotFound": "OpenHarmony SDK 路径不存在, 请检查IDE设置中的“ets.sdkPath”或者本地工作区下的“local.properties”里的“sdk.dir”属性是否设置正确。", "sdk.error.choiceSdkPathMasually": "手动选择一个 OpenHarmony SDK 路径", + "sdk.error.autoDetectSdkPath": "使用默认(自动从 DevEco Studio 检测)", + "sdk.error.sdkStructureInvalid": "所选文件夹不是有效的 OpenHarmony SDK,未找到: {0}", "sdk.error.downloadOrChoiceSdkPath": "下载或选择一个 OpenHarmony SDK 路径", "sdk.error.helpLabel": "帮助", "sdk.error.languageServerError": "ArkTS 语言服务器启动失败, 请尝试重启 IDE 或检查 OpenHarmony SDK 路径是否正确。错误详情: {0}", @@ -237,6 +243,9 @@ "walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK": "安装 OpenHarmony SDK", "walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK.description": "安装 OpenHarmony SDK 以支持 ArkTS 开发。\n[安装 OpenHarmony SDK](command:ets.installSDK)", "walkthroughs.ArkTS-Walkthrough.steps.installOpenHarmonySDK.media.markdown": "./assets/install-sdk.zh-cn.md", + "walkthroughs.ArkTS-Walkthrough.steps.createProject": "创建 ArkTS 项目", + "walkthroughs.ArkTS-Walkthrough.steps.createProject.description": "创建一个新的 ArkTS 项目,开始编写鸿蒙应用。\n[创建 ArkTS 项目](command:ets.createProject)", + "walkthroughs.ArkTS-Walkthrough.steps.createProject.media.markdown": "./assets/create-project.zh-cn.md", "walkthroughs.ArkTS-Walkthrough.title": "完成 OpenHarmony ArkTS 设置", "yes": "确定" } diff --git a/packages/vscode/scripts/clean-process.ts b/packages/vscode/scripts/clean-process.ts index 4bb9729b..8ee27b03 100644 --- a/packages/vscode/scripts/clean-process.ts +++ b/packages/vscode/scripts/clean-process.ts @@ -8,6 +8,15 @@ async function main() { fs.rmSync(path.resolve('node_modules'), { recursive: true, force: true }) if (fs.existsSync(path.resolve('package-lock.json'))) fs.rmSync(path.resolve('package-lock.json'), { force: true }) else globalLogger.warn('package-lock.json not found, skipping deletion') + + const pkgJsonPath = path.resolve('package.json') + const backupPath = `${pkgJsonPath}.orig` + if (fs.existsSync(backupPath)) { + globalLogger.info('Restoring original package.json from backup...') + fs.copyFileSync(backupPath, pkgJsonPath) + fs.rmSync(backupPath) + } + globalLogger.info('Reinstalling dependencies using pnpm...') execSync('pnpm install', { cwd: path.resolve('..', '..'), stdio: 'inherit' }) globalLogger.success('✨ Dependencies cleaning process done!') diff --git a/packages/vscode/scripts/pre-process.ts b/packages/vscode/scripts/pre-process.ts index 2441de80..fd24624c 100644 --- a/packages/vscode/scripts/pre-process.ts +++ b/packages/vscode/scripts/pre-process.ts @@ -18,6 +18,86 @@ export const PLATFORMS = { 'web': ['wasm32'], } +/** + * 读取 pnpm workspace catalog 定义,将 package.json 中的 catalog:xxx 替换为实际版本号。 + * npm 不支持 catalog: 协议,因此 pre-process 使用 npm install 前必须先平展这些引用。 + */ +function resolveCatalogDeps(pkgPath: string): void { + const workspaceRoot = path.resolve(pkgPath, '../..') + const workspaceFile = path.join(workspaceRoot, 'pnpm-workspace.yaml') + const pkgJsonPath = path.join(pkgPath, 'package.json') + + if (!fs.existsSync(workspaceFile) || !fs.existsSync(pkgJsonPath)) return + + const yamlLines = fs.readFileSync(workspaceFile, 'utf-8').split('\n') + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) + + const catalogMap = new Map() + + let inCatalogs = false + let currentCatalog = '' + + for (const rawLine of yamlLines) { + const line = rawLine.trimEnd() + const trimmed = line.trim() + + if (trimmed === 'catalogs:') { + inCatalogs = true + currentCatalog = '' + continue + } + + if (inCatalogs) { + if (trimmed === '' || trimmed.startsWith('#')) continue + // 如果碰到其他顶层键(如 catalog:),退出 catalogs 解析 + if (/^[a-z]/i.test(line) && !line.startsWith(' ') && !line.startsWith('\t')) { + inCatalogs = false + continue + } + // 子 catalog 头,如 " prod:" 或 " dev:" + const catalogHeader = trimmed.match(/^(\w+):$/) + if (catalogHeader) { + currentCatalog = catalogHeader[1] + continue + } + // 依赖行,如 " volar-service-typescript: volar-2.4" + const colonIdx = trimmed.indexOf(':') + if (colonIdx > 0) { + const depName = trimmed.slice(0, colonIdx).trim() + const depVersion = trimmed.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, '') + catalogMap.set(depName, depVersion) + catalogMap.set(`${currentCatalog}:${depName}`, depVersion) + } + } + } + + // 替换 package.json 中的 catalog 引用 + function replaceInDeps(deps: Record | undefined) { + if (!deps) return + for (const key of Object.keys(deps)) { + const val = deps[key] + if (typeof val === 'string' && val.startsWith('catalog:')) { + const catalogName = val.slice('catalog:'.length) || 'prod' + const lookupKey = `${catalogName}:${key}` + const resolved = catalogMap.get(lookupKey) || catalogMap.get(key) + if (resolved) { + globalLogger.info(`Resolved catalog dependency: ${key}@${val} -> ${resolved}`) + deps[key] = resolved + } + else { + globalLogger.warn(`Cannot resolve catalog dependency: ${key}@${val}, leaving unchanged`) + } + } + } + } + + replaceInDeps(pkgJson.dependencies) + replaceInDeps(pkgJson.devDependencies) + replaceInDeps(pkgJson.optionalDependencies) + + fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`) +} + /** * This script is used to copy the `@arkts/project-detector` and * its dependencies to the `node_modules/@arkts/project-detector` @@ -31,6 +111,13 @@ async function main() { globalLogger.info('Removing package-lock.json...') if (fs.existsSync(path.resolve('package-lock.json'))) fs.rmSync(path.resolve('package-lock.json')) else globalLogger.warn('package-lock.json not found, skipping deletion') + + globalLogger.info('Resolving catalog dependencies for npm compatibility...') + const pkgJsonPath = path.join(path.resolve('.'), 'package.json') + const backupPath = `${pkgJsonPath}.orig` + fs.copyFileSync(pkgJsonPath, backupPath) + resolveCatalogDeps(path.resolve('.')) + globalLogger.info('Installing dev & prod dependencies using npm...') const projectDetectorIdentifiers = collectIdentifiers(resolveDependenciesByIdentifier('@arkts/project-detector')).map((identifier) => { if (identifier === '@arkts/project-detector') return `${identifier}@next` diff --git a/packages/vscode/src/context/server-context.ts b/packages/vscode/src/context/server-context.ts index 23e0a2d9..0e6a986b 100644 --- a/packages/vscode/src/context/server-context.ts +++ b/packages/vscode/src/context/server-context.ts @@ -8,6 +8,26 @@ import path from 'node:path' import * as vscode from 'vscode' import { AbstractWatcher } from '../abstract-watcher' +async function uriExists(uri: vscode.Uri): Promise { + try { + await vscode.workspace.fs.stat(uri) + return true + } + catch { + return false + } +} + +async function isDirectory(uri: vscode.Uri): Promise { + try { + const stat = await vscode.workspace.fs.stat(uri) + return stat.type === vscode.FileType.Directory + } + catch { + return false + } +} + export abstract class LanguageServerContext extends AbstractWatcher implements IOnActivate { /** Start the language server. */ abstract start(force: boolean, overrideClientOptions: LanguageClientOptions): Promise<[LabsInfo | undefined, LanguageClientOptions]> @@ -46,23 +66,40 @@ export abstract class LanguageServerContext extends AbstractWatcher implements I const choiceSdkPath = this.translator.t('sdk.error.choiceSdkPathMasually') const downloadOrChoiceSdkPath = this.translator.t('sdk.error.downloadOrChoiceSdkPath') const helpLabel = this.translator.t('sdk.error.helpLabel') + const autoDetectLabel = this.translator.t('sdk.error.autoDetectSdkPath') const detail = this.errorToString(error) + const buttons = [choiceSdkPath, downloadOrChoiceSdkPath, helpLabel] + if (process.platform === 'darwin') { + buttons.unshift(autoDetectLabel) + } + let result: string | undefined = await vscode.window.showWarningMessage( 'ArkTS Language Server Warning', { modal: true, detail }, - choiceSdkPath, - downloadOrChoiceSdkPath, - helpLabel, + ...buttons, ) if (result === helpLabel) { - // 打开帮助文档,但不关闭对话框 const helpUrl = vscode.Uri.parse('https://arkcode.dev/arkts/install/#方法一-选择自带在-deveco-studio-中的-sdk-推荐') await vscode.env.openExternal(helpUrl) - // 继续循环,重新显示对话框 result = undefined } + else if (result === autoDetectLabel) { + const detected = await this.detectDevEcoStudioSdkPath() + if (detected) { + const validationError = await this.validateSdkStructure(detected) + if (validationError) { + vscode.window.showErrorMessage(this.translator.t('sdk.error.sdkStructureInvalid', validationError)) + } + else { + vscode.workspace.getConfiguration().update('ets.sdkPath', detected, vscode.ConfigurationTarget.Global) + } + } + else { + vscode.window.showErrorMessage(this.translator.t('sdk.error.validSdkPath')) + } + } else if (result === choiceSdkPath) { const [sdkPath] = await vscode.window.showOpenDialog({ canSelectFiles: false, @@ -74,7 +111,13 @@ export abstract class LanguageServerContext extends AbstractWatcher implements I vscode.window.showErrorMessage(this.translator.t('sdk.error.validSdkPath')) } else { - vscode.workspace.getConfiguration().update('ets.sdkPath', sdkPath.fsPath, vscode.ConfigurationTarget.Global) + const validationError = await this.validateSdkStructure(sdkPath.fsPath) + if (validationError) { + vscode.window.showErrorMessage(this.translator.t('sdk.error.sdkStructureInvalid', validationError)) + } + else { + vscode.workspace.getConfiguration().update('ets.sdkPath', sdkPath.fsPath, vscode.ConfigurationTarget.Global) + } } } else if (result === downloadOrChoiceSdkPath) { @@ -125,6 +168,65 @@ export abstract class LanguageServerContext extends AbstractWatcher implements I return vscode.Uri.file(hmsSdkPath) } + protected async detectDevEcoStudioSdkPath(): Promise { + const possiblePaths = [ + '/Applications/DevEco Studio.app', + '/Applications/DevEco-Studio.app', + ] + + for (const appPath of possiblePaths) { + const appUri = vscode.Uri.file(appPath) + if (!await uriExists(appUri)) continue + + const sdkDirUri = vscode.Uri.joinPath(appUri, 'Contents', 'sdk') + if (!await isDirectory(sdkDirUri)) continue + + const defaultOhosUri = vscode.Uri.joinPath(sdkDirUri, 'default', 'openharmony') + if (await isDirectory(defaultOhosUri)) { + const detected = defaultOhosUri.fsPath + this.getConsola().info(`Auto-detected OpenHarmony SDK path: ${detected}`) + return detected + } + + try { + const entries = await vscode.workspace.fs.readDirectory(sdkDirUri) + for (const [entry, fileType] of entries) { + if (entry === 'default' || entry === '.DS_Store') continue + if (fileType !== vscode.FileType.Directory) continue + const candidateUri = vscode.Uri.joinPath(sdkDirUri, entry, 'openharmony') + if (await isDirectory(candidateUri)) { + const detected = candidateUri.fsPath + this.getConsola().info(`Auto-detected OpenHarmony SDK path: ${detected}`) + return detected + } + } + } + catch {} + } + + this.getConsola().warn('DevEco Studio not found or no OpenHarmony SDK inside it.') + return undefined + } + + protected async validateSdkStructure(sdkPath: string): Promise { + const sdkUri = vscode.Uri.file(sdkPath) + const etsUri = vscode.Uri.joinPath(sdkUri, 'ets') + if (!await isDirectory(etsUri)) { + return 'ets' + } + const uniPackageUri = vscode.Uri.joinPath(etsUri, 'oh-uni-package.json') + try { + const stat = await vscode.workspace.fs.stat(uniPackageUri) + if (stat.type !== vscode.FileType.File) { + return 'ets/oh-uni-package.json (not a file)' + } + } + catch { + return 'ets/oh-uni-package.json' + } + return undefined + } + /** Configure the volar typescript plugin by `ClientOptions`. */ protected async configureTypeScriptPlugin(clientOptions: LanguageClientOptions): Promise { const typescriptPluginConfig: ETSPluginOptions = { diff --git a/packages/vscode/src/frontend/composables/project-configuration.ts b/packages/vscode/src/frontend/composables/project-configuration.ts index 5be4ac9a..e42a90b4 100644 --- a/packages/vscode/src/frontend/composables/project-configuration.ts +++ b/packages/vscode/src/frontend/composables/project-configuration.ts @@ -2,6 +2,15 @@ import type { FormRules } from 'naive-ui' import type { Ref } from 'vue' import path from 'node:path' import { ref, watch } from 'vue' +// [新增] 项目写入路径安全校验工具: +// - assertWritableProjectDirectoryPath: 校验路径是否可安全写入(防止系统路径/无效路径) +// - isRootDevEcoStudioProjectsPath: 判断是否为 DevEcoStudioProjects 根目录(用于智能路径替换) +// - ProjectWritePathError: 路径校验异常类 +import { + assertWritableProjectDirectoryPath, + isRootDevEcoStudioProjectsPath, + ProjectWritePathError, +} from '../../utils/project-write-path-guard' export interface Input { label: string @@ -71,11 +80,47 @@ export interface ProjectConfigurationContext { currentProject: Ref } +// [新增] 根据用户主目录和项目名构建默认保存路径 ~/DevEcoStudioProjects/ +function buildDefaultSavePath(homeDirectory: string, projectName: string): string { + return path.join(homeDirectory, 'DevEcoStudioProjects', projectName) +} + +// [新增] 将 ProjectWritePathError 转换为用户可读的 i18n 错误消息 +function formatProjectWritePathValidationError(error: ProjectWritePathError, t: (key: string, ...args: string[]) => string): string { + switch (error.code) { + case 'BLOCKED_SYSTEM_PATH': + return t('project.writePath.blockedSystemPath') + case 'INVALID_PATH': + return t('project.writePath.invalid') + default: + return t('project.createProject.savePathRequired') + } +} + export function createProjectConfigContext(homeDirectory: Ref): ProjectConfigurationContext { const { t } = useI18n() const { connection, createOpenDialog } = useProjectConnection() - const defaultBasePath = path.join(homeDirectory.value ?? '/', 'DevEcoStudioProjects') + const lastAutoSavePath = ref() const subscriptions = new Set<() => void>() + + // [新增] 智能应用默认保存路径:仅在用户尚未手动修改路径、或路径仍为 DevEcoStudioProjects 根目录时自动更新 + // 场景:用户修改项目名后自动同步保存路径,但保留用户已手动选择的路径 + function applyDefaultSavePath(project: Ref, projectName: string): void { + const home = homeDirectory.value + if (!home) return + + const savePathInput = project.value.input.savePath as TextButtonGroupInput + const nextSavePath = buildDefaultSavePath(home, projectName) + const currentSavePath = savePathInput.value + if ( + !currentSavePath + || currentSavePath === lastAutoSavePath.value + || isRootDevEcoStudioProjectsPath(currentSavePath) + ) { + savePathInput.value = nextSavePath + lastAutoSavePath.value = nextSavePath + } + } const modelVersionToSdkVersionMap = new Map( [ ['6.0.0', 20], @@ -102,6 +147,16 @@ export function createProjectConfigContext(homeDirectory: Ref): P savePath: { async asyncValidator(_rule, value: TextInput, callback) { if (!value.value) return callback(t('project.createProject.savePathRequired')) + // [新增] 保存路径写入安全性前置校验:拦截系统目录、无效路径等不安全的写入目标 + try { + assertWritableProjectDirectoryPath(value.value, homeDirectory.value ?? '') + } + catch (error) { + if (error instanceof ProjectWritePathError) { + return callback(formatProjectWritePathValidationError(error, t)) + } + throw error + } const stat = await connection.stat?.(value.value) if (stat && !stat.isDirectory) return callback(t('project.createProject.savePathNotDirectory')) let directoryFileNames = await connection.readDirectory?.(value.value) @@ -131,7 +186,8 @@ export function createProjectConfigContext(homeDirectory: Ref): P }, savePath: { type: 'text-button-group', - value: path.join(defaultBasePath, 'MyApplication'), + // [变更] 初始值从预填空字符串改为空,由 watch(homeDirectory) 回调动态填充默认路径 + value: '', placeholder: t('project.createProject.savePathPlaceholder'), label: t('project.createProject.savePath'), labelIcon: 'i-ph-folder-duotone', @@ -189,7 +245,21 @@ export function createProjectConfigContext(homeDirectory: Ref): P value: ['phone'], }, }, - onInit: project => ( + onInit: (project) => { + // [新增] 监听 homeDirectory 加载完成,自动填充默认保存路径(immediate 确保首次即有值) + subscriptions.add( + watch(homeDirectory, () => { + const projectName = project.value.input.projectName.value as string || 'MyApplication' + applyDefaultSavePath(project as Ref, projectName) + }, { immediate: true }), + ) + // [新增] 监听项目名称变化,自动同步更新保存路径(如 MyApplication → ~/DevEcoStudioProjects/MyApplication) + subscriptions.add( + watch(() => project.value.input.projectName.value as string, (projectName) => { + if (!projectName) return + applyDefaultSavePath(project as Ref, projectName) + }), + ) subscriptions.add( watch(() => project.value.input.compatibleSdkVersion.value as number, (value) => { if (value < 12) { @@ -229,7 +299,7 @@ export function createProjectConfigContext(homeDirectory: Ref): P } }, { immediate: true }), ) - ), + }, onDestroy: () => { subscriptions.forEach(subscription => subscription()) subscriptions.clear() diff --git a/packages/vscode/src/frontend/functions/project-server-function.ts b/packages/vscode/src/frontend/functions/project-server-function.ts index f38f823a..73b0be5a 100644 --- a/packages/vscode/src/frontend/functions/project-server-function.ts +++ b/packages/vscode/src/frontend/functions/project-server-function.ts @@ -1,4 +1,4 @@ -import * as fs from 'node:fs' +import type { ProjectWritePathErrorCode } from '../../utils/project-write-path-guard' import os from 'node:os' import path from 'node:path' import { effect, signal } from 'alien-signals' @@ -11,6 +11,17 @@ import { ExtensionContext, Translator } from 'unioc/vscode' import * as vscode from 'vscode' import { ProtocolContext } from '../../context/protocol-context' import { InitialCallbackEvent } from '../../context/webview-context' +import { + assertProjectFilesCanBeCreated, + ensureEmptyProjectDirectory, + validateProjectWriteDirectory, + writeFileExclusive, +} from '../../utils/project-write-path-fs' +import { + ProjectWritePathError, + + resolveArchiveEntryPath, +} from '../../utils/project-write-path-guard' import { ProjectConnectionProtocol } from '../interfaces/project-connection-protocol' hbs.registerHelper('equal', (a: number | string, b: number | string) => Number(a) === Number(b) || String(a) === String(b)) @@ -24,26 +35,56 @@ export class ProjectServerFunctionImpl extends ProtocolContext { - const isExists = fs.existsSync(path) - if (!isExists) return false - - return { - isFile: fs.statSync(path).isFile(), - isDirectory: fs.statSync(path).isDirectory(), + try { + const uri = vscode.Uri.file(path) + const stat = await vscode.workspace.fs.stat(uri) + return { + isFile: stat.type === vscode.FileType.File, + isDirectory: stat.type === vscode.FileType.Directory, + } + } + catch { + return false } } async readDirectory(path: string): Promise { - if (!fs.existsSync(path)) return false - const isDirectory = fs.statSync(path).isDirectory() - if (!isDirectory) return false - return fs.readdirSync(path) + try { + const uri = vscode.Uri.file(path) + const stat = await vscode.workspace.fs.stat(uri) + if (stat.type !== vscode.FileType.Directory) return false + const entries = await vscode.workspace.fs.readDirectory(uri) + return entries.map(([name]) => name) + } + catch { + return false + } } async getHomeDirectory(): Promise { return os.homedir() } + private formatProjectWritePathError(error: ProjectWritePathError): string { + const keys: Record = { + INVALID_PATH: 'project.writePath.invalid', + NOT_DIRECTORY: 'project.createProject.savePathNotDirectory', + NOT_EMPTY: 'project.createProject.savePathHasFiles', + FILE_EXISTS: 'project.writePath.fileExists', + ZIP_SLIP: 'project.writePath.zipSlip', + BLOCKED_SYSTEM_PATH: 'project.writePath.blockedSystemPath', + } + const key = keys[error.code] + return error.detail ? this.translator.t(key, error.detail) : this.translator.t(key) + } + + private rethrowProjectWritePathError(error: unknown): never { + if (error instanceof ProjectWritePathError) { + throw new Error(this.formatProjectWritePathError(error)) + } + throw error + } + async requestTemplateMarketList(request: ProjectConnectionProtocol.ServerFunction.RequestTemplateMarketList.Request): Promise { const response = await axios.post('https://svc-drcn.developer.huawei.com/partnerVectorServlet/market/product/list', { lang: 'zh_CN', @@ -79,7 +120,7 @@ export class ProjectServerFunctionImpl extends ProtocolContext { const dialogId = nanoid() - ServerFunctionImpl.openDialog( + ProjectServerFunctionImpl.openDialog( [ dialogId, vscode.window.showOpenDialog(options), @@ -106,6 +147,14 @@ export class ProjectServerFunctionImpl extends ProtocolContext { - if (!fs.existsSync(uri.fsPath)) fs.mkdirSync(uri.fsPath, { recursive: true }) + const writes: { outputPath: string, content: Uint8Array }[] = [] for (const [filePath, file] of Object.entries(extractedFileList)) { - const outputPath = path.resolve(uri.fsPath, filePath) - fs.mkdirSync(path.dirname(outputPath), { recursive: true }) - if (!file) continue - if (file.length === 0) continue - fs.writeFileSync(outputPath, file) + if (!file || file.length === 0) continue + writes.push({ + outputPath: resolveArchiveEntryPath(resolvedExtractRoot, filePath), + content: file, + }) + } + + try { + await assertProjectFilesCanBeCreated(writes.map(write => write.outputPath)) + await ensureEmptyProjectDirectory(resolvedExtractRoot) + for (const { outputPath, content } of writes) { + await writeFileExclusive(outputPath, content) + } } + catch (error) { + this.rethrowProjectWritePathError(error) + } + this.getConsola().info('[ServerFunction.DownloadAndExtractTemplate] Success!') }) @@ -169,6 +230,15 @@ export class ProjectServerFunctionImpl extends ProtocolContext, templateName: string, savePath: string): Promise { this.getConsola().info('[ServerFunction.CreateProject] Creating project...', JSON.stringify(context)) if (!this.extensionContext) return this.getConsola().error('[ServerFunction.CreateProject] Extension context not found, cannot create project.') + + let resolvedSavePath: string + try { + resolvedSavePath = await validateProjectWriteDirectory(savePath, os.homedir()) + } + catch (error) { + this.rethrowProjectWritePathError(error) + } + const templatePath = vscode.Uri.joinPath(this.extensionContext.extensionUri, 'templates', templateName) const templateFiles = await vscode.workspace.findFiles(new vscode.RelativePattern(templatePath, '**/*')) @@ -180,12 +250,21 @@ export class ProjectServerFunctionImpl extends ProtocolContext ({ + outputPath: path.resolve(resolvedSavePath, path.relative(templatePath.fsPath, filePath)), + content: fileContent, + })) + + try { + await assertProjectFilesCanBeCreated(writes.map(write => write.outputPath)) + await ensureEmptyProjectDirectory(resolvedSavePath) + for (const { outputPath, content } of writes) { + this.getConsola().info('[ServerFunction.CreateProject] Creating project file:', outputPath) + await writeFileExclusive(outputPath, content) + } + } + catch (error) { + this.rethrowProjectWritePathError(error) } this.getConsola().info('[ServerFunction.CreateProject] Project created successfully!') @@ -197,6 +276,6 @@ export class ProjectServerFunctionImpl extends ProtocolContext connection.getHomeDirectory?.()) +// [新增] 加 loading 状态:homeDirectory 异步获取完成前禁用创建按钮,避免用户在数据就绪前操作 +const { data: homeDirectory, loading: homeDirectoryLoading } = useAsyncData(async () => connection.getHomeDirectory?.()) const { projectConfigurations, currentProjectId, currentProject } = createProjectConfigContext(homeDirectory) const formRef = useTemplateRef('formRef') +// [新增] homeDirectory 未加载完成或为空时,禁止创建项目(按钮置灰 + loading 态) +const canCreateProject = computed(() => !homeDirectoryLoading.value && !!homeDirectory.value) async function handleSubmit(e: Event) { e.preventDefault() + // [新增] 前置守卫:homeDirectory 未就绪时阻止提交,防止空路径引发错误 + if (!canCreateProject.value) return await formRef.value?.validate() await currentProject.value?.onSubmit?.(currentProject as Ref) } @@ -29,7 +34,8 @@ function callItemOnClick(item: import('../../composables/project-configuration') {{ $t('project.templateMarket.title') }} - + +
diff --git a/packages/vscode/src/utils/project-write-path-fs.ts b/packages/vscode/src/utils/project-write-path-fs.ts new file mode 100644 index 00000000..ca93637f --- /dev/null +++ b/packages/vscode/src/utils/project-write-path-fs.ts @@ -0,0 +1,103 @@ +import { Buffer } from 'node:buffer' +import os from 'node:os' +import path from 'node:path' +/** + * 项目写入路径文件系统操作模块。 + * + * 在 project-write-path-guard 的安全校验基础上,封装实际的文件系统写入操作: + * 1. validateProjectWriteDirectory: 校验 + 路径解析 + 空目录检查的组合入口 + * 2. assertProjectFilesCanBeCreated: 批量检查目标文件是否已存在(防覆盖) + * 3. writeFileExclusive: 排他写入,文件已存在则拒绝(防竞态覆盖) + * 4. ensureEmptyProjectDirectory: 创建目录并确保为空 + * + * 调用方:project-server-function.ts 的 downloadAndExtractTemplate 和 createProject + */ +import * as vscode from 'vscode' +import { + assertWritableProjectDirectoryPath, + IGNORED_DIRECTORY_ENTRIES, + ProjectWritePathError, +} from './project-write-path-guard' + +async function uriExists(uri: vscode.Uri): Promise { + try { + await vscode.workspace.fs.stat(uri) + return true + } + catch { + return false + } +} + +export async function listSignificantDirectoryEntries(directoryUri: vscode.Uri): Promise { + if (!await uriExists(directoryUri)) return [] + + const stat = await vscode.workspace.fs.stat(directoryUri) + if (stat.type !== vscode.FileType.Directory) return [] + + const entries = await vscode.workspace.fs.readDirectory(directoryUri) + return entries + .filter(([name]) => !IGNORED_DIRECTORY_ENTRIES.has(name)) + .map(([name]) => name) +} + +export async function validateProjectWriteDirectory( + targetPath: string, + homeDirectory: string = os.homedir(), +): Promise { + const resolved = assertWritableProjectDirectoryPath(targetPath, homeDirectory) + const resolvedUri = vscode.Uri.file(resolved) + + if (await uriExists(resolvedUri)) { + const stat = await vscode.workspace.fs.stat(resolvedUri) + if (stat.type !== vscode.FileType.Directory) { + throw new ProjectWritePathError('NOT_DIRECTORY', 'Project write path is not a directory.', resolved) + } + } + + const existingEntries = await listSignificantDirectoryEntries(resolvedUri) + if (existingEntries.length > 0) { + throw new ProjectWritePathError( + 'NOT_EMPTY', + 'Project write directory is not empty.', + existingEntries.slice(0, 5).join(', '), + ) + } + + return resolved +} + +export async function assertProjectFilesCanBeCreated(outputPaths: string[]): Promise { + for (const outputPath of outputPaths) { + const uri = vscode.Uri.file(outputPath) + if (await uriExists(uri)) { + throw new ProjectWritePathError('FILE_EXISTS', 'A file already exists at the target path.', outputPath) + } + } +} + +export async function writeFileExclusive(outputPath: string, content: string | Uint8Array): Promise { + const uri = vscode.Uri.file(outputPath) + if (await uriExists(uri)) { + throw new ProjectWritePathError('FILE_EXISTS', 'Refusing to overwrite an existing file.', outputPath) + } + + const parentUri = vscode.Uri.file(path.dirname(outputPath)) + await vscode.workspace.fs.createDirectory(parentUri) + const uint8Content = typeof content === 'string' ? Buffer.from(content) : content + await vscode.workspace.fs.writeFile(uri, uint8Content) +} + +export async function ensureEmptyProjectDirectory(directoryPath: string): Promise { + const uri = vscode.Uri.file(directoryPath) + await vscode.workspace.fs.createDirectory(uri) + + const existingEntries = await listSignificantDirectoryEntries(uri) + if (existingEntries.length > 0) { + throw new ProjectWritePathError( + 'NOT_EMPTY', + 'Project write directory is not empty.', + existingEntries.slice(0, 5).join(', '), + ) + } +} diff --git a/packages/vscode/src/utils/project-write-path-guard.ts b/packages/vscode/src/utils/project-write-path-guard.ts new file mode 100644 index 00000000..e5acb73b --- /dev/null +++ b/packages/vscode/src/utils/project-write-path-guard.ts @@ -0,0 +1,128 @@ +/** + * 项目写入路径安全守卫模块。 + * + * 职责: + * 1. 定义 ProjectWritePathError 异常体系,统一表达路径写入相关的各类错误 + * 2. 拦截系统目录、文件系统根目录、Home 目录根等不安全的写入目标 + * 3. 提供 Zip Slip 攻击防护,防止模板压缩包中的恶意路径逃逸 + * 4. 提供 isRootDevEcoStudioProjectsPath 辅助判断,配合前端智能路径替换逻辑 + * + * 调用方:project-configuration.ts(前端表单校验)、project-server-function.ts(服务端写入前校验) + */ +import path from 'node:path' + +export const IGNORED_DIRECTORY_ENTRIES = new Set(['.DS_Store']) + +export type ProjectWritePathErrorCode + = | 'INVALID_PATH' + | 'NOT_DIRECTORY' + | 'NOT_EMPTY' + | 'FILE_EXISTS' + | 'ZIP_SLIP' + | 'BLOCKED_SYSTEM_PATH' + +export class ProjectWritePathError extends Error { + constructor( + public readonly code: ProjectWritePathErrorCode, + message: string, + public readonly detail?: string, + ) { + super(message) + this.name = 'ProjectWritePathError' + } +} + +export function isRootDevEcoStudioProjectsPath(savePath: string): boolean { + return savePath === '/DevEcoStudioProjects' || savePath.startsWith('/DevEcoStudioProjects/') +} + +function isWindowsSystemPath(resolved: string): boolean { + const root = path.parse(resolved).root.toLowerCase() + const lower = resolved.toLowerCase() + const blockedPrefixes = [ + `${root}windows`, + `${root}program files`, + `${root}program files (x86)`, + `${root}programdata`, + ] + return blockedPrefixes.some(prefix => lower === prefix || lower.startsWith(`${prefix}\\`)) +} + +function isUnixSystemPath(resolved: string): boolean { + if (resolved === '/Library' || resolved.startsWith('/Library/')) return true + + const blockedPrefixes = [ + '/etc', + '/usr', + '/bin', + '/sbin', + '/var', + '/opt', + '/System', + '/private/etc', + '/private/var', + ] + return blockedPrefixes.some(prefix => resolved === prefix || resolved.startsWith(`${prefix}/`)) +} + +export function assertWritableProjectDirectoryPath( + targetPath: string, + homeDirectory: string, +): string { + const trimmed = targetPath.trim() + if (!trimmed) { + throw new ProjectWritePathError('INVALID_PATH', 'Project write path is empty.') + } + + const resolved = path.resolve(trimmed) + const homeResolved = path.resolve(homeDirectory) + const root = path.parse(resolved).root + + const trailingBackslashRe = /\\$/ + + if (!root || resolved === root || resolved === root.replace(trailingBackslashRe, '')) { + throw new ProjectWritePathError('BLOCKED_SYSTEM_PATH', 'Cannot write project to filesystem root.', resolved) + } + + if (isRootDevEcoStudioProjectsPath(resolved)) { + throw new ProjectWritePathError('BLOCKED_SYSTEM_PATH', 'Cannot write project to root DevEcoStudioProjects path.', resolved) + } + + // eslint-disable-next-line node/prefer-global/process + const isWin = typeof process !== 'undefined' && process.platform === 'win32' + if (isWin) { + if (isWindowsSystemPath(resolved)) { + throw new ProjectWritePathError('BLOCKED_SYSTEM_PATH', 'Cannot write project to a system directory.', resolved) + } + } + else if (isUnixSystemPath(resolved)) { + throw new ProjectWritePathError('BLOCKED_SYSTEM_PATH', 'Cannot write project to a system directory.', resolved) + } + + if (resolved === homeResolved) { + throw new ProjectWritePathError('BLOCKED_SYSTEM_PATH', 'Cannot write project to home directory root.', resolved) + } + + return resolved +} + +export function resolveArchiveEntryPath(extractRoot: string, entryPath: string): string { + const normalizedEntry = entryPath.replace(/\\/g, '/') + if (path.isAbsolute(normalizedEntry)) { + throw new ProjectWritePathError('ZIP_SLIP', 'Archive entry uses an absolute path.', entryPath) + } + + const segments = normalizedEntry.split('/') + if (segments.includes('..')) { + throw new ProjectWritePathError('ZIP_SLIP', 'Archive entry traverses parent directories.', entryPath) + } + + const resolvedRoot = path.resolve(extractRoot) + const resolvedOutput = path.resolve(extractRoot, entryPath) + const relative = path.relative(resolvedRoot, resolvedOutput) + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new ProjectWritePathError('ZIP_SLIP', 'Archive entry escapes extract directory.', entryPath) + } + + return resolvedOutput +} diff --git a/packages/vscode/test/project-write-path-guard.test.ts b/packages/vscode/test/project-write-path-guard.test.ts new file mode 100644 index 00000000..0872bdee --- /dev/null +++ b/packages/vscode/test/project-write-path-guard.test.ts @@ -0,0 +1,49 @@ +/** + * project-write-path-guard 模块的单元测试。 + * + * 覆盖场景: + * - isRootDevEcoStudioProjectsPath: DevEcoStudioProjects 根/子目录识别 + * - assertWritableProjectDirectoryPath: 系统路径拦截、Home 目录拦截、合法路径放行 + * - resolveArchiveEntryPath: Zip Slip 攻击防护(路径遍历、绝对路径) + */ +import os from 'node:os' +import path from 'node:path' +import { describe, expect, it } from 'vite-plus/test' +import { + assertWritableProjectDirectoryPath, + isRootDevEcoStudioProjectsPath, + ProjectWritePathError, + resolveArchiveEntryPath, +} from '../src/utils/project-write-path-guard' + +const homeDir = os.homedir() + +describe('project-write-path-guard', () => { + it('detects root DevEcoStudioProjects paths', () => { + expect(isRootDevEcoStudioProjectsPath('/DevEcoStudioProjects')).toBe(true) + expect(isRootDevEcoStudioProjectsPath('/DevEcoStudioProjects/MyApplication')).toBe(true) + expect(isRootDevEcoStudioProjectsPath(`${homeDir}/DevEcoStudioProjects/MyApplication`)).toBe(false) + }) + + it('rejects filesystem root and system directories', () => { + expect(() => assertWritableProjectDirectoryPath('/', homeDir)).toThrow(ProjectWritePathError) + expect(() => assertWritableProjectDirectoryPath('/DevEcoStudioProjects/MyApplication', homeDir)).toThrow(ProjectWritePathError) + expect(() => assertWritableProjectDirectoryPath('/etc/project', homeDir)).toThrow(ProjectWritePathError) + expect(() => assertWritableProjectDirectoryPath(homeDir, homeDir)).toThrow(ProjectWritePathError) + }) + + it('allows user project directories', () => { + expect(assertWritableProjectDirectoryPath(`${homeDir}/DevEcoStudioProjects/MyApplication`, homeDir)) + .toBe(path.resolve(`${homeDir}/DevEcoStudioProjects/MyApplication`)) + expect(assertWritableProjectDirectoryPath('/tmp/arkts-project', homeDir)) + .toBe(path.resolve('/tmp/arkts-project')) + }) + + it('blocks zip slip entries', () => { + const extractRoot = `${homeDir}/DevEcoStudioProjects/MyApplication` + expect(resolveArchiveEntryPath(extractRoot, 'entry/src/main/module.json5')) + .toBe(path.resolve(extractRoot, 'entry/src/main/module.json5')) + expect(() => resolveArchiveEntryPath(extractRoot, '../outside.txt')).toThrow(ProjectWritePathError) + expect(() => resolveArchiveEntryPath(extractRoot, '/etc/passwd')).toThrow(ProjectWritePathError) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40402e05..453dfa71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -611,10 +611,10 @@ importers: version: 3.9.1 '@vstils/core': specifier: catalog:prod - version: 0.0.6(@types/vscode@1.116.0) + version: 0.0.6(@types/vscode@1.120.0) '@vstils/fs': specifier: catalog:prod - version: 0.0.5(@types/vscode@1.116.0) + version: 0.0.5(@types/vscode@1.120.0) alien-signals: specifier: catalog:prod version: 3.0.3 @@ -896,7 +896,7 @@ importers: version: 2.4.23 '@vstils/core': specifier: catalog:prod - version: 0.0.6(@types/vscode@1.116.0) + version: 0.0.6(@types/vscode@1.120.0) ts-macro: specifier: catalog:dev version: 0.3.6 @@ -926,10 +926,10 @@ importers: version: 2.4.23 '@vstils/core': specifier: catalog:prod - version: 0.0.6(@types/vscode@1.116.0) + version: 0.0.6(@types/vscode@1.120.0) '@vstils/fs': specifier: catalog:prod - version: 0.0.5(@types/vscode@1.116.0) + version: 0.0.5(@types/vscode@1.120.0) volar-service-typescript: specifier: catalog:prod version: 0.0.68(@volar/language-service@2.4.23) @@ -1062,6 +1062,9 @@ importers: '@napi-rs/wasm-runtime': specifier: ^1.0.7 version: 1.1.1 + volar-service-typescript: + specifier: catalog:prod + version: 0.0.68(@volar/language-service@2.4.23) devDependencies: '@types/vscode': specifier: ^1.22.0 @@ -3251,8 +3254,8 @@ packages: '@types/vscode@1.109.0': resolution: {integrity: sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==} - '@types/vscode@1.116.0': - resolution: {integrity: sha512-sYHp4MO6BqJ2PD7Hjt0hlIS3tMaYsVPJrd0RUjDJ8HtOYnyJIEej0bLSccM8rE77WrC+Xox/kdBwEFDO8MsxNA==} + '@types/vscode@1.120.0': + resolution: {integrity: sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==} '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -10651,7 +10654,7 @@ snapshots: '@types/vscode@1.109.0': {} - '@types/vscode@1.116.0': {} + '@types/vscode@1.120.0': {} '@types/web-bluetooth@0.0.21': {} @@ -10947,7 +10950,7 @@ snapshots: '@unioc/vscode@0.1.16': dependencies: - '@types/vscode': 1.116.0 + '@types/vscode': 1.120.0 '@unioc/core': 0.0.10 '@unioc/decorator': 0.0.7 '@vscode/l10n': 0.0.18 @@ -11473,10 +11476,10 @@ snapshots: transitivePeerDependencies: - '@types/vscode' - '@vstils/core@0.0.6(@types/vscode@1.116.0)': + '@vstils/core@0.0.6(@types/vscode@1.120.0)': dependencies: '@vstils/common': 0.0.1 - '@vstils/fs': 0.0.5(@types/vscode@1.116.0) + '@vstils/fs': 0.0.5(@types/vscode@1.120.0) transitivePeerDependencies: - '@types/vscode' @@ -11488,9 +11491,9 @@ snapshots: tinyglobby: 0.2.15 trash: 10.1.1 - '@vstils/fs@0.0.5(@types/vscode@1.116.0)': + '@vstils/fs@0.0.5(@types/vscode@1.120.0)': dependencies: - '@types/vscode': 1.116.0 + '@types/vscode': 1.120.0 '@vstils/common': 0.0.1 chokidar: 5.0.0 tinyglobby: 0.2.15