Fork me on GitHub
#clojurescript
<
2024-08-07
>
Vladimir Pouzanov17:08:59

What is the best strategy to extract part of an application into a library? Let's say I have an ApplicationA (shadow-cljs + node). I extracted some functionality of it into a separate namespace and I want to use it in ApplicationB. Do I bundle it into a npm package? a clojar? put it into a separate directory and add it to the search path? Basically my ideal scenario is that I can develop AppA and AppB that both depend on a shared LibC. It's a requirement that AppA and AppB are different code bases in different git repos. When I develop AppB, I still want to be able to quickly modify LibC code (in cases when e.g. there are still leftovers of AppA that I didn't refactor out proper).

p-himik17:08:58

I'd probably use a git subtree or submodule.

wevrem19:08:38

What I do 1. My shared cljs library, what you are calling “LibC”, is published to git and available in my other projects as a git dep, something like this: io.github.wevre/libc {:git/tag "v0.0.12" :git/sha "1234567"}. I have a build.clj that I can call with tools.build to automate tagging and pushing and updating README, etc. Of course, this is a private repo, only available to me, so whether or not I push updates to git is totally up to me. 2. In my project where I consume it, what you call “LibA”, I have these entries in deps.edn:

{...
 :aliases {:shadow {:extra-deps
                    ,, {...
                        io.github.wevre/libc {:git/tag "v0.0.12" :git/sha "1234567"}}}
           :dev {:override-deps {...
                                 io.github.wevre/libc {:local/root "..path/to/libc"}}}}}
I’m using shadow’s feature to defer to deps.edn, so in shadow-cljs.edn I have
{:deps {:aliases [:shadow :dev]}
 ...}
This gives me an “official” dependency to the git-published version of my shared library, but during development, I can make local changes to the shared library and those are picked up immediately by shadow without me needing to go through the whole build process.

Vladimir Pouzanov19:08:10

@UTFAPNRPT this sounds perfect and very close to what I'd want. Can you clarify, what's the expected file structure inside ..path/to/libc in this case? (I'm not too familiar with clj[s] file layouts) Basically, how do I structure files in there so that deps.edn can pick it up?

p-himik19:08:44

That's a reasonable approach. However, with git subtree you don't have to go through that many hoops. And you can contribute to LibC from both AppA and AppB - it becomes a regular git push with some extra args.

p-himik19:08:33

Correction - the more modern approach is called not "git subtree" but "git subrepo": https://github.com/ingydotnet/git-subrepo And only now I realized that it's by @U05H8N9V0HZ so maybe he can chime in as well.

Vladimir Pouzanov19:08:16

the reason why I wouldn't want to have submodule/subrepo setup is because I expect the shared code to stay dirty for a bit and I'm not a huge fan of git pushing "bump" commits up to then immediately pull them down in the repo next door. I'd rather share the files on the disk and create semantically meaningful commits when both AppA and AppB are in a stable spot

p-himik19:08:45

> shared code to stay dirty for a bit IIRC that's totally fine with the subrepo approach.

Ingy döt Net19:08:46

@U06KQ5FFNP2 I'm deep in other things atm but feel free to ping me if you need any specific clarifications for subrepo. Honestly I haven't touched it in years but its pretty solid and has an active maintainer. The docs and help system were good iirc.

Ingy döt Net19:08:00

Oh, just looking at your first sentence. > What is the best strategy to extract part of an application into a library? One of my favorite parts of subrepo (and not used much I think) is git subrepo init.

$ git subrepo help init

  Usage: git subrepo init  [-r ] [-b ] [--method ]


  Turn an existing subdirectory into a subrepo.

  If you want to expose a subdirectory of your project as a published subrepo,
  this command will do that. It will split out the content of a normal
  subdirectory into a branch and start tracking it as a subrepo. Afterwards
  your original repo will look exactly the same except that there will be a
  `/.gitrepo` file.

  If you specify the `--remote` (and optionally the `--branch`) option, the
  values will be added to the `/.gitrepo` file. The `--remote` option
  is the upstream URL, and the `--branch` option is the upstream branch to push
  to. These values will be needed to do a `git subrepo push` command, but they
  can be provided later on the `push` command (and saved to `/.gitrepo`
  if you also specify the `--update` option).

  Note: You will need to create the empty upstream repo and push to it on your
  own, using `git subrepo push `.

  The `--method` option will decide how the join process between branches
  are performed. The default option is merge.

  The `init` command accepts the `--branch=` and `--remote=` options.
If you really want to extract it and keep its history, give it a try. I guess it kind of assumes your "part" to extract is a subdirectory.

👍 1
Ingy döt Net19:08:56

You could continue using that subdiir as a subrepo. Or just turn the new branch into a new repository, publish that on its own, and depend on it from the original (and other places). IOW, use subrepo once to split, then never need subrepo again.

wevrem20:08:58

I don’t have any experience with subrepos. I’ll have to check them out. But in my case my LibC was already a project on its own before LibA came along. I just wanted the convenience of a local dep so I could benefit from hot reload. @U06KQ5FFNP2 the contents of ..path/to/libc is a full-blown, standalone project with its own deps.edn and tests and everything.

Vladimir Pouzanov20:08:12

gotcha, thanks. I suppose, I was trying to find an answer that was missing from my original question now that I researched this more (namely, how do I move npm deps into LibC). Using submodules doesn't quite cut it because I'll have to keep both projects' packages.json in sync then when LibC's deps change, which seems suboptimal. It seems that shadow-cljs expects me to fully embrace npm for that.

p-himik20:08:54

> Using submodules doesn't quite cut it because I'll have to keep both projects' packages.json in sync then when LibC's deps change Not really - you can add a local dep to package.json.

Vladimir Pouzanov20:08:22

> you can add a local dep to package.json. that'd be package.json in AppA and a separate in AppB though, no? Meaning if e.g. LibC requires protobufjs, I'll have to add it to both apps and keep its version reasonably in sync even though neither AppA nor AppB use protobufjs directly.

p-himik21:08:35

> that'd be package.json in AppA and a separate in AppB though, no? Of course. > Meaning if e.g. LibC requires protobufjs, I'll have to add it to both apps and keep its version reasonably in sync even though neither AppA nor AppB use protobufjs directly. No. What I meant is that package.json allows specifying local deps in a way similar to :local/root in deps.edn. It also automatically resolves transitive dependencies.

p-himik21:08:35

A small demonstration:

$ cat package.json
{
  "dependencies": {
    "a": "file:a"
  }
}

$ cat a/package.json
{
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

$ ls node_modules
a lodash

Vladimir Pouzanov09:08:35

> It also automatically resolves transitive dependencies. Right. So to make it work, I'll have to add deps.edn to LibC (so that cljs can resolve it as a dependency), and a packages.json to LibC (with its npm deps) and then in my apps I'll add the [local] dep in both deps.edn (again, for the clojure deps) and a local npm module so that the transitive deps are fetched. Sounds about right?

p-himik09:08:47

Overall, yes. There is an option of not having to use deps.edn at all if LibC has no dependencies that have to be managed by Clojure, but I wouldn't use it.

👍 1
Vladimir Pouzanov09:08:42

thanks! I really appreciate all the advice.

👍 1
Vladimir Pouzanov10:08:23

I've made an experiment and it seems that if I stick to shadow-cljs, then I can do the following: AppA's deps.edn: com.example/libc {:local/root "../libc"} LibC's deps.edn: {:paths ["src"]} LibC's src/deps.cljs: {:npm-deps {"cli-color" "2.0.4"}} and that allows me to build the appa along with pulling the transitive npm deps without having to deal with two dependency mechanisms in parallel (also, override the local builds with the deps.edn aliases). Not sure how fragile that is yet, but it fits my use case great. I can open the AppA project in vscode, edit it, open the LibC files and edit them and have them all available in the repl.

p-himik10:08:44

Ah, yes. I keep forgetting about :npm-deps because I don't use it myself. There's an obvious downside to that option - you can't use npm with those deps. But it might be a trade-off worth having, dunno.

Vladimir Pouzanov10:08:58

yeah, in my case it sounds like an upside, because if I add an AppD, I then only need to refer the LibC in one place, being AppD's deps.edn, and npm side of the deps is automatically sorted out for me