Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using simulacrum from a compiled binary #72

Open
jbolda opened this issue Jun 2, 2021 · 10 comments
Open

using simulacrum from a compiled binary #72

jbolda opened this issue Jun 2, 2021 · 10 comments

Comments

@jbolda
Copy link
Contributor

jbolda commented Jun 2, 2021

Use Case

We are investigating building simulacrum as a binary. I believe this serves two primary purposes. The first is using without specific dev tools installed (or more specifically without nodejs). This could be used in the context of QA running a test, and starting a server for the tests to run against. The second use case would be to run the server without installing it as part of your nodejs app dev dependencies. It could reduce compile time or alleviate issues with transitive dependencies (although likely quite a rare use case).

Options

The are two primary paths that we can take in creating these. One option is a strictly terminal-based binary that is invoked and runs a server. One may interact with the server to possibly adjust the server state or invoke it with options. It may be more complicated to orchestrate multiple services though with this method. The other option is some sort of GUI and/or taskbar-based app that could act much in the vein of docker desktop where we can start or run multiple services.

In all of these options, Apple is becoming more strict about these being signed which can complicate a workflow. Also, if we are compiling nodejs, it is more likely that we will need to set up a matrix build system to compile these binaries (or use a specific version of node that has a precompiled version available).

Binary

Creating a single binary appears to be the easiest start. The history of javascript means that there are more options for compiling javascript into something that can run in a browser, rollup (and any bundler) or alternatives like ncc come to mind that could possibly bundle our code including node_modules. We want to invoke the binary without nodejs available though which then limits our options. nexe and pkg appear to be the only options remotely capable of this.

I was able to get nexe working both from a precompiled version of nodejs (it downloads it if available and build: false is set). I was also able to build from my system version after following the nodejs steps for building to get all of the prereqs. If we are building from a precompiled version (the latest v14 is 14.15.3), it can handle cross compiling (using one OS to build everything), but it will need to be run on a Mac for the mac app signature. I installed nexe at the root and ran this script from the root.

// eslint-disable-next-line
const { compile } = require("nexe");

compile({
  input: "./packages/server/dist/start.js",
  build: true, //required to use patches
}).then(() => {
  console.log("success");
});
simulacrum on v0 [!?] is 📦 v0.0.0 via v14.17.0
❯ node bundle.js
i nexe 4.0.0-beta.18
√ Already downloaded...
√ Node binary compiled
√ Entry: 'packages\server\dist\start.js' written to: start.exe
√ Finished in 780.499s
success

I did also try pkg, but every time I try it (both here and in past projects), I run into issues with how it does the resolution. It does appear to build correctly, but it throws errors when trying to run it.

❯ nr pkg ./packages/server/dist/start.js -- --targets node16-w
in-x64

> @simulacrum/[email protected] pkg
> pkg "./packages/server/dist/start.js" "--targets" "node16-win-x64"

> [email protected]

❯ ./start.exe
pkg/prelude/bootstrap.js:1614
      throw error;
      ^

Error: Cannot find module 'fp-ts/Option'
Require stack:
- C:\snapshot\simulacrum\node_modules\@effection\atom\dist\atom.cjs.development.js
- C:\snapshot\simulacrum\node_modules\@effection\atom\dist\index.js
- C:\snapshot\simulacrum\packages\server\dist\server.js
- C:\snapshot\simulacrum\packages\server\dist\start.js
1) If you want to compile the package/file into executable, please pay attention to compilation warnings and specify a literal in 'require' call. 2) If you don't want to compile the package/file into executable and want to 'require' it from filesystem (likely plugin), specify an absolute path in 'require' call using process.cwd() or process.execPath.
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:941:15)
    at Function._resolveFilename (pkg/prelude/bootstrap.js:1711:46)
    at Function.Module._load (node:internal/modules/cjs/loader:774:27)
    at Module.require (node:internal/modules/cjs/loader:1013:19)
    at Module.require (pkg/prelude/bootstrap.js:1593:31)
    at require (node:internal/modules/cjs/helpers:93:18)
    at Object.<anonymous> (C:\snapshot\simulacrum\node_modules\@effection\atom\dist\atom.cjs.development.js:3:9)
    at Module._compile (pkg/prelude/bootstrap.js:1686:22)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1138:10)
    at Module.load (node:internal/modules/cjs/loader:989:32) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    'C:\\snapshot\\simulacrum\\node_modules\\@effection\\atom\\dist\\atom.cjs.development.js',
    'C:\\snapshot\\simulacrum\\node_modules\\@effection\\atom\\dist\\index.js',
    'C:\\snapshot\\simulacrum\\packages\\server\\dist\\server.js',
    'C:\\snapshot\\simulacrum\\packages\\server\\dist\\start.js'
  ],
  pkg: true
}

With this and pkg not seeming to be part of vercel's main business strategy, I suspect our best path forward would be using nexe. Also to note, any packages that have a .node or a gyp compiled asset start to cause issues. In this case, I did upgrade effection to a version that has [email protected] to get the n-api based binary. It did not work prior to that with the .node file.

GUI App

The big player is obviously Electron here, but it sounds like Tauri may also be an option. I think the big benefit we may gain is that it might be easier to run multiple services including having multiple versions of a service available (various projects could have various versions). Within Tauri, we could sidecar nodejs and bundle the services "browser style" with the expectation of being invoked by a node process (rather than compiling nodejs with each binary). This may make plugins easier and also reduce the possible issues involved with relying on a resolution algorithm to package node_modules and a virtual filesystem correctly.

Even if we would go this route, there is a case to be made to still bundle all of the binaries, but I suspect an option like rollup could bundle up a package into a single js file with less issues. There is also a switch happening with the support of native es-modules, and I can only imagine that will cause issues with pkg dependence on require.

@cowboyd do you think it is worth investigating and setting up a test for a GUI based version of this out of the gate?

@cowboyd
Copy link
Member

cowboyd commented Jun 2, 2021

This is excellent research @jbolda! It sounds like there is a clear winner in nexe.

@cowboyd do you think it is worth investigating and setting up a test for a GUI based version of this out of the gate?

I don't think that we need to do this yet. It's something we can consider down the road, especially around a given product. I say we get cracking with nexe

@taras
Copy link
Member

taras commented Jun 3, 2021

@jbolda like @cowboyd said, great research!

@jbolda
Copy link
Contributor Author

jbolda commented Jun 3, 2021

It seems that nexe doesn't support ESM yet which I don't think is necessarily an issue at the moment, but as libraries switch over this year then it might become a bigger issue. See nexe/nexe#815.

Through finding that out, I did find out about https://github.com/leafac/caxa and https://github.com/mongodb-js/boxednode. The former seems promising as well and does support ESM, but it newer relative to the others.

GitHub
📦 Package Node.js applications into executable binaries 📦 - leafac/caxa
GitHub
📦 boxednode – Ship a JS file with Node.js in a box - mongodb-js/boxednode

@jbolda
Copy link
Contributor Author

jbolda commented Jun 3, 2021

Looks like it doesn't have a problem using fs so we might be able to, at the very least, hack together some kind of require / plugin system.

test file:
test-file

binary creation:
binary-created

test run on test package.json:
test-run

test run on file in unrelated folder:
test-run2

@cowboyd
Copy link
Member

cowboyd commented Jun 4, 2021

@jbolda What about using System.import() ? or require() from the local file system?

@taras
Copy link
Member

taras commented Jun 4, 2021

image

What do you guys think about using deno compile at this stage? https://www.infoq.com/news/2021/02/deno-compiles-native-binaries/

InfoQ
Deno 1.6 introduced the compilation of Deno projects into standalone executables, whose size Deno 1.7 further reduced (up to 60%). Deno now has a dedicated language server that seeks to improve the experience of Deno developers in code editors. Deno also added support for data URLs, enabling the execution of computer-generated code.

@cowboyd
Copy link
Member

cowboyd commented Jun 4, 2021

This will definitely be a blocker at some point since we'll surely want a plugin system denoland/deno#8655

We could hope that it gets addressed by the time we need it, but the biggest concern I have is that in order to use deno compile, we'd also have to be using deno, which while an exciting prospect also violates the "one vanity technology per project" rule.

@taras
Copy link
Member

taras commented Jun 4, 2021

Maybe we can just keep it in mind.

@jbolda
Copy link
Contributor Author

jbolda commented Jun 5, 2021

@jbolda What about using System.import() ? or require() from the local file system?

Good question. I presume they should work, but it's easy to test so I'll try on Monday to be certain.

@jbolda
Copy link
Contributor Author

jbolda commented Jun 14, 2021

Confirmed that they work.

With this code:

const fs = require("fs/promises");
const path = require("path");

const interact = async (args) => {
  switch (args[2]) {
    case "read":
      return readFile(args);
    case "require":
      return requireFile(args);
    case "import":
      return importFile(args);
    default:
      return readFile(args);
  }
};

const readFile = async (args) => {
  const filePath = path.join(process.cwd(), args[3]);
  const content = await fs.readFile(filePath, "utf-8");
  console.log(content);
};

const requireFile = async (args) => {
  const filePath = path.join(process.cwd(), args[3]);
  const script = require(filePath);
  script();
};

const importFile = async (args) => {
  const filePath = `file://${path.join(process.cwd(), args[3])}`;
  const { script } = await import(filePath);
  script();
};

interact(process.argv);

And running them:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants