Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

watchChange is not triggered for this.addWatchFile #14823

Closed
7 tasks done
nojaf opened this issue Oct 31, 2023 · 7 comments · Fixed by #14822
Closed
7 tasks done

watchChange is not triggered for this.addWatchFile #14823

nojaf opened this issue Oct 31, 2023 · 7 comments · Fixed by #14822
Labels
p2-nice-to-have Not breaking anything but nice to have (priority) rollup plugin compat

Comments

@nojaf
Copy link

nojaf commented Oct 31, 2023

Describe the bug

I'm writing a new Vite plugin and would like to watch file changes outside my bundle.
watchChange in my plugin is not triggered.

This might be related to #3474, but I don't think it is a total duplicate.

Reproduction

https://github.com/nojaf/vite-watch-plugin-repro

Steps to reproduce

  • npm i
  • npm run dev
  • Edit notes.text

Expectation: see a log from the watchChange hook.

System Info

System:
    OS: Windows 10 10.0.22621
    CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700K
    Memory: 13.03 GB / 31.79 GB
  Binaries:
    Node: 18.12.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files\nodejs\yarn.CMD
    npm: 8.19.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 118.0.5993.118
    Edge: Chromium (118.0.2088.76)
    Internet Explorer: 11.0.22621.1
  npmPackages:
    vite: ^4.4.5 => 4.5.0

Used Package Manager

npm

Logs

Click to expand!
About to watch: C:/Users/nojaf/Projects/vite-watch-plugin-repro/notes.text
  vite:deps scanning for dependencies... +0ms

  VITE v4.5.0  ready in 173 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
  vite:deps Crawling dependencies using entries:
  vite:deps   C:/Users/nojaf/Projects/vite-watch-plugin-repro/index.html +0ms
  vite:resolve 0.29ms /main.js -> C:/Users/nojaf/Projects/vite-watch-plugin-repro/main.js +0ms
  vite:resolve 0.27ms ./counter.js -> C:/Users/nojaf/Projects/vite-watch-plugin-repro/counter.js +1ms
  vite:deps Scan completed in 23.77ms: no dependencies found +10ms
  vite:hmr [file change] notes.text +0ms
  vite:hmr [no modules matched] notes.text +3ms

Validations

@bluwy
Copy link
Member

bluwy commented Oct 31, 2023

Could be a coincidence, we just got a PR for it: #14822

Today you can use server.watcher to watch additional changes instead.

@sapphi-red sapphi-red added rollup plugin compat p2-nice-to-have Not breaking anything but nice to have (priority) labels Oct 31, 2023
@sapphi-red sapphi-red linked a pull request Oct 31, 2023 that will close this issue
9 tasks
@nojaf
Copy link
Author

nojaf commented Oct 31, 2023

Yes, this is very much a coincidence 🥳.
I hope this resolves the issue.

@nojaf
Copy link
Author

nojaf commented Nov 1, 2023

This works for me in 5.0.0-beta.15 Thanks everyone!

@patak-dev
Copy link
Member

Could you explain your use case for future reference?

@nojaf
Copy link
Author

nojaf commented Nov 2, 2023

Yes, I'm working on a plugin to enable Fable support in Vite.
The way Fable (or FSharp) works is that you have a project file that contains all your source files.
Each F# file is transpiled to JavaScript and I want to watch the F# files for changes.

Imagine the follow example:
App.fsproj (the project file) looks like:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Math.fs" />
    <Compile Include="Library.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Fable.Core" Version="4.2.0" />
  </ItemGroup>
</Project>

So it contains Math.fs and Library.fs.
The order in an F# project matters!
Library.fs can use functions defined in Math.fs but not the over way around.
My index.html uses<script type="module" src="/Library.js"></script> as entry point.

In my plugin, I communicate with a dotnet process to perform the actual transpilation of .fs files to JavaScript. Each file will be transpiled one on one, Math.fs becomes Math.js and Library.fs becomes Library.js.

Math.fs looks like:

module Math

let sum a b = a + b

Math.js transpiled will be:

export function sum(a, b) { return a + b;}

And Library.fs:

module App

open Fable.Core.JS
open Math

let r = sum 1 3

console.log $"meh from Fable, %i{r}"

Library.js:

import { sum } from "./Math.js";
import { some } from "./node_modules/fable-library/Option.js";
export const r = sum(1, 3);
console.log(some(`meh from Fable, ${r}`));

Note that the transpiled JavaScript, no longer has any notion it was once F#.
Library.js will import Math.js, this is something I need to juggle with in the plugin to resolve the module.

The plugin looks roughly like:

export default function fablePlugin({fsproj}) {
    // A map of <js filePath, code>
    const compilableFiles = new Map()
    let projectOptions = null;

    return {
        name: "vite-fable-plugin",
        buildStart: async function (options) {
            // Initial load of the project, this will compile each file in the project to JavaScript.
            // Everything happens in memory, there aren't any actual transpiled `.js` files on disk.
            const projectResponse = await getProjectFile(fsproj);
            projectOptions = projectResponse.ProjectOptions;
            projectOptions.SourceFiles.forEach(file => {
                const jsFile = file.replace('.fs','.js');
                compilableFiles.set(jsFile, projectResponse.CompiledFSharpFiles[file])
            });
        },
        resolveId: async function (source, importer, options) {
            // In here I detect if an imported JavaScript file is actually a `.fs` file part of my project.

            if (!source.endsWith('.js')) return null;
            let fsFile = source.replace('.js', '.fs');
            if(!projectOptions.SourceFiles.includes(fsFile) && importer) {
                // Might be /Library.fs
                const importerFolder = path.dirname(importer)
                const sourceRelativePath = source.startsWith('/') ? `.${fsFile}` : fsFile;
                fsFile = normalizePath(path.resolve(importerFolder, sourceRelativePath));
            }

            if(projectOptions.SourceFiles.includes(fsFile)) {
                return fsFile.replace(fsharpFileRegex ,'.js');
            }

            return null;
        },
        load: async function (id) {
            // Return the compiled JavaScript file
            if (!compilableFiles.has(id)) return null;
            return {
                code: compilableFiles.get(id)
            }
        },
        watchChange: async function (id, change) {
            // Changes to `.fs` or `.fsproj` will eventually need to reload the transpiled JavaScript.
            if (id.endsWith('.fsproj')){
                // Potentially all the files in a project are invalidated.
                console.log("Should reload project")
            }
            else if (fsharpFileRegex.test(id)) {
                // I know a file changed and need to recompile it and all the dependent files in the project graph.
                const compilationResult = await endpoint.send("fable/compile", { fileName: id });
                const loadPromises =
                    Object.keys(compilationResult.CompiledFSharpFiles).map(fsFile => {
                        const jsFile = fsFile.replace(fsharpFileRegex ,'.js')
                        compilableFiles.set(jsFile, compilationResult.CompiledFSharpFiles[fsFile]);
                        // The result can contain multiple files, these need to be reloaded.
                        return this.load({ id:jsFile });
                    });
                await Promise.all(loadPromises);
            }
        },
        handleHotUpdate({ file, server, modules }) {
            // If I know a file changed, each file that came after it in the project should be considered changed.
            if(fsharpFileRegex.test(file)) {
                const fileIdx = projectOptions.SourceFiles.indexOf(file);
                const sourceFiles = projectOptions.SourceFiles.filter((f,idx) => idx >= fileIdx);
                const modulesToCompile = [];
                for (const sourceFile of sourceFiles) {
                    const jsFile = sourceFile.replace(fsharpFileRegex ,'.js');
                    const module = server.moduleGraph.getModuleById(jsFile)
                    if (module) modulesToCompile.push(module)
                }
                if (modulesToCompile.length > 0) {
                    server.ws.send({
                        type: 'custom',
                        event: 'hot-update-dependents',
                        data: modulesToCompile.map(({ url }) => url),
                    })
                    return modulesToCompile
                } else {
                    return modules
                }               
            }
        },
        buildEnd: () => {
            dotnetProcess.kill();
        }
    }
}

So, to summarize. I need to watch the .fs (and .fsproj) files which are not part of the bundle.
I cannot use the transform hook, as after the first .fs file, I no longer have any notion of FSharp files.

I hope this makes some sense and was useful to read.

@patak-dev
Copy link
Member

Thanks for taking the time to write down your use case, and great to see this exploration.
One caveat about watchChange, we're currently watching all files under root recursively so this works now. @ArnaudBarre was pushing for Vite to stop doing this though, and only watch on demand. This would be a breaking change though, so we could only pull it off in the next major. So it would still be safe to do. I wonder if what is watched should be documented and part of the API, or document that all files in root are being watched as an implementation detail and not something to rely on.

If you need to restart the server when non-imported files are changed, another option would be to use something like https://github.com/antfu/vite-plugin-restart.

@nojaf
Copy link
Author

nojaf commented Nov 2, 2023

One caveat about watchChange, we're currently watching all files under root recursively so this works now.

Yes, I was a bit surprised I didn't have to call this.addWatchFile explicitly.

@github-actions github-actions bot locked and limited conversation to collaborators Nov 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
p2-nice-to-have Not breaking anything but nice to have (priority) rollup plugin compat
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants