Making a public CLI app backwards-compatible

The problem

I have a small app that helps me preprocess some files which are then shared with others via a Cloud Drive provider. The requirements of the project recently changed and we switched from Google to Proton, which carried a small problem - unlike Google, Proton doesn't have a simple API which we could use to upload files (eg. it uses fancier encyption, which needs an implementation on the sender's side).

Proton solves this issue, by providing a CLI tool for uploading files, but my app is running on a Raspberry Pi 2 (which is 32-bit) and the CLI tool supports only 64-bit environments. Another project which looked promising was rclone, which did work on the 32-bit machine, but doesn't support "Shared with me" files, so the solution wasn't viable.

At first this seemed like a dead end. But since the proton-drive-cli tool is open-source, we can "extend" the original code, by adding overrides for the things that aren't supported by our hardware.

How to "extend" the original

All the code that we reference here is located in this public repo.

The main idea

The original code uses Bun for the build, which only ships single-file executables for x64 and arm64. Our main job is to remove/migrate the Bun dependency into something that runs on 32-bit devices - like for example plain old Node. This sacrifices some perfomance, but sadly, you can't have it all 🥲

To achieve backwards compatability for our new "Bunless" code, we just need to locate all the code that referencess things that aren't supported on our target platform. Then we introduce "shims" - overrides inserted over the original code - which implement the same things as the original code, but without the unsupported dependencies.

The "shims" in practice

As mentioned before, shims are just files that remove the code refrences that are not compatible with our environment with files that have mostly the same functionality, but can run in the target environment.

For example in our repo we have the shims/bun-global.ts, meant for overriding the core Bun functionalities. We obviously don't want to port all of Bun, but rather just the classes and functions that are referenced in the original repo's code. Some functions or classes are more important than others though - when dealing for example with uploading files, we want to try our best to follow the original functionality as close as possible (since this is the whole point of the port), while other things (like Sentry support, especially if we don't use it) can be left behind for v2.

So the main idea with the port is to allow us some leniency - it doesn't have to be perfect, it just has to work on our target machine.

When the shims are done, we need to build the tool, to actually use it. The main steps of the build (for the full implementation see build.mjs) are: 1. Pull the original repo's code (via the provided version) 2. Analyse the repo for any unsupported API (so we can take action and implement it) 3. Build the final tool: - inject the shims into the repo's folder - create aliases for the shims to replace references to the original files

```js
esbuild.build({
    entryPoints: [path.join(CLI_DIR, 'src/proton-drive.ts')],
    outfile: path.join(OUT_DIR, 'proton-drive.mjs'),
    target: 'node20',
    format: 'esm',
    inject: [path.join(ROOT, 'shims/bun-global.ts')],
    alias: {
        'node:fs/promises': path.join(ROOT, 'shims/fs-promises.ts'),
        'node:child_process': path.join(ROOT, 'shims/child-process.ts'),
        'bun:sqlite': path.join(ROOT, 'shims/bun-sqlite.ts'),
        '@sentry/bun': path.join(ROOT, 'shims/sentry-bun.ts'),
    },
    external: ['better-sqlite3'],
    ...
});
```

This is basically it - after the build is done, you can use the backwards-compatible version of the CLI tool located in dist/proton-drive.mjs.

For more information I suggest looking at the repo, which explains some of the things in this article in more detail.