1- import { dirname } from 'node:path'
1+ import { dirname , parse as pathParse , relative } from 'node:path'
22
33import {
44 createVueLanguagePlugin ,
@@ -8,7 +8,44 @@ import {
88
99import { proxyCreateProgram } from '@volar/typescript'
1010import ts from '../ts-loader.cjs'
11- import { slash } from '../utils'
11+ import { normalizePath , resolve , slash } from '../utils'
12+
13+ const internalSourceFiles = new WeakSet < ts . SourceFile > ( )
14+ const programSourceFileReleasers = new WeakMap < ts . Program , ( fileName : string ) => void > ( )
15+ interface VueLanguage {
16+ scripts : { delete ( fileName : string ) : void } ,
17+ }
18+ const programLanguages = new WeakMap < ts . Program , VueLanguage > ( )
19+ let activeLanguage : VueLanguage | undefined
20+
21+ export const needsModuleResolutionFallback = true
22+
23+ interface VueRootPathApi {
24+ parse : typeof import ( 'node:path' ) . parse ,
25+ relative : typeof import ( 'node:path' ) . relative ,
26+ resolve : typeof import ( 'node:path' ) . resolve ,
27+ }
28+
29+ export function groupVueRootNames (
30+ vueRootNames : readonly string [ ] ,
31+ projectDirectory : string ,
32+ pathApi : VueRootPathApi = { parse : pathParse , relative, resolve } ,
33+ ) {
34+ projectDirectory = pathApi . resolve ( projectDirectory )
35+ const projectVolume = pathApi . parse ( projectDirectory ) . root
36+ const groups = new Map < string , string [ ] > ( )
37+
38+ for ( const rootName of vueRootNames ) {
39+ const fileName = pathApi . resolve ( projectDirectory , rootName )
40+ const fileVolume = pathApi . parse ( fileName ) . root
41+ const directory = pathApi . relative ( projectVolume , fileVolume ) ? fileVolume : projectDirectory
42+ const files = groups . get ( directory ) ?? [ ]
43+ files . push ( fileName )
44+ groups . set ( directory , files )
45+ }
46+
47+ return [ ...groups ] . map ( ( [ directory , rootNames ] ) => ( { directory, rootNames } ) )
48+ }
1249
1350export function createParsedCommandLine (
1451 _ts : typeof ts ,
@@ -42,7 +79,101 @@ export function createParsedCommandLine(
4279 }
4380}
4481
45- export const createProgram = proxyCreateProgram ( ts , ts . createProgram , ( ts , options ) => {
82+ function prepareVueProgramOptions ( options : ts . CreateProgramOptions ) {
83+ const originalHost = options . host
84+ if ( ! originalHost ) return { options }
85+
86+ const vueRootNames = options . rootNames . filter ( fileName => fileName . endsWith ( '.vue' ) )
87+
88+ const projectDirectory =
89+ typeof options . options . configFilePath === 'string'
90+ ? dirname ( options . options . configFilePath )
91+ : originalHost . getCurrentDirectory ( )
92+ const syntheticRoots = new Map <
93+ string ,
94+ { fileName : string , text : string , sourceFile ?: ts . SourceFile }
95+ > ( )
96+ for ( const group of groupVueRootNames ( vueRootNames , projectDirectory ) ) {
97+ let sequence = 0
98+ let syntheticFileName = resolve ( group . directory , '__unplugin_dts_vue_root__.d.ts' )
99+ while ( originalHost . fileExists ( syntheticFileName ) ) {
100+ syntheticFileName = resolve ( group . directory , `__unplugin_dts_vue_root_${ ++ sequence } __.d.ts` )
101+ }
102+
103+ const text = group . rootNames
104+ . map ( fileName => {
105+ const path = slash ( relative ( group . directory , fileName ) )
106+ const moduleName = path . startsWith ( '.' ) ? path : `./${ path } `
107+ return `import ${ JSON . stringify ( moduleName ) } `
108+ } )
109+ . join ( '\n' )
110+ const canonicalFileName = normalizePath ( originalHost . getCanonicalFileName ( syntheticFileName ) )
111+ syntheticRoots . set ( canonicalFileName , { fileName : syntheticFileName , text } )
112+ }
113+
114+ const getSourceFile = originalHost . getSourceFile . bind ( originalHost )
115+ const fileExists = originalHost . fileExists . bind ( originalHost )
116+ const readFile = originalHost . readFile . bind ( originalHost )
117+ const releasedSourceFiles = new Set < string > ( )
118+ const getCanonicalFileName = ( fileName : string ) =>
119+ normalizePath ( originalHost . getCanonicalFileName ( fileName ) )
120+ const getSyntheticRoot = ( fileName : string ) => syntheticRoots . get ( getCanonicalFileName ( fileName ) )
121+ const host : ts . CompilerHost = {
122+ ...originalHost ,
123+ fileExists ( fileName ) {
124+ return ! ! getSyntheticRoot ( fileName ) || fileExists ( fileName )
125+ } ,
126+ readFile ( fileName ) {
127+ return getSyntheticRoot ( fileName ) ?. text ?? readFile ( fileName )
128+ } ,
129+ getSourceFile ( fileName , languageVersionOrOptions , onError , shouldCreateNewSourceFile ) {
130+ if ( releasedSourceFiles . has ( getCanonicalFileName ( fileName ) ) ) return undefined
131+
132+ const syntheticRoot = getSyntheticRoot ( fileName )
133+ if ( ! syntheticRoot ) {
134+ return getSourceFile ( fileName , languageVersionOrOptions , onError , shouldCreateNewSourceFile )
135+ }
136+
137+ if ( ! syntheticRoot . sourceFile || shouldCreateNewSourceFile ) {
138+ syntheticRoot . sourceFile = ts . createSourceFile (
139+ syntheticRoot . fileName ,
140+ syntheticRoot . text ,
141+ languageVersionOrOptions ,
142+ true ,
143+ ts . ScriptKind . TS ,
144+ )
145+ internalSourceFiles . add ( syntheticRoot . sourceFile )
146+ }
147+ return syntheticRoot . sourceFile
148+ } ,
149+ }
150+
151+ const preparedOptions : ts . CreateProgramOptions = {
152+ ...options ,
153+ host,
154+ rootNames : [ ...options . rootNames , ...[ ...syntheticRoots . values ( ) ] . map ( root => root . fileName ) ] ,
155+ }
156+ return {
157+ options : preparedOptions ,
158+ releaseSourceFile ( fileName : string ) {
159+ const canonicalFileName = getCanonicalFileName ( fileName )
160+ releasedSourceFiles . add ( canonicalFileName )
161+ try {
162+ preparedOptions . host ?. getSourceFile ( fileName , ts . ScriptTarget . Latest )
163+ } finally {
164+ releasedSourceFiles . delete ( canonicalFileName )
165+ }
166+ } ,
167+ }
168+ }
169+
170+ /**
171+ * 通过仅存在于 CompilerHost 的声明 root 引入未引用 SFC。
172+ *
173+ * TypeScript 会在 rootNames 中保留任意扩展文件,却不会主动为它们创建 SourceFile;
174+ * 虚拟声明 root 让 Volar 继续使用自身的模块解析和 service-script 生成路径。
175+ */
176+ const createVueProgram = proxyCreateProgram ( ts , ts . createProgram , ( ts , options ) => {
46177 const { configFilePath } = options . options
47178 const vueOptions =
48179 typeof configFilePath === 'string'
@@ -55,5 +186,29 @@ export const createProgram = proxyCreateProgram(ts, ts.createProgram, (ts, optio
55186 vueOptions ,
56187 id => id ,
57188 )
58- return { languagePlugins : [ vueLanguagePlugin ] }
189+ return {
190+ languagePlugins : [ vueLanguagePlugin ] ,
191+ setup ( instance ) {
192+ activeLanguage = instance
193+ } ,
194+ }
59195} )
196+
197+ export const createProgram = ( ( options : ts . CreateProgramOptions ) => {
198+ const prepared = prepareVueProgramOptions ( options )
199+ const program = createVueProgram ( prepared . options )
200+ if ( activeLanguage ) programLanguages . set ( program , activeLanguage )
201+ if ( prepared . releaseSourceFile ) {
202+ programSourceFileReleasers . set ( program , prepared . releaseSourceFile )
203+ }
204+ return program
205+ } ) as typeof ts . createProgram
206+
207+ export function isInternalSourceFile ( sourceFile : ts . SourceFile ) {
208+ return internalSourceFiles . has ( sourceFile )
209+ }
210+
211+ export function releaseSourceFile ( program : ts . Program , fileName : string ) {
212+ programLanguages . get ( program ) ?. scripts . delete ( fileName )
213+ programSourceFileReleasers . get ( program ) ?.( fileName )
214+ }
0 commit comments