shadow-cljs

mitchelkuijpers 2025-05-19T10:07:47.734109Z

I am trying to change some shadow-cljs output mainly because we have to handle .css imports and I would really like to not compile with shadow-cljs and then pipe it through another tool (because this breaks nice stuff like a REPL and hot reload). So I was looking at hooking into the compilation stages would that be a valid use case for this? I am thinking I need the flush stage. But I would actually like to change that file input just before it get's flushed but it doesnt seem there is a stage for that

thheller 2025-05-19T10:49:03.919069Z

not entirely sure I understand which problem you are trying to solve?

thheller 2025-05-19T10:49:30.301389Z

what shadow-cljs output do you want to change?

thheller 2025-05-19T10:50:12.150509Z

I assume you are talking about a .css import a npm JS file is doing? not CLJS?

Roman Liutikov 2025-05-19T10:57:26.422229Z

@thheller fyi, importing CSS is actually a part of JS standard now you can do

the output is an instance of CSSStyleSheet same for type: "json"

thheller 2025-05-19T10:58:05.091609Z

seriously?

Roman Liutikov 2025-05-19T10:58:20.055929Z

yeah. here's a demo https://jsbin.com/janacorajo/1/edit?html,output

mitchelkuijpers 2025-05-19T10:58:23.514529Z

I have to use @atlaskit components from atlassian and those are NPM packages which contain css-imports

Roman Liutikov 2025-05-19T10:59:09.559329Z

I just learnt yesterday that import got whole bunch of bells and whistles over the last couple of years

thheller 2025-05-19T11:00:07.819509Z

yay .. looking forward to that shit 😛

thheller 2025-05-19T11:00:30.823409Z

@mitchelkuijpers do you have an example?

thheller 2025-05-19T11:00:59.479949Z

CSS import does not equal CSS import unfortunately. so it isn't entirely clear what is needed without seeing an actual example

mitchelkuijpers 2025-05-19T11:33:46.225649Z

I literally see stuff like this:

import "./spinner.compiled.css";

thheller 2025-05-19T11:41:02.731469Z

yes, what library is that in?

mitchelkuijpers 2025-05-19T11:41:29.333479Z

@atlaskit/spinner

mitchelkuijpers 2025-05-19T11:42:34.323849Z

They use this: https://compiledcssinjs.com/docs/how-it-works#extracted-styles

thheller 2025-05-19T11:46:41.433059Z

ok, that is the "simplest" variant which I have been meaning to add support for for a while now

thheller 2025-05-19T11:47:54.784729Z

never quite got around to implementing it yet though

thheller 2025-05-19T11:48:19.031509Z

basically all it needs to do is collect all those references and in the end concatenate all those files together and put a css file somewhere

mitchelkuijpers 2025-05-19T11:48:26.779379Z

Yeah I quite like how they did this honestly

thheller 2025-05-19T11:48:32.754209Z

at least in the simple version

thheller 2025-05-19T11:49:01.532689Z

gets more complicated when styles need to be scoped or renamed or whatever else other things do

thheller 2025-05-19T11:49:21.087049Z

but just import/require with unused return values should be pretty straightforward

thheller 2025-05-19T11:49:53.984389Z

kinda probably also want some kind of CSS processing (de-duplication) but that could come later

thheller 2025-05-19T11:50:14.383289Z

gist is that currently shadow-cljs just throws all this info away and doesn't collect it in any meaningful way

thheller 2025-05-19T11:58:10.669219Z

fwiw this compiled css stuff is exactly how shadow-css works, so I like that. just that its done as part of a JS build is kinda annoying. but its JS so what choice do they have 😉

mitchelkuijpers 2025-05-19T12:12:56.936839Z

Yeah true, I don't hate this solution too much tbh

mitchelkuijpers 2025-05-19T12:13:23.276569Z

But shadow-css also extracts a css file and gives you map that links to the generated classnames?

thheller 2025-05-19T12:13:56.059249Z

it could, but currently doesn't since css is a macro and already generates a classname https://github.com/gamb/shadow-css?tab=readme-ov-file#using-the-css-macro

thheller 2025-05-19T12:15:10.167699Z

this de-duping atomic css has never felt necessary yet. css is already tiny when gzip'd

mitchelkuijpers 2025-05-19T12:15:59.993109Z

Ah I see how shadow-css works that is also nice

thheller 2025-05-19T12:16:11.102639Z

the css being a macro helps and JS just can't do that

mitchelkuijpers 2025-05-19T12:16:25.403549Z

I was also thinking a bit about how loaders work in webpack but not sure if you would ever want to go that route

thheller 2025-05-19T12:17:24.211579Z

well thats why I asked. there are many cases where CSS is included as CSS modules, as in your write .foo { ...} ins a css file, require and an then use css.foo to get the actual classname which ends up getting scoped and renamed

thheller 2025-05-19T12:17:48.469959Z

that is much more involved and harder to process, and require special webpack loaders even in JS. so not that common anymore these days

thheller 2025-05-19T12:19:32.291089Z

but there are some libs that still do this, but then just ship a .css.js file that basically does all this and looks like a regular .js file to shadow. that just works, which is nice, but doesn't produce ideal css

mitchelkuijpers 2025-05-19T12:30:15.024569Z

Yeah that makes it way harder, I like how atlassian does it because it doesn't force us to use it in that way

thheller 2025-05-19T12:31:25.598499Z

I can add that shadow stores this in a way that a build hook can access

thheller 2025-05-19T12:31:41.420539Z

so that a build hook could just collect all those css references and do whatever

mitchelkuijpers 2025-05-19T12:32:54.140929Z

Could it for now also work to just use the flush build hook?

thheller 2025-05-19T12:33:02.737139Z

not really no, this info is just thrown away

mitchelkuijpers 2025-05-19T12:33:05.869639Z

Because I basically only want to update the import calls for those components

thheller 2025-05-19T12:33:39.271949Z

update?

mitchelkuijpers 2025-05-19T12:34:26.928939Z

Yeah sorry i need to be more clear. I am currently playing around with rollup if I give it the esm_import$$atlaskit.... file it then fixes the imports of this crap

mitchelkuijpers 2025-05-19T12:34:55.681329Z

and writes the css file an basically leaves the output from shadow-cljs alone but only adds the inlined npm module

thheller 2025-05-19T12:35:28.597849Z

inlined npm module? I'm so confused. so you are processing this stuff with a second tool? thought you didn't want that?

mitchelkuijpers 2025-05-19T12:36:30.654349Z

It transforms the import ... @atlaskit/spinner to the actual compiled source

mitchelkuijpers 2025-05-19T12:36:43.814629Z

I was playing around with how to fix this for now

thheller 2025-05-19T12:36:59.179019Z

ok, lets start from scratch. you are building a CLJS project with shadow-cljs. but it fails because it encounters a .css require in some npm dependency?

mitchelkuijpers 2025-05-19T12:37:46.792739Z

Yes you are exactly right

mitchelkuijpers 2025-05-19T12:37:53.501399Z

that is the core problem

thheller 2025-05-19T12:38:04.834829Z

ok, and you can't just ignore the .css file, you actually want the css to end up somewhere

thheller 2025-05-19T12:38:25.670969Z

if ignoring is fine you could just set :js-options {:ignore-asset-requires true} and shadow will just ignore them

mitchelkuijpers 2025-05-19T12:38:32.405549Z

Oh damn

thheller 2025-05-19T12:38:35.029339Z

but then the css is lost entirely

mitchelkuijpers 2025-05-19T12:38:54.514309Z

I think that might be enough for now since I fixed it for the frontend and that uses the same UI code

thheller 2025-05-19T12:39:00.603919Z

so you want that css to still be collected and accessible somewhere

mitchelkuijpers 2025-05-19T12:39:17.814349Z

Yeah in ideal world I would like that then I can ditch other bundlers

thheller 2025-05-19T12:39:47.770879Z

yes, that is what I was proposing. instead of throwing away encountered css requires I could just store them in a way that a build hook can access

thheller 2025-05-19T12:40:09.970769Z

so that the build hook could copy the contents somewhere, or really do whatever. without any need for other tools

thheller 2025-05-19T12:40:52.255949Z

collecting all that info instead of throwing it away is fairly straightforward and I think would be enough to get you what you want

thheller 2025-05-19T12:41:12.504139Z

you just can't do it currently because the info is not stored anywhere

mitchelkuijpers 2025-05-19T12:42:19.619089Z

Yeah exactly, if I would not the imports I could even feed it to a bundler to only fix that css crap for me if necessary

thheller 2025-05-19T12:43:46.086789Z

sure, something like :build-hooks [(some.thing/write-css-to "foo/bar.css")] or so would be trivial

mitchelkuijpers 2025-05-19T12:43:58.813949Z

Btw does :ignore-asset-requires work with :js-provider :import?

mitchelkuijpers 2025-05-19T12:44:12.104559Z

Because I don't get how that would work

thheller 2025-05-19T12:44:23.070629Z

no, why would it? it will never encounter any assets requires

mitchelkuijpers 2025-05-19T12:44:32.661979Z

Yeah that is what I thought

thheller 2025-05-19T12:45:09.267049Z

but that would't work with the build hook either .. shadow isn't processing any JS in that case

mitchelkuijpers 2025-05-19T12:45:55.843229Z

I have this problem currently also for a nodejs target

thheller 2025-05-19T12:46:34.256419Z

what problem? 😛

mitchelkuijpers 2025-05-19T12:47:02.347699Z

:target :esm
                      :runtime :node
                      :output-dir "target/shadow-cljs-output/functions"
                      :js-options {:js-provider :import}

thheller 2025-05-19T12:47:17.090759Z

ok and?

mitchelkuijpers 2025-05-19T12:47:23.876489Z

This also imports stuff with those stupid css imports

thheller 2025-05-19T12:47:46.294919Z

yes, but its node doing the importing, not shadow ... so nothing shadow can do about it 😛

mitchelkuijpers 2025-05-19T12:48:22.429379Z

That why i was "inlining" those modules where some other tool transforms it to something that works with nodejs

mitchelkuijpers 2025-05-19T12:48:38.574039Z

But then I break hot reload and the repl

thheller 2025-05-19T12:48:41.048269Z

ok, shadow can already do that

thheller 2025-05-19T12:49:04.906179Z

:js-options {:keep-as-import #{"foo"}}, that set listing all the dependencies shadow should NOT bundle

thheller 2025-05-19T12:49:10.818199Z

but bundle all else. so you then also add :js-options {:ignore-asset-requires true} and you are good

thheller 2025-05-19T12:50:08.500519Z

:js-provider :import needs to be removed of course

mitchelkuijpers 2025-05-19T12:50:14.975079Z

Yeah ofcourse

mitchelkuijpers 2025-05-19T12:51:19.377779Z

I am going to play around with this to see if I get it working. I should then probably use: :node-library ? Because this won't work with target esm right?

thheller 2025-05-19T12:51:39.996109Z

should be fine? in fact :keep-as-import will ONLY work with :esm, for :node-library it would be :keep-as-require

mitchelkuijpers 2025-05-19T12:52:22.677579Z

Oh cool thank you so much. And writing the css to a separate file would still be amazing then we can remove a bundler

Roman Liutikov 2025-05-19T13:36:28.189189Z

@thheller you should really document all of those flags, I feel like I’ve seen at least 10 of ad hoc flags now for various JS workarounds 🫠

thheller 2025-05-20T07:52:24.161209Z

leave it to the JS people to always come up with new and interesting ways to do things ...

thheller 2025-05-20T08:03:05.659719Z

"./runtime": {
      "import": [
        "./dist/esm/runtime.js",
        "./src/runtime.ts"
      ],
      "require": [
        "./dist/cjs/runtime.js",
        "./src/runtime.ts"
      ]
    },

thheller 2025-05-20T08:04:22.189809Z

the @atlaskit/spinner lib has these exports. first time I see a package with this structure and shadow-cljs gets confused by it. why the fuck is there a .ts file referenced ...

thheller 2025-05-20T08:04:49.784009Z

so yeah, even if I remove the asset require the lib still won't build because of that. fun times.

mitchelkuijpers 2025-05-20T08:07:24.337729Z

Wtf? I'll check some other packages to see if they do the same

mitchelkuijpers 2025-05-20T08:10:16.125399Z

I don't see this in the spinner lib, I see this:

"main": "dist/cjs/index.js",
	"module": "dist/esm/index.js",
	"module:es2019": "dist/es2019/index.js",

thheller 2025-05-20T08:10:28.995789Z

I added a workaround but its still kinda annoying to always find these weird edge cases

mitchelkuijpers 2025-05-20T08:10:31.238069Z

Is it compiled/react stuff?

thheller 2025-05-20T08:10:49.769549Z

yeah sorry, from the @compiled/react package

mitchelkuijpers 2025-05-20T08:11:25.125219Z

I was also fighting with this:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/mitkuijp/Development/atlas-crm-connect/node_modules/@compiled/react/dist/esm/runtime/index' imported from /Users/mitkuijp/Development/atlas-crm-connect/node_modules/@compiled/react/dist/esm/runtime.js
    at finalizeResolution (node:internal/modules/esm/resolve:283:11)
    at moduleResolve (node:internal/modules/esm/resolve:952:10)
    at defaultResolve (node:internal/modules/esm/resolve:1188:11)
    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:642:12)
    at #cachedDefaultResolve (node:internal/modules/esm/loader:591:25)
    at ModuleLoader.getModuleJobForRequire (node:internal/modules/esm/loader:347:53)
    at new ModuleJobSync (node:internal/modules/esm/module_job:333:34)
    at ModuleLoader.getModuleJobForRequire (node:internal/modules/esm/loader:407:11)
    at new ModuleJobSync (node:internal/modules/esm/module_job:333:34)
    at ModuleLoader.getModuleJobForRequire (node:internal/modules/esm/loader:407:11) {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///Users/mitkuijp/Development/atlas-crm-connect/node_modules/@compiled/react/dist/esm/runtime/index'
}
I had added it to the ignore stuff

mitchelkuijpers 2025-05-20T08:11:55.896359Z

But it also doesn't work out of the box

thheller 2025-05-20T08:12:16.594569Z

good to know that it isn't just shadow-cljs getting confused by this 😛

mitchelkuijpers 2025-05-20T08:15:23.274679Z

Yeah I have no idea what they are doing

mitchelkuijpers 2025-05-20T09:28:00.640409Z

I noticed they also have nasty exports where they check if the dist folder exists and then point to the right folder.

thheller 2025-05-20T09:36:41.560559Z

yeah I suspect they use this setup for development and want to use actual .ts files and stuff. then just publish it as is instead of having a dedicated actual "dist"

mitchelkuijpers 2025-05-20T09:41:34.749449Z

Yeah I guess so too

mitchelkuijpers 2025-05-20T09:42:03.016989Z

I was trying to quickly fix this locally like this:

:resolve {"@compiled/react/runtime" {:target :file :file "node_modules/@compiled/react/dist/esm/runtime.js"}}
But that kinda blows up Would it be possible to cut a release with this fix?

thheller 2025-05-20T09:42:53.352329Z

I was going to do the asset stuff to a bit later and then make a release

thheller 2025-05-20T09:43:01.271549Z

I can make one now if that helps you?

mitchelkuijpers 2025-05-20T09:43:22.537049Z

No worries I'll clone it locally I mainly want to play around with it to see if I can get our build working

mitchelkuijpers 2025-05-20T09:43:23.298469Z

NP

thheller 2025-05-20T09:43:48.419759Z

no need for a full clone. just put the shadow/build/npm.clj file into your classpath

thheller 2025-05-20T09:44:00.311419Z

or just :local/root should be fine too, git directly doesn't work because some java files need compilation (i.e. lein javac)

mitchelkuijpers 2025-05-20T09:45:10.120009Z

Oh I'll just put that file in my classpath then, thank you so much 🙏

thheller 2025-05-20T09:45:55.969129Z

just don't forget to remove it later 😉

mitchelkuijpers 2025-05-20T09:46:02.395369Z

Hehe

thheller 2025-05-20T16:48:32.119929Z

alright so I just pushed 3.1.2 which changes how css files are handled. instead of just failing or throwing out the info about them, it just collects and ignores them quietly by default

thheller 2025-05-20T16:48:58.837329Z

but you can add :build-hooks [(shadow.build.css/extract-hook)] to the build config and that will create a css file per module

thheller 2025-05-20T16:49:39.470669Z

this is basically meant as an example as I don't really know what to best do with these files

thheller 2025-05-20T16:49:56.228269Z

anyone can in theory write a hook and do whatever makes sense

thheller 2025-05-20T16:50:20.661279Z

I'm open to ideas for all this, but this was the simplest examples I could come up with

thheller 2025-05-20T16:51:44.195779Z

its kinda hacky but seems to work (only tested with the @atlaskit/spinner)

thheller 2025-05-20T16:52:18.338159Z

the generated css file isn't loaded or referenced by the generated JS in any way, so something else is responsible for actually including it in the page

thheller 2025-05-20T16:53:06.893299Z

might make sense to just inline the thing into the .js file and just create a <style> tag, but I'm not sure thats a good way to go

mitchelkuijpers 2025-05-20T17:09:03.537599Z

Awesome I think we will just collect all css into one big file and see how that goes

mitchelkuijpers 2025-05-20T18:18:49.464339Z

It seems to work perfectly with :target :browser