ptrdom / scalajs-vite

Bundles Scala.js projects and their npm dependencies with Vite
MIT License
12 stars 1 forks source link

Resolving Scala.js exports from non-Scala.js sources #41

Open johnhungerford opened 5 months ago

johnhungerford commented 5 months ago

If I understand correctly, scalajs-vite currently injects Scala.js compiled (xLinkJS) outputs into the root of the build directory. This means that it should be possible for other non-Scala.js sources to import from these files just as, e.g., import App from '/main.js';. This leads to potential name clashes, however, especially if you want to name a JS entrypoint main.js, which is the default entrypoint of Scala.js outputs.

I propose using the convention established by vite-plugin-scalajs, which is to allow users to import Scala.js by prefixing their import paths with scalajs:. So you if do a default top level export to a named module such as JSImportTopLevel("default", "myModule"), you can import it in your JS code as: import MyModule from 'scalajs:myModule.js';. I propose additionally allowing users to use scalajs: with nothing after the colon to stand in for a reference to the default module main.js so that they don't need to know the Scala.js linker's conventions to use its outputs.

I have adapted the vite-scalajs-plugin functionality to accomplish this in sbt-vite. Note that this also requires generating a vite.config.js which imports the plugin.

ptrdom commented 5 months ago

This process is unfamiliar and - I got to be honest - a bit funky to me, I wonder if it can be modified somewhat to fit the tooling better. I have no experience in Scala.js and Javascript project interop though. But I know that workflows not always translate one-to-one - for example a single Javascript project might have to become multiple sbt Scala.js modules, because Scala.js plugin does not support emitting for multiple environments/purposes from a single sbt module too well - I had my frustrations on that with Electron plugins I have been working on. It would be easier to just conform to how Scala.js plugin and sbt works, rather than trying to fight them.

If you need non-bundled exported Scala.js components, then @JSExportTopLevel should be enough. If you need them bundled, then I think you need to do library bundling. Can you see that working for you?

ptrdom commented 5 months ago

If you need non-bundled exported Scala.js components, then @JSExportTopLevel should be enough.

Just to correctly myself, I should have just linked https://www.scala-js.org/doc/interoperability/export-to-javascript.html and https://www.scala-js.org/doc/project/module.html.

Best course of actions might just be to try and implement a library project with scalajs-vite as an sbt-test and see what is missing to successfully achieve that.

And also on this:

This leads to potential name clashes, however, especially if you want to name a JS entrypoint main.js, which is the default entrypoint of Scala.js outputs.

I think you might just want to split exports (the library part) and main module intializer (the entrypoint) into separate Scala.js modules and not try to do both at once.

johnhungerford commented 5 months ago

I'm going to paste what I put in the testing thread here, since it is more relevant to this issue. As I said there, I see three cases in which Scala.js exports are going to need to be resolved properly:

  1. Your application entrypoint is JavaScript. Example: I have a JS react app, and one or more components of it are in Scala.js. I have some App.jsx file that imports 'scalajs:ScalaJSComponent1.js' and 'scalajs:ScalaJSComponent2.js'. I expect vite to bundle App.jsx with Scala.js artifacts produced by fullLinkJS (which I expect to include ScalaJSComponent1.js and ScalaJSComponent2.js)
  2. Your application entrypoint is Scala.js, but it depends on some non-Scala.js sources. These sources in turn depend on exports from Scala.js. Imagine, e.g., fullLinkJS produces main.js (runnable entrypoint), and utils.js (module generated by a call to JSExportTopLevel("someUtility", "utils")). main.js imports a JS module someJsModule.js, and someJsModule.js in turns imports 'scalajs:utils.js'. These imports all need to be resolvable for vite to generate a runnable bundle from main.js.
  3. As a corollary to (2), now image you have a test in your Scala.js source that includes the dependency to someJsModule.js. For this test to run, the bundler is going to need access not only to the compiled test code and the JS sources, but also the compiled Scala.js application code, since theTest / fastLinkJS outputs will not necessarily include the utils.js export!

Just to be clear, all three of these cases apply to me! I'm currently migrating an application from JS to Scala.js, and I need to be able to migrate components one at a time. This means the original TS codebase has to be able to import parts of my Scala.js codebase, and my Scala.js codebase needs to be able to import parts of my TS codebase. I currently have this working in my jsbundler project (see this example project).

ptrdom commented 5 months ago

I totally understand your issue, but just to repeat https://github.com/ptrdom/scalajs-vite/issues/39#issuecomment-1930390189, I think you have both a bundler and a workspace issue, and this project should only be solving the former and enable you to implement the latter yourself.

johnhungerford commented 5 months ago

Ok so it's clear now that there is not the issue I thought there was here (see my comment on #39).

Let me then reduce my proposal to this: to avoid, or at least reduce the likelihood of, file collisions, would it be acceptable to inject the xLinkJS outputs into a scalajs directory within the build directory? The result would be that you could import Scala.js outputs into JS sources using import xxx from '/scalajs/main.js'; and not worry that it's going to collide with your main.js file in viteResourceDirectory.

ptrdom commented 5 months ago

Sure, that seems like a reasonable feature to implement.

Currently viteCompile task does the copying from scalaJSLinkerOutputDirectory to viteInstall / crossTarget. The way I see it the implementation can be done in two ways:

  1. Add viteCompile / crossTarget setting that would allow copying scalaJSLinkerOutputDirectory to something other than viteInstall / crossTarget. New viteCompile / crossTarget setting would default to viteInstall / crossTarget.
  2. Get rid of viteCompile and just point scalaJSLinkerOutputDirectory to the subdirectory in viteInstall / crossTarget. Would probably need to add https://www.scala-sbt.org/1.x/docs/Howto-Track-File-Inputs-and-Outputs.html for file change detection.

Case 1 is easier to implement, but 2 is probably more forward thinking. I am leaning towards 1 just for the sake of simplicity.

johnhungerford commented 5 months ago

Case 2 had not occurred to me. I think I prefer case 1 by a small margin simply because I could see users getting confused that xLinkJS was not putting artifacts in the usual place. For instance, case 2 would break any scripts a user might have had in place that pulled artifacts from target/scala-[x.x.x]/[project]-opt/, which is not that uncommon.