@@ -3,6 +3,8 @@ import { mapToRelative } from '../utils/sourcemaps.js';
3
3
import * as svelte from 'svelte/compiler' ;
4
4
import { log } from '../utils/log.js' ;
5
5
import { arraify } from '../utils/options.js' ;
6
+ import fs from 'node:fs' ;
7
+ import path from 'node:path' ;
6
8
7
9
/**
8
10
* @param {import('../types/plugin-api.d.ts').PluginAPI } api
@@ -14,10 +16,16 @@ export function preprocess(api) {
14
16
*/
15
17
let options ;
16
18
19
+ /**
20
+ * @type {DependenciesCache }
21
+ */
22
+ let dependenciesCache ;
23
+
17
24
/**
18
25
* @type {import("../types/compile.d.ts").PreprocessSvelte }
19
26
*/
20
27
let preprocessSvelte ;
28
+
21
29
/** @type {import('vite').Plugin } */
22
30
const plugin = {
23
31
name : 'vite-plugin-svelte:preprocess' ,
@@ -37,19 +45,39 @@ export function preprocess(api) {
37
45
delete plugin . transform ;
38
46
}
39
47
} ,
40
-
48
+ configureServer ( server ) {
49
+ dependenciesCache = new DependenciesCache ( server ) ;
50
+ } ,
51
+ buildStart ( ) {
52
+ dependenciesCache ?. clear ( ) ;
53
+ } ,
41
54
transform : {
42
55
async handler ( code , id ) {
43
- const cache = api . getEnvironmentCache ( this ) ;
44
56
const ssr = this . environment . config . consumer === 'server' ;
45
57
const svelteRequest = api . idParser ( id , ssr ) ;
46
58
if ( ! svelteRequest ) {
47
59
return ;
48
60
}
49
61
try {
50
- return await preprocessSvelte ( svelteRequest , code , options ) ;
62
+ const preprocessed = await preprocessSvelte ( svelteRequest , code , options ) ;
63
+ dependenciesCache ?. update ( svelteRequest , preprocessed ?. dependencies ?? [ ] ) ;
64
+ if ( ! preprocessed ) {
65
+ return ;
66
+ }
67
+ if ( options . isBuild && this . environment . config . build . watch && preprocessed . dependencies ) {
68
+ for ( const dep of preprocessed . dependencies ) {
69
+ this . addWatchFile ( dep ) ;
70
+ }
71
+ }
72
+
73
+ /** @type {import('vite').Rollup.SourceDescription }*/
74
+ const result = { code : preprocessed . code } ;
75
+ if ( preprocessed . map ) {
76
+ // @ts -expect-error type differs but should work
77
+ result . map = preprocessed . map ;
78
+ }
79
+ return result ;
51
80
} catch ( e ) {
52
- cache . setError ( svelteRequest , e ) ;
53
81
throw toRollupError ( e , options ) ;
54
82
}
55
83
}
@@ -63,8 +91,6 @@ export function preprocess(api) {
63
91
* @returns {import('../types/compile.d.ts').PreprocessSvelte }
64
92
*/
65
93
function createPreprocessSvelte ( options , resolvedConfig ) {
66
- /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined } */
67
- let stats ;
68
94
/** @type {Array<import('svelte/compiler').PreprocessorGroup> } */
69
95
const preprocessors = arraify ( options . preprocess ) ;
70
96
@@ -75,59 +101,105 @@ function createPreprocessSvelte(options, resolvedConfig) {
75
101
}
76
102
77
103
/** @type {import('../types/compile.d.ts').PreprocessSvelte } */
78
- return async function preprocessSvelte ( svelteRequest , code , options ) {
79
- const { filename, ssr } = svelteRequest ;
80
-
81
- if ( options . stats ) {
82
- if ( options . isBuild ) {
83
- if ( ! stats ) {
84
- // build is either completely ssr or csr, create stats collector on first compile
85
- // it is then finished in the buildEnd hook.
86
- stats = options . stats . startCollection ( `${ ssr ? 'ssr' : 'dom' } preprocess` , {
87
- logInProgress : ( ) => false
88
- } ) ;
89
- }
90
- } else {
91
- // dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting
92
- if ( ssr && ! stats ) {
93
- stats = options . stats . startCollection ( 'ssr preprocess' ) ;
94
- }
95
- // stats are being collected but this isn't an ssr request, assume page loaded and stop collecting
96
- if ( ! ssr && stats ) {
97
- stats . finish ( ) ;
98
- stats = undefined ;
99
- }
100
- // TODO find a way to trace dom compile during dev
101
- // problem: we need to call finish at some point but have no way to tell if page load finished
102
- // also they for hmr updates too
103
- }
104
- }
105
-
104
+ return async function preprocessSvelte ( svelteRequest , code ) {
105
+ const { filename } = svelteRequest ;
106
106
let preprocessed ;
107
-
108
107
if ( preprocessors && preprocessors . length > 0 ) {
109
108
try {
110
- const endStat = stats ?. start ( filename ) ;
111
109
preprocessed = await svelte . preprocess ( code , preprocessors , { filename } ) ; // full filename here so postcss works
112
- endStat ?. ( ) ;
113
110
} catch ( e ) {
114
111
e . message = `Error while preprocessing ${ filename } ${ e . message ? ` - ${ e . message } ` : '' } ` ;
115
112
throw e ;
116
113
}
117
-
118
114
if ( typeof preprocessed ?. map === 'object' ) {
119
115
mapToRelative ( preprocessed ?. map , filename ) ;
120
116
}
121
- return /** @type {import('../types/compile.d.ts').PreprocessTransformOutput } */ {
122
- code : preprocessed . code ,
123
- // @ts -expect-error
124
- map : preprocessed . map ,
125
- meta : {
126
- svelte : {
127
- preprocessed
128
- }
129
- }
130
- } ;
117
+ return preprocessed ;
131
118
}
132
119
} ;
133
120
}
121
+
122
+ /**
123
+ * @class
124
+ *
125
+ * caches dependencies of preprocessed files and emit change events on dependants
126
+ */
127
+ class DependenciesCache {
128
+ /** @type {Map<string, string[]> } */
129
+ #dependencies = new Map ( ) ;
130
+ /** @type {Map<string, Set<string>> } */
131
+ #dependants = new Map ( ) ;
132
+
133
+ /** @type {import('vite').ViteDevServer } */
134
+ #server;
135
+ /**
136
+ *
137
+ * @param {import('vite').ViteDevServer } server
138
+ */
139
+ constructor ( server ) {
140
+ this . #server = server ;
141
+ /** @type {(filename: string) => void } */
142
+ const emitChangeEventOnDependants = ( filename ) => {
143
+ const dependants = this . #dependants. get ( filename ) ;
144
+ dependants ?. forEach ( ( dependant ) => {
145
+ if ( fs . existsSync ( dependant ) ) {
146
+ log . debug (
147
+ `emitting virtual change event for "${ dependant } " because dependency "${ filename } " changed` ,
148
+ undefined ,
149
+ 'hmr'
150
+ ) ;
151
+ server . watcher . emit ( 'change' , dependant ) ;
152
+ }
153
+ } ) ;
154
+ } ;
155
+ server . watcher . on ( 'change' , emitChangeEventOnDependants ) ;
156
+ server . watcher . on ( 'unlink' , emitChangeEventOnDependants ) ;
157
+ }
158
+
159
+ /**
160
+ * @param {string } file
161
+ */
162
+ #ensureWatchedFile( file ) {
163
+ const root = this . #server. config . root ;
164
+ if (
165
+ file &&
166
+ // only need to watch if out of root
167
+ ! file . startsWith ( root + '/' ) &&
168
+ // some rollup plugins use null bytes for private resolved Ids
169
+ ! file . includes ( '\0' ) &&
170
+ fs . existsSync ( file )
171
+ ) {
172
+ // resolve file to normalized system path
173
+ this . #server. watcher . add ( path . resolve ( file ) ) ;
174
+ }
175
+ }
176
+
177
+ clear ( ) {
178
+ this . #dependencies. clear ( ) ;
179
+ this . #dependants. clear ( ) ;
180
+ }
181
+
182
+ /**
183
+ *
184
+ * @param {import('../types/id.d.ts').SvelteRequest } svelteRequest
185
+ * @param {string[] } dependencies
186
+ */
187
+ update ( svelteRequest , dependencies ) {
188
+ const id = svelteRequest . normalizedFilename ;
189
+ const prevDependencies = this . #dependencies. get ( id ) || [ ] ;
190
+
191
+ this . #dependencies. set ( id , dependencies ) ;
192
+ const removed = prevDependencies . filter ( ( d ) => ! dependencies . includes ( d ) ) ;
193
+ const added = dependencies . filter ( ( d ) => ! prevDependencies . includes ( d ) ) ;
194
+ added . forEach ( ( d ) => {
195
+ this . #ensureWatchedFile( d ) ;
196
+ if ( ! this . #dependants. has ( d ) ) {
197
+ this . #dependants. set ( d , new Set ( ) ) ;
198
+ }
199
+ /** @type {Set<string> } */ ( this . #dependants. get ( d ) ) . add ( svelteRequest . filename ) ;
200
+ } ) ;
201
+ removed . forEach ( ( d ) => {
202
+ /** @type {Set<string> } */ ( this . #dependants. get ( d ) ) . delete ( svelteRequest . filename ) ;
203
+ } ) ;
204
+ }
205
+ }
0 commit comments