@@ -3,6 +3,8 @@ import { mapToRelative } from '../utils/sourcemaps.js';
33import * as svelte from 'svelte/compiler' ;
44import { log } from '../utils/log.js' ;
55import { arraify } from '../utils/options.js' ;
6+ import fs from 'node:fs' ;
7+ import path from 'node:path' ;
68
79/**
810 * @param {import('../types/plugin-api.d.ts').PluginAPI } api
@@ -14,10 +16,16 @@ export function preprocess(api) {
1416 */
1517 let options ;
1618
19+ /**
20+ * @type {DependenciesCache }
21+ */
22+ let dependenciesCache ;
23+
1724 /**
1825 * @type {import("../types/compile.d.ts").PreprocessSvelte }
1926 */
2027 let preprocessSvelte ;
28+
2129 /** @type {import('vite').Plugin } */
2230 const plugin = {
2331 name : 'vite-plugin-svelte:preprocess' ,
@@ -37,19 +45,39 @@ export function preprocess(api) {
3745 delete plugin . transform ;
3846 }
3947 } ,
40-
48+ configureServer ( server ) {
49+ dependenciesCache = new DependenciesCache ( server ) ;
50+ } ,
51+ buildStart ( ) {
52+ dependenciesCache ?. clear ( ) ;
53+ } ,
4154 transform : {
4255 async handler ( code , id ) {
43- const cache = api . getEnvironmentCache ( this ) ;
4456 const ssr = this . environment . config . consumer === 'server' ;
4557 const svelteRequest = api . idParser ( id , ssr ) ;
4658 if ( ! svelteRequest ) {
4759 return ;
4860 }
4961 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 ;
5180 } catch ( e ) {
52- cache . setError ( svelteRequest , e ) ;
5381 throw toRollupError ( e , options ) ;
5482 }
5583 }
@@ -63,8 +91,6 @@ export function preprocess(api) {
6391 * @returns {import('../types/compile.d.ts').PreprocessSvelte }
6492 */
6593function createPreprocessSvelte ( options , resolvedConfig ) {
66- /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined } */
67- let stats ;
6894 /** @type {Array<import('svelte/compiler').PreprocessorGroup> } */
6995 const preprocessors = arraify ( options . preprocess ) ;
7096
@@ -75,59 +101,105 @@ function createPreprocessSvelte(options, resolvedConfig) {
75101 }
76102
77103 /** @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 ;
106106 let preprocessed ;
107-
108107 if ( preprocessors && preprocessors . length > 0 ) {
109108 try {
110- const endStat = stats ?. start ( filename ) ;
111109 preprocessed = await svelte . preprocess ( code , preprocessors , { filename } ) ; // full filename here so postcss works
112- endStat ?. ( ) ;
113110 } catch ( e ) {
114111 e . message = `Error while preprocessing ${ filename } ${ e . message ? ` - ${ e . message } ` : '' } ` ;
115112 throw e ;
116113 }
117-
118114 if ( typeof preprocessed ?. map === 'object' ) {
119115 mapToRelative ( preprocessed ?. map , filename ) ;
120116 }
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 ;
131118 }
132119 } ;
133120}
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