Dependency resolution and installation (provision) of mixed CLJS (maven) and JavaScript (npm) projects.
Lets consider a relatively simplified example in the following diagram:
+--------------+
|[email protected] +<--------------------------+
|from npm | |
+-------+------+ |
^ |
| |depends on via package.json peerDependencies
|depends on via deps.cljs |
| |
| |
+-------+-------+ +-------+------------+
|[email protected] | |[email protected] |
|from maven | |from npm |
| | | |
+-------+-------+ +--------+-----------+
^ ^
| |
|depends on via project.clj |depends on via deps.cljs
| |
| |
| +--------------+ |
| |My Application| |
+--------+ +-----------+
| |
+--------------+
- 'My Application' declares two dependencies:
[email protected]
via its ownmy-app/project.clj/:dependencies
[email protected]
via its ownmy-app/src/deps.cljs/:npm-deps
which is equivalent tomy-app/package.json/:dependencies
[email protected]
declares many dependencies, but for our example only one we will mention:[email protected]
via its ownreagent/src/deps.cljs
which is found on the class path byshadow-cljs
at build time (notlein-shadow
, or other tools) and injected vianpm install --save [email protected]
intomy-app/package.json
[email protected]
declares many dependencies, but for our example only one we will mention:react@^16.8
via its ownframer-motion/package.json/:peerDependencies
Some important things to take away from this diagram include:
- To successfully build 'My Application' there are TWO independent, yet merged into one graph, dependency systems. Maven and NPM.
- A dependency can be required via multiple paths that need to be satisfied via both dependency systems; e.g.
[email protected]
vsreact@^16.8
- Transitive NPM dependencies can be via NPM
package.json
files (NPM dependencies of NPM dependencies), or viashadow-cljs
deps.cljs
files (NPM dependencies of CLJS dependencies).
- 'Vanilla
shadow-cljs
'. Justshadow-cljs
andnpm
(asnpm
, oryarn
which we don't consider in this discussion, is a required dependency ofshadow-cljs
). shadow-cljs
,npm
andlein
BUT NOTlein-shadow
; probably via a lot of lein alaises andlein-shell
out to theshadow-cljs
CLI.shadow-cljs
,npm
,lein
andlein-shadow
; usesshadow-cljs
as a JAR library, not theshadow-cljs
CLI.- Option 2. or 3. with the addition of a
package-lock.json
file
What dependency resolution use cases are actually different between these stacks ? Turns out, not a lot. Only Use Case 2. has any variation.
Only one option. Maven dependency in project.clj/:dependencies
(or deps.edn
, or shadow-cljs.edn
).
There are two options, which are very similar:
- NPM dependency in
package.json/:dependencies
, managed directly withnpm
andshadow-cljs
('Vanillashadow-cljs
') - NPM dependency in
src/deps.cljs/:npm-deps
, managed vialein-shadow
which will write it out topackage.json/:dependencies
.
Technically, if we include tooling other than shadow-cljs
there is the option of repackaged JavaScript in Maven dependencies via CLJSJS but we rule that out of the discussion as it has too many disadvantages.
Only one option. NPM dependency in deps.cljs
in the CLJS dependency project, handled by shadow-cljs
and npm
. 'Vanilla shadow-cljs
' searches for deps.cljs
files on the classpath, does conflict resolution with the current project's package.json
then if it is required executes npm install dep@ver --save...
.
Only one option. Maven dependency in project.clj
in the CLJS dependency project, handled by lein
.
Only one option. NPM dependency in package.json
in the JS dependency project, handled by npm
.
Feature | 1. shadow-cljs & npm |
2. lein-shadow & npm |
3. lein-shadow & npm & package-lock.json |
---|---|---|---|
A. NPM interactions | ❌ Manual | ✔️ Automated | ✔️ Automated |
B. NPM integration | ✔️ Simple | ❌ Complex | ❌ Complex |
C. NPM peerDependencies | ❌ Manual 1 2 | ✔️ Semi-Automated | ✔️ Semi-Automated |
D. NPM locked deps | ✔️ | ❌ | ✔️ |
E. package-lock.json in Git |
❌ Manual Commits | ✔️ Nothing to Commit | ❌ Manual Commits |
F. Git tag-based versioning | ❓ TBA | ✔️ lein-git-inject |
✔️ lein-git-inject |
G. Dynamic shadow-cljs.edn config | ✔️ | ✔️ |
NPM interactions include:
npm install
ornpm ci
; i.e. creating anode_modules
folder frompackage.json
andpackage-lock.json
npm install name@ver --save{,-dev}
; i.e. adding a dependency
NPM integration points include:
package.json
andpackage-lock.json
filesnpm
cli toolshadow-cljs
which, in any of the options, readspackage.json
to compare it withdeps.cljs
files on the classpath (transitive npm dependencies, e.g.react
fromreagent
) then runsnpm install react@ver --save
.lein-shadow
which, if used, readspackage.json
and runsnpm install
and/ornpm install dep@ver --save{,-dev}
.
NPM 5 removed support for automatic installation of 'peerDependencies'. This includes react
in
some scenarios.
i.e. 'package-lock.json
'
Solves the problem that we can specify exact versions for our own dependencies, but dependencies of our dependencies (transitive dependencies) may be pulled in via version ranges that auto-upgrade without our knowledge or intent.
If we track package-lock.json
in Git then we need to regularly commit the changes manually at times
that it should be changed such as adding or deliberately upgrading a dependency. This requires
developers to:
- recognise correctly when
package-lock.json
should be changed, and when it should not. - commit and push those changes.
- avoid merge conflicts of
package-lock.json
such as by not maintain multiple branches ofpackage-lock.json
changes.
Accurate versioning is critical to operational and technical support use cases; e.g. simply being able to answer the question, what code built this misbehaving app in production?
To reduce errors since the Figwheel days we have programatically generated parts of the compiler
configuration in project.clj
; e.g. dir/file paths.
XXX
Where we use npm install
to mean 'install all the things in package.json
', we would probably be
better served by npm ci
which has some key differences.
Currently peer dependencies work because shadow-cljs.edn
and/or lein-shadow
run multiple
npm install dep@ver --save{,-dev}
commands when a package.json
file does NOT exist. In this case,
the dependency (e.g. react
) is always installed.
If a package.json
file already exists, such as the case needs to be when using package-lock.json
then in some cases peer dependencies are not installed with npm install
because of it being removed
in NPM 5.