diff --git a/.vscode/launch.json b/.vscode/launch.json index 5a2e25418d..011a916e1d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -121,6 +121,24 @@ "smartStep": true, "skipFiles": ["/**"] }, + { + "name": "E2E Test (Monorepo)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/dist-test/test/monorepo", + "--user-data-dir=${workspaceFolder}/test/monorepo/data-dir", + "--disable-extensions", + "${workspaceFolder}/test/monorepo/fixture" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/dist-test/test/**/*.js"], + "smartStep": true, + "skipFiles": ["/**"] + }, { "type": "node", "request": "launch", diff --git a/CHANGELOG.md b/CHANGELOG.md index 55685b335f..8eac2e9fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,32 @@ # Changelog ### 0.31.0 + +---- + +#### 🎉 RFC release 🎉 + +We support monorepo and multi-root workspace in this version. +We have also added a new config file called `vetur.config.js`. + +See more: https://vetur.github.io/vetur/guide/setup.html#advanced +Reference: https://vetur.github.io/vetur/reference/ + +---- + - Fix pug format. #2460 - Fix scss autocompletion. #2522 - Fix templates in custom blocks are parsed as root elements. #1336 +- Support multi-root workspace +- Support monorepo +- Register global components +- Support `vetur.config.js` for monorepo, global components. +- Watch config file changed, Like: `package.json`, `tsconfig.json` +- Warn some probably problem when open project. +- Add `Vetur: doctor` command for debug. +- Improve docs. +- Support yarn PnP support. Thanks to contribution from [@merceyz](https://github.com/merceyz). #2478. + ### 0.30.3 | 2020-11-26 | [VSIX](https://marketplace.visualstudio.com/_apis/public/gallery/publishers/octref/vsextensions/vetur/0.30.3/vspackage) diff --git a/README.md b/README.md index fa2d069e62..fcfbb40187 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,17 @@ Thanks to the following companies for supporting Vetur's development: ## Features -- Syntax-highlighting -- Snippet -- Emmet -- Linting / Error Checking -- Formatting -- Auto Completion -- [Component Data](https://vuejs.github.io/vetur/component-data.html): auto-completion and hover-information for popular Vue frameworks and your own custom components -- [Experimental Interpolation Features](https://vuejs.github.io/vetur/interpolation.html): auto-completion, hover information and type-checking in Vue template -- [VTI](https://vuejs.github.io/vetur/vti.html): Surface template type-checking errors on CLI +- [Syntax-highlighting](/guide/highlighting.md) +- [Snippet](/guide/snippet.md) +- [Emmet](/guide/emmet.md) +- [Linting / Error Checking](/guide/linting-error.md) +- [Formatting](/guide/formatting.md) +- [IntelliSense](/guide/intellisense.md) +- [Debugging](/guide/debugging.md) +- [Component Data](/guide/framework.md): auto-completion and hover-information for popular Vue frameworks and your own custom components +- [Experimental Interpolation Features](/guide/interpolation.md): auto-completion, hover information and type-checking in Vue template +- [VTI](/guide/vti.md): Surface template type-checking errors on CLI +- [Global components](/guide/global-components.md): support define global components. ## Quick Start @@ -75,9 +77,8 @@ Thanks to the following companies for supporting Vetur's development: ## Limitations -- No multi root suppport yet ([#424](https://github.com/vuejs/vetur/issues/424)) -- Cannot handle tsconfig from non-top-level folder ([#815](https://github.com/vuejs/vetur/issues/815)) - You can restart Vue language service when Vetur slow ([#2192](https://github.com/vuejs/vetur/issues/2192)) +- yarn pnp (https://vuejs.github.io/vetur/guide/setup.html#yarn-pnp) ## Roadmap diff --git a/client/commands/doctorCommand.ts b/client/commands/doctorCommand.ts new file mode 100644 index 0000000000..1b42551743 --- /dev/null +++ b/client/commands/doctorCommand.ts @@ -0,0 +1,21 @@ +import vscode from 'vscode'; +import { LanguageClient } from 'vscode-languageclient'; + +export function generateDoctorCommand (client: LanguageClient) { + return async () => { + if (!vscode.window.activeTextEditor || !vscode.window.activeTextEditor.document.fileName.endsWith('.vue')) { + return vscode.window.showInformationMessage( + 'Failed to doctor. Make sure the current file is a .vue file.' + ); + } + + const fileName = vscode.window.activeTextEditor.document.fileName; + + const result = await client.sendRequest('$/doctor', { fileName }) as string; + const showText = result.slice(0, 1000) + '....'; + const action = await vscode.window.showInformationMessage(showText, { modal: true }, 'Ok', 'Copy'); + if (action === 'Copy') { + await vscode.env.clipboard.writeText(result); + } + }; +} diff --git a/client/vueMain.ts b/client/vueMain.ts index 665fc58528..6cec587cbc 100644 --- a/client/vueMain.ts +++ b/client/vueMain.ts @@ -11,6 +11,7 @@ import { } from './commands/virtualFileCommand'; import { getGlobalSnippetDir } from './userSnippetDir'; import { generateOpenUserScaffoldSnippetFolderCommand } from './commands/openUserScaffoldSnippetFolderCommand'; +import { generateDoctorCommand } from './commands/doctorCommand'; export async function activate(context: vscode.ExtensionContext) { const isInsiders = vscode.env.appName.includes('Insiders'); @@ -86,14 +87,8 @@ function registerRestartVLSCommand(context: vscode.ExtensionContext, client: Lan } function registerCustomClientNotificationHandlers(client: LanguageClient) { - client.onNotification('$/displayInfo', (msg: string) => { - vscode.window.showInformationMessage(msg); - }); - client.onNotification('$/displayWarning', (msg: string) => { - vscode.window.showWarningMessage(msg); - }); - client.onNotification('$/displayError', (msg: string) => { - vscode.window.showErrorMessage(msg); + client.onNotification('$/openWebsite', (url: string) => { + vscode.env.openExternal(vscode.Uri.parse(url)); }); client.onNotification('$/showVirtualFile', (virtualFileSource: string, prettySourceMap: string) => { setVirtualContents(virtualFileSource, prettySourceMap); @@ -102,6 +97,8 @@ function registerCustomClientNotificationHandlers(client: LanguageClient) { function registerCustomLSPCommands(context: vscode.ExtensionContext, client: LanguageClient) { context.subscriptions.push( - vscode.commands.registerCommand('vetur.showCorrespondingVirtualFile', generateShowVirtualFileCommand(client)) + vscode.commands.registerCommand('vetur.showCorrespondingVirtualFile', generateShowVirtualFileCommand(client)), + vscode.commands.registerCommand('vetur.showOutputChannel', () => client.outputChannel.show()), + vscode.commands.registerCommand('vetur.showDoctorInfo', generateDoctorCommand(client)) ); } diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 813b0c7f0e..68f32291ff 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -9,24 +9,38 @@ module.exports = { repo: 'vuejs/vetur', editLinks: true, docsDir: 'docs', - sidebar: [ - '/setup', - { - title: 'Features', - collapsable: false, - children: [ - '/highlighting', - '/snippet', - '/emmet', - '/linting-error', - '/formatting', - '/intellisense', - '/debugging', - '/component-data', - '/interpolation', - '/vti' - ] - } - ] + nav: [ + { text: 'Guide', link: '/guide/' }, + { text: 'Reference', link: '/reference/' }, + { text: 'FAQ', link: '/guide/FAQ' }, + { text: 'Roadmap', link: 'https://github.com/vuejs/vetur/issues/873' }, + { text: 'Credits', link: '/credits' }, + { text: 'Contribution Guide', link: 'https://github.com/vuejs/vetur/wiki#contribution-guide' } + ], + sidebar: { + '/guide/': [ + '', + 'setup', + { + title: 'Features', + collapsable: false, + children: [ + 'highlighting', + 'snippet', + 'emmet', + 'linting-error', + 'formatting', + 'intellisense', + 'debugging', + 'component-data', + 'interpolation', + 'vti', + 'global-components' + ] + }, + 'FAQ' + ], + '/reference/': ['', 'tsconfig'] + } } }; diff --git a/docs/README.md b/docs/README.md index 9a8d33402e..bdab945c7f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,16 +15,17 @@ You can [open an issue](https://github.com/vuejs/vetur/issues/new) for bugs or f ## Features -- [Syntax-highlighting](highlighting.md) -- [Snippet](snippet.md) -- [Emmet](emmet.md) -- [Linting / Error Checking](linting-error.md) -- [Formatting](formatting.md) -- [IntelliSense](intellisense.md) -- [Debugging](debugging.md) -- [Component Data](framework.md): auto-completion and hover-information for popular Vue frameworks and your own custom components -- [Experimental Interpolation Features](interpolation.md): auto-completion, hover information and type-checking in Vue template -- [VTI](vti.md): Surface template type-checking errors on CLI +- [Syntax-highlighting](/guide/highlighting.md) +- [Snippet](/guide/snippet.md) +- [Emmet](/guide/emmet.md) +- [Linting / Error Checking](/guide/linting-error.md) +- [Formatting](/guide/formatting.md) +- [IntelliSense](/guide/intellisense.md) +- [Debugging](/guide/debugging.md) +- [Component Data](/guide/framework.md): auto-completion and hover-information for popular Vue frameworks and your own custom components +- [Experimental Interpolation Features](/guide/interpolation.md): auto-completion, hover information and type-checking in Vue template +- [VTI](/guide/vti.md): Surface template type-checking errors on CLI +- [Global components](/guide/global-components.md): support define global components. ## Quick Start diff --git a/docs/credits.md b/docs/credits.md index 41f4aecacb..a91a680dc5 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -5,6 +5,12 @@ Main Developer: [@octref](https://github.com/octref) Contributors: - [@HerringtonDarkholme](https://github.com/HerringtonDarkholme) - [@sandersn](https://github.com/sandersn) +- [@yoyo930021](https://github.com/yoyo930021) +- [@ktsn](https://github.com/ktsn) +- [@rchl](https://github.com/rchl) +- [@Uninen](https://github.com/Uninen) + +Others: https://github.com/vuejs/vetur/graphs/contributors ### Attributions @@ -15,3 +21,5 @@ Others: - Grammar based on [vuejs/vue-syntax-highlight](https://github.com/vuejs/vue-syntax-highlight) - Sass grammar based on [TheRealSyler/vscode-sass-indented](https://github.com/TheRealSyler/vscode-sass-indented) - PostCSS grammar based on [hudochenkov/Syntax-highlighting-for-PostCSS](https://github.com/hudochenkov/Syntax-highlighting-for-PostCSS) +- TypeScript/JavaScript based on [TypeScript](https://github.com/microsoft/TypeScript/#readme) +- CSS/SCSS/LESS feature based on [vscode-css-languageservice](https://github.com/microsoft/vscode-css-languageservice) diff --git a/docs/framework.md b/docs/framework.md deleted file mode 100644 index b00b01be57..0000000000 --- a/docs/framework.md +++ /dev/null @@ -1,3 +0,0 @@ -# Framework Support - -Renamed as [Component Data Support](./component-data.md) \ No newline at end of file diff --git a/docs/FAQ.md b/docs/guide/FAQ.md similarity index 73% rename from docs/FAQ.md rename to docs/guide/FAQ.md index 17ffc49eec..a1188286f6 100644 --- a/docs/FAQ.md +++ b/docs/guide/FAQ.md @@ -1,17 +1,5 @@ # FAQ -- [Install an old version of Vetur](#install-an-old-version-of-vetur) -- [No Syntax Highlighting & No Language Features working](#no-syntax-highlighting--no-language-features-working) -- [Vetur Crash](#vetur-crash) -- [Vetur can't recognize components imported using webpack's alias](#vetur-cant-recognize-components-imported-using-webpacks-alias) -- [Property 'xxx' does not exist on type 'CombinedVueInstance'](#property-xxx-does-not-exist-on-type-combinedvueinstance) -- [Vetur cannot recognize my Vue component import, such as `import Comp from './comp'`](#vetur-cannot-recognize-my-vue-component-import-such-as-import-comp-from-comp) -- [Template Interpolation auto completion does not work](#template-interpolation-auto-completion-does-not-work) -- [.vue file cannot be imported in TS file](#vue-file-cannot-be-imported-in-ts-file) -- [How to build and install from source](#how-to-build-and-install-from-source) -- [Vetur uses different version of TypeScript in .vue files to what I installed in `node_modules`.](#vetur-uses-different-version-of-typescript-in-vue-files-to-what-i-installed-in-node_modules) -- [Vetur is slow](#vetur-is-slow) - ## Install an old version of Vetur Sometimes new releases have bugs that you want to avoid. Here's an easy way to downgrade Vetur to a working version: @@ -131,4 +119,46 @@ NB: It will use `typescript.tsdk` setting as the path to look for if defined, de You can run the command `Vetur: Restart VLS (Vue Language Server)` to restart VLS. -However, we'd appreciate it if you can file a [performance issue report with a profile](https://github.com/vuejs/vetur/blob/master/.github/PERF_ISSUE.md) to help us find the cause of the issue. \ No newline at end of file +However, we'd appreciate it if you can file a [performance issue report with a profile](https://github.com/vuejs/vetur/blob/master/.github/PERF_ISSUE.md) to help us find the cause of the issue. + +## Vetur can't find `tsconfig.json`, `jsconfig.json` in /xxxx/xxxxxx. + +If you don't have any `tsconfig.json`, `jsconfig.json` in project, +Vetur will use fallback settings. Some feature isn't work. +Like: path alias, decorator, import json. + +You can add this config in correct position in project. +Or use `vetur.config.js` to set file path in project. + +- [Read more project setup](/guide/setup.html#project-setup) +- [Read more `vetur.config.js`](/guide/setup.html#advanced) + +If you want debug info, you can use `Vetur: show doctor info` command. +You can use `vetur.ignoreProjectWarning: false` in vscode setting to close this warning. + +## Vetur can't find `package.json` in /xxxx/xxxxxx. + +If you don't have any `package.json` in project, Vetur can't know vue version and component data from other libs. +Vetur assume that vue version is less than 2.5 in your project. +If the version is wrong, you will get wrong diagnostic from typescript and eslint template validation. + +You can add this config in correct position in project. +Or use `vetur.config.js` to set file path in project. + +- [Read more `vetur.config.js`](/guide/setup.html#advanced) + +If you want debug info, you can use `Vetur: show doctor info` command. +You can use `vetur.ignoreProjectWarning: false` in vscode setting to close this warning. + +## Vetur find xxx, but they aren\'t in the project root. +Vetur find the file, but it may not actually be what you want. +If it is wrong, it will cause same result as the previous two. [ref1](/guide/FAQ.html#vetur-can-t-find-tsconfig-json-jsconfig-json-in-xxxx-xxxxxx), [ref2](/guide/FAQ.html#vetur-can-t-find-package-json-in-xxxx-xxxxxx) + +You can add this config in correct position in project. +Or use `vetur.config.js` to set file path in project. + +- [Read more `vetur.config.js`](/guide/setup.html#advanced) + +If you want debug info, you can use `Vetur: show doctor info` command. +You can use `vetur.ignoreProjectWarning: false` in vscode setting to close this warning. + diff --git a/docs/guide/Readme.md b/docs/guide/Readme.md new file mode 100644 index 0000000000..3a3d466d85 --- /dev/null +++ b/docs/guide/Readme.md @@ -0,0 +1,50 @@ +# Quick start + +Here are five common case. + +- [Vue CLI](#vue-cli) +- [Veturpack]($veturpack) +- [Laravel](#laravel-custom-project) +- [Custom project](#laravel-custom-project) +- [Monorepo](#monorepo) + +## Vue CLI +[Offical Website](https://cli.vuejs.org/) +When you create project with Vue CLI, +If no use typescript, please add `jsconfig.json` at opened project root. +```json +{ + "compilerOptions": { + "target": "es2015", + "module": "esnext", + "baseUrl": "./", + "paths": { + "@/*": ["components/*"] + } + }, + "include": [ + "src/**/*.vue", + "src/**/*.js" + ] +} +``` +[Add shim-types file for import Vue SFC in typescript file.](/guide/setup.html#typescript) + +If use typescript, you don't need to do any thing in your project. + +## Veturpack +[Github](https://github.com/octref/veturpack) +It is out of box. + +## Laravel / Custom project +Please keep `package.json` and `tsconfig.json`/`jsconfig.json` at opened project root. +If you can't do it, please add `vetur.config.js` for set config file path. +[Add shim-types file for import Vue SFC in typescript file.](/guide/setup.html#typescript) + +- [Read more `tsconfig.json`/`jsconfig.json`](/guide/setup.html#project-setup). +- [Read more `vetur.config.js` doc](/guide/setup.html#advanced). + +## Monorepo +please add `vetur.config.js` for define projects. + +- [Read more `vetur.config.js` doc](/guide/setup.html#advanced). diff --git a/docs/component-data.md b/docs/guide/component-data.md similarity index 100% rename from docs/component-data.md rename to docs/guide/component-data.md diff --git a/docs/debugging.md b/docs/guide/debugging.md similarity index 100% rename from docs/debugging.md rename to docs/guide/debugging.md diff --git a/docs/emmet.md b/docs/guide/emmet.md similarity index 100% rename from docs/emmet.md rename to docs/guide/emmet.md diff --git a/docs/formatting.md b/docs/guide/formatting.md similarity index 100% rename from docs/formatting.md rename to docs/guide/formatting.md diff --git a/docs/guide/global-components.md b/docs/guide/global-components.md new file mode 100644 index 0000000000..ad65b30bbb --- /dev/null +++ b/docs/guide/global-components.md @@ -0,0 +1,35 @@ +# Global components + +Vetur support define global components. +You can register template interpolation for that components anywhere in the project. + +Please add `projects.globalComponents` in `vetur.config.js`. + +## Example +When your project isn't a monorepo and `package.json/(ts|js)config.json` at project root. +```javascript +// vetur.config.js +/** @type {import('vti').VeturConfig} */ +module.exports = { + projects: [ + { + root: './', + // **optional** default: `[]` + // Register globally Vue component glob. + // If you set it, you can get completion by that components. + // It is relative to root property. + // Notice: It won't actually do it. You need to use `require.context` or `Vue.component` + globalComponents: [ + './src/components/**/*.vue', + { + // Component name + name: 'FakeButton', + // Component file path, please use '/'. + path: './src/app/components/AppButton.vue' + } + ] + } + ] +} +``` + diff --git a/docs/highlighting.md b/docs/guide/highlighting.md similarity index 100% rename from docs/highlighting.md rename to docs/guide/highlighting.md diff --git a/docs/intellisense.md b/docs/guide/intellisense.md similarity index 100% rename from docs/intellisense.md rename to docs/guide/intellisense.md diff --git a/docs/interpolation.md b/docs/guide/interpolation.md similarity index 100% rename from docs/interpolation.md rename to docs/guide/interpolation.md diff --git a/docs/linting-error.md b/docs/guide/linting-error.md similarity index 100% rename from docs/linting-error.md rename to docs/guide/linting-error.md diff --git a/docs/guide/setup.md b/docs/guide/setup.md new file mode 100644 index 0000000000..8192e15825 --- /dev/null +++ b/docs/guide/setup.md @@ -0,0 +1,179 @@ +# Setup + +## Extensions + +- Install [Sass](https://marketplace.visualstudio.com/items?itemName=Syler.sass-indented) for sass syntax highlighting. +- Install [language-stylus](https://marketplace.visualstudio.com/items?itemName=sysoev.language-stylus) for stylus syntax highlighting. +- Install [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for linting vue and js files. + +## VS Code Config + +- Add `vue` to your `eslint.validate` setting, for example: + + ```json + "eslint.validate": [ + "javascript", + "javascriptreact", + "vue" + ] + ``` + +## Project Setup + +- At project root exist `package.json` file, Vetur use it for infer vue version and get component date. +- At project root create a `jsconfig.json` or `tsconfig.json` that `include` all vue files and files that they import from, for example: + +- `jsconfig.json` + + ```json + { + "include": [ + "./src/**/*" + ] + } + ``` + +- `tsconfig.json` + + ```json + { + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "module": "es2015", + "moduleResolution": "node", + "target": "es5", + "sourceMap": true, + "allowJs": true + } + } + ``` +- [What is a tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) +- [Reference](https://www.typescriptlang.org/tsconfig) + +#### jsconfig vs tsconfig + +- Use `tsconfig` for pure TS project. +- Use `jsconfig` for pure JS project. +- Use `jsconfig` or `tsconfig` with `allowJs: true` for mixed JS / TS project. + +### Path mapping + +If you are using [Webpack's alias](https://webpack.js.org/configuration/resolve/) or [TypeScript's path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html) to resolve components, you need to update Vetur's `tsconfig.json` or `jsconfig.json`. + +For example: + +```html +└── src + ├── components + │ ├── a.vue + │ └── b.vue + ├── containers + │ └── index.vue + ├── index.js + └── jsconfig.json +``` + +jsconfig.json: + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "components/*": [ + "src/components/*" + ] + } + } +} +``` + +index.vue + +```javascript +import a from 'components/a.vue' +import b from 'components/b.vue' +``` + +## Typescript + +You need to add a shim type file for import vue SFC in typescript file. +### Vue2 +```typescript +// shims-vue.d.ts +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} +``` +### Vue3 +```typescript +// shims-vue.d.ts +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} +``` + + +## Advanced +If you use monorepo or VTI or not exist `package.json` and `tsconfig.json/jsconfig.json` at project root, +You can use `vetur.config.js` for advanced setting. + +Please add `vetur.config.js` at project root or monorepo project root. +```javascript +// vetur.config.js +/** @type {import('vti').VeturConfig} */ +module.exports = { + // **optional** default: `{}` + // override vscode settings + // Notice: It only affects the settings used by Vetur. + settings: { + "vetur.useWorkspaceDependencies": true, + "vetur.experimental.templateInterpolationService": true + }, + // **optional** default: `[{ root: './' }]` + // support monorepos + projects: [ + './packages/repo2', // shorthand for only root. + { + // **required** + // Where is your project? + // It is relative to `vetur.config.js`. + root: './packages/repo1', + // **optional** default: `'package.json'` + // Where is `package.json` in the project? + // We use it to determine the version of vue. + // It is relative to root property. + package: './package.json', + // **optional** + // Where is TypeScript config file in the project? + // It is relative to root property. + tsconfig: './tsconfig.json', + // **optional** default: `'./.vscode/vetur/snippets'` + // Where is vetur custom snippets folders? + snippetFolder: './.vscode/vetur/snippets' + // **optional** default: `[]` + // Register globally Vue component glob. + // If you set it, you can get completion by that components. + // It is relative to root property. + // Notice: It won't actually do it. You need to use `require.context` or `Vue.component` + globalComponents: [ + './src/components/**/*.vue' + ] + } + ] +} +``` + +- [Read more `vetur.config.js` reference](/reference/). +- [Read RFC](https://github.com/vuejs/vetur/blob/master/rfcs/001-vetur-config-file.md). + +## Yarn pnp +Vetur support this project now, but have some limits. + +- Don't mix common project and pnp project in multi-root/monorepo +- Prettier don't support yarn pnp, so can't load plugin automatic. diff --git a/docs/snippet.md b/docs/guide/snippet.md similarity index 100% rename from docs/snippet.md rename to docs/guide/snippet.md diff --git a/docs/vti.md b/docs/guide/vti.md similarity index 93% rename from docs/vti.md rename to docs/guide/vti.md index 0de1fda0c7..188f0ccf62 100644 --- a/docs/vti.md +++ b/docs/guide/vti.md @@ -28,6 +28,8 @@ vti diagnostics ![VTI demo](https://user-images.githubusercontent.com/4033249/72225084-911ef580-3581-11ea-9943-e7165126ace9.gif). +You also can use [`vetur.config.js`](/reference/) for setting VTI. + Currently, this is only used for generating interpolation type-checking errors on CLI, which neither Vue's compiler nor Webpack would catch. diff --git a/docs/reference/Readme.md b/docs/reference/Readme.md new file mode 100644 index 0000000000..be0ebc4eca --- /dev/null +++ b/docs/reference/Readme.md @@ -0,0 +1,201 @@ +# `vetur.config.js` + +A new configuration file for Vetur and VTI + +## Example + +```javascript +// vetur.config.js +/** @type {import('vti').VeturConfig} */ +module.exports = { + // **optional** default: `{}` + // override vscode settings part + // Notice: It only affects the settings used by Vetur. + settings: { + "vetur.useWorkspaceDependencies": true, + "vetur.experimental.templateInterpolationService": true + }, + // **optional** default: `[{ root: './' }]` + // support monorepos + projects: [ + './packages/repo2', // shorthand for only root. + { + // **required** + // Where is your project? + // It is relative to `vetur.config.js`. + root: './packages/repo1', + // **optional** default: `'package.json'` + // Where is `package.json` in the project? + // We use it to determine the version of vue. + // It is relative to root property. + package: './package.json', + // **optional** + // Where is TypeScript config file in the project? + // It is relative to root property. + tsconfig: './tsconfig.json', + // **optional** default: `'./.vscode/vetur/snippets'` + // Where is vetur custom snippets folders? + snippetFolder: './.vscode/vetur/snippets' + // **optional** default: `[]` + // Register globally Vue component glob. + // If you set it, you can get completion by that components. + // It is relative to root property. + // Notice: It won't actually do it. You need to use `require.context` or `Vue.component` + globalComponents: [ + './src/components/**/*.vue' + ] + } + ] +} +``` + +## Noun +- Vetur: a VSCode extension for Vue support. +- VTI: a CLI for Vue file type-check, diagnostics or some feature. +- VLS: vue language server, The core of everything. It is base on [language server protocol](https://microsoft.github.io/language-server-protocol/). + +## Spec +- All path formats are used with `/`. + > Helpful for cross-platform use project. +- Only support commonjs format. + > We can use it quickly and directly. +- Use pure JavaScript. + > Same as above. + > You can get typings like `@JSDoc`. +- UTF-8 charset + +## How to use + +### VTI +You can use it to override VTI default settings. +```bash +vti `action` +vti -c vetur.config.js `action` +vti --config vetur.config.js `action` +``` + +### Vetur +This profile takes precedence over vscode setting. +It will find it when Vetur initialization. +If it isn't exist, It will use `{ settings: {}, projects: ['./'] }`. +This will ensure consistency with past behavior. + +### How to find `vetur.config.js` +- Start from the root and work your way up until the file is found. +- The root is set `process.cwd()` value in VTI and you can set file path in CLI params. + +PS. Each root can have its own vetur.config.js in VSCode Multi root feature. + +## Detail + +### Definition +```typescript +type Glob = string + +export interface VeturConfig { + settings?: { [key: string]: boolean | string | Enum }, + projects?: Array + }> +} +``` + +### `settings` +Incoming to vue language server config. + +In VLS, it will merge (vscode setting or VTL default config) and vetur.config.js `settings`. +```typescript +import _ from 'lodash' + +// original vscode config or VTI default config +const config: VLSFullConfig = params.initializationOptions?.config + ? _.merge(getDefaultVLSConfig(), params.initializationOptions.config) + : getDefaultVLSConfig(); + +// From vetur.config.js +const veturConfig = getVeturConfigInWorkspace() +// Merge vetur.config.js +Object.keys(veturConfig.setting).forEach((key) => { + _.set(config, key, veturConfig.setting[key]) +}) +``` + +Notice: It only affects the settings used by Vetur. +For example, we use `typescript.preferences.quoteStyle` in Vetur. so you can set it. +But it don't affect original TypeScript support in VSCode. + +### `projects` +The monorepo need a baseline or logic. +Possible options are `package.json` or `tsconfig.js`. +But both are used for node and typescript projects. +We're likely to waste unnecessary resources on things we don't need. +So I figured the best way to do it was through the setup. + +For detailed discussion, see this [RFC](https://github.com/vuejs/vetur/pull/2377). + +if `projects[]` is only a string, It is a shorthand when you only need to define `root`. + +### `projects[].root` +All runtime dependencies is base on value of this property. +Like `typescript`, `prettier`, `@prettier/pug`. +Also Vetur find `./package.json` and `./tsconfig.js` by default. + +### `projects[].package` +We can get the project name or dependency info from here. +But We only use it to determine the version of vue now. +But it doesn't rule out the use of more. + +### `projects[].tsconfig` +Typescript project profile. +It's the key to helping us support JavaScript and TypeScript. +We also use it for support template interpolation. + +#### Why isn't array? +If you are familiar with typescript, You know TypeScript allow support multiple discrete `tsconfig`. +But in the vue ecosystem, It's almost completely unsupported. +For example, We often use webpack to compile Vue projects. +The `vue-loader` call `ts-loader` for support typescript. +But `ts-loader` is only support only one `tsconfig.json`. + +For these reasons, we also don't support it. +It can reduce development and maintenance costs. + +PS. `jsconfig.json` is also support it. + +### `projects[].snippetFolder` +Vetur Custom snippets folder path + +### `projects[].globalComponents` +We have some amazing features, Like `template interpolation`. +But it only work when register component in component. +For example: +```javascript +import Comp from '@/components/Comp.vue' + +export default { + components: { + Comp + } +} +``` + +With this property available, we will parse vue component files that match the glob on vls startup. +You can support `template interpolation` for that components anywhere in the project. + +This property allow two type values in array. +- Glob (`string`) [format](https://github.com/mrmlnc/fast-glob#pattern-syntax) + Vetur will call glob lib with `projects[].root` for loading component when value is string. + It use `path.basename(fileName, path.extname(fileName))` as component name. +- Object (`{ name: string, path: string }`) + Vetur use this data directly. + It's the most flexible way. + If this is a relative path, It is based on `projects[].root`. + +Notice: It won't actually do it. You need to use `require.context` and `Vue.component` in your project. [more](https://vuejs.org/v2/guide/components-registration.html#Automatic-Global-Registration-of-Base-Components) + + +- [Read RFC](https://github.com/vuejs/vetur/blob/master/rfcs/001-vetur-config-file.md). diff --git a/docs/reference/package.md b/docs/reference/package.md new file mode 100644 index 0000000000..3525bd67f2 --- /dev/null +++ b/docs/reference/package.md @@ -0,0 +1,4 @@ +## package.json + +Same as `package.json` in nodejs project. +Vetur infer vue version and support other libs from this file. diff --git a/docs/reference/tsconfig.md b/docs/reference/tsconfig.md new file mode 100644 index 0000000000..b6324aaee1 --- /dev/null +++ b/docs/reference/tsconfig.md @@ -0,0 +1,3 @@ +# Javascript / Typescript config + +See https://www.typescriptlang.org/tsconfig diff --git a/docs/roadmap.md b/docs/roadmap.md deleted file mode 100644 index d6a97488cf..0000000000 --- a/docs/roadmap.md +++ /dev/null @@ -1,3 +0,0 @@ -# Roadmap - -See https://github.com/vuejs/vetur/issues/873. \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md deleted file mode 100644 index 04e5db8350..0000000000 --- a/docs/setup.md +++ /dev/null @@ -1,95 +0,0 @@ -# Setup - -## Extensions - -- Install [Sass](https://marketplace.visualstudio.com/items?itemName=Syler.sass-indented) for sass syntax highlighting. -- Install [language-stylus](https://marketplace.visualstudio.com/items?itemName=sysoev.language-stylus) for stylus syntax highlighting. -- Install [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for linting vue and js files. - -## VS Code Config - -- Add `vue` to your `eslint.validate` setting, for example: - - ```json - "eslint.validate": [ - "javascript", - "javascriptreact", - "vue" - ] - ``` - -## Project Setup - -- At project root create a `jsconfig.json` or `tsconfig.json` that `include` all vue files and files that they import from, for example: - -- `jsconfig.json` - - ```json - { - "include": [ - "./src/**/*" - ] - } - ``` - -- `tsconfig.json` - - ```json - { - "include": [ - "./src/**/*" - ], - "compilerOptions": { - "module": "es2015", - "moduleResolution": "node", - "target": "es5", - "sourceMap": true, - "allowJs": true - } - } - ``` - -#### jsconfig vs tsconfig - -- Use `tsconfig` for pure TS project. -- Use `jsconfig` for pure JS project. -- Use `jsconfig` or `tsconfig` with `allowJs: true` for mixed JS / TS project. - -### Path mapping - -If you are using [Webpack's alias](https://webpack.js.org/configuration/resolve/) or [TypeScript's path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html) to resolve components, you need to update Vetur's `tsconfig.json` or `jsconfig.json`. - -For example: - -```html -└── src - ├── components - │ ├── a.vue - │ └── b.vue - ├── containers - │ └── index.vue - ├── index.js - └── jsconfig.json -``` - -jsconfig.json: - -```json -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "components/*": [ - "src/components/*" - ] - } - } -} -``` - -index.vue - -```javascript -import a from 'components/a.vue' -import b from 'components/b.vue' -``` diff --git a/package.json b/package.json index 67f40e6bd1..37343129cb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:int": "node ./dist-test/test/codeTestRunner.js interpolation", "test:vue3": "node ./dist-test/test/codeTestRunner.js vue3", "test:componentData": "node ./dist-test/test/codeTestRunner.js componentData", + "test:monorepo": "node ./dist-test/test/codeTestRunner.js monorepo", "test": "run-s test:server test:e2e", "docs": "bash ./build/update-docs.sh", "prepare-publish": "./build/release-cleanup.sh" @@ -64,19 +65,33 @@ "commands": [ { "command": "vetur.restartVLS", - "title": "Vetur: Restart VLS (Vue Language Server)" + "category": "Vetur", + "title": "Restart VLS (Vue Language Server)" }, { "command": "vetur.generateGrammar", - "title": "Vetur: Generate grammar from `vetur.grammar.customBlocks`" + "category": "Vetur", + "title": "Generate grammar from `vetur.grammar.customBlocks`" }, { "command": "vetur.showCorrespondingVirtualFile", - "title": "Vetur: Show corresponding virtual file and sourcemap" + "category": "Vetur", + "title": "Show corresponding virtual file and sourcemap" }, { "command": "vetur.openUserScaffoldSnippetFolder", - "title": "Vetur: Open user scaffold snippet folder" + "category": "Vetur", + "title": "Open user scaffold snippet folder" + }, + { + "command": "vetur.showOutputChannel", + "category": "Vetur", + "title": "Show Output Channel" + }, + { + "command": "vetur.showDoctorInfo", + "category": "Vetur", + "title": "Show Doctor info" } ], "breakpoints": [ @@ -183,6 +198,12 @@ "configuration": { "title": "Vetur", "properties": { + "vetur.ignoreProjectWarning": { + "type": "boolean", + "default": false, + "description": "Vetur will warn about not setup correctly for the project. You can disable it.", + "scope": "application" + }, "vetur.useWorkspaceDependencies": { "type": "boolean", "default": false, diff --git a/server/src/config.ts b/server/src/config.ts index 355a3792c4..c8f6ecebd0 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,3 +1,9 @@ +import path from 'path'; +import { getPathDepth, normalizeFileNameToFsPath, normalizeFileNameResolve } from './utils/paths'; +import fg from 'fast-glob'; +import { findConfigFile } from './utils/workspace'; +import { flatten } from 'lodash'; + export interface VLSFormatConfig { defaultFormatter: { [lang: string]: string; @@ -15,6 +21,7 @@ export interface VLSFormatConfig { export interface VLSConfig { vetur: { + ignoreProjectWarning: boolean; useWorkspaceDependencies: boolean; completion: { autoImport: boolean; @@ -71,6 +78,7 @@ export interface VLSFullConfig extends VLSConfig { emmet?: any; html?: any; css?: any; + sass?: any; javascript?: any; typescript?: any; prettier?: any; @@ -80,6 +88,7 @@ export interface VLSFullConfig extends VLSConfig { export function getDefaultVLSConfig(): VLSFullConfig { return { vetur: { + ignoreProjectWarning: false, useWorkspaceDependencies: false, validation: { template: true, @@ -141,3 +150,92 @@ export function getDefaultVLSConfig(): VLSFullConfig { stylusSupremacy: {} }; } + +export interface BasicComponentInfo { + name: string; + path: string; +} + +export type Glob = string; + +export interface VeturProject { + root: string; + package?: string; + tsconfig?: string; + snippetFolder: string; + globalComponents: C[]; +} + +export interface VeturFullConfig { + settings: Record; + projects: VeturProject[]; +} + +export type VeturConfig = Partial> & { + projects?: Array & Partial)>; +}; + +export async function getVeturFullConfig( + rootPathForConfig: string, + workspacePath: string, + veturConfig: VeturConfig +): Promise { + const oldProjects = veturConfig.projects ?? [workspacePath]; + const projects = oldProjects + .map(project => { + const getFallbackPackagePath = (projectRoot: string) => { + const fallbackPackage = findConfigFile(projectRoot, 'package.json'); + return fallbackPackage ? normalizeFileNameToFsPath(fallbackPackage) : undefined; + }; + const getFallbackTsconfigPath = (projectRoot: string) => { + const jsconfigPath = findConfigFile(projectRoot, 'jsconfig.json'); + const tsconfigPath = findConfigFile(projectRoot, 'tsconfig.json'); + if (jsconfigPath && tsconfigPath) { + const tsconfigFsPath = normalizeFileNameToFsPath(tsconfigPath); + const jsconfigFsPath = normalizeFileNameToFsPath(jsconfigPath); + return getPathDepth(tsconfigPath, '/') >= getPathDepth(jsconfigFsPath, '/') ? tsconfigFsPath : jsconfigFsPath; + } + const configPath = tsconfigPath || jsconfigPath; + return configPath ? normalizeFileNameToFsPath(configPath) : undefined; + }; + + if (typeof project === 'string') { + const projectRoot = normalizeFileNameResolve(rootPathForConfig, project); + + return { + root: projectRoot, + package: getFallbackPackagePath(projectRoot), + tsconfig: getFallbackTsconfigPath(projectRoot), + snippetFolder: normalizeFileNameResolve(projectRoot, '.vscode/vetur/snippets'), + globalComponents: [] + } as VeturProject; + } + + const projectRoot = normalizeFileNameResolve(rootPathForConfig, project.root); + return { + root: projectRoot, + package: project.package ?? getFallbackPackagePath(projectRoot), + tsconfig: project.tsconfig ?? getFallbackTsconfigPath(projectRoot), + snippetFolder: normalizeFileNameResolve(projectRoot, '.vscode/vetur/snippets'), + globalComponents: flatten( + project.globalComponents?.map(comp => { + if (typeof comp === 'string') { + return fg + .sync(comp, { cwd: normalizeFileNameResolve(rootPathForConfig, projectRoot), absolute: true }) + .map(fileName => ({ + name: path.basename(fileName, path.extname(fileName)), + path: normalizeFileNameToFsPath(fileName) + })); + } + return comp; + }) ?? [] + ) + } as VeturProject; + }) + .sort((a, b) => getPathDepth(b.root, '/') - getPathDepth(a.root, '/')); + + return { + settings: veturConfig.settings ?? {}, + projects + } as VeturFullConfig; +} diff --git a/server/src/embeddedSupport/languageModes.ts b/server/src/embeddedSupport/languageModes.ts index 1822e794c7..ae6183a2d9 100644 --- a/server/src/embeddedSupport/languageModes.ts +++ b/server/src/embeddedSupport/languageModes.ts @@ -35,11 +35,12 @@ import { VueInfoService } from '../services/vueInfoService'; import { DependencyService } from '../services/dependencyService'; import { nullMode } from '../modes/nullMode'; import { getServiceHost, IServiceHost } from '../services/typescriptService/serviceHost'; -import { VLSFullConfig } from '../config'; +import { BasicComponentInfo, VLSFullConfig } from '../config'; import { SassLanguageMode } from '../modes/style/sass/sassLanguageMode'; import { getPugMode } from '../modes/pug'; import { VCancellationToken } from '../utils/cancellationToken'; -import { createAutoImportVueService } from '../services/autoImportVueService'; +import { createAutoImportSfcPlugin } from '../modes/plugins/autoImportSfcPlugin'; +import { EnvironmentService } from '../services/EnvironmentService'; export interface VLSServices { dependencyService: DependencyService; @@ -48,7 +49,6 @@ export interface VLSServices { export interface LanguageMode { getId(): string; - configure?(options: VLSFullConfig): void; updateFileInfo?(doc: TextDocument): void; doValidation?(document: TextDocument, cancellationToken?: VCancellationToken): Promise; @@ -111,7 +111,7 @@ export class LanguageModes { this.modelCaches.push(this.documentRegions); } - async init(workspacePath: string, services: VLSServices, globalSnippetDir?: string) { + async init(env: EnvironmentService, services: VLSServices, globalSnippetDir?: string) { const tsModule = services.dependencyService.get('typescript').module; /** @@ -121,41 +121,43 @@ export class LanguageModes { const vueDocument = this.documentRegions.refreshAndGet(document); return vueDocument.getSingleTypeDocument('script'); }); - this.serviceHost = getServiceHost(tsModule, workspacePath, scriptRegionDocuments); - const autoImportVueService = createAutoImportVueService(tsModule, services.infoService); - autoImportVueService.setGetTSScriptTarget(() => this.serviceHost.getComplierOptions().target); - autoImportVueService.setGetFilesFn(() => + this.serviceHost = getServiceHost(tsModule, env, scriptRegionDocuments); + const autoImportSfcPlugin = createAutoImportSfcPlugin(tsModule, services.infoService); + autoImportSfcPlugin.setGetTSScriptTarget(() => this.serviceHost.getComplierOptions().target); + autoImportSfcPlugin.setGetFilesFn(() => this.serviceHost.getFileNames().filter(fileName => fileName.endsWith('.vue')) ); const vueHtmlMode = new VueHTMLMode( tsModule, this.serviceHost, + env, this.documentRegions, - workspacePath, - autoImportVueService, + autoImportSfcPlugin, services.dependencyService, services.infoService ); const jsMode = await getJavascriptMode( this.serviceHost, + env, this.documentRegions, - workspacePath, services.dependencyService, + env.getGlobalComponentInfos(), services.infoService ); - autoImportVueService.setGetJSResolve(jsMode.doResolve!); + autoImportSfcPlugin.setGetConfigure(env.getConfig); + autoImportSfcPlugin.setGetJSResolve(jsMode.doResolve!); - this.modes['vue'] = getVueMode(workspacePath, globalSnippetDir); + this.modes['vue'] = getVueMode(env, globalSnippetDir); this.modes['vue-html'] = vueHtmlMode; - this.modes['pug'] = getPugMode(services.dependencyService); - this.modes['css'] = getCSSMode(this.documentRegions, services.dependencyService); - this.modes['postcss'] = getPostCSSMode(this.documentRegions, services.dependencyService); - this.modes['scss'] = getSCSSMode(this.documentRegions, services.dependencyService); - this.modes['sass'] = new SassLanguageMode(); - this.modes['less'] = getLESSMode(this.documentRegions, services.dependencyService); - this.modes['stylus'] = getStylusMode(this.documentRegions, services.dependencyService); + this.modes['pug'] = getPugMode(env, services.dependencyService); + this.modes['css'] = getCSSMode(env, this.documentRegions, services.dependencyService); + this.modes['postcss'] = getPostCSSMode(env, this.documentRegions, services.dependencyService); + this.modes['scss'] = getSCSSMode(env, this.documentRegions, services.dependencyService); + this.modes['sass'] = new SassLanguageMode(env); + this.modes['less'] = getLESSMode(env, this.documentRegions, services.dependencyService); + this.modes['stylus'] = getStylusMode(env, this.documentRegions, services.dependencyService); this.modes['javascript'] = jsMode; this.modes['typescript'] = jsMode; this.modes['tsx'] = jsMode; diff --git a/server/src/services/autoImportVueService.ts b/server/src/modes/plugins/autoImportSfcPlugin.ts similarity index 94% rename from server/src/services/autoImportVueService.ts rename to server/src/modes/plugins/autoImportSfcPlugin.ts index 00668d26af..f0306285fb 100644 --- a/server/src/services/autoImportVueService.ts +++ b/server/src/modes/plugins/autoImportSfcPlugin.ts @@ -5,16 +5,16 @@ import type ts from 'typescript'; import { CompletionItem } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { TextEdit } from 'vscode-languageserver-types'; -import { VLSFullConfig } from '../config'; -import { modulePathToValidIdentifier, toMarkupContent } from '../utils/strings'; -import { RuntimeLibrary } from './dependencyService'; -import { ChildComponent, VueInfoService } from './vueInfoService'; +import { VLSFullConfig } from '../../config'; +import { modulePathToValidIdentifier, toMarkupContent } from '../../utils/strings'; +import { RuntimeLibrary } from '../../services/dependencyService'; +import { ChildComponent, VueInfoService } from '../../services/vueInfoService'; -export interface AutoImportVueService { +export interface AutoImportSfcPlugin { setGetConfigure(fn: () => VLSFullConfig): void; setGetFilesFn(fn: () => string[]): void; setGetJSResolve(fn: (doc: TextDocument, item: CompletionItem) => CompletionItem): void; - setGetTSScriptTarget (fn: () => ts.ScriptTarget | undefined): void; + setGetTSScriptTarget(fn: () => ts.ScriptTarget | undefined): void; doComplete(document: TextDocument): CompletionItem[]; isMyResolve(item: CompletionItem): boolean; doResolve(document: TextDocument, item: CompletionItem): CompletionItem; @@ -41,10 +41,10 @@ export interface AutoImportVueService { * } * ``` */ -export function createAutoImportVueService( +export function createAutoImportSfcPlugin( tsModule: RuntimeLibrary['typescript'], vueInfoService?: VueInfoService -): AutoImportVueService { +): AutoImportSfcPlugin { let getConfigure: () => VLSFullConfig; let getVueFiles: () => string[]; let getJSResolve: (doc: TextDocument, item: CompletionItem) => CompletionItem; @@ -98,7 +98,7 @@ export function createAutoImportVueService( setGetJSResolve(fn) { getJSResolve = fn; }, - setGetTSScriptTarget (fn) { + setGetTSScriptTarget(fn) { getTSScriptTarget = fn; }, doComplete(document): CompletionItem[] { @@ -149,7 +149,7 @@ import ${upperFirst(camelCase(tagName))} from '${fileName}' } const componentDefine = componentInfo?.componentsDefine; - const childComponents = componentInfo?.childComponents; + const childComponents = componentInfo?.childComponents?.filter(c => !c.global); const nameForTriggerResolveInTs = modulePathToValidIdentifier( tsModule, item.data.path, diff --git a/server/src/modes/pug/index.ts b/server/src/modes/pug/index.ts index 112552aec0..03761832bd 100644 --- a/server/src/modes/pug/index.ts +++ b/server/src/modes/pug/index.ts @@ -5,19 +5,15 @@ import { prettierPluginPugify } from '../../utils/prettier'; import { VLSFormatConfig } from '../../config'; import { getFileFsPath } from '../../utils/paths'; import { DependencyService } from '../../services/dependencyService'; +import { EnvironmentService } from '../../services/EnvironmentService'; -export function getPugMode(dependencyService: DependencyService): LanguageMode { - let config: any = {}; - +export function getPugMode(env: EnvironmentService, dependencyService: DependencyService): LanguageMode { return { getId() { return 'pug'; }, - configure(c) { - config = c; - }, format(document, currRange, formattingOptions) { - if (config.vetur.format.defaultFormatter['pug'] === 'none') { + if (env.getConfig().vetur.format.defaultFormatter['pug'] === 'none') { return []; } @@ -28,7 +24,7 @@ export function getPugMode(dependencyService: DependencyService): LanguageMode { value, getFileFsPath(document.uri), range, - config.vetur.format as VLSFormatConfig, + env.getConfig().vetur.format as VLSFormatConfig, // @ts-expect-error 'pug', false diff --git a/server/src/modes/script/childComponents.ts b/server/src/modes/script/childComponents.ts index 2e0846b57d..b0b85fa18f 100644 --- a/server/src/modes/script/childComponents.ts +++ b/server/src/modes/script/childComponents.ts @@ -9,7 +9,7 @@ import { import { kebabCase } from 'lodash'; import { RuntimeLibrary } from '../../services/dependencyService'; -interface InternalChildComponent { +export interface InternalChildComponent { name: string; documentation?: string; definition?: { diff --git a/server/src/modes/script/componentInfo.ts b/server/src/modes/script/componentInfo.ts index b3d35249e1..48c0ad889d 100644 --- a/server/src/modes/script/componentInfo.ts +++ b/server/src/modes/script/componentInfo.ts @@ -1,4 +1,5 @@ import type ts from 'typescript'; +import { BasicComponentInfo } from '../../config'; import { RuntimeLibrary } from '../../services/dependencyService'; import { VueFileInfo, @@ -9,11 +10,13 @@ import { ChildComponent } from '../../services/vueInfoService'; import { analyzeComponentsDefine } from './childComponents'; +import { getGlobalComponents } from './globalComponents'; export function getComponentInfo( tsModule: RuntimeLibrary['typescript'], service: ts.LanguageService, fileFsPath: string, + globalComponentInfos: BasicComponentInfo[], config: any ): VueFileInfo | undefined { const program = service.getProgram(); @@ -51,6 +54,7 @@ export function getComponentInfo( name: c.name, documentation: c.documentation, definition: c.definition, + global: false, info: c.defaultExportNode ? analyzeDefaultExportExpr(tsModule, c.defaultExportNode, checker) : undefined }); }); @@ -58,6 +62,20 @@ export function getComponentInfo( vueFileInfo.componentInfo.componentsDefine = defineInfo; } + const globalComponents = getGlobalComponents(tsModule, service, globalComponentInfos); + if (globalComponents.length > 0) { + vueFileInfo.componentInfo.childComponents = [ + ...(vueFileInfo.componentInfo.childComponents ?? []), + ...globalComponents.map(c => ({ + name: c.name, + documentation: c.documentation, + definition: c.definition, + global: true, + info: c.defaultExportNode ? analyzeDefaultExportExpr(tsModule, c.defaultExportNode, checker) : undefined + })) + ]; + } + return vueFileInfo; } diff --git a/server/src/modes/script/globalComponents.ts b/server/src/modes/script/globalComponents.ts new file mode 100644 index 0000000000..04e41baf95 --- /dev/null +++ b/server/src/modes/script/globalComponents.ts @@ -0,0 +1,53 @@ +import { kebabCase } from 'lodash'; +import type ts from 'typescript'; +import { BasicComponentInfo } from '../../config'; +import { RuntimeLibrary } from '../../services/dependencyService'; +import { InternalChildComponent } from './childComponents'; +import { buildDocumentation, getDefaultExportNode } from './componentInfo'; + +export function getGlobalComponents( + tsModule: RuntimeLibrary['typescript'], + service: ts.LanguageService, + componentInfos: BasicComponentInfo[], + tagCasing = 'kebab' +): InternalChildComponent[] { + const program = service.getProgram(); + if (!program) { + return []; + } + + const checker = program.getTypeChecker(); + + const result: InternalChildComponent[] = []; + componentInfos.forEach(info => { + const sourceFile = program.getSourceFile(info.path); + if (!sourceFile) { + return; + } + + const defaultExportNode = getDefaultExportNode(tsModule, sourceFile); + if (!defaultExportNode) { + return; + } + + const defaultExportSymbol = checker.getTypeAtLocation(defaultExportNode); + if (!defaultExportSymbol) { + return; + } + + const name = tagCasing === 'kebab' ? kebabCase(info.name) : info.name; + + result.push({ + name, + documentation: buildDocumentation(tsModule, defaultExportSymbol.symbol, checker), + definition: { + path: sourceFile.fileName, + start: defaultExportNode.getStart(sourceFile, true), + end: defaultExportNode.getEnd() + }, + defaultExportNode + }); + }); + + return result; +} diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index a18d377c30..0db673d275 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -38,7 +38,7 @@ import type ts from 'typescript'; import _ from 'lodash'; import { nullMode, NULL_SIGNATURE } from '../nullMode'; -import { VLSFormatConfig } from '../../config'; +import { BasicComponentInfo, VLSFormatConfig } from '../../config'; import { VueInfoService } from '../../services/vueInfoService'; import { getComponentInfo } from './componentInfo'; import { DependencyService, RuntimeLibrary } from '../../services/dependencyService'; @@ -47,6 +47,7 @@ import { IServiceHost } from '../../services/typescriptService/serviceHost'; import { toCompletionItemKind, toSymbolKind } from '../../services/typescriptService/util'; import * as Previewer from './previewer'; import { isVCancellationRequested, VCancellationToken } from '../../utils/cancellationToken'; +import { EnvironmentService } from '../../services/EnvironmentService'; // Todo: After upgrading to LS server 4.0, use CompletionContext for filtering trigger chars // https://microsoft.github.io/language-server-protocol/specification#completion-request-leftwards_arrow_with_hook @@ -56,16 +57,12 @@ export const APPLY_REFACTOR_COMMAND = 'vetur.applyRefactorCommand'; export async function getJavascriptMode( serviceHost: IServiceHost, + env: EnvironmentService, documentRegions: LanguageModelCache, - workspacePath: string | undefined, dependencyService: DependencyService, + globalComponentInfos: BasicComponentInfo[], vueInfoService?: VueInfoService ): Promise { - if (!workspacePath) { - return { - ...nullMode - }; - } const jsDocuments = getLanguageModelCache(10, 60, document => { const vueDocument = documentRegions.refreshAndGet(document); return vueDocument.getSingleTypeDocument('script'); @@ -80,11 +77,10 @@ export async function getJavascriptMode( const tsModule: RuntimeLibrary['typescript'] = dependencyService.get('typescript').module; const { updateCurrentVueTextDocument } = serviceHost; - let config: any = {}; let supportedCodeFixCodes: Set; function getUserPreferences(scriptDoc: TextDocument): ts.UserPreferences { - const baseConfig = config[scriptDoc.languageId === 'javascript' ? 'javascript' : 'typescript']; + const baseConfig = env.getConfig()[scriptDoc.languageId === 'javascript' ? 'javascript' : 'typescript']; const preferencesConfig = baseConfig?.preferences; if (!baseConfig || !preferencesConfig) { @@ -124,9 +120,6 @@ export async function getJavascriptMode( getId() { return 'javascript'; }, - configure(c) { - config = c; - }, updateFileInfo(doc: TextDocument): void { if (!vueInfoService) { return; @@ -134,7 +127,7 @@ export async function getJavascriptMode( const { service } = updateCurrentVueTextDocument(doc); const fileFsPath = getFileFsPath(doc.uri); - const info = getComponentInfo(tsModule, service, fileFsPath, config); + const info = getComponentInfo(tsModule, service, fileFsPath, globalComponentInfos, env.getConfig()); if (info) { vueInfoService.updateInfo(doc, info); } @@ -207,7 +200,7 @@ export async function getJavascriptMode( ...getUserPreferences(scriptDoc), triggerCharacter: getTsTriggerCharacter(triggerChar), includeCompletionsWithInsertText: true, - includeCompletionsForModuleExports: config.vetur.completion.autoImport + includeCompletionsForModuleExports: env.getConfig().vetur.completion.autoImport }); if (!completions) { return { isIncomplete: false, items: [] }; @@ -298,7 +291,7 @@ export async function getJavascriptMode( fileFsPath, item.data.offset, item.label, - getFormatCodeSettings(config), + getFormatCodeSettings(env.getConfig()), item.data.source, getUserPreferences(scriptDoc) ); @@ -321,7 +314,7 @@ export async function getJavascriptMode( } } - if (details.codeActions && config.vetur.completion.autoImport) { + if (details.codeActions && env.getConfig().vetur.completion.autoImport) { const textEdits = convertCodeAction(doc, details.codeActions, firstScriptRegion); item.additionalTextEdits = textEdits; @@ -593,7 +586,7 @@ export async function getJavascriptMode( return []; } - const formatSettings: ts.FormatCodeSettings = getFormatCodeSettings(config); + const formatSettings: ts.FormatCodeSettings = getFormatCodeSettings(env.getConfig()); const result: CodeAction[] = []; const fixes = service.getCodeFixesAtPosition( @@ -633,16 +626,16 @@ export async function getJavascriptMode( const defaultFormatter = scriptDoc.languageId === 'javascript' - ? config.vetur.format.defaultFormatter.js - : config.vetur.format.defaultFormatter.ts; + ? env.getConfig().vetur.format.defaultFormatter.js + : env.getConfig().vetur.format.defaultFormatter.ts; if (defaultFormatter === 'none') { return []; } const parser = scriptDoc.languageId === 'javascript' ? 'babel' : 'typescript'; - const needInitialIndent = config.vetur.format.scriptInitialIndent; - const vlsFormatConfig: VLSFormatConfig = config.vetur.format; + const needInitialIndent = env.getConfig().vetur.format.scriptInitialIndent; + const vlsFormatConfig: VLSFormatConfig = env.getConfig().vetur.format; if ( defaultFormatter === 'prettier' || @@ -663,7 +656,7 @@ export async function getJavascriptMode( } else { const initialIndentLevel = needInitialIndent ? 1 : 0; const formatSettings: ts.FormatCodeSettings = - scriptDoc.languageId === 'javascript' ? config.javascript.format : config.typescript.format; + scriptDoc.languageId === 'javascript' ? env.getConfig().javascript.format : env.getConfig().typescript.format; const convertedFormatSettings = convertOptions( formatSettings, { diff --git a/server/src/modes/style/index.ts b/server/src/modes/style/index.ts index 1eaa0a90e8..e288710279 100644 --- a/server/src/modes/style/index.ts +++ b/server/src/modes/style/index.ts @@ -19,39 +19,45 @@ import { NULL_HOVER } from '../nullMode'; import { VLSFormatConfig } from '../../config'; import { DependencyService } from '../../services/dependencyService'; import { BuiltInParserName } from 'prettier'; +import { EnvironmentService } from '../../services/EnvironmentService'; export function getCSSMode( + env: EnvironmentService, documentRegions: LanguageModelCache, dependencyService: DependencyService ): LanguageMode { const languageService = getCSSLanguageService(); - return getStyleMode('css', languageService, documentRegions, dependencyService); + return getStyleMode(env, 'css', languageService, documentRegions, dependencyService); } export function getPostCSSMode( + env: EnvironmentService, documentRegions: LanguageModelCache, dependencyService: DependencyService ): LanguageMode { const languageService = getCSSLanguageService(); - return getStyleMode('postcss', languageService, documentRegions, dependencyService); + return getStyleMode(env, 'postcss', languageService, documentRegions, dependencyService); } export function getSCSSMode( + env: EnvironmentService, documentRegions: LanguageModelCache, dependencyService: DependencyService ): LanguageMode { const languageService = getSCSSLanguageService(); - return getStyleMode('scss', languageService, documentRegions, dependencyService); + return getStyleMode(env, 'scss', languageService, documentRegions, dependencyService); } export function getLESSMode( + env: EnvironmentService, documentRegions: LanguageModelCache, dependencyService: DependencyService ): LanguageMode { const languageService = getLESSLanguageService(); - return getStyleMode('less', languageService, documentRegions, dependencyService); + return getStyleMode(env, 'less', languageService, documentRegions, dependencyService); } function getStyleMode( + env: EnvironmentService, languageId: LanguageId, languageService: LanguageService, documentRegions: LanguageModelCache, @@ -61,17 +67,22 @@ function getStyleMode( documentRegions.refreshAndGet(document).getSingleLanguageDocument(languageId) ); const stylesheets = getLanguageModelCache(10, 60, document => languageService.parseStylesheet(document)); - let config: any = {}; + + let latestConfig = env.getConfig().css; + function syncConfig() { + if (_.isEqual(latestConfig, env.getConfig().css)) { + return; + } + latestConfig = env.getConfig().css; + languageService.configure(env.getConfig().css); + } return { getId() { return languageId; }, - configure(c) { - languageService.configure(c && c.css); - config = c; - }, async doValidation(document) { + syncConfig(); if (languageId === 'postcss') { return []; } else { @@ -80,6 +91,7 @@ function getStyleMode( } }, doComplete(document, position) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); const emmetSyntax = languageId === 'postcss' ? 'css' : languageId; const lsCompletions = languageService.doComplete(embedded, position, stylesheets.refreshAndGet(embedded)); @@ -92,7 +104,7 @@ function getStyleMode( }) : []; - const emmetCompletions = emmet.doComplete(document, position, emmetSyntax, config.emmet); + const emmetCompletions = emmet.doComplete(document, position, emmetSyntax, env.getConfig().emmet); if (!emmetCompletions) { return { isIncomplete: false, items: lsItems }; } else { @@ -109,18 +121,22 @@ function getStyleMode( } }, doHover(document, position) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.doHover(embedded, position, stylesheets.refreshAndGet(embedded)) || NULL_HOVER; }, findDocumentHighlight(document, position) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.findDocumentHighlights(embedded, position, stylesheets.refreshAndGet(embedded)); }, findDocumentSymbols(document) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.findDocumentSymbols(embedded, stylesheets.refreshAndGet(embedded)); }, findDefinition(document, position) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); const definition = languageService.findDefinition(embedded, position, stylesheets.refreshAndGet(embedded)); if (!definition) { @@ -129,28 +145,33 @@ function getStyleMode( return definition; }, findReferences(document, position) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.findReferences(embedded, position, stylesheets.refreshAndGet(embedded)); }, findDocumentColors(document) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.findDocumentColors(embedded, stylesheets.refreshAndGet(embedded)); }, getFoldingRanges(document) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.getFoldingRanges(embedded); }, getColorPresentations(document, color, range) { + syncConfig(); const embedded = embeddedDocuments.refreshAndGet(document); return languageService.getColorPresentations(embedded, stylesheets.refreshAndGet(embedded), color, range); }, format(document, currRange, formattingOptions) { - if (config.vetur.format.defaultFormatter[languageId] === 'none') { + if (env.getConfig().vetur.format.defaultFormatter[languageId] === 'none') { return []; } + syncConfig(); const { value, range } = getValueAndRange(document, currRange); - const needIndent = config.vetur.format.styleInitialIndent; + const needIndent = env.getConfig().vetur.format.styleInitialIndent; const parserMap: { [k: string]: BuiltInParserName } = { css: 'css', postcss: 'css', @@ -162,7 +183,7 @@ function getStyleMode( value, getFileFsPath(document.uri), range, - config.vetur.format as VLSFormatConfig, + env.getConfig().vetur.format as VLSFormatConfig, parserMap[languageId], needIndent ); diff --git a/server/src/modes/style/sass/sassLanguageMode.ts b/server/src/modes/style/sass/sassLanguageMode.ts index a2280a6afc..e3572fe451 100644 --- a/server/src/modes/style/sass/sassLanguageMode.ts +++ b/server/src/modes/style/sass/sassLanguageMode.ts @@ -9,22 +9,17 @@ import { SassFormatter, SassFormatterConfig } from 'sass-formatter'; import * as emmet from 'vscode-emmet-helper'; import { Priority } from '../emmet'; +import { EnvironmentService } from '../../../services/EnvironmentService'; export class SassLanguageMode implements LanguageMode { - private config: any = {}; - - constructor() {} + constructor(private env: EnvironmentService) {} getId() { return 'sass'; } - configure(c: any) { - this.config = c; - } - doComplete(document: TextDocument, position: Position): CompletionList { - const emmetCompletions = emmet.doComplete(document, position, 'sass', this.config.emmet); + const emmetCompletions = emmet.doComplete(document, position, 'sass', this.env.getConfig().emmet); if (!emmetCompletions) { return { isIncomplete: false, items: [] }; } else { @@ -42,11 +37,11 @@ export class SassLanguageMode implements LanguageMode { } format(document: TextDocument, range: Range, formattingOptions: FormattingOptions) { - if (this.config.vetur.format.defaultFormatter.sass === 'sass-formatter') { + if (this.env.getConfig().vetur.format.defaultFormatter.sass === 'sass-formatter') { return [ TextEdit.replace( range, - SassFormatter.Format(document.getText(range), { ...formattingOptions, ...this.config.sass.format }) + SassFormatter.Format(document.getText(range), { ...formattingOptions, ...this.env.getConfig().sass.format }) ) ]; } diff --git a/server/src/modes/style/stylus/index.ts b/server/src/modes/style/stylus/index.ts index af10bd11f7..c6d0a741f2 100644 --- a/server/src/modes/style/stylus/index.ts +++ b/server/src/modes/style/stylus/index.ts @@ -14,22 +14,20 @@ import { stylusHover } from './stylus-hover'; import { getFileFsPath } from '../../../utils/paths'; import { VLSFormatConfig } from '../../../config'; import { DependencyService } from '../../../services/dependencyService'; +import { EnvironmentService } from '../../../services/EnvironmentService'; +import { sync } from 'glob'; export function getStylusMode( + env: EnvironmentService, documentRegions: LanguageModelCache, dependencyService: DependencyService ): LanguageMode { const embeddedDocuments = getLanguageModelCache(10, 60, document => documentRegions.refreshAndGet(document).getSingleLanguageDocument('stylus') ); - let baseIndentShifted = false; - let config: any = {}; + return { getId: () => 'stylus', - configure(c) { - baseIndentShifted = _.get(c, 'vetur.format.styleInitialIndent', false); - config = c; - }, onDocumentRemoved() {}, dispose() {}, doComplete(document, position) { @@ -43,7 +41,7 @@ export function getStylusMode( }; }); - const emmetCompletions: CompletionList = emmet.doComplete(document, position, 'stylus', config.emmet); + const emmetCompletions: CompletionList = emmet.doComplete(document, position, 'stylus', env.getConfig().emmet); if (!emmetCompletions) { return { isIncomplete: false, items: lsItems }; } else { @@ -68,7 +66,7 @@ export function getStylusMode( return stylusHover(embedded, position); }, format(document, range, formatParams) { - if (config.vetur.format.defaultFormatter.stylus === 'none') { + if (env.getConfig().vetur.format.defaultFormatter.stylus === 'none') { return []; } @@ -77,7 +75,7 @@ export function getStylusMode( const inputText = document.getText(range); - const vlsFormatConfig = config.vetur.format as VLSFormatConfig; + const vlsFormatConfig = env.getConfig().vetur.format as VLSFormatConfig; const tabStopChar = vlsFormatConfig.options.useTabs ? '\t' : ' '.repeat(vlsFormatConfig.options.tabSize); // Note that this would have been `document.eol` ideally @@ -93,13 +91,15 @@ export function getStylusMode( } // Add one more indentation when `vetur.format.styleInitialIndent` is set to `true` - if (baseIndentShifted) { + if (env.getConfig().vetur.format.scriptInitialIndent) { baseIndent += tabStopChar; } // Build the formatting options for Stylus Supremacy // See https://thisismanta.github.io/stylus-supremacy/#options - const stylusSupremacyFormattingOptions = stylusSupremacy.createFormattingOptions(config.stylusSupremacy || {}); + const stylusSupremacyFormattingOptions = stylusSupremacy.createFormattingOptions( + env.getConfig().stylusSupremacy || {} + ); const formattingOptions = { ...stylusSupremacyFormattingOptions, tabStopChar, diff --git a/server/src/modes/template/htmlMode.ts b/server/src/modes/template/htmlMode.ts index 1f88959d60..cc8711b457 100644 --- a/server/src/modes/template/htmlMode.ts +++ b/server/src/modes/template/htmlMode.ts @@ -24,56 +24,46 @@ import { DocumentContext } from '../../types'; import { VLSFormatConfig, VLSFullConfig } from '../../config'; import { VueInfoService } from '../../services/vueInfoService'; import { getComponentInfoTagProvider } from './tagProviders/componentInfoTagProvider'; -import { VueVersion } from '../../services/typescriptService/vueVersion'; import { doPropValidation } from './services/vuePropValidation'; import { getFoldingRanges } from './services/htmlFolding'; import { DependencyService } from '../../services/dependencyService'; import { isVCancellationRequested, VCancellationToken } from '../../utils/cancellationToken'; -import { AutoImportVueService } from '../../services/autoImportVueService'; +import { AutoImportSfcPlugin } from '../plugins/autoImportSfcPlugin'; +import { EnvironmentService } from '../../services/EnvironmentService'; export class HTMLMode implements LanguageMode { private tagProviderSettings: CompletionConfiguration; private enabledTagProviders: IHTMLTagProvider[]; private embeddedDocuments: LanguageModelCache; - - private config: VLSFullConfig; - private lintEngine: any; constructor( documentRegions: LanguageModelCache, - workspacePath: string | undefined, - vueVersion: VueVersion, + private env: EnvironmentService, private dependencyService: DependencyService, private vueDocuments: LanguageModelCache, - private autoImportVueService: AutoImportVueService, + private autoImportSfcPlugin: AutoImportSfcPlugin, private vueInfoService?: VueInfoService ) { - this.tagProviderSettings = getTagProviderSettings(workspacePath); + this.tagProviderSettings = getTagProviderSettings(env.getPackagePath()); this.enabledTagProviders = getEnabledTagProviders(this.tagProviderSettings); this.embeddedDocuments = getLanguageModelCache(10, 60, document => documentRegions.refreshAndGet(document).getSingleLanguageDocument('vue-html') ); - this.lintEngine = createLintEngine(vueVersion); + this.lintEngine = createLintEngine(env.getVueVersion()); } getId() { return 'html'; } - configure(c: VLSFullConfig) { - this.enabledTagProviders = getEnabledTagProviders(this.tagProviderSettings); - this.config = c; - this.autoImportVueService.setGetConfigure(() => c); - } - async doValidation(document: TextDocument, cancellationToken?: VCancellationToken) { const diagnostics = []; if (await isVCancellationRequested(cancellationToken)) { return []; } - if (this.config.vetur.validation.templateProps) { + if (this.env.getConfig().vetur.validation.templateProps) { const info = this.vueInfoService ? this.vueInfoService.getInfo(document) : undefined; if (info && info.componentInfo.childComponents) { diagnostics.push(...doPropValidation(document, this.vueDocuments.refreshAndGet(document), info)); @@ -83,7 +73,7 @@ export class HTMLMode implements LanguageMode { if (await isVCancellationRequested(cancellationToken)) { return diagnostics; } - if (this.config.vetur.validation.template) { + if (this.env.getConfig().vetur.validation.template) { const embedded = this.embeddedDocuments.refreshAndGet(document); diagnostics.push(...(await doESLintValidation(embedded, this.lintEngine))); } @@ -104,8 +94,8 @@ export class HTMLMode implements LanguageMode { position, this.vueDocuments.refreshAndGet(embedded), tagProviders, - this.config.emmet, - this.autoImportVueService.doComplete(document) + this.env.getConfig().emmet, + this.autoImportSfcPlugin.doComplete(document) ); } doHover(document: TextDocument, position: Position) { @@ -124,7 +114,7 @@ export class HTMLMode implements LanguageMode { return findDocumentSymbols(document, this.vueDocuments.refreshAndGet(document)); } format(document: TextDocument, range: Range, formattingOptions: FormattingOptions) { - return htmlFormat(this.dependencyService, document, range, this.config.vetur.format as VLSFormatConfig); + return htmlFormat(this.dependencyService, document, range, this.env.getConfig().vetur.format as VLSFormatConfig); } findDefinition(document: TextDocument, position: Position) { const embedded = this.embeddedDocuments.refreshAndGet(document); @@ -135,6 +125,16 @@ export class HTMLMode implements LanguageMode { const embedded = this.embeddedDocuments.refreshAndGet(document); return getFoldingRanges(embedded); } + onDocumentChanged(filePath: string) { + if (filePath !== this.env.getPackagePath()) { + return; + } + + // reload package + this.tagProviderSettings = getTagProviderSettings(this.env.getPackagePath()); + this.enabledTagProviders = getEnabledTagProviders(this.tagProviderSettings); + this.lintEngine = createLintEngine(this.env.getVueVersion()); + } onDocumentRemoved(document: TextDocument) { this.vueDocuments.onDocumentRemoved(document); } diff --git a/server/src/modes/template/index.ts b/server/src/modes/template/index.ts index 4f789af395..999aaeb766 100644 --- a/server/src/modes/template/index.ts +++ b/server/src/modes/template/index.ts @@ -9,48 +9,43 @@ import { HTMLMode } from './htmlMode'; import { VueInterpolationMode } from './interpolationMode'; import { IServiceHost } from '../../services/typescriptService/serviceHost'; import { HTMLDocument, parseHTMLDocument } from './parser/htmlParser'; -import { inferVueVersion } from '../../services/typescriptService/vueVersion'; +import { inferVueVersion } from '../../utils/vueVersion'; import { DependencyService, RuntimeLibrary } from '../../services/dependencyService'; import { VCancellationToken } from '../../utils/cancellationToken'; -import { AutoImportVueService } from '../../services/autoImportVueService'; +import { AutoImportSfcPlugin } from '../plugins/autoImportSfcPlugin'; +import { EnvironmentService } from '../../services/EnvironmentService'; type DocumentRegionCache = LanguageModelCache; export class VueHTMLMode implements LanguageMode { private htmlMode: HTMLMode; private vueInterpolationMode: VueInterpolationMode; - private autoImportVueService: AutoImportVueService; + private autoImportSfcPlugin: AutoImportSfcPlugin; constructor( tsModule: RuntimeLibrary['typescript'], serviceHost: IServiceHost, + env: EnvironmentService, documentRegions: DocumentRegionCache, - workspacePath: string, - autoImportVueService: AutoImportVueService, + autoImportSfcPlugin: AutoImportSfcPlugin, dependencyService: DependencyService, vueInfoService?: VueInfoService ) { const vueDocuments = getLanguageModelCache(10, 60, document => parseHTMLDocument(document)); - const vueVersion = inferVueVersion(workspacePath); this.htmlMode = new HTMLMode( documentRegions, - workspacePath, - vueVersion, + env, dependencyService, vueDocuments, - autoImportVueService, + autoImportSfcPlugin, vueInfoService ); - this.vueInterpolationMode = new VueInterpolationMode(tsModule, serviceHost, vueDocuments, vueInfoService); - this.autoImportVueService = autoImportVueService; + this.vueInterpolationMode = new VueInterpolationMode(tsModule, serviceHost, env, vueDocuments, vueInfoService); + this.autoImportSfcPlugin = autoImportSfcPlugin; } getId() { return 'vue-html'; } - configure(c: any) { - this.htmlMode.configure(c); - this.vueInterpolationMode.configure(c); - } queryVirtualFileInfo(fileName: string, currFileText: string) { return this.vueInterpolationMode.queryVirtualFileInfo(fileName, currFileText); } @@ -69,8 +64,8 @@ export class VueHTMLMode implements LanguageMode { }; } doResolve(document: TextDocument, item: CompletionItem): CompletionItem { - if (this.autoImportVueService.isMyResolve(item)) { - return this.autoImportVueService.doResolve(document, item); + if (this.autoImportSfcPlugin.isMyResolve(item)) { + return this.autoImportSfcPlugin.doResolve(document, item); } return this.vueInterpolationMode.doResolve(document, item); } diff --git a/server/src/modes/template/interpolationMode.ts b/server/src/modes/template/interpolationMode.ts index 1321ac248d..b16af8fa04 100644 --- a/server/src/modes/template/interpolationMode.ts +++ b/server/src/modes/template/interpolationMode.ts @@ -31,13 +31,13 @@ import * as Previewer from '../script/previewer'; import { HTMLDocument } from './parser/htmlParser'; import { isInsideInterpolation } from './services/isInsideInterpolation'; import { RuntimeLibrary } from '../../services/dependencyService'; +import { EnvironmentService } from '../../services/EnvironmentService'; export class VueInterpolationMode implements LanguageMode { - private config: VLSFullConfig; - constructor( private tsModule: RuntimeLibrary['typescript'], private serviceHost: IServiceHost, + private env: EnvironmentService, private vueDocuments: LanguageModelCache, private vueInfoService?: VueInfoService ) {} @@ -46,18 +46,20 @@ export class VueInterpolationMode implements LanguageMode { return 'vue-html-interpolation'; } - configure(c: any) { - this.config = c; - } - queryVirtualFileInfo(fileName: string, currFileText: string) { return this.serviceHost.queryVirtualFileInfo(fileName, currFileText); } + private getChildComponents(document: TextDocument) { + return this.env.getConfig().vetur.validation.templateProps + ? this.vueInfoService && this.vueInfoService.getInfo(document)?.componentInfo.childComponents + : []; + } + async doValidation(document: TextDocument, cancellationToken?: VCancellationToken): Promise { if ( - !_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true) || - !this.config.vetur.validation.interpolation + !this.env.getConfig().vetur.experimental.templateInterpolationService || + !this.env.getConfig().vetur.validation.interpolation ) { return []; } @@ -74,13 +76,9 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const childComponents = this.config.vetur.validation.templateProps - ? this.vueInfoService && this.vueInfoService.getInfo(document)?.componentInfo.childComponents - : []; - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( templateDoc, - childComponents + this.getChildComponents(document) ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { @@ -111,7 +109,7 @@ export class VueInterpolationMode implements LanguageMode { } doComplete(document: TextDocument, position: Position): CompletionList { - if (!_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true)) { + if (!this.env.getConfig().vetur.experimental.templateInterpolationService) { return NULL_COMPLETION; } @@ -131,7 +129,10 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(templateDoc); + const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( + templateDoc, + this.getChildComponents(document) + ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { return NULL_COMPLETION; } @@ -201,7 +202,7 @@ export class VueInterpolationMode implements LanguageMode { } doResolve(document: TextDocument, item: CompletionItem): CompletionItem { - if (!_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true)) { + if (!this.env.getConfig().vetur.experimental.templateInterpolationService) { return item; } @@ -221,7 +222,10 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(templateDoc); + const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( + templateDoc, + this.getChildComponents(document) + ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { return item; } @@ -270,7 +274,7 @@ export class VueInterpolationMode implements LanguageMode { contents: MarkedString[]; range?: Range; } { - if (!_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true)) { + if (!this.env.getConfig().vetur.experimental.templateInterpolationService) { return { contents: [] }; } @@ -282,7 +286,10 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(templateDoc); + const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( + templateDoc, + this.getChildComponents(document) + ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { return { contents: [] @@ -325,7 +332,7 @@ export class VueInterpolationMode implements LanguageMode { } findDefinition(document: TextDocument, position: Position): Location[] { - if (!_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true)) { + if (!this.env.getConfig().vetur.experimental.templateInterpolationService) { return []; } @@ -337,7 +344,10 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(templateDoc); + const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( + templateDoc, + this.getChildComponents(document) + ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { return []; } @@ -373,7 +383,7 @@ export class VueInterpolationMode implements LanguageMode { } findReferences(document: TextDocument, position: Position): Location[] { - if (!_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true)) { + if (!this.env.getConfig().vetur.experimental.templateInterpolationService) { return []; } @@ -385,7 +395,10 @@ export class VueInterpolationMode implements LanguageMode { document.getText() ); - const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(templateDoc); + const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument( + templateDoc, + this.getChildComponents(document) + ); if (!languageServiceIncludesFile(templateService, templateDoc.uri)) { return []; } diff --git a/server/src/modes/template/services/htmlEslintValidation.ts b/server/src/modes/template/services/htmlEslintValidation.ts index 474cabca5b..b68ef570db 100644 --- a/server/src/modes/template/services/htmlEslintValidation.ts +++ b/server/src/modes/template/services/htmlEslintValidation.ts @@ -3,7 +3,7 @@ import { configs } from 'eslint-plugin-vue'; import { Diagnostic, Range, DiagnosticSeverity } from 'vscode-languageserver-types'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import { resolve } from 'path'; -import { VueVersion } from '../../../services/typescriptService/vueVersion'; +import { VueVersion } from '../../../utils/vueVersion'; function toDiagnostic(error: Linter.LintMessage): Diagnostic { const line = error.line - 1; diff --git a/server/src/modes/template/tagProviders/externalTagProviders.ts b/server/src/modes/template/tagProviders/externalTagProviders.ts index 15144b93f2..b3a32533e7 100644 --- a/server/src/modes/template/tagProviders/externalTagProviders.ts +++ b/server/src/modes/template/tagProviders/externalTagProviders.ts @@ -25,12 +25,12 @@ export const gridsomeTagProvider = getExternalTagProvider('gridsome', gridsomeTa /** * Get tag providers specified in workspace root's packaage.json */ -export function getWorkspaceTagProvider(workspacePath: string, rootPkgJson: any): IHTMLTagProvider | null { +export function getWorkspaceTagProvider(packageRoot: string, rootPkgJson: any): IHTMLTagProvider | null { if (!rootPkgJson.vetur) { return null; } - const tagsPath = findConfigFile(workspacePath, rootPkgJson.vetur.tags); - const attrsPath = findConfigFile(workspacePath, rootPkgJson.vetur.attributes); + const tagsPath = findConfigFile(packageRoot, rootPkgJson.vetur.tags); + const attrsPath = findConfigFile(packageRoot, rootPkgJson.vetur.attributes); try { if (tagsPath && attrsPath) { @@ -47,15 +47,15 @@ export function getWorkspaceTagProvider(workspacePath: string, rootPkgJson: any) /** * Get tag providers specified in packaage.json's `vetur` key */ -export function getDependencyTagProvider(workspacePath: string, depPkgJson: any): IHTMLTagProvider | null { +export function getDependencyTagProvider(packageRoot: string, depPkgJson: any): IHTMLTagProvider | null { if (!depPkgJson.vetur) { return null; } try { - const tagsPath = require.resolve(path.join(depPkgJson.name, depPkgJson.vetur.tags), { paths: [workspacePath] }); + const tagsPath = require.resolve(path.join(depPkgJson.name, depPkgJson.vetur.tags), { paths: [packageRoot] }); const attrsPath = require.resolve(path.join(depPkgJson.name, depPkgJson.vetur.attributes), { - paths: [workspacePath] + paths: [packageRoot] }); const tagsJson = JSON.parse(fs.readFileSync(tagsPath, 'utf-8')); diff --git a/server/src/modes/template/tagProviders/index.ts b/server/src/modes/template/tagProviders/index.ts index b3480ab99c..bb96a5161a 100644 --- a/server/src/modes/template/tagProviders/index.ts +++ b/server/src/modes/template/tagProviders/index.ts @@ -15,7 +15,7 @@ export { IHTMLTagProvider } from './common'; import fs from 'fs'; import { join } from 'path'; import { getNuxtTagProvider } from './nuxtTags'; -import { findConfigFile } from '../../../utils/workspace'; +import { normalizeFileNameResolve } from '../../../utils/paths'; export let allTagProviders: IHTMLTagProvider[] = [ getHTML5TagProvider(), @@ -31,7 +31,7 @@ export interface CompletionConfiguration { [provider: string]: boolean; } -export function getTagProviderSettings(workspacePath: string | null | undefined) { +export function getTagProviderSettings(packagePath: string | undefined) { const settings: CompletionConfiguration = { '__vetur-workspace': true, html5: true, @@ -47,15 +47,13 @@ export function getTagProviderSettings(workspacePath: string | null | undefined) nuxt: false, gridsome: false }; - if (!workspacePath) { - return settings; - } try { - const packagePath = findConfigFile(workspacePath, 'package.json'); if (!packagePath) { return settings; } + const packageRoot = normalizeFileNameResolve(packagePath, '../'); + const rootPkgJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8')); const dependencies = rootPkgJson.dependencies || {}; const devDependencies = rootPkgJson.devDependencies || {}; @@ -99,7 +97,7 @@ export function getTagProviderSettings(workspacePath: string | null | undefined) dependencies['quasar-framework'] = '^0.0.17'; } if (dependencies['nuxt'] || dependencies['nuxt-edge'] || devDependencies['nuxt'] || devDependencies['nuxt-edge']) { - const nuxtTagProvider = getNuxtTagProvider(workspacePath); + const nuxtTagProvider = getNuxtTagProvider(packageRoot); if (nuxtTagProvider) { settings['nuxt'] = true; allTagProviders.push(nuxtTagProvider); @@ -109,7 +107,7 @@ export function getTagProviderSettings(workspacePath: string | null | undefined) settings['gridsome'] = true; } - const workspaceTagProvider = getWorkspaceTagProvider(workspacePath, rootPkgJson); + const workspaceTagProvider = getWorkspaceTagProvider(packageRoot, rootPkgJson); if (workspaceTagProvider) { allTagProviders.push(workspaceTagProvider); } @@ -117,7 +115,7 @@ export function getTagProviderSettings(workspacePath: string | null | undefined) for (const dep of [...Object.keys(dependencies), ...Object.keys(devDependencies)]) { let runtimePkgJsonPath; try { - runtimePkgJsonPath = require.resolve(join(dep, 'package.json'), { paths: [workspacePath] }); + runtimePkgJsonPath = require.resolve(join(dep, 'package.json'), { paths: [packageRoot] }); } catch { continue; } @@ -127,7 +125,7 @@ export function getTagProviderSettings(workspacePath: string | null | undefined) continue; } - const depTagProvider = getDependencyTagProvider(workspacePath, runtimePkgJson); + const depTagProvider = getDependencyTagProvider(packageRoot, runtimePkgJson); if (!depTagProvider) { continue; } diff --git a/server/src/modes/template/tagProviders/nuxtTags.ts b/server/src/modes/template/tagProviders/nuxtTags.ts index edf39d6b30..16982902c6 100644 --- a/server/src/modes/template/tagProviders/nuxtTags.ts +++ b/server/src/modes/template/tagProviders/nuxtTags.ts @@ -3,20 +3,20 @@ import { getExternalTagProvider } from './externalTagProviders'; const NUXT_JSON_SOURCES = ['@nuxt/vue-app-edge', '@nuxt/vue-app', 'nuxt-helper-json']; -export function getNuxtTagProvider(workspacePath: string) { +export function getNuxtTagProvider(packageRoot: string) { let nuxtTags, nuxtAttributes; for (const source of NUXT_JSON_SOURCES) { - if (tryResolve(join(source, 'package.json'), workspacePath)) { - nuxtTags = tryRequire(join(source, 'vetur/nuxt-tags.json'), workspacePath); - nuxtAttributes = tryRequire(join(source, 'vetur/nuxt-attributes.json'), workspacePath); + if (tryResolve(join(source, 'package.json'), packageRoot)) { + nuxtTags = tryRequire(join(source, 'vetur/nuxt-tags.json'), packageRoot); + nuxtAttributes = tryRequire(join(source, 'vetur/nuxt-attributes.json'), packageRoot); if (nuxtTags) { break; } } } - const componentsTags = tryRequire(join(workspacePath, '.nuxt/vetur/tags.json'), workspacePath); - const componentsAttributes = tryRequire(join(workspacePath, '.nuxt/vetur/attributes.json'), workspacePath); + const componentsTags = tryRequire(join(packageRoot, '.nuxt/vetur/tags.json'), packageRoot); + const componentsAttributes = tryRequire(join(packageRoot, '.nuxt/vetur/attributes.json'), packageRoot); return getExternalTagProvider( 'nuxt', @@ -25,17 +25,17 @@ export function getNuxtTagProvider(workspacePath: string) { ); } -function tryRequire(modulePath: string, workspacePath: string) { +function tryRequire(modulePath: string, findPath: string) { try { - const resolved = tryResolve(modulePath, workspacePath); + const resolved = tryResolve(modulePath, findPath); return resolved ? require(resolved) : undefined; } catch (_err) {} } -function tryResolve(modulePath: string, workspacePath: string) { +function tryResolve(modulePath: string, findPath: string) { try { return require.resolve(modulePath, { - paths: [workspacePath, __dirname] + paths: [findPath, __dirname] }); } catch (_err) {} } diff --git a/server/src/modes/vue/index.ts b/server/src/modes/vue/index.ts index 205d4814c4..2c8e5f7e1a 100644 --- a/server/src/modes/vue/index.ts +++ b/server/src/modes/vue/index.ts @@ -1,12 +1,11 @@ import { LanguageMode } from '../../embeddedSupport/languageModes'; import { SnippetManager, ScaffoldSnippetSources } from './snippets'; import { Range } from 'vscode-css-languageservice'; +import { EnvironmentService } from '../../services/EnvironmentService'; -export function getVueMode(workspacePath: string, globalSnippetDir?: string): LanguageMode { - let config: any = {}; - - const snippetManager = new SnippetManager(workspacePath, globalSnippetDir); - let scaffoldSnippetSources: ScaffoldSnippetSources = { +export function getVueMode(env: EnvironmentService, globalSnippetDir?: string): LanguageMode { + const snippetManager = new SnippetManager(env.getSnippetFolder(), globalSnippetDir); + const scaffoldSnippetSources: ScaffoldSnippetSources = { workspace: '💼', user: '🗒️', vetur: '✌' @@ -16,13 +15,9 @@ export function getVueMode(workspacePath: string, globalSnippetDir?: string): La getId() { return 'vue'; }, - configure(c) { - config = c; - if (c.vetur.completion['scaffoldSnippetSources']) { - scaffoldSnippetSources = c.vetur.completion['scaffoldSnippetSources']; - } - }, doComplete(document, position) { + const scaffoldSnippetSources: ScaffoldSnippetSources = env.getConfig().vetur.completion.scaffoldSnippetSources; + if ( scaffoldSnippetSources['workspace'] === '' && scaffoldSnippetSources['user'] === '' && @@ -32,10 +27,7 @@ export function getVueMode(workspacePath: string, globalSnippetDir?: string): La } const offset = document.offsetAt(position); - const lines = document - .getText() - .slice(0, offset) - .split('\n'); + const lines = document.getText().slice(0, offset).split('\n'); const currentLine = lines[position.line]; const items = snippetManager ? snippetManager.completeSnippets(scaffoldSnippetSources) : []; diff --git a/server/src/modes/vue/snippets.ts b/server/src/modes/vue/snippets.ts index 8fe8c8efab..f77827815c 100644 --- a/server/src/modes/vue/snippets.ts +++ b/server/src/modes/vue/snippets.ts @@ -21,8 +21,8 @@ export interface ScaffoldSnippetSources { export class SnippetManager { private _snippets: Snippet[] = []; - constructor(workspacePath: string, globalSnippetDir?: string) { - const workspaceSnippets = loadAllSnippets(path.resolve(workspacePath, '.vscode/vetur/snippets'), 'workspace'); + constructor(snippetFolder: string, globalSnippetDir?: string) { + const workspaceSnippets = loadAllSnippets(snippetFolder, 'workspace'); const userSnippets = globalSnippetDir ? loadAllSnippets(globalSnippetDir, 'user') : []; const veturSnippets = loadAllSnippets(path.resolve(__dirname, './veturSnippets'), 'vetur'); diff --git a/server/src/services/EnvironmentService.ts b/server/src/services/EnvironmentService.ts new file mode 100644 index 0000000000..29a89a0352 --- /dev/null +++ b/server/src/services/EnvironmentService.ts @@ -0,0 +1,40 @@ +import { BasicComponentInfo, VLSConfig, VLSFullConfig } from '../config'; +import { inferVueVersion, VueVersion } from '../utils/vueVersion'; + +export interface EnvironmentService { + configure(config: VLSFullConfig): void; + getConfig(): VLSFullConfig; + getRootPathForConfig(): string; + getProjectRoot(): string; + getTsConfigPath(): string | undefined; + getPackagePath(): string | undefined; + getVueVersion(): VueVersion; + getSnippetFolder(): string; + getGlobalComponentInfos(): BasicComponentInfo[]; +} + +export function createEnvironmentService( + rootPathForConfig: string, + projectPath: string, + tsconfigPath: string | undefined, + packagePath: string | undefined, + snippetFolder: string, + globalComponentInfos: BasicComponentInfo[], + initialConfig: VLSConfig +): EnvironmentService { + let $config = initialConfig; + + return { + configure(config: VLSFullConfig) { + $config = config; + }, + getConfig: () => $config, + getRootPathForConfig: () => rootPathForConfig, + getProjectRoot: () => projectPath, + getTsConfigPath: () => tsconfigPath, + getPackagePath: () => packagePath, + getVueVersion: () => inferVueVersion(packagePath), + getSnippetFolder: () => snippetFolder, + getGlobalComponentInfos: () => globalComponentInfos + }; +} diff --git a/server/src/services/dependencyService.ts b/server/src/services/dependencyService.ts index 3faa8b94f7..997118a737 100644 --- a/server/src/services/dependencyService.ts +++ b/server/src/services/dependencyService.ts @@ -2,6 +2,9 @@ import path from 'path'; import fg from 'fast-glob'; import fs from 'fs'; import util from 'util'; +import { performance } from 'perf_hooks'; +import { logger } from '../log'; +import { getPathDepth } from '../utils/paths'; // dependencies import ts from 'typescript'; import prettier from 'prettier'; @@ -10,16 +13,14 @@ import prettierEslint from 'prettier-eslint'; import * as prettierTslint from 'prettier-tslint'; import stylusSupremacy from 'stylus-supremacy'; import * as prettierPluginPug from '@prettier/plugin-pug'; -import { performance } from 'perf_hooks'; -import { logger } from '../log'; const readFileAsync = util.promisify(fs.readFile); const accessFileAsync = util.promisify(fs.access); -async function createNodeModulesPaths(workspacePath: string) { +export function createNodeModulesPaths(rootPath: string) { const startTime = performance.now(); - const nodeModules = await fg('**/node_modules', { - cwd: workspacePath.replace(/\\/g, '/'), + const nodeModules = fg.sync('**/node_modules', { + cwd: rootPath.replace(/\\/g, '/'), absolute: true, unique: true, onlyFiles: false, @@ -29,7 +30,7 @@ async function createNodeModulesPaths(workspacePath: string) { ignore: ['**/node_modules/**/node_modules'] }); - logger.logInfo(`Find node_modules paths in ${workspacePath} - ${Math.round(performance.now() - startTime)}ms`); + logger.logInfo(`Find node_modules paths in ${rootPath} - ${Math.round(performance.now() - startTime)}ms`); return nodeModules; } @@ -62,13 +63,9 @@ async function findAllPackages(nodeModulesPaths: string[], moduleName: string) { return packages; } -function getPathDepth(filePath: string) { - return filePath.split(path.sep).length; -} - function compareDependency(a: Dependency, b: Dependency) { - const aDepth = getPathDepth(a.dir); - const bDepth = getPathDepth(b.dir); + const aDepth = getPathDepth(a.dir, path.sep); + const bDepth = getPathDepth(b.dir, path.sep); return bDepth - aDepth; } @@ -91,124 +88,123 @@ export interface RuntimeLibrary { } export interface DependencyService { - useWorkspaceDependencies: boolean; - workspacePath: string; - init(workspacePath: string, useWorkspaceDependencies: boolean, tsSDKPath?: string): Promise; + readonly useWorkspaceDependencies: boolean; + readonly nodeModulesPaths: string[]; get(lib: L, filePath?: string): Dependency; getBundled(lib: L): Dependency; } -export const createDependencyService = () => { - let useWorkspaceDeps: boolean; - let rootPath: string; - let loaded: { [K in keyof RuntimeLibrary]: Dependency[] }; - - const bundledModules = { - typescript: ts, - prettier: prettier, - '@starptech/prettyhtml': prettyHTML, - 'prettier-eslint': prettierEslint, - 'prettier-tslint': prettierTslint, - 'stylus-supremacy': stylusSupremacy, - '@prettier/plugin-pug': prettierPluginPug - }; - - async function init(workspacePath: string, useWorkspaceDependencies: boolean, tsSDKPath?: string) { - const nodeModulesPaths = useWorkspaceDependencies ? await createNodeModulesPaths(workspacePath) : []; - - const loadTypeScript = async (): Promise[]> => { - try { - if (useWorkspaceDependencies && tsSDKPath) { - const dir = path.isAbsolute(tsSDKPath) - ? path.resolve(tsSDKPath, '..') - : path.resolve(workspacePath, tsSDKPath, '..'); - const tsModule = require(dir); - logger.logInfo(`Loaded typescript@${tsModule.version} from ${dir} for tsdk.`); - - return [ - { - dir, - version: tsModule.version as string, - bundled: false, - module: tsModule as typeof ts - } - ]; - } - - if (useWorkspaceDependencies) { - const packages = await findAllPackages(nodeModulesPaths, 'typescript'); - if (packages.length === 0) { - throw new Error(`No find any packages in ${workspacePath}.`); - } +const bundledModules = { + typescript: ts, + prettier, + '@starptech/prettyhtml': prettyHTML, + 'prettier-eslint': prettierEslint, + 'prettier-tslint': prettierTslint, + 'stylus-supremacy': stylusSupremacy, + '@prettier/plugin-pug': prettierPluginPug +}; - return packages - .map(pkg => { - logger.logInfo(`Loaded typescript@${pkg.version} from ${pkg.dir}.`); +export const createDependencyService = async ( + rootPathForConfig: string, + workspacePath: string, + useWorkspaceDependencies: boolean, + nodeModulesPaths: string[], + tsSDKPath?: string +): Promise => { + let loaded: { [K in keyof RuntimeLibrary]: Dependency[] }; - return { - dir: pkg.dir, - version: pkg.version as string, - bundled: false, - module: pkg.module as typeof ts - }; - }) - .sort(compareDependency); - } + const loadTypeScript = async (): Promise[]> => { + try { + if (useWorkspaceDependencies && tsSDKPath) { + const dir = path.isAbsolute(tsSDKPath) + ? path.resolve(tsSDKPath, '..') + : path.resolve(workspacePath, tsSDKPath, '..'); + const tsModule = require(dir); + logger.logInfo(`Loaded typescript@${tsModule.version} from ${dir} for tsdk.`); - throw new Error('No useWorkspaceDependencies.'); - } catch (e) { - logger.logDebug(e.message); - logger.logInfo(`Loaded bundled typescript@${ts.version}.`); return [ { - dir: '', - version: ts.version, - bundled: true, - module: ts + dir, + version: tsModule.version as string, + bundled: false, + module: tsModule as typeof ts } ]; } - }; - const loadCommonDep = async (name: N, bundleModule: BM): Promise[]> => { - try { - if (useWorkspaceDependencies) { - const packages = await findAllPackages(nodeModulesPaths, name); - if (packages.length === 0) { - throw new Error(`No find ${name} packages in ${workspacePath}.`); - } + if (useWorkspaceDependencies) { + const packages = await findAllPackages(nodeModulesPaths, 'typescript'); + if (packages.length === 0) { + throw new Error(`No find any packages in ${rootPathForConfig}.`); + } - return packages - .map(pkg => { - logger.logInfo(`Loaded ${name}@${pkg.version} from ${pkg.dir}.`); + return packages + .map(pkg => { + logger.logInfo(`Loaded typescript@${pkg.version} from ${pkg.dir}.`); - return { - dir: pkg.dir, - version: pkg.version as string, - bundled: false, - module: pkg.module as BM - }; - }) - .sort(compareDependency); + return { + dir: pkg.dir, + version: pkg.version as string, + bundled: false, + module: pkg.module as typeof ts + }; + }) + .sort(compareDependency); + } + + throw new Error('No useWorkspaceDependencies.'); + } catch (e) { + logger.logDebug(e.message); + logger.logInfo(`Loaded bundled typescript@${ts.version}.`); + return [ + { + dir: '', + version: ts.version, + bundled: true, + module: ts } - throw new Error('No useWorkspaceDependencies.'); - } catch (e) { - logger.logDebug(e.message); - // TODO: Get bundle package version - logger.logInfo(`Loaded bundled ${name}.`); - return [ - { - dir: '', - version: '', - bundled: true, - module: bundleModule as BM - } - ]; + ]; + } + }; + + const loadCommonDep = async (name: N, bundleModule: BM): Promise[]> => { + try { + if (useWorkspaceDependencies) { + const packages = await findAllPackages(nodeModulesPaths, name); + if (packages.length === 0) { + throw new Error(`No find ${name} packages in ${rootPathForConfig}.`); + } + + return packages + .map(pkg => { + logger.logInfo(`Loaded ${name}@${pkg.version} from ${pkg.dir}.`); + + return { + dir: pkg.dir, + version: pkg.version as string, + bundled: false, + module: pkg.module as BM + }; + }) + .sort(compareDependency); } - }; + throw new Error('No useWorkspaceDependencies.'); + } catch (e) { + logger.logDebug(e.message); + // TODO: Get bundle package version + logger.logInfo(`Loaded bundled ${name}.`); + return [ + { + dir: '', + version: '', + bundled: true, + module: bundleModule as BM + } + ]; + } + }; - useWorkspaceDeps = useWorkspaceDependencies; - rootPath = workspacePath; + if (!process.versions.pnp) { loaded = { typescript: await loadTypeScript(), prettier: await loadCommonDep('prettier', bundledModules['prettier']), @@ -221,6 +217,17 @@ export const createDependencyService = () => { } const get = (lib: L, filePath?: string): Dependency => { + // We find it when yarn pnp. https://yarnpkg.com/features/pnp + if (process.versions.pnp) { + const pkgPath = require.resolve(lib, { paths: [filePath ?? workspacePath] }); + + return { + dir: path.dirname(pkgPath), + version: '', + bundled: false, + module: require(pkgPath) + }; + } if (!loaded) { throw new Error('Please call init function before get dependency.'); } @@ -237,7 +244,10 @@ export const createDependencyService = () => { const possiblePaths: string[] = []; let tempPath = path.dirname(filePath); - while (rootPath === tempPath || getPathDepth(rootPath) > getPathDepth(tempPath)) { + while ( + rootPathForConfig === tempPath || + getPathDepth(rootPathForConfig, path.sep) > getPathDepth(tempPath, path.sep) + ) { possiblePaths.push(path.resolve(tempPath, `node_modules/${lib}`)); tempPath = path.resolve(tempPath, '../'); } @@ -256,13 +266,8 @@ export const createDependencyService = () => { }; return { - get useWorkspaceDependencies() { - return useWorkspaceDeps; - }, - get workspacePath() { - return rootPath; - }, - init, + useWorkspaceDependencies, + nodeModulesPaths, get, getBundled }; diff --git a/server/src/services/projectService.ts b/server/src/services/projectService.ts new file mode 100644 index 0000000000..3cacf3ff5c --- /dev/null +++ b/server/src/services/projectService.ts @@ -0,0 +1,349 @@ +import path from 'path'; +import { + CodeAction, + CodeActionParams, + ColorInformation, + ColorPresentation, + ColorPresentationParams, + CompletionItem, + CompletionList, + CompletionParams, + CompletionTriggerKind, + Definition, + Diagnostic, + DocumentColorParams, + DocumentFormattingParams, + DocumentHighlight, + DocumentLink, + DocumentLinkParams, + DocumentSymbolParams, + FoldingRange, + FoldingRangeParams, + Hover, + Location, + SignatureHelp, + SymbolInformation, + TextDocumentPositionParams, + TextEdit, + WorkspaceEdit +} from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; +import { LanguageId } from '../embeddedSupport/embeddedSupport'; +import { LanguageMode, LanguageModes } from '../embeddedSupport/languageModes'; +import { NULL_COMPLETION, NULL_HOVER, NULL_SIGNATURE } from '../modes/nullMode'; +import { DocumentContext, RefactorAction } from '../types'; +import { VCancellationToken } from '../utils/cancellationToken'; +import { getFileFsPath } from '../utils/paths'; +import { DependencyService } from './dependencyService'; +import { DocumentService } from './documentService'; +import { EnvironmentService } from './EnvironmentService'; +import { VueInfoService } from './vueInfoService'; + +export interface ProjectService { + env: EnvironmentService; + languageModes: LanguageModes; + onDocumentFormatting(params: DocumentFormattingParams): Promise; + onCompletion(params: CompletionParams): Promise; + onCompletionResolve(item: CompletionItem): Promise; + onHover(params: TextDocumentPositionParams): Promise; + onDocumentHighlight(params: TextDocumentPositionParams): Promise; + onDefinition(params: TextDocumentPositionParams): Promise; + onReferences(params: TextDocumentPositionParams): Promise; + onDocumentLinks(params: DocumentLinkParams): Promise; + onDocumentSymbol(params: DocumentSymbolParams): Promise; + onDocumentColors(params: DocumentColorParams): Promise; + onColorPresentations(params: ColorPresentationParams): Promise; + onSignatureHelp(params: TextDocumentPositionParams): Promise; + onFoldingRanges(params: FoldingRangeParams): Promise; + onCodeAction(params: CodeActionParams): Promise; + doValidate(doc: TextDocument, cancellationToken?: VCancellationToken): Promise; + getRefactorEdits(refactorAction: RefactorAction): Promise; + dispose(): Promise; +} + +export async function createProjectService( + env: EnvironmentService, + documentService: DocumentService, + globalSnippetDir: string | undefined, + dependencyService: DependencyService +): Promise { + const vueInfoService = new VueInfoService(); + const languageModes = new LanguageModes(); + + function getValidationFlags(): Record { + const config = env.getConfig(); + return { + 'vue-html': config.vetur.validation.template, + css: config.vetur.validation.style, + postcss: config.vetur.validation.style, + scss: config.vetur.validation.style, + less: config.vetur.validation.style, + javascript: config.vetur.validation.script + }; + } + + vueInfoService.init(languageModes); + await languageModes.init( + env, + { + infoService: vueInfoService, + dependencyService + }, + globalSnippetDir + ); + + return { + env, + languageModes, + async onDocumentFormatting({ textDocument, options }) { + if (!env.getConfig().vetur.format.enable) { + return []; + } + + const doc = documentService.getDocument(textDocument.uri)!; + + const modeRanges = languageModes.getAllLanguageModeRangesInDocument(doc); + const allEdits: TextEdit[] = []; + + const errMessages: string[] = []; + + modeRanges.forEach(modeRange => { + if (modeRange.mode && modeRange.mode.format) { + try { + const edits = modeRange.mode.format(doc, { start: modeRange.start, end: modeRange.end }, options); + for (const edit of edits) { + allEdits.push(edit); + } + } catch (err) { + errMessages.push(err.toString()); + } + } + }); + + if (errMessages.length !== 0) { + console.error('Formatting failed: "' + errMessages.join('\n') + '"'); + return []; + } + + return allEdits; + }, + async onCompletion({ textDocument, position, context }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, position); + if (mode && mode.doComplete) { + /** + * Only use space as trigger character in `vue-html` mode + */ + if ( + mode.getId() !== 'vue-html' && + context && + context?.triggerKind === CompletionTriggerKind.TriggerCharacter && + context.triggerCharacter === ' ' + ) { + return NULL_COMPLETION; + } + + return mode.doComplete(doc, position); + } + + return NULL_COMPLETION; + }, + async onCompletionResolve(item) { + if (item.data) { + const uri: string = item.data.uri; + const languageId: LanguageId = item.data.languageId; + + /** + * Template files need to go through HTML-template service + */ + if (uri.endsWith('.template')) { + const doc = documentService.getDocument(uri.slice(0, -'.template'.length)); + const mode = languageModes.getMode(languageId); + if (doc && mode && mode.doResolve) { + return mode.doResolve(doc, item); + } + } + + if (uri && languageId) { + const doc = documentService.getDocument(uri); + const mode = languageModes.getMode(languageId); + if (doc && mode && mode.doResolve) { + return mode.doResolve(doc, item); + } + } + } + + return item; + }, + async onHover({ textDocument, position }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, position); + if (mode && mode.doHover) { + return mode.doHover(doc, position); + } + return NULL_HOVER; + }, + async onDocumentHighlight({ textDocument, position }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, position); + if (mode && mode.findDocumentHighlight) { + return mode.findDocumentHighlight(doc, position); + } + return []; + }, + async onDefinition({ textDocument, position }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, position); + if (mode && mode.findDefinition) { + return mode.findDefinition(doc, position); + } + return []; + }, + async onReferences({ textDocument, position }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, position); + if (mode && mode.findReferences) { + return mode.findReferences(doc, position); + } + return []; + }, + async onDocumentLinks({ textDocument }) { + const doc = documentService.getDocument(textDocument.uri)!; + const documentContext: DocumentContext = { + resolveReference: ref => { + if (ref[0] === '/') { + return URI.file(path.resolve(env.getProjectRoot(), ref)).toString(); + } + const fsPath = getFileFsPath(doc.uri); + return URI.file(path.resolve(fsPath, '..', ref)).toString(); + } + }; + + const links: DocumentLink[] = []; + languageModes.getAllLanguageModeRangesInDocument(doc).forEach(m => { + if (m.mode.findDocumentLinks) { + links.push.apply(links, m.mode.findDocumentLinks(doc, documentContext)); + } + }); + return links; + }, + async onDocumentSymbol({ textDocument }) { + const doc = documentService.getDocument(textDocument.uri)!; + const symbols: SymbolInformation[] = []; + + languageModes.getAllLanguageModeRangesInDocument(doc).forEach(m => { + if (m.mode.findDocumentSymbols) { + symbols.push.apply(symbols, m.mode.findDocumentSymbols(doc)); + } + }); + return symbols; + }, + async onDocumentColors({ textDocument }) { + const doc = documentService.getDocument(textDocument.uri)!; + const colors: ColorInformation[] = []; + + const distinctModes: Set = new Set(); + languageModes.getAllLanguageModeRangesInDocument(doc).forEach(m => { + distinctModes.add(m.mode); + }); + + for (const mode of distinctModes) { + if (mode.findDocumentColors) { + colors.push.apply(colors, mode.findDocumentColors(doc)); + } + } + + return colors; + }, + async onColorPresentations({ textDocument, color, range }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, range.start); + if (mode && mode.getColorPresentations) { + return mode.getColorPresentations(doc, color, range); + } + return []; + }, + async onSignatureHelp({ textDocument, position }) { + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, position); + if (mode && mode.doSignatureHelp) { + return mode.doSignatureHelp(doc, position); + } + return NULL_SIGNATURE; + }, + async onFoldingRanges({ textDocument }) { + const doc = documentService.getDocument(textDocument.uri)!; + const lmrs = languageModes.getAllLanguageModeRangesInDocument(doc); + + const result: FoldingRange[] = []; + + lmrs.forEach(lmr => { + if (lmr.mode.getFoldingRanges) { + lmr.mode.getFoldingRanges(doc).forEach(r => result.push(r)); + } + + result.push({ + startLine: lmr.start.line, + startCharacter: lmr.start.character, + endLine: lmr.end.line, + endCharacter: lmr.end.character + }); + }); + + return result; + }, + async onCodeAction({ textDocument, range, context }: CodeActionParams) { + if (!env.getConfig().vetur.languageFeatures.codeActions) { + return []; + } + + const doc = documentService.getDocument(textDocument.uri)!; + const mode = languageModes.getModeAtPosition(doc, range.start); + if (languageModes.getModeAtPosition(doc, range.end) !== mode) { + return []; + } + if (mode && mode.getCodeActions) { + return mode.getCodeActions(doc, range, /*formatParams*/ {} as any, context); + } + return []; + }, + async doValidate(doc: TextDocument, cancellationToken?: VCancellationToken) { + const diagnostics: Diagnostic[] = []; + if (doc.languageId === 'vue') { + const validationFlags = getValidationFlags(); + for (const lmr of languageModes.getAllLanguageModeRangesInDocument(doc)) { + if (lmr.mode.doValidation) { + if (validationFlags[lmr.mode.getId()]) { + diagnostics.push.apply(diagnostics, await lmr.mode.doValidation(doc, cancellationToken)); + } + // Special case for template type checking + else if ( + lmr.mode.getId() === 'vue-html' && + env.getConfig().vetur.experimental.templateInterpolationService + ) { + diagnostics.push.apply(diagnostics, await lmr.mode.doValidation(doc, cancellationToken)); + } + } + } + } + if (cancellationToken?.isCancellationRequested) { + return null; + } + return diagnostics; + }, + async getRefactorEdits(refactorAction: RefactorAction) { + const uri = URI.file(refactorAction.fileName).toString(); + const doc = documentService.getDocument(uri)!; + const startPos = doc.positionAt(refactorAction.textRange.pos); + const mode = languageModes.getModeAtPosition(doc, startPos); + if (mode && mode.getRefactorEdits) { + return mode.getRefactorEdits(doc, refactorAction); + } + return undefined; + }, + async dispose() { + languageModes.dispose(); + } + }; +} diff --git a/server/src/services/typescriptService/preprocess.ts b/server/src/services/typescriptService/preprocess.ts index 6dacb59d29..05444b4ec9 100644 --- a/server/src/services/typescriptService/preprocess.ts +++ b/server/src/services/typescriptService/preprocess.ts @@ -60,10 +60,6 @@ export function createUpdater( const modificationTracker = new WeakSet(); const printer = tsModule.createPrinter(); - function isTSLike(scriptKind: ts.ScriptKind | undefined) { - return scriptKind === tsModule.ScriptKind.TS || scriptKind === tsModule.ScriptKind.TSX; - } - function modifySourceFile( fileName: string, sourceFile: ts.SourceFile, @@ -75,7 +71,7 @@ export function createUpdater( return; } - if (isVueFile(fileName) && !isTSLike(scriptKind)) { + if (isVueFile(fileName)) { modifyVueScript(tsModule, sourceFile); modificationTracker.add(sourceFile); return; diff --git a/server/src/services/typescriptService/serviceHost.ts b/server/src/services/typescriptService/serviceHost.ts index dde788b460..00e892245c 100644 --- a/server/src/services/typescriptService/serviceHost.ts +++ b/server/src/services/typescriptService/serviceHost.ts @@ -14,9 +14,10 @@ import { isVirtualVueTemplateFile, isVueFile } from './util'; import { logger } from '../../log'; import { ModuleResolutionCache } from './moduleResolutionCache'; import { globalScope } from './transformTemplate'; -import { inferVueVersion, VueVersion } from './vueVersion'; import { ChildComponent } from '../vueInfoService'; import { RuntimeLibrary } from '../dependencyService'; +import { EnvironmentService } from '../EnvironmentService'; +import { VueVersion } from '../../utils/vueVersion'; const NEWLINE = process.platform === 'win32' ? '\r\n' : '\n'; @@ -85,38 +86,75 @@ export interface IServiceHost { */ export function getServiceHost( tsModule: RuntimeLibrary['typescript'], - workspacePath: string, + env: EnvironmentService, updatedScriptRegionDocuments: LanguageModelCache ): IServiceHost { patchTS(tsModule); let currentScriptDoc: TextDocument; + // host variable + let vueVersion = env.getVueVersion(); let projectVersion = 1; - const versions = new Map(); - const localScriptRegionDocuments = new Map(); - const nodeModuleSnapshots = new Map(); - const projectFileSnapshots = new Map(); - const moduleResolutionCache = new ModuleResolutionCache(); - - const parsedConfig = getParsedConfig(tsModule, workspacePath); - /** - * Only js/ts files in local project - */ - const initialProjectFiles = parsedConfig.fileNames; - logger.logDebug( - `Initializing ServiceHost with ${initialProjectFiles.length} files: ${JSON.stringify(initialProjectFiles)}` - ); - const scriptFileNameSet = new Set(initialProjectFiles); - - const vueSys = getVueSys(tsModule, scriptFileNameSet); + let versions = new Map(); + let localScriptRegionDocuments = new Map(); + let nodeModuleSnapshots = new Map(); + let projectFileSnapshots = new Map(); + let moduleResolutionCache = new ModuleResolutionCache(); + + let parsedConfig: ts.ParsedCommandLine; + let scriptFileNameSet: Set; + + let vueSys: ts.System; + let compilerOptions: ts.CompilerOptions; + + let jsHost: ts.LanguageServiceHost; + let templateHost: ts.LanguageServiceHost; + + let registry: ts.DocumentRegistry; + let jsLanguageService: ts.LanguageService; + let templateLanguageService: ts.LanguageService; + init(); + + function getCompilerOptions() { + const compilerOptions = { + ...getDefaultCompilerOptions(tsModule), + ...parsedConfig.options + }; + compilerOptions.allowNonTsExtensions = true; + return compilerOptions; + } - const vueVersion = inferVueVersion(workspacePath); - const compilerOptions = { - ...getDefaultCompilerOptions(tsModule), - ...parsedConfig.options - }; - compilerOptions.allowNonTsExtensions = true; + function init() { + vueVersion = env.getVueVersion(); + projectVersion = 1; + versions = new Map(); + localScriptRegionDocuments = new Map(); + nodeModuleSnapshots = new Map(); + projectFileSnapshots = new Map(); + moduleResolutionCache = new ModuleResolutionCache(); + + parsedConfig = getParsedConfig(tsModule, env.getProjectRoot(), env.getTsConfigPath()); + const initialProjectFiles = parsedConfig.fileNames; + logger.logDebug( + `Initializing ServiceHost with ${initialProjectFiles.length} files: ${JSON.stringify(initialProjectFiles)}` + ); + scriptFileNameSet = new Set(initialProjectFiles); + vueSys = getVueSys(tsModule, scriptFileNameSet); + compilerOptions = getCompilerOptions(); + + jsHost = createLanguageServiceHost(compilerOptions); + templateHost = createLanguageServiceHost({ + ...compilerOptions, + noUnusedLocals: false, + noUnusedParameters: false, + allowJs: true, + checkJs: true + }); + registry = tsModule.createDocumentRegistry(true); + jsLanguageService = tsModule.createLanguageService(jsHost, registry); + templateLanguageService = patchTemplateService(tsModule.createLanguageService(templateHost, registry)); + } function queryVirtualFileInfo( fileName: string, @@ -200,6 +238,13 @@ export function getServiceHost( // External Documents: JS/TS, non Vue documents function updateExternalDocument(fileFsPath: string) { + // reload `tsconfig.json` or vue version changed + if (fileFsPath === env.getTsConfigPath() || vueVersion !== env.getVueVersion()) { + logger.logInfo(`refresh ts language service when ${fileFsPath} changed.`); + init(); + return; + } + // respect tsconfig // use *internal* function const configFileSpecs = (parsedConfig as any).configFileSpecs; @@ -207,7 +252,7 @@ export function getServiceHost( if ( isExcludedFile && configFileSpecs && - isExcludedFile(fileFsPath, configFileSpecs, workspacePath, true, workspacePath) + isExcludedFile(fileFsPath, configFileSpecs, env.getProjectRoot(), true, env.getProjectRoot()) ) { return; } @@ -430,26 +475,13 @@ export function getServiceHost( getChangeRange: () => void 0 }; }, - getCurrentDirectory: () => workspacePath, + getCurrentDirectory: () => env.getProjectRoot(), getDefaultLibFileName: tsModule.getDefaultLibFilePath, getNewLine: () => NEWLINE, useCaseSensitiveFileNames: () => true }; } - const jsHost = createLanguageServiceHost(compilerOptions); - const templateHost = createLanguageServiceHost({ - ...compilerOptions, - noUnusedLocals: false, - noUnusedParameters: false, - allowJs: true, - checkJs: true - }); - - const registry = tsModule.createDocumentRegistry(true); - let jsLanguageService = tsModule.createLanguageService(jsHost, registry); - const templateLanguageService = patchTemplateService(tsModule.createLanguageService(templateHost, registry)); - return { queryVirtualFileInfo, updateCurrentVirtualVueTextDocument, @@ -490,9 +522,9 @@ function patchTemplateService(original: ts.LanguageService): ts.LanguageService }; } -function defaultIgnorePatterns(tsModule: RuntimeLibrary['typescript'], workspacePath: string) { +function defaultIgnorePatterns(tsModule: RuntimeLibrary['typescript'], projectPath: string) { const nodeModules = ['node_modules', '**/node_modules/*']; - const gitignore = tsModule.findConfigFile(workspacePath, tsModule.sys.fileExists, '.gitignore'); + const gitignore = tsModule.findConfigFile(projectPath, tsModule.sys.fileExists, '.gitignore'); if (!gitignore) { return nodeModules; } @@ -509,18 +541,21 @@ function getScriptKind(tsModule: RuntimeLibrary['typescript'], langId: string): : tsModule.ScriptKind.JS; } -function getParsedConfig(tsModule: RuntimeLibrary['typescript'], workspacePath: string) { - const configFilename = - tsModule.findConfigFile(workspacePath, tsModule.sys.fileExists, 'tsconfig.json') || - tsModule.findConfigFile(workspacePath, tsModule.sys.fileExists, 'jsconfig.json'); +function getParsedConfig( + tsModule: RuntimeLibrary['typescript'], + projectRoot: string, + tsconfigPath: string | undefined +) { + const configFilename = tsconfigPath; const configJson = (configFilename && tsModule.readConfigFile(configFilename, tsModule.sys.readFile).config) || { - exclude: defaultIgnorePatterns(tsModule, workspacePath) + include: ['**/*.vue'], + exclude: defaultIgnorePatterns(tsModule, projectRoot) }; // existingOptions should be empty since it always takes priority return tsModule.parseJsonConfigFileContent( configJson, tsModule.sys, - workspacePath, + projectRoot, /*existingOptions*/ {}, configFilename, /*resolutionStack*/ undefined, diff --git a/server/src/services/vls.ts b/server/src/services/vls.ts index e3a48fba63..5584d75157 100644 --- a/server/src/services/vls.ts +++ b/server/src/services/vls.ts @@ -1,6 +1,11 @@ import path from 'path'; -import fs from 'fs'; -import { getFileFsPath, normalizeFileNameToFsPath } from '../utils/paths'; +import { + getFileFsPath, + getFsPathToUri, + getPathDepth, + normalizeFileNameToFsPath, + normalizeFileNameResolve +} from '../utils/paths'; import { DidChangeConfigurationParams, @@ -19,7 +24,6 @@ import { DocumentSymbolParams, CodeActionParams, CompletionParams, - CompletionTriggerKind, ExecuteCommandParams, ApplyWorkspaceEditRequest, FoldingRangeParams @@ -29,7 +33,6 @@ import { CompletionItem, CompletionList, Definition, - Diagnostic, DocumentHighlight, DocumentLink, Hover, @@ -38,101 +41,86 @@ import { SymbolInformation, TextEdit, ColorPresentation, - Range, - FoldingRange + FoldingRange, + DocumentUri } from 'vscode-languageserver-types'; import type { TextDocument } from 'vscode-languageserver-textdocument'; import { URI } from 'vscode-uri'; -import { LanguageModes, LanguageModeRange, LanguageMode } from '../embeddedSupport/languageModes'; import { NULL_COMPLETION, NULL_HOVER, NULL_SIGNATURE } from '../modes/nullMode'; -import { VueInfoService } from './vueInfoService'; -import { createDependencyService, DependencyService } from './dependencyService'; +import { createDependencyService, createNodeModulesPaths } from './dependencyService'; import _ from 'lodash'; -import { DocumentContext, RefactorAction } from '../types'; +import { RefactorAction } from '../types'; import { DocumentService } from './documentService'; import { VueHTMLMode } from '../modes/template'; import { logger } from '../log'; -import { getDefaultVLSConfig, VLSFullConfig, VLSConfig } from '../config'; -import { LanguageId } from '../embeddedSupport/embeddedSupport'; +import { getDefaultVLSConfig, VLSFullConfig, getVeturFullConfig, VeturFullConfig, BasicComponentInfo } from '../config'; import { APPLY_REFACTOR_COMMAND } from '../modes/script/javascript'; import { VCancellationToken, VCancellationTokenSource } from '../utils/cancellationToken'; +import { findConfigFile, requireUncached } from '../utils/workspace'; +import { createProjectService, ProjectService } from './projectService'; +import { createEnvironmentService } from './EnvironmentService'; +import { getVueVersionKey } from '../utils/vueVersion'; +import { accessSync, constants, existsSync } from 'fs'; + +interface ProjectConfig { + vlsFullConfig: VLSFullConfig; + isExistVeturConfig: boolean; + rootPathForConfig: string; + workspaceFsPath: string; + rootFsPath: string; + tsconfigPath: string | undefined; + packagePath: string | undefined; + snippetFolder: string; + globalComponents: BasicComponentInfo[]; +} export class VLS { - // @Todo: Remove this and DocumentContext - private workspacePath: string | undefined; - + private workspaces: Map< + string, + VeturFullConfig & { name: string; workspaceFsPath: string; isExistVeturConfig: boolean } + >; + private nodeModulesMap: Map; private documentService: DocumentService; - private vueInfoService: VueInfoService; - private dependencyService: DependencyService; - - private languageModes: LanguageModes; - + private globalSnippetDir: string; + private projects: Map; private pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; private cancellationTokenValidationRequests: { [uri: string]: VCancellationTokenSource } = {}; private validationDelayMs = 200; - private validation: { [k: string]: boolean } = { - 'vue-html': true, - html: true, - css: true, - scss: true, - less: true, - postcss: true, - javascript: true - }; - private templateInterpolationValidation = false; private documentFormatterRegistration: Disposable | undefined; - private config: VLSFullConfig; + private workspaceConfig: unknown; constructor(private lspConnection: Connection) { this.documentService = new DocumentService(this.lspConnection); - this.vueInfoService = new VueInfoService(); - this.dependencyService = createDependencyService(); - - this.languageModes = new LanguageModes(); + this.workspaces = new Map(); + this.projects = new Map(); + this.nodeModulesMap = new Map(); } async init(params: InitializeParams) { - const config = this.getFullConfig(params.initializationOptions?.config); + const workspaceFolders = + !Array.isArray(params.workspaceFolders) && params.rootPath + ? [{ name: '', fsPath: normalizeFileNameToFsPath(params.rootPath) }] + : params.workspaceFolders?.map(el => ({ name: el.name, fsPath: getFileFsPath(el.uri) })) ?? []; - const workspacePath = params.rootPath; - if (!workspacePath) { + if (workspaceFolders.length === 0) { console.error('No workspace path found. Vetur initialization failed.'); return { capabilities: {} }; } - this.workspacePath = normalizeFileNameToFsPath(workspacePath); + this.globalSnippetDir = params.initializationOptions?.globalSnippetDir; - // Enable Yarn PnP support https://yarnpkg.com/features/pnp - if (!process.versions.pnp) { - if (fs.existsSync(path.join(this.workspacePath, '.pnp.js'))) { - require(path.join(workspacePath, '.pnp.js')).setup(); - } else if (fs.existsSync(path.join(this.workspacePath, '.pnp.cjs'))) { - require(path.join(workspacePath, '.pnp.cjs')).setup(); - } - } + await Promise.all(workspaceFolders.map(workspace => this.addWorkspace(workspace))); - this.vueInfoService.init(this.languageModes); - await this.dependencyService.init( - this.workspacePath, - config.vetur.useWorkspaceDependencies, - config.typescript.tsdk - ); + this.workspaceConfig = this.getVLSFullConfig({}, params.initializationOptions?.config); - await this.languageModes.init( - this.workspacePath, - { - infoService: this.vueInfoService, - dependencyService: this.dependencyService - }, - params.initializationOptions?.globalSnippetDir - ); - - this.configure(config); + if (params.capabilities.workspace?.workspaceFolders) { + this.setupWorkspaceListeners(); + } this.setupConfigListeners(); this.setupLSPHandlers(); this.setupCustomLSPHandlers(); @@ -147,20 +135,205 @@ export class VLS { this.lspConnection.listen(); } - private getFullConfig(config: any | undefined): VLSFullConfig { - return config ? _.merge(getDefaultVLSConfig(), config) : getDefaultVLSConfig(); + private getVLSFullConfig(settings: VeturFullConfig['settings'], config: any | undefined): VLSFullConfig { + const result = config ? _.merge(getDefaultVLSConfig(), config) : getDefaultVLSConfig(); + Object.keys(settings).forEach(key => { + _.set(result, key, settings[key]); + }); + return result; + } + + private async addWorkspace(workspace: { name: string; fsPath: string }) { + // Enable Yarn PnP support https://yarnpkg.com/features/pnp + if (!process.versions.pnp) { + if (existsSync(path.join(workspace.fsPath, '.pnp.js'))) { + require(path.join(workspace.fsPath, '.pnp.js')).setup(); + } else if (existsSync(path.join(workspace.fsPath, '.pnp.cjs'))) { + require(path.join(workspace.fsPath, '.pnp.cjs')).setup(); + } + } + + const veturConfigPath = findConfigFile(workspace.fsPath, 'vetur.config.js'); + const rootPathForConfig = normalizeFileNameToFsPath( + veturConfigPath ? path.dirname(veturConfigPath) : workspace.fsPath + ); + if (!this.workspaces.has(rootPathForConfig)) { + this.workspaces.set(rootPathForConfig, { + name: workspace.name, + ...(await getVeturFullConfig( + rootPathForConfig, + workspace.fsPath, + veturConfigPath ? requireUncached(veturConfigPath) : {} + )), + isExistVeturConfig: !!veturConfigPath, + workspaceFsPath: workspace.fsPath + }); + } + } + + private setupWorkspaceListeners() { + this.lspConnection.onInitialized(() => { + this.lspConnection.workspace.onDidChangeWorkspaceFolders(async e => { + await Promise.all(e.added.map(el => this.addWorkspace({ name: el.name, fsPath: getFileFsPath(el.uri) }))); + }); + }); } private setupConfigListeners() { this.lspConnection.onDidChangeConfiguration(async ({ settings }: DidChangeConfigurationParams) => { - const config = this.getFullConfig(settings); - this.configure(config); - this.setupDynamicFormatters(config); + let isFormatEnable = false; + this.projects.forEach(project => { + const veturConfig = this.workspaces.get(project.env.getRootPathForConfig()); + if (!veturConfig) { + return; + } + const fullConfig = this.getVLSFullConfig(veturConfig.settings, settings); + project.env.configure(fullConfig); + isFormatEnable = isFormatEnable || fullConfig.vetur.format.enable; + }); + this.setupDynamicFormatters(isFormatEnable); }); this.documentService.getAllDocuments().forEach(this.triggerValidation); } + private getAllProjectConfigs(): ProjectConfig[] { + return _.flatten( + Array.from(this.workspaces.entries()).map(([rootPathForConfig, veturConfig]) => + veturConfig.projects.map(project => ({ + ...project, + rootPathForConfig, + vlsFullConfig: this.getVLSFullConfig(veturConfig.settings, this.workspaceConfig), + workspaceFsPath: veturConfig.workspaceFsPath, + isExistVeturConfig: veturConfig.isExistVeturConfig + })) + ) + ) + .map(project => ({ + vlsFullConfig: project.vlsFullConfig, + isExistVeturConfig: project.isExistVeturConfig, + rootPathForConfig: project.rootPathForConfig, + workspaceFsPath: project.workspaceFsPath, + rootFsPath: normalizeFileNameResolve(project.rootPathForConfig, project.root), + tsconfigPath: project.tsconfig, + packagePath: project.package, + snippetFolder: project.snippetFolder, + globalComponents: project.globalComponents + })) + .sort((a, b) => getPathDepth(b.rootFsPath, '/') - getPathDepth(a.rootFsPath, '/')); + } + + private warnProjectIfNeed(projectConfig: ProjectConfig) { + if (projectConfig.vlsFullConfig.vetur.ignoreProjectWarning) { + return; + } + + const showErrorIfCantAccess = (name: string, fsPath: string) => { + try { + accessSync(fsPath, constants.R_OK); + } catch { + this.lspConnection.window.showErrorMessage(`Vetur can't access ${projectConfig.tsconfigPath} for ${name}.`); + } + }; + + const showWarningAndLearnMore = (message: string, url: string) => { + this.lspConnection.window.showWarningMessage(message, { title: 'Learn More' }).then(action => { + if (action) { + this.openWebsite(url); + } + }); + }; + + const getCantFindMessage = (fileNames: string[]) => + `Vetur can't find ${fileNames.map(el => `\`${el}\``).join(' or ')} in ${projectConfig.rootPathForConfig}.`; + if (!projectConfig.tsconfigPath) { + showWarningAndLearnMore( + getCantFindMessage(['tsconfig.json', 'jsconfig.json']), + 'https://vuejs.github.io/vetur/guide/FAQ.html#vetur-can-t-find-tsconfig-json-jsconfig-json-in-xxxx-xxxxxx' + ); + } else { + showErrorIfCantAccess('ts/js config', projectConfig.tsconfigPath); + } + if (!projectConfig.packagePath) { + showWarningAndLearnMore( + getCantFindMessage(['package.json']), + 'https://vuejs.github.io/vetur/guide/FAQ.html#vetur-can-t-find-package-json-in-xxxx-xxxxxx' + ); + } else { + showErrorIfCantAccess('ts/js config', projectConfig.packagePath); + } + + // ignore not in project root warning when vetur config file is exist. + if (projectConfig.isExistVeturConfig) { + return; + } + + if ( + ![ + normalizeFileNameResolve(projectConfig.rootPathForConfig, 'tsconfig.json'), + normalizeFileNameResolve(projectConfig.rootPathForConfig, 'jsconfig.json') + ].includes(projectConfig.tsconfigPath ?? '') + ) { + showWarningAndLearnMore( + `Vetur find \`tsconfig.json\`/\`jsconfig.json\`, but they aren\'t in the project root.`, + 'https://vuejs.github.io/vetur/guide/FAQ.html#vetur-find-xxx-but-they-aren-t-in-the-project-root' + ); + } + + if (normalizeFileNameResolve(projectConfig.rootPathForConfig, 'package.json') !== projectConfig.packagePath) { + showWarningAndLearnMore( + `Vetur find \`package.json\`/, but they aren\'t in the project root.`, + 'https://vuejs.github.io/vetur/guide/FAQ.html#vetur-find-xxx-but-they-aren-t-in-the-project-root' + ); + } + } + + private async getProjectService(uri: DocumentUri): Promise { + const projectConfigs = this.getAllProjectConfigs(); + const docFsPath = getFileFsPath(uri); + const projectConfig = projectConfigs.find(projectConfig => docFsPath.startsWith(projectConfig.rootFsPath)); + if (!projectConfig) { + return undefined; + } + if (this.projects.has(projectConfig.rootFsPath)) { + return this.projects.get(projectConfig.rootFsPath); + } + + // init project + // Yarn Pnp don't need this. https://yarnpkg.com/features/pnp + const nodeModulePaths = !process.versions.pnp + ? this.nodeModulesMap.get(projectConfig.rootPathForConfig) ?? + createNodeModulesPaths(projectConfig.rootPathForConfig) + : []; + if (this.nodeModulesMap.has(projectConfig.rootPathForConfig)) { + this.nodeModulesMap.set(projectConfig.rootPathForConfig, nodeModulePaths); + } + const dependencyService = await createDependencyService( + projectConfig.rootPathForConfig, + projectConfig.workspaceFsPath, + projectConfig.vlsFullConfig.vetur.useWorkspaceDependencies, + nodeModulePaths, + projectConfig.vlsFullConfig.typescript.tsdk + ); + this.warnProjectIfNeed(projectConfig); + const project = await createProjectService( + createEnvironmentService( + projectConfig.rootPathForConfig, + projectConfig.rootFsPath, + projectConfig.tsconfigPath, + projectConfig.packagePath, + projectConfig.snippetFolder, + projectConfig.globalComponents, + projectConfig.vlsFullConfig + ), + this.documentService, + this.globalSnippetDir, + dependencyService + ); + this.projects.set(projectConfig.rootFsPath, project); + return project; + } + private setupLSPHandlers() { this.lspConnection.onCompletion(this.onCompletion.bind(this)); this.lspConnection.onCompletionResolve(this.onCompletionResolve.bind(this)); @@ -183,8 +356,31 @@ export class VLS { } private setupCustomLSPHandlers() { - this.lspConnection.onRequest('$/queryVirtualFileInfo', ({ fileName, currFileText }) => { - return (this.languageModes.getMode('vue-html') as VueHTMLMode).queryVirtualFileInfo(fileName, currFileText); + this.lspConnection.onRequest('$/doctor', async ({ fileName }) => { + const uri = getFsPathToUri(fileName); + const projectConfigs = this.getAllProjectConfigs(); + const project = await this.getProjectService(uri); + + return JSON.stringify( + { + name: 'Vetur doctor info', + fileName, + currentProject: { + vueVersion: project ? getVueVersionKey(project?.env.getVueVersion()) : null, + rootPathForConfig: project?.env.getRootPathForConfig() ?? null, + projectRootFsPath: project?.env.getProjectRoot() ?? null + }, + activeProjects: Array.from(this.projects.keys()), + projectConfigs + }, + null, + 2 + ); + }); + + this.lspConnection.onRequest('$/queryVirtualFileInfo', async ({ fileName, currFileText }) => { + const project = await this.getProjectService(getFsPathToUri(fileName)); + return (project?.languageModes.getMode('vue-html') as VueHTMLMode).queryVirtualFileInfo(fileName, currFileText); }); this.lspConnection.onRequest('$/getDiagnostics', async params => { @@ -197,8 +393,8 @@ export class VLS { }); } - private async setupDynamicFormatters(settings: VLSFullConfig) { - if (settings.vetur.format.enable) { + private async setupDynamicFormatters(enable: boolean) { + if (enable) { if (!this.documentFormatterRegistration) { this.documentFormatterRegistration = await this.lspConnection.client.register(DocumentFormattingRequest.type, { documentSelector: [{ language: 'vue' }] @@ -220,15 +416,31 @@ export class VLS { this.lspConnection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }); }); this.lspConnection.onDidChangeWatchedFiles(({ changes }) => { - const jsMode = this.languageModes.getMode('javascript'); - if (!jsMode) { - throw Error(`Can't find JS mode.`); - } - - changes.forEach(c => { + changes.forEach(async c => { if (c.type === FileChangeType.Changed) { const fsPath = getFileFsPath(c.uri); - jsMode.onDocumentChanged!(fsPath); + + // when `vetur.config.js` changed + if (this.workspaces.has(fsPath)) { + logger.logInfo(`refresh vetur config when ${fsPath} changed.`); + const name = this.workspaces.get(fsPath)?.name ?? ''; + this.workspaces.delete(fsPath); + await this.addWorkspace({ name, fsPath }); + this.projects.forEach((project, projectRoot) => { + if (project.env.getRootPathForConfig() === fsPath) { + project.dispose(); + this.projects.delete(projectRoot); + } + }); + return; + } + + const project = await this.getProjectService(c.uri); + project?.languageModes.getAllModes().forEach(m => { + if (m.onDocumentChanged) { + m.onDocumentChanged(fsPath); + } + }); } }); @@ -238,284 +450,105 @@ export class VLS { }); } - configure(config: VLSConfig): void { - this.config = config; - - const veturValidationOptions = config.vetur.validation; - this.validation['vue-html'] = veturValidationOptions.template; - this.validation.css = veturValidationOptions.style; - this.validation.postcss = veturValidationOptions.style; - this.validation.scss = veturValidationOptions.style; - this.validation.less = veturValidationOptions.style; - this.validation.javascript = veturValidationOptions.script; - - this.templateInterpolationValidation = config.vetur.experimental.templateInterpolationService; - - this.languageModes.getAllModes().forEach(m => { - if (m.configure) { - m.configure(config); - } - }); - - logger.setLevel(config.vetur.dev.logLevel); - } - /** * Custom Notifications */ - - displayInfoMessage(msg: string): void { - this.lspConnection.sendNotification('$/displayInfo', msg); - } - displayWarningMessage(msg: string): void { - this.lspConnection.sendNotification('$/displayWarning', msg); - } - displayErrorMessage(msg: string): void { - this.lspConnection.sendNotification('$/displayError', msg); + openWebsite(url: string): void { + this.lspConnection.sendNotification('$/openWebsite', url); } /** * Language Features */ - onDocumentFormatting({ textDocument, options }: DocumentFormattingParams): TextEdit[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - - const modeRanges = this.languageModes.getAllLanguageModeRangesInDocument(doc); - const allEdits: TextEdit[] = []; - - const errMessages: string[] = []; - - modeRanges.forEach(modeRange => { - if (modeRange.mode && modeRange.mode.format) { - try { - const edits = modeRange.mode.format(doc, this.toSimpleRange(modeRange), options); - for (const edit of edits) { - allEdits.push(edit); - } - } catch (err) { - errMessages.push(err.toString()); - } - } - }); - - if (errMessages.length !== 0) { - this.displayErrorMessage('Formatting failed: "' + errMessages.join('\n') + '"'); - return []; - } - - return allEdits; - } + async onDocumentFormatting(params: DocumentFormattingParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - private toSimpleRange(modeRange: LanguageModeRange): Range { - return { - start: modeRange.start, - end: modeRange.end - }; + return project?.onDocumentFormatting(params) ?? []; } - onCompletion({ textDocument, position, context }: CompletionParams): CompletionList { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, position); - if (mode && mode.doComplete) { - /** - * Only use space as trigger character in `vue-html` mode - */ - if ( - mode.getId() !== 'vue-html' && - context && - context?.triggerKind === CompletionTriggerKind.TriggerCharacter && - context.triggerCharacter === ' ' - ) { - return NULL_COMPLETION; - } - - return mode.doComplete(doc, position); - } + async onCompletion(params: CompletionParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - return NULL_COMPLETION; + return project?.onCompletion(params) ?? NULL_COMPLETION; } - onCompletionResolve(item: CompletionItem): CompletionItem { - if (item.data) { - const uri: string = item.data.uri; - const languageId: LanguageId = item.data.languageId; + async onCompletionResolve(item: CompletionItem): Promise { + const project = await this.getProjectService(item.data.uri); - /** - * Template files need to go through HTML-template service - */ - if (uri.endsWith('.template')) { - const doc = this.documentService.getDocument(uri.slice(0, -'.template'.length)); - const mode = this.languageModes.getMode(languageId); - if (doc && mode && mode.doResolve) { - return mode.doResolve(doc, item); - } - } + return project?.onCompletionResolve(item) ?? item; + } - if (uri && languageId) { - const doc = this.documentService.getDocument(uri); - const mode = this.languageModes.getMode(languageId); - if (doc && mode && mode.doResolve) { - return mode.doResolve(doc, item); - } - } - } + async onHover(params: TextDocumentPositionParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - return item; + return project?.onHover(params) ?? NULL_HOVER; } - onHover({ textDocument, position }: TextDocumentPositionParams): Hover { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, position); - if (mode && mode.doHover) { - return mode.doHover(doc, position); - } - return NULL_HOVER; - } + async onDocumentHighlight(params: TextDocumentPositionParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - onDocumentHighlight({ textDocument, position }: TextDocumentPositionParams): DocumentHighlight[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, position); - if (mode && mode.findDocumentHighlight) { - return mode.findDocumentHighlight(doc, position); - } - return []; + return project?.onDocumentHighlight(params) ?? []; } - onDefinition({ textDocument, position }: TextDocumentPositionParams): Definition { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, position); - if (mode && mode.findDefinition) { - return mode.findDefinition(doc, position); - } - return []; - } + async onDefinition(params: TextDocumentPositionParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - onReferences({ textDocument, position }: TextDocumentPositionParams): Location[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, position); - if (mode && mode.findReferences) { - return mode.findReferences(doc, position); - } - return []; + return project?.onDefinition(params) ?? []; } - onDocumentLinks({ textDocument }: DocumentLinkParams): DocumentLink[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const documentContext: DocumentContext = { - resolveReference: ref => { - if (this.workspacePath && ref[0] === '/') { - return URI.file(path.resolve(this.workspacePath, ref)).toString(); - } - const fsPath = getFileFsPath(doc.uri); - return URI.file(path.resolve(fsPath, '..', ref)).toString(); - } - }; + async onReferences(params: TextDocumentPositionParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - const links: DocumentLink[] = []; - this.languageModes.getAllLanguageModeRangesInDocument(doc).forEach(m => { - if (m.mode.findDocumentLinks) { - pushAll(links, m.mode.findDocumentLinks(doc, documentContext)); - } - }); - return links; + return project?.onReferences(params) ?? []; } - onDocumentSymbol({ textDocument }: DocumentSymbolParams): SymbolInformation[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const symbols: SymbolInformation[] = []; + async onDocumentLinks(params: DocumentLinkParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - this.languageModes.getAllLanguageModeRangesInDocument(doc).forEach(m => { - if (m.mode.findDocumentSymbols) { - pushAll(symbols, m.mode.findDocumentSymbols(doc)); - } - }); - return symbols; + return project?.onDocumentLinks(params) ?? []; } - onDocumentColors({ textDocument }: DocumentColorParams): ColorInformation[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const colors: ColorInformation[] = []; + async onDocumentSymbol(params: DocumentSymbolParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - const distinctModes: Set = new Set(); - this.languageModes.getAllLanguageModeRangesInDocument(doc).forEach(m => { - distinctModes.add(m.mode); - }); + return project?.onDocumentSymbol(params) ?? []; + } - for (const mode of distinctModes) { - if (mode.findDocumentColors) { - pushAll(colors, mode.findDocumentColors(doc)); - } - } + async onDocumentColors(params: DocumentColorParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - return colors; + return project?.onDocumentColors(params) ?? []; } - onColorPresentations({ textDocument, color, range }: ColorPresentationParams): ColorPresentation[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, range.start); - if (mode && mode.getColorPresentations) { - return mode.getColorPresentations(doc, color, range); - } - return []; - } + async onColorPresentations(params: ColorPresentationParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - onSignatureHelp({ textDocument, position }: TextDocumentPositionParams): SignatureHelp | null { - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, position); - if (mode && mode.doSignatureHelp) { - return mode.doSignatureHelp(doc, position); - } - return NULL_SIGNATURE; + return project?.onColorPresentations(params) ?? []; } - onFoldingRanges({ textDocument }: FoldingRangeParams): FoldingRange[] { - const doc = this.documentService.getDocument(textDocument.uri)!; - const lmrs = this.languageModes.getAllLanguageModeRangesInDocument(doc); + async onSignatureHelp(params: TextDocumentPositionParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - const result: FoldingRange[] = []; + return project?.onSignatureHelp(params) ?? NULL_SIGNATURE; + } - lmrs.forEach(lmr => { - if (lmr.mode.getFoldingRanges) { - lmr.mode.getFoldingRanges(doc).forEach(r => result.push(r)); - } + async onFoldingRanges(params: FoldingRangeParams): Promise { + const project = await this.getProjectService(params.textDocument.uri); - result.push({ - startLine: lmr.start.line, - startCharacter: lmr.start.character, - endLine: lmr.end.line, - endCharacter: lmr.end.character - }); - }); - - return result; + return project?.onFoldingRanges(params) ?? []; } - onCodeAction({ textDocument, range, context }: CodeActionParams) { - if (!this.config.vetur.languageFeatures.codeActions) { - return []; - } + async onCodeAction(params: CodeActionParams) { + const project = await this.getProjectService(params.textDocument.uri); - const doc = this.documentService.getDocument(textDocument.uri)!; - const mode = this.languageModes.getModeAtPosition(doc, range.start); - if (this.languageModes.getModeAtPosition(doc, range.end) !== mode) { - return []; - } - if (mode && mode.getCodeActions) { - return mode.getCodeActions(doc, range, /*formatParams*/ {} as any, context); - } - return []; + return project?.onCodeAction(params) ?? []; } - getRefactorEdits(refactorAction: RefactorAction) { - const uri = URI.file(refactorAction.fileName).toString(); - const doc = this.documentService.getDocument(uri)!; - const startPos = doc.positionAt(refactorAction.textRange.pos); - const mode = this.languageModes.getModeAtPosition(doc, startPos); - if (mode && mode.getRefactorEdits) { - return mode.getRefactorEdits(doc, refactorAction); - } - return undefined; + async getRefactorEdits(refactorAction: RefactorAction) { + const project = await this.getProjectService(URI.file(refactorAction.fileName).toString()); + + return project?.getRefactorEdits(refactorAction) ?? undefined; } private triggerValidation(textDocument: TextDocument): void { @@ -527,8 +560,7 @@ export class VLS { this.cancelPastValidation(textDocument); this.pendingValidationRequests[textDocument.uri] = setTimeout(() => { delete this.pendingValidationRequests[textDocument.uri]; - const tsDep = this.dependencyService.get('typescript'); - this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource(tsDep.module); + this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource(); this.validateTextDocument(textDocument, this.cancellationTokenValidationRequests[textDocument.uri].token); }, this.validationDelayMs); } @@ -558,30 +590,16 @@ export class VLS { } async doValidate(doc: TextDocument, cancellationToken?: VCancellationToken) { - const diagnostics: Diagnostic[] = []; - if (doc.languageId === 'vue') { - for (const lmr of this.languageModes.getAllLanguageModeRangesInDocument(doc)) { - if (lmr.mode.doValidation) { - if (this.validation[lmr.mode.getId()]) { - pushAll(diagnostics, await lmr.mode.doValidation(doc, cancellationToken)); - } - // Special case for template type checking - else if (lmr.mode.getId() === 'vue-html' && this.templateInterpolationValidation) { - pushAll(diagnostics, await lmr.mode.doValidation(doc, cancellationToken)); - } - } - } - } - if (cancellationToken?.isCancellationRequested) { - return null; - } - return diagnostics; + const project = await this.getProjectService(doc.uri); + + return project?.doValidate(doc, cancellationToken) ?? null; } async executeCommand(arg: ExecuteCommandParams) { if (arg.command === APPLY_REFACTOR_COMMAND && arg.arguments) { const edit = this.getRefactorEdits(arg.arguments[0] as RefactorAction); if (edit) { + // @ts-expect-error this.lspConnection.sendRequest(ApplyWorkspaceEditRequest.type, { edit }); } return; @@ -590,17 +608,21 @@ export class VLS { logger.logInfo(`Unknown command ${arg.command}.`); } - removeDocument(doc: TextDocument): void { - this.languageModes.onDocumentRemoved(doc); + async removeDocument(doc: TextDocument): Promise { + const project = await this.getProjectService(doc.uri); + project?.languageModes.onDocumentRemoved(doc); } dispose(): void { - this.languageModes.dispose(); + this.projects.forEach(project => { + project.dispose(); + }); } get capabilities(): ServerCapabilities { return { textDocumentSync: TextDocumentSyncKind.Incremental, + workspace: { workspaceFolders: { supported: true, changeNotifications: true } }, completionProvider: { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', "'", '/', '@', '*', ' '] }, signatureHelpProvider: { triggerCharacters: ['('] }, documentFormattingProvider: false, @@ -621,11 +643,3 @@ export class VLS { }; } } - -function pushAll(to: T[], from: T[]) { - if (from) { - for (let i = 0; i < from.length; i++) { - to.push(from[i]); - } - } -} diff --git a/server/src/services/vueInfoService.ts b/server/src/services/vueInfoService.ts index 429aa0a5b5..d0816941e3 100644 --- a/server/src/services/vueInfoService.ts +++ b/server/src/services/vueInfoService.ts @@ -46,6 +46,7 @@ export interface ChildComponent { start: number; end: number; }; + global: boolean; info?: VueFileInfo; } diff --git a/server/src/utils/cancellationToken.ts b/server/src/utils/cancellationToken.ts index 13adc5cfcf..a0c056af9e 100644 --- a/server/src/utils/cancellationToken.ts +++ b/server/src/utils/cancellationToken.ts @@ -7,12 +7,7 @@ export interface VCancellationToken extends LSPCancellationToken { } export class VCancellationTokenSource extends CancellationTokenSource { - constructor(private tsModule: RuntimeLibrary['typescript']) { - super(); - } - get token(): VCancellationToken { - const operationCancelException = this.tsModule.OperationCanceledException; const token = super.token as VCancellationToken; token.tsToken = { isCancellationRequested() { @@ -20,7 +15,7 @@ export class VCancellationTokenSource extends CancellationTokenSource { }, throwIfCancellationRequested() { if (token.isCancellationRequested) { - throw new operationCancelException(); + throw new Error('OperationCanceledException'); } } }; diff --git a/server/src/utils/paths.ts b/server/src/utils/paths.ts index 9f279e21cb..fabb852f78 100644 --- a/server/src/utils/paths.ts +++ b/server/src/utils/paths.ts @@ -1,4 +1,5 @@ import { platform } from 'os'; +import { resolve } from 'path'; import { URI } from 'vscode-uri'; /** @@ -62,3 +63,15 @@ export function getFilePath(documentUri: string): string { export function normalizeFileNameToFsPath(fileName: string) { return URI.file(fileName).fsPath; } + +export function normalizeFileNameResolve(...paths: string[]) { + return normalizeFileNameToFsPath(resolve(...paths)); +} + +export function getPathDepth(filePath: string, sep: string) { + return filePath.split(sep).length; +} + +export function getFsPathToUri(fsPath: string) { + return URI.file(fsPath).toString(); +} diff --git a/server/src/utils/prettier/index.ts b/server/src/utils/prettier/index.ts index 3580dbfb41..dd7d6f4acc 100644 --- a/server/src/utils/prettier/index.ts +++ b/server/src/utils/prettier/index.ts @@ -149,7 +149,9 @@ function getPrettierOptions( prettierrcOptions.parser = parser; if (dependencyService.useWorkspaceDependencies) { // For loading plugins such as @prettier/plugin-pug - (prettierrcOptions as { pluginSearchDirs: string[] }).pluginSearchDirs = [dependencyService.workspacePath]; + (prettierrcOptions as { + pluginSearchDirs: string[]; + }).pluginSearchDirs = dependencyService.nodeModulesPaths.map(el => path.dirname(el)); } return prettierrcOptions; @@ -160,7 +162,7 @@ function getPrettierOptions( vscodePrettierOptions.parser = parser; if (dependencyService.useWorkspaceDependencies) { // For loading plugins such as @prettier/plugin-pug - vscodePrettierOptions.pluginSearchDirs = [dependencyService.workspacePath]; + vscodePrettierOptions.pluginSearchDirs = dependencyService.nodeModulesPaths.map(el => path.dirname(el)); } return vscodePrettierOptions; diff --git a/server/src/services/typescriptService/vueVersion.ts b/server/src/utils/vueVersion.ts similarity index 74% rename from server/src/services/typescriptService/vueVersion.ts rename to server/src/utils/vueVersion.ts index d3868bdf30..7fbfdfa10a 100644 --- a/server/src/services/typescriptService/vueVersion.ts +++ b/server/src/utils/vueVersion.ts @@ -1,5 +1,4 @@ import { readFileSync } from 'fs'; -import { findConfigFile } from '../../utils/workspace'; export enum VueVersion { VPre25, @@ -17,9 +16,16 @@ function floatVersionToEnum(v: number) { } } -export function inferVueVersion(workspacePath: string): VueVersion { - const packageJSONPath = findConfigFile(workspacePath, 'package.json'); +export function getVueVersionKey(version: VueVersion) { + return Object.keys(VueVersion)?.[Object.values(VueVersion).indexOf(version)]; +} + +export function inferVueVersion(packagePath: string | undefined): VueVersion { + const packageJSONPath = packagePath; try { + if (!packageJSONPath) { + throw new Error(`Can't find package.json in project`); + } const packageJSON = packageJSONPath && JSON.parse(readFileSync(packageJSONPath, { encoding: 'utf-8' })); const vueDependencyVersion = packageJSON.dependencies.vue || packageJSON.devDependencies.vue; @@ -30,7 +36,7 @@ export function inferVueVersion(workspacePath: string): VueVersion { return floatVersionToEnum(sloppyVersion); } - const nodeModulesVuePackagePath = require.resolve('vue/package.json', { paths: [workspacePath] }); + const nodeModulesVuePackagePath = require.resolve('vue/package.json', { paths: [packageJSONPath] }); const nodeModulesVuePackageJSON = JSON.parse(readFileSync(nodeModulesVuePackagePath, { encoding: 'utf-8' })!); const nodeModulesVueVersion = parseFloat(nodeModulesVuePackageJSON.version.match(/\d+\.\d+/)[0]); diff --git a/server/src/utils/workspace.ts b/server/src/utils/workspace.ts index 7b26863ef4..b81e004ca2 100644 --- a/server/src/utils/workspace.ts +++ b/server/src/utils/workspace.ts @@ -1,5 +1,10 @@ import ts from 'typescript'; -export function findConfigFile(workspacePath: string, configName: string) { - return ts.findConfigFile(workspacePath, ts.sys.fileExists, configName); +export function findConfigFile(findPath: string, configName: string) { + return ts.findConfigFile(findPath, ts.sys.fileExists, configName); +} + +export function requireUncached(module: string) { + delete require.cache[require.resolve(module)]; + return require(module); } diff --git a/server/src/vueServerMain.ts b/server/src/vueServerMain.ts index 59ef4cf900..ba26c11f11 100644 --- a/server/src/vueServerMain.ts +++ b/server/src/vueServerMain.ts @@ -3,8 +3,8 @@ import { VLS } from './services/vls'; const connection = process.argv.length <= 2 ? createConnection(process.stdin, process.stdout) : createConnection(); -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); +console.log = (...args: any[]) => connection.console.log(args.join(' ')); +console.error = (...args: any[]) => connection.console.error(args.join(' ')); const vls = new VLS(connection); connection.onInitialize( diff --git a/test/monorepo/data-dir/User/settings.json b/test/monorepo/data-dir/User/settings.json new file mode 100644 index 0000000000..756721312a --- /dev/null +++ b/test/monorepo/data-dir/User/settings.json @@ -0,0 +1,3 @@ +{ + "update.mode": "none" +} diff --git a/test/monorepo/features/beforeAll/beforeAll.test.ts b/test/monorepo/features/beforeAll/beforeAll.test.ts new file mode 100644 index 0000000000..e464c29363 --- /dev/null +++ b/test/monorepo/features/beforeAll/beforeAll.test.ts @@ -0,0 +1,5 @@ +import { activateLS } from '../../../editorHelper'; + +before(async () => { + await activateLS(); +}); diff --git a/test/monorepo/features/completion/alias.test.ts b/test/monorepo/features/completion/alias.test.ts new file mode 100644 index 0000000000..0357d98e4b --- /dev/null +++ b/test/monorepo/features/completion/alias.test.ts @@ -0,0 +1,11 @@ +import { position } from '../../../util'; +import { testCompletion } from '../../../completionHelper'; +import { getDocUri } from '../../path'; + +describe('Should autocomplete for diff --git a/test/monorepo/fixture/packages/vue2/components/AppSpinner.vue b/test/monorepo/fixture/packages/vue2/components/AppSpinner.vue new file mode 100644 index 0000000000..7b8b46cb04 --- /dev/null +++ b/test/monorepo/fixture/packages/vue2/components/AppSpinner.vue @@ -0,0 +1,3 @@ + diff --git a/test/monorepo/fixture/packages/vue2/diagnostics/ESLint.vue b/test/monorepo/fixture/packages/vue2/diagnostics/ESLint.vue new file mode 100644 index 0000000000..0e5ef1511d --- /dev/null +++ b/test/monorepo/fixture/packages/vue2/diagnostics/ESLint.vue @@ -0,0 +1,8 @@ + diff --git a/test/monorepo/fixture/packages/vue2/jsconfig.json b/test/monorepo/fixture/packages/vue2/jsconfig.json new file mode 100644 index 0000000000..4533977a8a --- /dev/null +++ b/test/monorepo/fixture/packages/vue2/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "esnext", + "strict": true, + "baseUrl": "./", + "paths": { + "@/*": ["components/*"] + } + } +} diff --git a/test/monorepo/fixture/packages/vue2/package.json b/test/monorepo/fixture/packages/vue2/package.json new file mode 100644 index 0000000000..7363b26c19 --- /dev/null +++ b/test/monorepo/fixture/packages/vue2/package.json @@ -0,0 +1,10 @@ +{ + "name": "vue2", + "version": "1.0.0", + "dependencies": { + "vue": "^2.6.11" + }, + "vetur": { + "tags": "./tags.json" + } +} diff --git a/test/monorepo/fixture/packages/vue3/package.json b/test/monorepo/fixture/packages/vue3/package.json new file mode 100644 index 0000000000..64236a420f --- /dev/null +++ b/test/monorepo/fixture/packages/vue3/package.json @@ -0,0 +1,7 @@ +{ + "name": "vue3", + "version": "1.0.0", + "dependencies": { + "vue": "^3.0.0" + } +} diff --git a/test/monorepo/fixture/packages/vue3/src/App.vue b/test/monorepo/fixture/packages/vue3/src/App.vue new file mode 100644 index 0000000000..eaf00e378a --- /dev/null +++ b/test/monorepo/fixture/packages/vue3/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/test/monorepo/fixture/packages/vue3/src/components/AppButton.vue b/test/monorepo/fixture/packages/vue3/src/components/AppButton.vue new file mode 100644 index 0000000000..f6071b3dff --- /dev/null +++ b/test/monorepo/fixture/packages/vue3/src/components/AppButton.vue @@ -0,0 +1,23 @@ + + + diff --git a/test/monorepo/fixture/packages/vue3/src/tsconfig.json b/test/monorepo/fixture/packages/vue3/src/tsconfig.json new file mode 100644 index 0000000000..185240c371 --- /dev/null +++ b/test/monorepo/fixture/packages/vue3/src/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "esnext", + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts", + "tests/**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/test/monorepo/fixture/vetur.config.js b/test/monorepo/fixture/vetur.config.js new file mode 100644 index 0000000000..4c1c1177d5 --- /dev/null +++ b/test/monorepo/fixture/vetur.config.js @@ -0,0 +1,9 @@ +module.exports = { + settings: { + 'vetur.validation.templateProps': true + }, + projects: [ + './packages/vue2', + { root: './packages/vue3', tsconfig: './src/tsconfig.json', globalComponents: ['./src/components/**/*.vue'] } + ] +}; diff --git a/test/monorepo/fixture/yarn.lock b/test/monorepo/fixture/yarn.lock new file mode 100644 index 0000000000..96d383c920 --- /dev/null +++ b/test/monorepo/fixture/yarn.lock @@ -0,0 +1,109 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/parser@^7.12.0": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" + integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== + +"@babel/types@^7.12.0": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" + integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@vue/compiler-core@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.3.tgz#dbb4d5eb91f294038f0bed170a1c25f59f7dc74f" + integrity sha512-iWlRT8RYLmz7zkg84pTOriNUzjH7XACWN++ImFkskWXWeev29IKi7p76T9jKDaMZoPiGcUZ0k9wayuASWVxOwg== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/shared" "3.0.3" + estree-walker "^2.0.1" + source-map "^0.6.1" + +"@vue/compiler-dom@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.3.tgz#582ba30bc82da8409868bc1153ff0e0e2be617e5" + integrity sha512-6GdUbDPjsc0MDZGAgpi4lox+d+aW9/brscwBOLOFfy9wcI9b6yLPmBbjdIsJq3pYdJWbdvACdJ77avBBdHEP8A== + dependencies: + "@vue/compiler-core" "3.0.3" + "@vue/shared" "3.0.3" + +"@vue/reactivity@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.3.tgz#681ee01ceff9219bc4da6bbb7d9c97d452e44d1d" + integrity sha512-t39Qmc42MX7wJtf8L6tHlu17eP9Rc5w4aRnxpLHNWoaRxddv/7FBhWqusJ2Bwkk8ixFHOQeejcLMt5G469WYJw== + dependencies: + "@vue/shared" "3.0.3" + +"@vue/runtime-core@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.0.3.tgz#edab3c9ad122cf8afd034b174cd20c073fbf950a" + integrity sha512-Fd1JVnYI6at0W/2ERwJuTSq4S22gNt8bKEbICcvCAac7hJUZ1rylThlrhsvrgA+DVkWU01r0niNZQ4UddlNw7g== + dependencies: + "@vue/reactivity" "3.0.3" + "@vue/shared" "3.0.3" + +"@vue/runtime-dom@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.0.3.tgz#5e3e5e5418b9defcac988d2be0cf65596fa2cc03" + integrity sha512-ytTvSlRaEYvLQUkkpruIBizWIwuIeHER0Ch/evO6kUaPLjZjX3NerVxA40cqJx8rRjb9keQso21U2Jcpk8GsTg== + dependencies: + "@vue/runtime-core" "3.0.3" + "@vue/shared" "3.0.3" + csstype "^2.6.8" + +"@vue/shared@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.3.tgz#ef12ebff93a446df281e8a0fd765b5aea8e7745b" + integrity sha512-yGgkF7u4W0Dmwri9XdeY50kOowN4UIX7aBQ///jbxx37itpzVjK7QzvD3ltQtPfWaJDGBfssGL0wpAgwX9OJpQ== + +csstype@^2.6.8: + version "2.6.14" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de" + integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A== + +estree-walker@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0" + integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== + +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +vue@^2.6.11: + version "2.6.12" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123" + integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg== + +vue@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.3.tgz#ad94a475e6ebbf3904673b6a0ae46e47b957bd72" + integrity sha512-BZG5meD5vLWdvfnRL5WqfDy+cnXO1X/SweModGUna78bdFPZW6+ZO1tU9p0acrskX3DKFcfSp2s4SZnMjABx6w== + dependencies: + "@vue/compiler-dom" "3.0.3" + "@vue/runtime-dom" "3.0.3" + "@vue/shared" "3.0.3" diff --git a/test/monorepo/index.ts b/test/monorepo/index.ts new file mode 100644 index 0000000000..5f328763e2 --- /dev/null +++ b/test/monorepo/index.ts @@ -0,0 +1,46 @@ +import path from 'path'; +import Mocha from 'mocha'; +import glob from 'glob'; + +export function run(): Promise { + const args = {}; + + Object.keys(process.env) + .filter(k => k.startsWith('MOCHA_')) + .forEach(k => { + args[k.slice('MOCHA_'.length)] = process.env[k]; + }); + + const mocha = new Mocha({ + ui: 'bdd', + timeout: 100000, + color: true, + ...args + }); + + const testsRoot = __dirname; + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + e(err); + } + }); + }); +} diff --git a/test/monorepo/path.ts b/test/monorepo/path.ts new file mode 100644 index 0000000000..a1c31cd5b4 --- /dev/null +++ b/test/monorepo/path.ts @@ -0,0 +1,9 @@ +import { Uri } from 'vscode'; +import { resolve } from 'path'; + +export const getDocPath = (p: string) => { + return resolve(__dirname, `../../../test/monorepo/fixture`, p); +}; +export const getDocUri = (p: string) => { + return Uri.file(getDocPath(p)); +};