GoogleChromeLabs / carlo

Web rendering surface for Node applications
Apache License 2.0
9.32k stars 309 forks source link

Steps towards implementing require() in Carlo #149

Open trusktr opened 5 years ago

trusktr commented 5 years ago

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

Some questions:

trusktr commented 5 years ago

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

trusktr commented 5 years ago

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 commented 5 years ago

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?

pavelfeldman commented 5 years ago

You can use rpc to solve some of your problems.

trusktr commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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
VandeurenGlenn commented 5 years ago

@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>
...
trusktr commented 5 years ago

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 commented 5 years ago

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

VandeurenGlenn commented 5 years ago

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

trusktr commented 5 years ago

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?