carlo icon indicating copy to clipboard operation
carlo copied to clipboard

Steps towards implementing require() in Carlo

Open trusktr opened this issue 6 years ago • 12 comments

Hello, I'm coming from Electron, and would like to be able to import Node.js modules on the front end.

Some questions:

  • Is this architecturally possible, or is there a limitation? Is data from Node passable by reference? (If data from Node is always serialized and not passable as a reference, then that puts up a road block)
  • If possible, what steps might I take towards starting?

trusktr avatar Apr 05 '19 17:04 trusktr

Does a return value from a call to a function added with App.exposeFunction get returned serialized, or by reference? :crossed_fingers:

trusktr avatar Apr 05 '19 17:04 trusktr

Yeah, looks like it serializes. The following example results in an error.

example.js:

const carlo = require('carlo');

(async () => {
  // Launch the browser.
  const app = await carlo.launch();

  // Terminate Node.js process on app window closing.
  app.on('exit', () => process.exit());

  // Tell carlo where your web files are located.
  app.serveFolder(__dirname);
  
  class Box {
      constructor(x, y) {
          this.x = x
          this.y = y
      }
      
      getSize() {
          return [this.x, this.y]
      }
  }
  
  function makeBox(size) {
      return new Box(size[0], size[1])
  }

  // Expose 'env' function in the web environment.
  await app.exposeFunction('makeBox', makeBox);

  // Navigate to the main page of your app.
  await app.load('example.html');
})();

example.html

<script>
async function run() {
    const box = await makeBox([10, 10])
    console.log(box.getSize()) // ERROR
}
</script>
<body onload="run()">

Results in

Uncaught (in promise) TypeError: box.getSize is not a function
    at run (example.html:4)

I suppose we'd have to re-implement modules like fs on the browser side so they can send the data to/from with serialization if we go that path.


Is there another way? Seems like this is where Electron wins at the moment, and there's not much incentive to switch over just to save hard disk space from re-using Chrome binaries.

trusktr avatar Apr 05 '19 21:04 trusktr

Ah, README says

Node v8 and Chrome v8 engines are decoupled in Carlo,

So obviously it's probably not possible to just pass references, if there's two engines running.


Maybe the opposite is a better idea: implement a DOM interface on the Node side that can proxy to DOM on the browser side? In either case, there will be gotchas and complications.

It'd be nice if somehow Chrome could use Node's v8. Is that possible?

trusktr avatar Apr 05 '19 22:04 trusktr

You can use rpc to solve some of your problems.

pavelfeldman avatar Apr 05 '19 22:04 pavelfeldman

@pavelfeldman Thanks, that looks like it! Interesting that it is possible to wrap reference across engines like that.

So how do we use it? Where do I get the rpc module from? Where is example.html? How do I get the rpc handle in the browser? Where's an example I can run?

trusktr avatar Apr 05 '19 22:04 trusktr

Ah, I see

  const [app, term] = await carlo.loadParams();

in the terminal example's index.html. Nice!

By the way, why is that Terminal example so slow (I mean moving around in vim is really slow, for example)? In Atom text editor terminals, it is much zippier (running in Electron).

Is it the xterm.js's UI rendering in particular, or is it the rpc communication?

trusktr avatar Apr 05 '19 22:04 trusktr

Alright, gave it a try, but no luck:

example.js

const carlo = require('carlo')
const { rpc, rpc_process } = require('carlo/rpc')
const requireHandle = rpc.handle(require)

main()

async function main() {
    // Launch the browser.
    const app = await carlo.launch({
        bgcolor: '#2b2e3b',
        title: 'Require all the things',
        width: 800,
        height: 800,
    })
    
    app.on('exit', () => process.exit())
    app.serveFolder(__dirname)
    app.on('window', win => initWindow(win))
    initWindow(app.mainWindow())
}

function initWindow(win) {
    win.load('example.html', requireHandle)
}

example.html

<script>
async function run() {
    const [require] = await carlo.loadParams()
    
    require('fs').then(fs => {
        console.log(fs.readFileSync('example.html'))
    })
}
</script>
<body onload="run()">

results in

Screen Shot 2019-04-05 at 3 57 30 PM

trusktr avatar Apr 05 '19 22:04 trusktr

@trusktr I try avoiding rpc, mostly by doing something like below.

import fs from 'fs';

...
await app.exposeFunction('fs', (desired, args) => {
  return fs[desired](args.path);
});
...
...
<script>
  const run = async () => {
    const response = await fs('readFileSync', {path: 'README.md'});
  }
</script>
...

VandeurenGlenn avatar Apr 11 '19 19:04 VandeurenGlenn

I'd like to run existing Node.js code without modification.

Did I do my attempt correctly? Is there a way to do it? (existing code expects require() to be synchronous)

trusktr avatar Apr 24 '19 06:04 trusktr

Does the app.exposeFunction tool (de)serialize information between the contexts under the hood? If so, that's a no-go then.

trusktr avatar May 28 '19 21:05 trusktr

@trusktr https://github.com/GoogleChrome/puppeteer/blob/e2e6b8893481ab771c307d413e6d66d8bf05ec6b/lib/Page.js#L393

VandeurenGlenn avatar May 30 '19 17:05 VandeurenGlenn

Ah, ok, so no. Is there any plan for future Node.js support? Or is the plan to keep the web context purely a web context for always?

trusktr avatar Jul 04 '19 03:07 trusktr