I just released shadow-cljs version 3.0.2. the major version bump is due to the breaking change of the closure compiler requiring java version 21+. pretty much the only change is getting the newer closure compiler to work again, nothing else feature wise. please test and report back. this includes the first closure compiler update in over a year, so no idea what might be broken now. needs broader testing. shouldn't require any cljs related changes, so only maybe bumping your java version if too old.
Now that public class fields syntax support has landed in Closure Compiler and private fields are still in development (as mentioned on GitHub they might work on it in Q3 this years but who knows), but the syntax is already parsed by Closure, would it make sense to add a custom compiler pass in shadow that rewrites private fields into prefixed public fields?
I have not checked the state of this at all yet, so no clue what even happens.
but yes I want this to work. ideally just by leaving things as they are without rewriting
dunno what JS tools actually do?
> ideally just by leaving things as they are without rewriting that's an option as well, browser support seems to be quite good https://caniuse.com/mdn-javascript_classes_private_class_fields
> dunno what JS tools actually do? depending on language version settings they will either rewrite or leave it as is
for rewrite, here's what Babel is doing
class Hello {
#yo = "asd"
#pp = "888"
}
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
var _yo = /*#__PURE__*/new WeakMap();
var _pp = /*#__PURE__*/new WeakMap();
class Hello {
constructor() {
_classPrivateFieldInitSpec(this, _yo, "asd");
_classPrivateFieldInitSpec(this, _pp, "888");
}
}meh
given that a private field can only be accessed in the context of that class definition that should be easier to just do
class Hello {
__some_prefix__yo = "asd";
__some_prefix__pp = "888";
}and then just replace all uses of #yo in that class context with __some_prefix__yo where __some_prefix is something unique, maybe based on filename
I don't care about enforcing private field semantics, like checking redeclaration. assuming the code we get is already "correct"
but likely I will just bump the default :output-feature-set to something very recent
really doesn't make sense anymore to be targeting 5 year old browsers by default
there should be fallback if needed, but not used by default
not sure if expected but :output-feature-set :es-unsupported and :language-in/ou :unsupported didn't work
:language-out/in you never need
:language-in :ecmascript-next-in is the default. doesn't go any higher than that
what does "didn't work" mean? some closure error or what is happening?
> what does "didn't work" mean? some closure error or what is happening?
compiler still spits out the same error This language feature is not currently supported by the compiler: Private class properties.
I wonder if it's an error though shadow prints this
[:examples] Compiling ...
Closure compilation failed with 2 errors
--- uix/hello.js:3
This language feature is not currently supported by the compiler: Private class properties.:output-feature-set :no-transpile?
but in closure code it's a warning
static String languageFeatureWarningMessage(Feature feature) {
LanguageMode forFeature = LanguageMode.minimumRequiredFor(feature);
if (forFeature == LanguageMode.UNSUPPORTED) {
return "This language feature is not currently supported by the compiler: " + feature;
} else {
return "This language feature is only supported for "
+ LanguageMode.minimumRequiredFor(feature)
+ " mode or better: "
+ feature;
}
}
void maybeWarnForFeature(ParseTree node, Feature feature) {
features = features.with(feature);
if (withinClosureUnawareCodeRange(node.location.start.line, node.location.start.column)) {
return;
}
if (!isSupportedForInputLanguageMode(feature)) {
errorReporter.warning(
languageFeatureWarningMessage(feature), sourceName, lineno(node), charno(node));
}
}or was that :language-out :no-transpile? can't remember
note a check for LanguageMode.UNSUPPORTED above
from what context is that warning snippet? maybe just a compiler pass than can be disabled?
seems like it's parser https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/parsing/IRFactory.java
that doesn't seem relevant? we do not want to create IR, only use existing one?
or does it fail because of the initial parse? that means its still not parseable, so nothing shadow-cljs can do?
that's the only place where the error message appears in the codebase also https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/parsing/parser/FeatureSet.java#L270-L272
// ES_UNSUPPORTED: Features that we can parse, but not yet supported in all checks
// Part of ES2022. Support will improve as implementation progresses.
PRIVATE_CLASS_PROPERTIES("Private class properties", LangVersion.ES_UNSUPPORTED),ha and there's already a placeholder class for rewriting private props https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/RewritePrivateClassProperties.java
oh I see, so the PRIVATE_CLASS_PROPERTIES feature set is a part of ES_UNSUPPORTED language mode which is a part of UNSUPPORTED language mode, but UNSUPPORTED lang mode is not present in shadow's config
this lang version stuff is confusing. IIRC :output-feature-set is the relevant part. lang version being a subset of that
but regardless. I just tested :language-out :unsupported after adding that. same error.
:output-feature-set :es-unsupported
:language-out :unsupportedshouldn't go any higher than this right?
should be :language-in
since the check is called isSupportedForInputLanguageMode
set that to :unsupported to, no change
it did work for me
js input
export class Hello {
static kuku() { return 11111; }
#name = 'Roman'
sayHi() { console.log(this.#name, Hello.kuku()); }
}
advanced compiled output
class b{g="Roman";sayHi(){console.log(this.g,11111)}}a={};a.h=b;(new a.h).sayHi();seems like the private field #name was rewritten into a public field
I just patched shadow's config fn
(alter-var-root #'shadow.build.closure/lang-key->lang-mode
(fn [f]
(fn [key] CompilerOptions$LanguageMode/UNSUPPORTED)))https://github.com/thheller/shadow-cljs/commit/eb7613c0368d41dbc173852f69d14ac73876caff
ah nvm ..
I adjusted the setting in the npm code path, using non npm code to test 😛
ah, that's because npm stuff is processed with a diff set of settings?
I adjusted things here directly, so not from config
but that is the npm path, so pointless to do there 😛
well, regardless. it switching this to just public fields doesn't seem safe
just FYI, I had to rm -r .shadow-cljs/ when updating, to fix these errors:
The required namespace "goog" is not available, it was required by "shadow/test/env.cljs"
...
The required namespace "goog" is not available, it was required by "shadow/cljs/devtools/client/console.cljs"
weird. all caches invalidate themselves on version change, so not sure why wiping the caches manually would do anything
idk, I was using different versions for clojurescript and google closure (like you recommend not to...) and so I also had to switch to just let shadow-cljs provide them maybe something leftover
I'm getting a stack trace after updating my dep to 3.0.2 -- using openjdk 21.0.5. I have no need to upgrade, but just reporting back per the request in initial post -- see below. If you want more info I can raise an issue on github:
❯ java --version
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+1-nixos)
OpenJDK 64-Bit Server VM (build 21.0.5+1-nixos, mixed mode, sharing)
> shadow-cljs -A:dev watch example test portfolio --config-merge ./env-local.edn --config-merge ./ui-themes.edn
shadow-cljs - config: /home/cormacc/nmd/products/connect/portal/shadow-cljs.edn
shadow-cljs - starting via "clojure"
/*! 🌼 daisyUI 5.0.28 */
≈ tailwindcss v4.1.4
Done in 311ms
Downloading: thheller/shadow-cljs/3.0.2/shadow-cljs-3.0.2.pom from clojars
<.... more jar downloads ....>
Exception in thread "main" Syntax error macroexpanding at (shadow/build.clj:1:1).
at clojure.lang.Compiler.load(Compiler.java:8177)
at clojure.lang.RT.loadResourceScript(RT.java:401)
at clojure.lang.RT.loadResourceScript(RT.java:392)
at clojure.lang.RT.load(RT.java:479)
at clojure.lang.RT.load(RT.java:444)
at clojure.core$load$fn__6931.invoke(core.clj:6189)
at clojure.core$load.invokeStatic(core.clj:6188)
at clojure.core$load.doInvoke(core.clj:6172)
at clojure.lang.RestFn.invoke(RestFn.java:411)
at clojure.core$load_one.invokeStatic(core.clj:5961)
at clojure.core$load_one.invoke(core.clj:5956)
at clojure.core$load_lib$fn__6873.invoke(core.clj:6003)
at clojure.core$load_lib.invokeStatic(core.clj:6002)
at clojure.core$load_lib.doInvoke(core.clj:5981)
at clojure.lang.RestFn.applyTo(RestFn.java:145)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$load_libs.invokeStatic(core.clj:6044)
at clojure.core$load_libs.doInvoke(core.clj:6028)
at clojure.lang.RestFn.applyTo(RestFn.java:140)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$require.invokeStatic(core.clj:6066)
at clojure.core$require.doInvoke(core.clj:6066)
at clojure.lang.RestFn.invoke(RestFn.java:3662)
at shadow.cljs.devtools.api$eval142$loading__6812__auto____143.invoke(api.clj:1)
at shadow.cljs.devtools.api$eval142.invokeStatic(api.clj:1)
at shadow.cljs.devtools.api$eval142.invoke(api.clj:1)
at clojure.lang.Compiler.eval(Compiler.java:7700)
at clojure.lang.Compiler.eval(Compiler.java:7689)
at clojure.lang.Compiler.load(Compiler.java:8165)
at clojure.lang.RT.loadResourceScript(RT.java:401)
at clojure.lang.RT.loadResourceScript(RT.java:392)
at clojure.lang.RT.load(RT.java:479)
at clojure.lang.RT.load(RT.java:444)
at clojure.core$load$fn__6931.invoke(core.clj:6189)
at clojure.core$load.invokeStatic(core.clj:6188)
at clojure.core$load.doInvoke(core.clj:6172)
at clojure.lang.RestFn.invoke(RestFn.java:411)
at clojure.core$load_one.invokeStatic(core.clj:5961)
at clojure.core$load_one.invoke(core.clj:5956)
at clojure.core$load_lib$fn__6873.invoke(core.clj:6003)
at clojure.core$load_lib.invokeStatic(core.clj:6002)
at clojure.core$load_lib.doInvoke(core.clj:5981)
at clojure.lang.RestFn.applyTo(RestFn.java:145)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$load_libs.invokeStatic(core.clj:6044)
at clojure.core$load_libs.doInvoke(core.clj:6028)
at clojure.lang.RestFn.applyTo(RestFn.java:140)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$require.invokeStatic(core.clj:6066)
at clojure.core$require.doInvoke(core.clj:6066)
at clojure.lang.RestFn.invoke(RestFn.java:424)
at user$eval136$loading__6812__auto____137.invoke(user.clj:2)
at user$eval136.invokeStatic(user.clj:2)
at user$eval136.invoke(user.clj:2)
at clojure.lang.Compiler.eval(Compiler.java:7700)
at clojure.lang.Compiler.eval(Compiler.java:7689)
at clojure.lang.Compiler.load(Compiler.java:8165)
at clojure.lang.RT.loadResourceScript(RT.java:401)
at clojure.lang.RT.loadResourceScript(RT.java:388)
at clojure.lang.RT.maybeLoadResourceScript(RT.java:384)
at clojure.lang.RT.doInit(RT.java:506)
at clojure.lang.RT.init(RT.java:487)
at clojure.main.main(main.java:38)
Caused by: java.lang.ExceptionInInitializerError
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:534)
at java.base/java.lang.Class.forName(Class.java:513)
at clojure.lang.RT.classForName(RT.java:2229)
at clojure.lang.RT.classForName(RT.java:2238)
at clojure.lang.RT.loadClassForName(RT.java:2257)
at clojure.lang.RT.load(RT.java:469)
at clojure.lang.RT.load(RT.java:444)
at clojure.core$load$fn__6931.invoke(core.clj:6189)
at clojure.core$load.invokeStatic(core.clj:6188)
at clojure.core$load.doInvoke(core.clj:6172)
at clojure.lang.RestFn.invoke(RestFn.java:411)
at clojure.core$load_one.invokeStatic(core.clj:5961)
at clojure.core$load_one.invoke(core.clj:5956)
at clojure.core$load_lib$fn__6873.invoke(core.clj:6003)
at clojure.core$load_lib.invokeStatic(core.clj:6002)
at clojure.core$load_lib.doInvoke(core.clj:5981)
at clojure.lang.RestFn.applyTo(RestFn.java:145)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$load_libs.invokeStatic(core.clj:6044)
at clojure.core$load_libs.doInvoke(core.clj:6028)
at clojure.lang.RestFn.applyTo(RestFn.java:140)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$require.invokeStatic(core.clj:6066)
at clojure.core$require.doInvoke(core.clj:6066)
at clojure.lang.RestFn.invoke(RestFn.java:424)
at cljs.env$loading__6706__auto____754.invoke(env.cljc:9)
at cljs.env__init.load(Unknown Source)
at cljs.env__init.<clinit>(Unknown Source)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:534)
at java.base/java.lang.Class.forName(Class.java:513)
at clojure.lang.RT.classForName(RT.java:2229)
at clojure.lang.RT.classForName(RT.java:2238)
at clojure.lang.RT.loadClassForName(RT.java:2257)
at clojure.lang.RT.load(RT.java:469)
at clojure.lang.RT.load(RT.java:444)
at clojure.core$load$fn__6931.invoke(core.clj:6189)
at clojure.core$load.invokeStatic(core.clj:6188)
at clojure.core$load.doInvoke(core.clj:6172)
at clojure.lang.RestFn.invoke(RestFn.java:411)
at clojure.core$load_one.invokeStatic(core.clj:5961)
at clojure.core$load_one.invoke(core.clj:5956)
at clojure.core$load_lib$fn__6873.invoke(core.clj:6003)
at clojure.core$load_lib.invokeStatic(core.clj:6002)
at clojure.core$load_lib.doInvoke(core.clj:5981)
at clojure.lang.RestFn.applyTo(RestFn.java:145)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$load_libs.invokeStatic(core.clj:6044)
at clojure.core$load_libs.doInvoke(core.clj:6028)
at clojure.lang.RestFn.applyTo(RestFn.java:140)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$require.invokeStatic(core.clj:6066)
at clojure.core$require.doInvoke(core.clj:6066)
at clojure.lang.RestFn.invoke(RestFn.java:1526)
at cljs.analyzer$loading__6706__auto____645.invoke(analyzer.cljc:9)
at cljs.analyzer__init.load(Unknown Source)
at cljs.analyzer__init.<clinit>(Unknown Source)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:534)
at java.base/java.lang.Class.forName(Class.java:513)
at clojure.lang.RT.classForName(RT.java:2229)
at clojure.lang.RT.classForName(RT.java:2238)
at clojure.lang.RT.loadClassForName(RT.java:2257)
at clojure.lang.RT.load(RT.java:469)
at clojure.lang.RT.load(RT.java:444)
at clojure.core$load$fn__6931.invoke(core.clj:6189)
at clojure.core$load.invokeStatic(core.clj:6188)
at clojure.core$load.doInvoke(core.clj:6172)
at clojure.lang.RestFn.invoke(RestFn.java:411)
at clojure.core$load_one.invokeStatic(core.clj:5961)
at clojure.core$load_one.invoke(core.clj:5956)
at clojure.core$load_lib$fn__6873.invoke(core.clj:6003)
at clojure.core$load_lib.invokeStatic(core.clj:6002)
at clojure.core$load_lib.doInvoke(core.clj:5981)
at clojure.lang.RestFn.applyTo(RestFn.java:145)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$load_libs.invokeStatic(core.clj:6044)
at clojure.core$load_libs.doInvoke(core.clj:6028)
at clojure.lang.RestFn.applyTo(RestFn.java:140)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$require.invokeStatic(core.clj:6066)
at clojure.core$require.doInvoke(core.clj:6066)
at clojure.lang.RestFn.invoke(RestFn.java:3662)
at shadow.build$eval9551$loading__6812__auto____9552.invoke(build.clj:1)
at shadow.build$eval9551.invokeStatic(build.clj:1)
at shadow.build$eval9551.invoke(build.clj:1)
at clojure.lang.Compiler.eval(Compiler.java:7700)
at clojure.lang.Compiler.eval(Compiler.java:7689)
at clojure.lang.Compiler.load(Compiler.java:8165)
... 62 more
Caused by: java.lang.ClassNotFoundException: com.google.javascript.jscomp.JsAst
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:534)
at java.base/java.lang.Class.forName(Class.java:513)
at clojure.lang.RT.classForName(RT.java:2229)
at clojure.lang.RT.classForNameNonLoading(RT.java:2242)
at cljs.externs$loading__6706__auto____1358.invoke(externs.clj:9)
at cljs.externs__init.load(Unknown Source)
at cljs.externs__init.<clinit>(Unknown Source)
... 152 morethats a dependency conflict. still using the older closure compiler version.
D'oh 🙂 Thank you
its fun right? closure people removing/renaming classes for no reason 😛
@thheller re private fields, I was previously running tests against JS required into cljs from a local file which puts it through advanced optimisations, and that's were private fields were rewritten into public fields Just tried 3.0.2 with NPM library that uses private fields, and release build outputs NPM libs with private syntax unchanged
presumably because npm stuff is bundled with :simple optimisations in shadow
oh nice. I'll check it out over the weekend
it wasn’t super clear from the thread - but given I’m ok with translating a private field to public (it’s just a UI library, nothing particularly private) - should shadow-cljs 3.1.7 and closure compiler 20250407 be able to consume js that uses private fields? I’ve set the compiler options as so:
:compiler-options
{:language-in :unsupported
:output-feature-set :es-unsupported}
but still seeing the following errors:
--- node_modules/@radix-ui/react-collection/dist/index.js:223
This language feature is not currently supported by the compiler: Private class properties.
@dazld you need :js-options, that's for npm stuff
:js-options {:output-feature-set :es-unsupported
:language-in :unsupported}[:frontend] Compiling ...
[:frontend] Build completed. (260 files, 162 compiled, 0 warnings, 8.94s)
thanks a ton @roman01la 🙌I wonder if I should just make this the default? "downgrading" hasn't seemed necessary for a while since browsers are pretty fast at supporting new stuff
I think your instinct earlier about not making private fields magically public was right
the default of es2020 is rather conservative
in this case it’s ok (just some API aesthetics) but in other stituations, that would be a problem (eg api keys)
what do api keys have to do with this?
ie, if there was a private field with an api key, which suddenly became public
that isn't relevant at all. private is just a "human" helper to structure code. it isn't actually "hiding" anything
ah, but in plain JS - it is actually private, right?
depends on what you mean by actually private
as in able to protect secrets then no, absolutely not. its just plain text in the source code, anyone can see it
class { #key = "literal" } I mean anyone can read this, there is nothing private about it
imho its a nonsense computer language thing. telling users of certain classes "you are not supposed to use this, its internal". I mean the convention is of course good. modifying the language to get it is the nonsense part
at runtime I'm not actually sure if you can get it. seems beside the point if anyone can just look at the source 😛
private fields are only accessible to the class - so it’s not programatically accessible outside. In theory, we all have strict CSP disabling arbitrary scripts, right? so it’s not a problem there.. but in reality.. not everyone is doing that.
yeah dunno. I guess if you have a private field and pass an instance of that class to some third party thing then it does protect it sort of
but who'd run code that tries to steal secrets in the first place
otherwise I can just open the sources panel in the devtools and that lets me find any secret I want
automated script injection attacks, looking for globals, recurse down..
again ... why would anything do that at runtime? just download the damn .js file 😛
heh
JWTs are a fun example - many people use third party auth, which does stuff in an iframe.
your bearer token may live on a private field
again .. if the thing tries to steal your secrets you probably shouldn't rely on private fields, you should not use it 😛
JWTs are also commonly cookies, so trivial to read for anyone
not always, and cookies aren’t always accessible from JS
but think we’re talking past each other - I’m thinking very specifically about keeping code “safe by default”
ie, I’m not intentionally hammering on private fields as the app dev, but making sure that an accidental backdoor isn’t exposed
for bad actors
I think there is no "safe by default" when talking web stuff. in any way ever. all the stupid stuff we invented still doesn't prevent shit if you ask me
if you are doing stuff that needs to be safe then there should be no third party scripts ever
there are no absolutes in security
heck it probably shouldn't even enter the client/browser
there are only a huge range of stuff you do to try and keep as much as possible away
if someone is determined to get in, they will
I don’t leave my keys in my car when I go to the shops, but if someone really wants to steal it, they will.. stuff like this
I think most of the stuff gives a false sense of security and since its all so complicated may make you think its actually safe when it never is
so best to assume nothing is safe and act accordingly and forget about the other stuff
that’s also very true. TS private fields for example, iirc, were also public. not sure if they fixed that.
but oh well third party scripts are so common that this is all a fantasy to begin with 😛
I wonder if there were CVEs that were raised on this.. let me take a look edit there’s a couple about prototype pollution, but seems this is mostly app level security problems, so wouldn’t be a CVE..
however - here’s a better example. if you iterate over the object, then the previously private field would then pop up in the iteration.
> class Foo { bla="abc"; #key="sekrit"; bar() {return this.#key}}
> f = new Foo
< Foo {#key: "sekrit", bla: "abc"}
> for (let v in f) { console.log(v, f[v]) }
[Log] bla – "abc"
< undefined
see how key doesn’t appear. I wouldn’t write code like this that blindly literates, but some people do.When using :esm is there a way to pass through a css import? e.g., import "./ts_page.css"; or in our case (:require ["dustingetz/root.css"] - which compiles to import "./shadow.esm.esm_import$dustingetz$root_css.js"; (unsurprisingly)
incorrect. it compiles down to import * from "dustingetz/root.css";
the shim is just a JS file doing the actual import
ooo
good work
i had no idea
for :import any string require is just passed through. shadow doesn't attempt to locate or parse anything.