diff --git a/extensions/vscode/schemas/vue-tsconfig.schema.json b/extensions/vscode/schemas/vue-tsconfig.schema.json index d9c03434d1..58e0d998bf 100644 --- a/extensions/vscode/schemas/vue-tsconfig.schema.json +++ b/extensions/vscode/schemas/vue-tsconfig.schema.json @@ -16,6 +16,14 @@ ], "markdownDescription": "Target version of Vue." }, + "lib": { + "default": "vue", + "markdownDescription": "Specify module name for import regular types." + }, + "globalTypesPath": { + "type": "string", + "markdownDescription": "Path to the global types file. Manual configuration is required when `node_modules` does not exist in the environment." + }, "extensions": { "type": "array", "default": [".vue"], @@ -31,10 +39,6 @@ "default": [".html"], "markdownDescription": "Valid file extensions that should be considered as regular PetiteVue SFC." }, - "lib": { - "default": "vue", - "markdownDescription": "Specify module name for import regular types." - }, "jsxSlots": { "type": "boolean", "default": false, diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 200edcece2..54581177c9 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -27,7 +27,7 @@ export function createCheckerByJsonConfigBase( rootDir = rootDir.replace(windowsPathReg, '/'); return baseCreate( ts, - () => vue.createParsedCommandLineByJson(ts, ts.sys, rootDir, json, undefined, true), + () => vue.createParsedCommandLineByJson(ts, ts.sys, rootDir, json), checkerOptions, rootDir, path.join(rootDir, 'jsconfig.json.global.vue'), @@ -42,7 +42,7 @@ export function createCheckerBase( tsconfig = tsconfig.replace(windowsPathReg, '/'); return baseCreate( ts, - () => vue.createParsedCommandLine(ts, ts.sys, tsconfig, true), + () => vue.createParsedCommandLine(ts, ts.sys, tsconfig), checkerOptions, path.dirname(tsconfig), tsconfig + '.global.vue', @@ -63,6 +63,13 @@ export function baseCreate( let fileNames = new Set(commandLine.fileNames.map(path => path.replace(windowsPathReg, '/'))); let projectVersion = 0; + if (commandLine.vueOptions.globalTypesPath) { + ts.sys.writeFile( + commandLine.vueOptions.globalTypesPath, + vue.generateGlobalTypes(commandLine.vueOptions), + ); + } + const projectHost: TypeScriptProjectHost = { getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), @@ -136,41 +143,6 @@ export function baseCreate( const { languageServiceHost } = createLanguageServiceHost(ts, ts.sys, language, s => s, projectHost); const tsLs = ts.createLanguageService(languageServiceHost); - const directoryExists = languageServiceHost.directoryExists?.bind(languageServiceHost); - const fileExists = languageServiceHost.fileExists.bind(languageServiceHost); - const getScriptSnapshot = languageServiceHost.getScriptSnapshot.bind(languageServiceHost); - const globalTypesName = vue.getGlobalTypesFileName(commandLine.vueOptions); - const globalTypesContents = `// @ts-nocheck\nexport {};\n` + vue.generateGlobalTypes(commandLine.vueOptions); - const globalTypesSnapshot: ts.IScriptSnapshot = { - getText: (start, end) => globalTypesContents.slice(start, end), - getLength: () => globalTypesContents.length, - getChangeRange: () => undefined, - }; - if (directoryExists) { - languageServiceHost.directoryExists = path => { - if (path.endsWith('.vue-global-types')) { - return true; - } - return directoryExists(path); - }; - } - languageServiceHost.fileExists = path => { - if ( - path.endsWith(`.vue-global-types/${globalTypesName}`) || path.endsWith(`.vue-global-types\\${globalTypesName}`) - ) { - return true; - } - return fileExists(path); - }; - languageServiceHost.getScriptSnapshot = path => { - if ( - path.endsWith(`.vue-global-types/${globalTypesName}`) || path.endsWith(`.vue-global-types\\${globalTypesName}`) - ) { - return globalTypesSnapshot; - } - return getScriptSnapshot(path); - }; - if (checkerOptions.forceUseTs) { const getScriptKind = languageServiceHost.getScriptKind?.bind(languageServiceHost); languageServiceHost.getScriptKind = fileName => { diff --git a/packages/language-core/lib/codegen/globalTypes.ts b/packages/language-core/lib/codegen/globalTypes.ts index e77233ff9d..88c7822830 100644 --- a/packages/language-core/lib/codegen/globalTypes.ts +++ b/packages/language-core/lib/codegen/globalTypes.ts @@ -27,7 +27,7 @@ export function generateGlobalTypes({ const fnPropsType = `(T extends { $props: infer Props } ? Props : {})${ checkUnknownProps ? '' : ' & Record' }`; - let text = ``; + let text = `// @ts-nocheck\nexport {};\n`; if (target < 3.5) { text += ` ; declare module '${lib}' { diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index 2f6ea37506..fb3fe474cf 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -4,7 +4,6 @@ import type { ScriptRanges } from '../../parsers/scriptRanges'; import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; import type { Code, Sfc, VueCompilerOptions } from '../../types'; import { codeFeatures } from '../codeFeatures'; -import { generateGlobalTypes, getGlobalTypesFileName } from '../globalTypes'; import type { TemplateCodegenContext } from '../template/context'; import { endOfLine, generateSfcBlockSection, newLine } from '../utils'; import { generateComponentSelf } from './componentSelf'; @@ -25,27 +24,26 @@ export interface ScriptCodegenOptions { templateCodegen: TemplateCodegenContext & { codes: Code[] } | undefined; destructuredPropNames: Set; templateRefNames: Set; - appendGlobalTypes: boolean; } export function* generateScript(options: ScriptCodegenOptions): Generator { const ctx = createScriptCodegenContext(options); - if (options.vueCompilerOptions.__setupedGlobalTypes) { - const globalTypes = options.vueCompilerOptions.__setupedGlobalTypes; - if (typeof globalTypes === 'object') { - let relativePath = path.relative(path.dirname(options.fileName), globalTypes.absolutePath); + if (options.vueCompilerOptions.globalTypesPath) { + const globalTypesPath = options.vueCompilerOptions.globalTypesPath; + if (path.isAbsolute(globalTypesPath)) { + let relativePath = path.relative(path.dirname(options.fileName), globalTypesPath); if ( - relativePath !== globalTypes.absolutePath && !relativePath.startsWith('./') && !relativePath.startsWith('../') + relativePath !== globalTypesPath + && !relativePath.startsWith('./') + && !relativePath.startsWith('../') ) { relativePath = './' + relativePath; } yield `/// ${newLine}`; } else { - yield `/// ${newLine}`; + yield `/// ${newLine}`; } } else { @@ -165,9 +163,6 @@ export function* generateScript(options: ScriptCodegenOptions): Generator>(); const validLangs = new Set(['js', 'jsx', 'ts', 'tsx']); const plugin: VueLanguagePlugin = ctx => { - let appendedGlobalTypes = false; - return { version: 2.1, @@ -46,12 +44,7 @@ const plugin: VueLanguagePlugin = ctx => { function useCodegen(fileName: string, sfc: Sfc) { if (!tsCodegen.has(sfc)) { - let appendGlobalTypes = false; - if (!ctx.vueCompilerOptions.__setupedGlobalTypes && !appendedGlobalTypes) { - appendGlobalTypes = true; - appendedGlobalTypes = true; - } - tsCodegen.set(sfc, createTsx(fileName, sfc, ctx, appendGlobalTypes)); + tsCodegen.set(sfc, createTsx(fileName, sfc, ctx)); } return tsCodegen.get(sfc)!; } @@ -63,7 +56,6 @@ function createTsx( fileName: string, sfc: Sfc, ctx: Parameters[0], - appendGlobalTypes: boolean, ) { const ts = ctx.modules.typescript; @@ -231,7 +223,6 @@ function createTsx( templateCodegen: getGeneratedTemplate(), destructuredPropNames: getSetupDestructuredPropNames(), templateRefNames: getSetupTemplateRefNames(), - appendGlobalTypes, }); let current = codegen.next(); diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index ffc672abbe..1fa66323ed 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -25,6 +25,7 @@ export type Code = Segment; export interface VueCompilerOptions { target: number; lib: string; + globalTypesPath?: string; extensions: string[]; vitePressExtensions: string[]; petiteVueExtensions: string[]; @@ -72,11 +73,6 @@ export interface VueCompilerOptions { string, Record | Record[]> >; - - // internal - __setupedGlobalTypes?: true | { - absolutePath: string; - }; } export const validVersions = [2, 2.1] as const; diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index 597db97bb0..40de52010b 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -1,7 +1,7 @@ import { camelize } from '@vue/shared'; import { posix as path } from 'path-browserify'; import type * as ts from 'typescript'; -import { generateGlobalTypes, getGlobalTypesFileName } from '../codegen/globalTypes'; +import { getGlobalTypesFileName } from '../codegen/globalTypes'; import { getAllExtensions } from '../languagePlugin'; import type { RawVueCompilerOptions, VueCompilerOptions, VueLanguagePlugin } from '../types'; import { hyphenateTag } from './shared'; @@ -18,12 +18,11 @@ export function createParsedCommandLineByJson( rootDir: string, json: any, configFileName = rootDir + '/jsconfig.json', - skipGlobalTypesSetup = false, ): ParsedCommandLine { const proxyHost = proxyParseConfigHostForExtendConfigPaths(parseConfigHost); ts.parseJsonConfigFileContent(json, proxyHost.host, rootDir, {}, configFileName); - const resolver = new CompilerOptionsResolver(); + const resolver = new CompilerOptionsResolver(parseConfigHost.fileExists); for (const extendPath of proxyHost.extendConfigPaths.reverse()) { try { @@ -35,14 +34,10 @@ export function createParsedCommandLineByJson( catch {} } - const resolvedVueOptions = resolver.build(); + // ensure the rootDir is added to the config roots + resolver.addConfig({}, rootDir); - if (skipGlobalTypesSetup) { - resolvedVueOptions.__setupedGlobalTypes = true; - } - else { - resolvedVueOptions.__setupedGlobalTypes = setupGlobalTypes(rootDir, resolvedVueOptions, parseConfigHost); - } + const resolvedVueOptions = resolver.build(); const parsed = ts.parseJsonConfigFileContent( json, proxyHost.host, @@ -73,14 +68,14 @@ export function createParsedCommandLine( ts: typeof import('typescript'), parseConfigHost: ts.ParseConfigHost, tsConfigPath: string, - skipGlobalTypesSetup = false, ): ParsedCommandLine { try { + const rootDir = path.dirname(tsConfigPath); const proxyHost = proxyParseConfigHostForExtendConfigPaths(parseConfigHost); const config = ts.readJsonConfigFile(tsConfigPath, proxyHost.host.readFile); - ts.parseJsonSourceFileConfigFileContent(config, proxyHost.host, path.dirname(tsConfigPath), {}, tsConfigPath); + ts.parseJsonSourceFileConfigFileContent(config, proxyHost.host, rootDir, {}, tsConfigPath); - const resolver = new CompilerOptionsResolver(); + const resolver = new CompilerOptionsResolver(parseConfigHost.fileExists); for (const extendPath of proxyHost.extendConfigPaths.reverse()) { try { @@ -93,17 +88,6 @@ export function createParsedCommandLine( } const resolvedVueOptions = resolver.build(); - - if (skipGlobalTypesSetup) { - resolvedVueOptions.__setupedGlobalTypes = true; - } - else { - resolvedVueOptions.__setupedGlobalTypes = setupGlobalTypes( - path.dirname(tsConfigPath), - resolvedVueOptions, - parseConfigHost, - ); - } const parsed = ts.parseJsonSourceFileConfigFileContent( config, proxyHost.host, @@ -162,21 +146,31 @@ function proxyParseConfigHostForExtendConfigPaths(parseConfigHost: ts.ParseConfi } export class CompilerOptionsResolver { + configRoots = new Set(); options: Omit = {}; - fallbackTarget: number | undefined; target: number | undefined; + globalTypesPath: string | undefined; plugins: VueLanguagePlugin[] = []; + constructor( + public fileExists?: (path: string) => boolean, + ) {} + addConfig(options: RawVueCompilerOptions, rootDir: string) { + this.configRoots.add(rootDir); for (const key in options) { switch (key) { case 'target': - const target = options.target!; - if (typeof target === 'string') { + if (options[key] === 'auto') { this.target = findVueVersion(rootDir); } else { - this.target = target; + this.target = options[key]; + } + break; + case 'globalTypesPath': + if (options[key] !== undefined) { + this.globalTypesPath = path.join(rootDir, options[key]); } break; case 'plugins': @@ -205,15 +199,15 @@ export class CompilerOptionsResolver { break; } } - if (this.target === undefined) { - this.fallbackTarget = findVueVersion(rootDir); + if (options.target === undefined) { + this.target ??= findVueVersion(rootDir); } } - build(defaults?: VueCompilerOptions): VueCompilerOptions { - const target = this.target ?? this.fallbackTarget; - defaults ??= getDefaultCompilerOptions(target, this.options.lib, this.options.strictTemplates); - return { + build(defaults?: VueCompilerOptions) { + defaults ??= getDefaultCompilerOptions(this.target, this.options.lib, this.options.strictTemplates); + + const resolvedOptions: VueCompilerOptions = { ...defaults, ...this.options, plugins: this.plugins, @@ -237,6 +231,31 @@ export class CompilerOptionsResolver { ).map(([k, v]) => [camelize(k), v]), ), }; + + if (this.fileExists && this.globalTypesPath === undefined) { + root: for (const rootDir of [...this.configRoots].reverse()) { + let dir = rootDir; + while (!this.fileExists(path.join(dir, 'node_modules', resolvedOptions.lib, 'package.json'))) { + const parentDir = path.dirname(dir); + if (dir === parentDir) { + continue root; + } + dir = parentDir; + } + resolvedOptions.globalTypesPath = path.join( + dir, + 'node_modules', + '.vue-global-types', + getGlobalTypesFileName(resolvedOptions), + ); + break; + } + } + else { + resolvedOptions.globalTypesPath = this.globalTypesPath; + } + + return resolvedOptions; } } @@ -330,27 +349,3 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTempla }, }; } - -export function setupGlobalTypes(rootDir: string, vueOptions: VueCompilerOptions, host: { - fileExists(path: string): boolean; - writeFile?(path: string, data: string): void; -}): VueCompilerOptions['__setupedGlobalTypes'] { - if (!host.writeFile) { - return; - } - try { - let dir = rootDir; - while (!host.fileExists(path.join(dir, 'node_modules', vueOptions.lib, 'package.json'))) { - const parentDir = path.dirname(dir); - if (dir === parentDir) { - throw 0; - } - dir = parentDir; - } - const globalTypesPath = path.join(dir, 'node_modules', '.vue-global-types', getGlobalTypesFileName(vueOptions)); - const globalTypesContents = `// @ts-nocheck\nexport {};\n` + generateGlobalTypes(vueOptions); - host.writeFile(globalTypesPath, globalTypesContents); - return { absolutePath: globalTypesPath }; - } - catch {} -} diff --git a/packages/language-server/index.ts b/packages/language-server/index.ts index 1c797ffe12..201a557b7b 100644 --- a/packages/language-server/index.ts +++ b/packages/language-server/index.ts @@ -5,9 +5,9 @@ import { createConnection, createServer } from '@volar/language-server/node'; import { createLanguage, createParsedCommandLine, + createParsedCommandLineByJson, createVueLanguagePlugin, forEachEmbeddedCode, - getDefaultCompilerOptions, isReferencesEnabled, } from '@vue/language-core'; import { @@ -162,12 +162,9 @@ connection.onInitialize(params => { } function createProjectLanguageService(server: LanguageServer, tsconfig: string | undefined) { - const commonLine = tsconfig + const commonLine = tsconfig && !ts.server.isInferredProjectName(tsconfig) ? createParsedCommandLine(ts, ts.sys, tsconfig) - : { - options: ts.getDefaultCompilerOptions(), - vueOptions: getDefaultCompilerOptions(), - }; + : createParsedCommandLineByJson(ts, ts.sys, ts.sys.getCurrentDirectory(), {}); const language = createLanguage( [ { diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index dba1948322..171c14e351 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -24,6 +24,7 @@ import { create as createVueDocumentDropPlugin } from './lib/plugins/vue-documen import { create as createVueDocumentHighlightsPlugin } from './lib/plugins/vue-document-highlights'; import { create as createVueDocumentLinksPlugin } from './lib/plugins/vue-document-links'; import { create as createVueExtractFilePlugin } from './lib/plugins/vue-extract-file'; +import { create as createVueGlobalTypesErrorPlugin } from './lib/plugins/vue-global-types-error'; import { create as createVueInlayHintsPlugin } from './lib/plugins/vue-inlayhints'; import { create as createVueMissingPropsHintsPlugin } from './lib/plugins/vue-missing-props-hints'; import { create as createVueSfcPlugin } from './lib/plugins/vue-sfc'; @@ -86,6 +87,7 @@ function getCommonLanguageServicePlugins( createVueInlayHintsPlugin(ts), createVueDirectiveCommentsPlugin(), createVueExtractFilePlugin(ts, getTsPluginClient), + createVueGlobalTypesErrorPlugin(), createEmmetPlugin({ mappedLanguages: { 'vue-root-tags': 'html', diff --git a/packages/language-service/lib/plugins/vue-global-types-error.ts b/packages/language-service/lib/plugins/vue-global-types-error.ts new file mode 100644 index 0000000000..e72c2ac26a --- /dev/null +++ b/packages/language-service/lib/plugins/vue-global-types-error.ts @@ -0,0 +1,37 @@ +import type { DiagnosticSeverity, LanguageServicePlugin } from '@volar/language-service'; + +export function create(): LanguageServicePlugin { + return { + name: 'vue-compiler-dom-errors', + capabilities: { + diagnosticProvider: { + interFileDependencies: false, + workspaceDiagnostics: false, + }, + }, + create(context) { + return { + provideDiagnostics(document) { + if (document.languageId !== 'vue-root-tags') { + return; + } + + const vueCompilerOptions = context.project.vue?.compilerOptions; + if (vueCompilerOptions && vueCompilerOptions.globalTypesPath === undefined) { + return [{ + range: { + start: document.positionAt(0), + end: document.positionAt(0), + }, + severity: 1 satisfies typeof DiagnosticSeverity.Error, + code: 404, + source: 'vue', + message: + `Write global types file failed. Please ensure that "node_modules" exists and "${vueCompilerOptions.lib}" is a direct dependency, or set "vueCompilerOptions.globalTypesPath" in "tsconfig.json" manually.`, + }]; + } + }, + }; + }, + }; +} diff --git a/packages/tsc/index.ts b/packages/tsc/index.ts index 86202e4d3b..6eff1f843e 100644 --- a/packages/tsc/index.ts +++ b/packages/tsc/index.ts @@ -15,7 +15,14 @@ export function run(tscPath = require.resolve('typescript/lib/tsc')) { const { configFilePath } = options.options; const vueOptions = typeof configFilePath === 'string' ? vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions - : vue.getDefaultCompilerOptions(); + : vue.createParsedCommandLineByJson(ts, ts.sys, (options.host ?? ts.sys).getCurrentDirectory(), {}) + .vueOptions; + if (vueOptions.globalTypesPath) { + ts.sys.writeFile( + vueOptions.globalTypesPath, + vue.generateGlobalTypes(vueOptions), + ); + } const allExtensions = vue.getAllExtensions(vueOptions); if ( runExtensions.length === allExtensions.length diff --git a/packages/tsc/tests/dts.spec.ts b/packages/tsc/tests/dts.spec.ts index ed6b2c2b78..8fd4f768b6 100644 --- a/packages/tsc/tests/dts.spec.ts +++ b/packages/tsc/tests/dts.spec.ts @@ -32,12 +32,14 @@ describe('vue-tsc-dts', () => { vueOptions = vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions; } else { - vueOptions = vue.getDefaultCompilerOptions(); - vueOptions.extensions = ['.vue', '.cext']; - vueOptions.__setupedGlobalTypes = vue.setupGlobalTypes( - workspace.replace(windowsPathReg, '/'), - vueOptions, - ts.sys, + vueOptions = vue.createParsedCommandLineByJson(ts, ts.sys, workspace.replace(windowsPathReg, '/'), {}).vueOptions; + vueOptions.target = 99; + vueOptions.extensions = ['vue', 'cext']; + } + if (vueOptions.globalTypesPath) { + ts.sys.writeFile( + vueOptions.globalTypesPath, + vue.generateGlobalTypes(vueOptions), ); } const vueLanguagePlugin = vue.createVueLanguagePlugin( diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 9ce0c6f217..6c185d6e5b 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -19,6 +19,12 @@ const project2Service = new WeakMap { const vueOptions = getVueCompilerOptions(); + if (vueOptions.globalTypesPath) { + ts.sys.writeFile( + vueOptions.globalTypesPath, + vue.generateGlobalTypes(vueOptions), + ); + } const languagePlugin = vue.createVueLanguagePlugin( ts, info.languageServiceHost.getCompilationSettings(),