vite-plugin-svelte: Circular filesystem watch events cascade, including self-referential, leading to RangeError: Maximum call stack size exceeded

Describe the bug

I have encountered crash of dev script.

RangeError: Maximum call stack size exceeded

Initial investigation lead me to emitChangeEventOnDependants function in src/utils/watch.js, where I’ve discovered self-referential loops, where supposed dependant was same as filename.

Unfortunately, blocking self-referential event emitting revealed another problem - circular emission of virtual change event.

It is challenging to create an isolated reproduction. Which may or may not be related to how vite-plugin-svelte-cache choses to build dependency tree.

However, it is clear that self-referential, and circular references may exist, and therefore may overwhelm nodejs process with storm of emited events.

While it is not as good as reproduction, I hope, you would find this information instrumental.

Reproduction URL

https://github.com/sveltejs/vite-plugin-svelte

Reproduction

No response

Logs

No response

System Info

System:
    OS: macOS 13.5.2
    CPU: (8) arm64 Apple M2
    Memory: 208.14 MB / 24.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.8.0 - ~/.asdf/installs/nodejs/20.8.0/bin/node
    npm: 10.1.0 - ~/.asdf/plugins/nodejs/shims/npm
  Browsers:
    Chrome: 117.0.5938.149
    Safari: 16.6
  npmPackages:
    @sveltejs/adapter-auto: ^2.0.0 => 2.1.0 
    @sveltejs/adapter-static: ^2.0.3 => 2.0.3 
    @sveltejs/kit: ^1.20.4 => 1.25.1 
    svelte: ^4.0.5 => 4.2.1 
    vite: ^4.4.2 => 4.4.10

About this issue

  • Original URL
  • State: closed
  • Created 9 months ago
  • Reactions: 1
  • Comments: 22 (8 by maintainers)

Most upvoted comments

@RowanAldean , I had same story, in your project’s folder you may find file node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js on line 27 you may see watcher.emit('change', dependant); comment this line to avoid crashes.

P.S. I have tailwind installed. What would you say about your setup? Are you use anything beyond vanilla setup? Style preprocessors?

Sure, I wanted Tailwind utilities in my scopes, but I get your point, thank you.

“This explains why we don’t see more reports of it happening, you really shouldn’t do this.”

  1. I don’t think I got the resoning part. Why we should not do it?
  2. As you see there were 2 reports in a week. It is unclear how many just give up.

I’m leaning towards throwing an error if we detect the file itself in preprocess dependencies, its a sign of something seriously wrong.

yeah, so whats happening here is not pretty at all… in the project above, these are the dependencies for src/routes/+page.svelte returned by preprocess:

4:37:24 PM [vite-plugin-svelte] dependencies of andriytyurnikov.github.io/src/routes/+page.svelte
andriytyurnikov.github.io/src/pro-rata.css
andriytyurnikov.github.io/src/index.test.js
andriytyurnikov.github.io/src/lib/index.js
andriytyurnikov.github.io/src/lib/proRata.js
andriytyurnikov.github.io/src/routes/+layout.js
andriytyurnikov.github.io/src/app.html
andriytyurnikov.github.io/src/routes/+layout.svelte
andriytyurnikov.github.io/src/routes/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/+layout.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/List.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/NavBar.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/Shell.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/ideas/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/layouts/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/pro-rata/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/ring-buoy/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/web-font-size-calculator/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/list/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/navbar/+page.svelte
andriytyurnikov.github.io/src/routes/(app)/garage/fundamental/shell/+page.svelte
andriytyurnikov.github.io/tailwind.config.js

including the file we just preprocessed in the dependencies output is obviously super wrong, so step 1 is filtering that from the returned dependencies before passing them on. This means that the direct circle from +page.svelte to +page.svelte no longer happens.

but notice how it includes all the others too? change any of those files and +page.svelte gets preprocessed/compiled again just in case the change actually affected the output. We have no way to tell which of these dependencies are real and which are just there because tailwind postcss decided to go nuclear.

I assume it returns similar dependencies for all the other .svelte files too, essentially leading to a “compile every svelte file as soon as you edit something” scenario. Great!

yes, tailwind with it’s postcss directory dependencies could cause this. I’d recommend not using their @apply feature in svelte style blocks. see below

in general I’d recommend unocss instead of it. But given it’s popularity and use in 3rd party libraries we have to make it work somehow. still leaning towards keeping it as simple as possible which might mean just avoiding emitting the event in case it’s a circle.

@dominikg I wonder if all this jazz is connected with Tailwind recompilation or CSS purging…

… as Tailwind generates CSS classes depending on js files (configs, extensions, plugins), and basically change to any file may cause change of CSS styles, including those inside svelte components

<style></<style>

which leads to update of components, which leads to virtual event, which may lead to recompilation of tailwind… and so on, and so forth…

@andriytyurnikov Thanks, i’ll try this and edit comment with result(s).

I’m using svelte-preprocessor to support a global app.scss via prependData in my svelte.config.js.

I also moved to using yarn to handle all the dependency hell (if that is relevant 😉)

Also using tailwind for skeletonUI and recently for shadcn-svelte which is what forced me to do this whole updating everything as I had a bunch of dependency issues.

Also your solution seems painful as this would need to be done anytime I rebuild node_modules from package.json😢

update: @andriytyurnikov above solution works as a workaround to not interrupt developer workflow - hello future googler probably struggling with this pain in the ass watcher 👋

I just upgraded my entire project to svelte 4 (meaning sveltekit, vite and the rest all updated). Watcher failing and same error as described, I can’t save a file without a crash that bubbles up from:

at Array.forEach (<anonymous>)
at emitChangeEventOnDependants (node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:22:14)
at node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:94:66

Here’s a full stack trace:

Exception in PromiseRejectCallback:
/node_modules/vite/dist/node/chunks/dep-2b82a1ce.js:65205
    });
    ^

RangeError: Maximum call stack size exceeded
/node_modules/@sveltejs/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js:167
                const dependants = this.#dependants.get(path);
                                   ^

RangeError: Maximum call stack size exceeded
    at VitePluginSvelteCache.getDependants (/node_modules/@sveltejs/vite-plugin-svelte/src/utils/vite-plugin-svelte-cache.js:167:22)
    at emitChangeEventOnDependants (/node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:21:28)
    at /node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:94:66
    at Array.forEach (<anonymous>)
    at FSWatcher.<anonymous> (/node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:94:44)
    at FSWatcher.emit (node:events:525:35)
    at /node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:27:13
    at Array.forEach (<anonymous>)
    at emitChangeEventOnDependants (/node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:22:14)
    at /node_modules/@sveltejs/vite-plugin-svelte/src/utils/watch.js:94:66

Node.js v18.12.1

Much of what you have discussed is far beyond my technical understanding of the sveltekit/svelte ecosystem so i’ll pass on trying to understand for now as i’m looking to just build! However I hope the above is of some value (and proof this is happening to others too 👀)

TLDR; Things are complex and there are 3 paths.

  • no “virtual events” (preffered)
  • single(!) context for dependency tree checking
  • fundamentally redesigned failsafe watcher, capable to withstand circular cascades Danger! Also, there is a potential workaround hack: accumulate “virtual events” in a buffer, and then - periodically, out of the event loop, remove duplicates, and flush that buffer by emitting those events periodically - once in a second or something. Limit the buffer size, for extra safety. Dirty-dirty-dirty.

Details: Sad irony is that vite relies on 3rd party watchers (chokidar?), I remember reading their discussion about choosing new one.

I don’t mean to be pushy, yet I would like to reiterate for clarity ( and disagreements, questions and critique are totally welcome): My understanding is that current implementation optimistically assumes:

  • impossibility of self-referatial inputs from vite-plugin-svelte-cache
  • ability of vite’s watcher (!!!) to resolve such cases As both assumptions are proven(?) to be wrong, I am yet again advise against “virtual event emission”, as every such event is a potential trigger for cascade. Without cascade-breaker/preventer mechanism in either watcher, vite-plugin-svelte-cache or vite-plugin-svelte such behaviour is fundamentally unsafe.

Also, consider this - circular dependency prevention mechanism based on reference checks is only possible on a level, where full dependency graph is known. With this in mind - having more then one dependency-tracking context is fundamentally vulnerable to cascades as well, as such contexts would be able of triggering each-other.

It is not obvious how exactly watcher (and code triggers event on watcher, which is not the same as communicating dependency tree to vite!) is supposed resolve such an issue: watcher receives “event”, and it is not aware that event is “virtual”, so watcher tries hard to do his job, regardless of dependency tree - because allowing or disallowing circular references is not it’s job - maybe you building a language friendly to circular behaviour.

Apparently, watchers leave issue of dependencies beyond their scope. And, again, it is not surprising, given the need of access to dependency tree for comparison-based implementation. Yet insufficient watcher resiliency is indeed surprising.