braidnetworks / dynohot

Hot module reloading for nodejs
ISC License
90 stars 3 forks source link

Example usage with SolidJS (jsx) #3

Closed kireerik closed 11 months ago

kireerik commented 11 months ago

Do you have an idea on how to use it with SolidJS (jsx)?

laverdet commented 11 months ago

Could you provide more information on your project? Are you trying to set up SSR? SolidJS is a frontend tool and dynohot is a backend tool. We personally do use dynohot to implement hot reloading of an SSR React project but I suspect this isn't what you're trying to do.

kireerik commented 11 months ago

Sure. Yes, for static site generation (SolidJS is only used for static site generation). I am reimplementing refo (A static site and resume example with styled components.) using dynohot and a (Node.js) loader (that I am writing) (instead of using Astro).

I was able to import a (Node.js) module (which is handled by dynohot) from the loader that I am writing, but I was unable to set it up in a way that I am getting an up to date module when importing from my loader. Is this somehow possible?

What I currently have and is working so far is the following: In my loader modules ending with .html.js are handled. Their result is written into a .html file (in a static folder) with the same name and path as the handled module. I am transforming the code of modules ending with .html.js using Babel like this:

import {transformFileSync} from '@babel/core'

const {code} = transformFileSync(/*path*/, {
    presets: [
        ['solid', {generate: 'ssr'}]
    ]
})

I am evaluating the transformed codde and writing it's result (into a .html file (in the static folder)) like this:

import {renderToString} from 'solid-js/web'

import {writeFileSync} from 'fs'
const module = new SourceTextModule(code) // source: https://stackoverflow.com/questions/72130595/how-to-link-the-imported-dependencies-of-module-created-by-vm-sourcetextmodule-t/73282303#73282303

await module.link(async identifier => {
    const module = await import(identifier)
        var imported = new SyntheticModule(
        Object.keys(module)
        , () =>
            Object.entries(module).forEach(
                ([key, value]) =>
                    imported.setExport(key, value)
            )
        , {identifier}
    )
        return imported
})

await module.evaluate()

writeFileSync(
    //target // '.html'
    , renderToString(() => module.namespace.default)
)

Would be nice if I could just import the transformed module (Which I can and it works, but I couldn't get hot module reloading working for this case like this. So the imported module is always outdated here in this case.) like this:

writeFileSync(
    //target // '.html'
    , renderToString(() => (await import(/*path*/)).default)
)

Is there a way I can use the dynohot loader for the loader I am writing as well? (Currently I am only able to use it for the main code that are handled by the loaders.)

laverdet commented 11 months ago

My first guess is that your resolve hook is resolving to the html file, and therefore dynohot doesn't know what to watch.

I actually just open sourced our TypeScript loader which is a good place to get started: https://github.com/braidnetworks/loaderkit/blob/main/packages/ts/loader.ts

The key takeaway is that resolve should always return url as the source file (in your case .html.js) and not something else. url is the file which dynohot will watch for changes. In the load hook you can set responseURL to whatever you want, it doesn't exist or even be unique.

That's kind of a shot in the dark without more information. I can also take a look at the project if you want to host it somewhere. It sounds like a cool use case.

kireerik commented 11 months ago

No. My resolve hook just calls and returns nextResolve(specifier, context) and in my load hook I return the transformed source (of the .html.js module). Writing (generating) the .html file (in the static directory) is just a side effect of importing a .html.js module. Example: import 'index.html.js' So this part is working and dynohot watches it properly I think. I am not sure if I need to set responseURL. Currently I am not setting it.

Yes, I am planning to update the refo project once I finished the reimplementation and share it with you here. So far overall dynohot seems to work really well.

So basically what I would like to do I think is to use dynohot for my loader as well. So do something like this if somehow possible:

node --loader $(node --loader dynohot ./myLoader.js) --loader dynohot index

Currently I have this:

node --loader ./myLoader.js --loader dynohot index
kireerik commented 11 months ago

So I would like dynohot to watch my loader as well if somehow possible.

kireerik commented 11 months ago

That way instead of the evaluation script I could just import the module as usual in my loader as well.

kireerik commented 11 months ago

This also means that right now if I make a change in my loader then my loader is not hot reloaded. So it would be nice to somehow have hot module reloading for my loader as well.

kireerik commented 11 months ago

Do you have an idea on how to do that?

laverdet commented 11 months ago

I don't think it will be possible to hot reload another loader. It would invalidate the entire module graph which would be more expensive than simply restarting nodejs.

kireerik commented 11 months ago

I see. So I guess I can't import a (hot reloadable) module from the loader itself in a way that I get the up to date module (I can import the module, but it is always the version when I started the project and not the up to date one.).

laverdet commented 11 months ago

An update to a loader would invalidate the entire module graph since all modules implicitly have a dependency on the loader chain. Loaders should, in general, be pretty lightweight and stable. Once you get yours working you shouldn't need to be modifying the loader all that much right? Just transform the incoming source and pass it along. For reference the actual loader part of dynohot only includes the source transformation, and is entirely stateless.

kireerik commented 11 months ago

Yes. I also thought that it works like you describe.

Thank you for the help!

Now I had the idea to just add the code that writes the file into the source code of the module that I am handling with the loader where I can just import the module itself and write it's result into the file. So I don't need the previously shared evaluation script that relies on the experimental-vm-modules feature. This seems to also work well and I can just use a normal import.

I am planning to share a minor issue here or maybe in a new one.

kireerik commented 11 months ago

Actually this is even better. Unlike the module evaluation solution this covers an edge case as well by design.

kireerik commented 11 months ago

Do you know any raw (Node.js) loader? (Similar to for example this one: https://vitejs.dev/guide/assets.html#importing-asset-as-string)

laverdet commented 11 months ago

I don't know of a raw loader. The ecosystem is still very new so there's a lot of territory that's open for staking. If you build this I would recommend a new URL scheme instead of a query param like Vite. import file from "content:./module"

I'd also recommend not performing the file write operations in your development tool and have a separate "production" build step.

kireerik commented 11 months ago

As far as I know Vite supports a query parameter (https://vitejs.dev/guide/assets.html#importing-asset-as-string) only. So you mean you recommend a new URL scheme (instead of a query parameter (like Vite supports)).

The way I implemented it right now is that the write operations are performed by the (imported) modules themselves (my loader adds the writing script into their source codes).

I think there should be no difference. The only main difference is that when I am developing I keep the process running using setInterval(() => {}, 2 ** 31 - 1).

So there is only the script that generates a static site and if I keep this script's process running then as I make changes to the imported modules they are writing their output into the static folder and also removing their output if they are no longer imported.