Repository navigation
Add build option for Node CLI - #397
Conversation
There was a problem hiding this comment.
@NTillmann This is the Call I was referring to. I want to do the equivalent of realm.$GlobalEnv.execute but call a function value that was the completion value of the previous execution. This is a very common way to initialize internals in JS engines/browsers in a way that doesn't leak those internals onto the global object (unlike React Native).
There was a problem hiding this comment.
This seems reasonable to me. I think you should break it out into a separate function and co-located it with execute. Likewise, you may want to break out lines 176-197 and co-locate it with execute in environment.js.
There was a problem hiding this comment.
How can this be? Do you mean value === null?
There was a problem hiding this comment.
This exception catching for ...Completions seems dubious, but I can see how to copy&pasted it from evaluateCompletion. Let me check with Herman what should happen here.
There was a problem hiding this comment.
Okay, here, you can collapse all of those ...Completions cases into a single "if (err instanceof Completion) res = err; else ...". This will log the issue in a reasonable way, and there is no way to recover at this point anyway.
There was a problem hiding this comment.
It seems like call of this belongs in a helper somewhere but I'm not sure where.
There was a problem hiding this comment.
We have realm.createErrorThrowCompletion to make this easier.
There was a problem hiding this comment.
You filed in assume to make the "jsc" compatibility more precise by adding a version identifier. #395 Maybe. But then you probably also want to make "node"/"node-cli" versioned.
There was a problem hiding this comment.
I didn't quite understand what "node" is. Looks like it is currently just for testing source maps. It probably should have some general "CommonJS" mode where you can't assume a particular version and many of the variables are abstract.
"node-cli" is interesting because I think we can make the versioning very automatic based on what node version you're running but then the environment is locked to that version of node. So it doesn't quite make sense to specify it.
There was a problem hiding this comment.
Is it safe to hide this for the React Native builds or does it need it for some feature detection thing?
There was a problem hiding this comment.
For now, let's just keep it under this compatibility flag.
hermanventer
left a comment
There was a problem hiding this comment.
I'm not quite sure I understand what you are trying to do here. What exactly does the bootstrap function do? Where does it get is input from? How do you designate input as unknown?
There was a problem hiding this comment.
I'm none too sure about this comment. It seems to me that you are taking JS value and making a corresponding Prepack value.
There was a problem hiding this comment.
This seems reasonable to me. I think you should break it out into a separate function and co-located it with execute. Likewise, you may want to break out lines 176-197 and co-locate it with execute in environment.js.
|
@hermanventer The bootstrap function will get its input from The first argument is normally the JavaScript module that is going to be executed. I was thinking that will be set up as a constant string value equal to the file about to be Prepacked. This will cause the Node environment to load that module file into memory via a call to the The abstract input is the rest of the Since the Node.js uses dynamic module source code loading as the program executes, it needs this different mechanism to be compatible with the ecosystem. Unlike browser/react native where bundles are created by statically analyzing dependency graphs. |
|
Seeing what |
|
The most I can figure out quickly from all this is that you might be able to prepack the bootstrap function. I'm wondering just how much value this would unlock. What am I missing? |
|
@hermanventer That is correct. I still need to implement contextify and fs module. The next follow ups on the |
e4303b3 to
1aa70c8
Compare
a453fbe to
21b632d
Compare
| 'transform-es2015-parameters', | ||
| ], | ||
| retainLines: true, | ||
| }); |
There was a problem hiding this comment.
Note that this depends on a "devDependency" atm as a hack. I didn't want to add it as a hard dependency since this is still experimental and we'll just implement these directly eventually.
The purpose of this is to be able to land facebookarchive#397 with some test coverage. In its current form it is too specific in its implementation and only works on a specific version of Node. In the future we'll want to make that more abstract and general so that it can work on ranges and other major versions.
The purpose of this is to be able to land facebookarchive#397 with some test coverage. In its current form it is too specific in its implementation and only works on a specific version of Node. In the future we'll want to make that more abstract and general so that it can work on ranges and other major versions.
The purpose of this is to be able to land #397 with some test coverage. In its current form it is too specific in its implementation and only works on a specific version of Node. In the future we'll want to make that more abstract and general so that it can work on ranges and other major versions.
9634a7e to
ef7448f
Compare
|
I added a unit test that is only enabled in CI for now (not |
|
I added an additional test case that tests the path where ArrayBuffer #598 is needed. CI should now fail until that's fixed. |
f0dcbe6 to
c3c3261
Compare
|
After rebasing on top of the ArrayBuffer fix I think that this should now be in a landable state. After land, I'll follow up with Issues for a few ideas for future improvements. |
| @@ -0,0 +1,17 @@ | |||
| # Prepack test input | |||
| node ./bin/prepack.js ./test/node-cli/Simple.js --out Simple-test.js --compatibility node-cli | |||
There was a problem hiding this comment.
How does prepack.js get into ./bin?
There was a problem hiding this comment.
It's checked into the repo already. https://github.com/facebook/prepack/tree/master/bin
It's an alias for lib/prepack-cli.js which gets there from the build.
|
|
||
| declare var process: any; | ||
|
|
||
| function getBufferFromArg(realm: Realm, value: Value): Uint8Array { |
There was a problem hiding this comment.
Just type value as an ObjectValue.
There was a problem hiding this comment.
And add the invariant at each callsite instead?
There was a problem hiding this comment.
Yes, that is actually preferable because it makes it easier for the reader to verify that the invariant is true.
| declare var process: any; | ||
|
|
||
| function getBufferFromArg(realm: Realm, value: Value): Uint8Array { | ||
| invariant(value instanceof ObjectValue && value.$ViewedArrayBuffer); |
There was a problem hiding this comment.
This invariant is redundant.
|
|
||
| declare var process: any; | ||
|
|
||
| function getBufferFromArg(realm: Realm, value: Value): Uint8Array { |
There was a problem hiding this comment.
You should probably use DataBlock instead of Uint8Array.
There was a problem hiding this comment.
This is supposed to return a native Uint8Array in the Node environment. Not the Prepack value. It's what I pass to nativeFS.read etc. If the internal representation of DataBlock changes, I want that to be an error so that we can do a conversion from that format to Uint8Array here.
There was a problem hiding this comment.
Fair enough, but it makes me wonder why we have DataBlock type in the first place.
|
|
||
| export default function (realm: Realm): ObjectValue { | ||
| let nativeBuffer = process.binding('buffer'); | ||
| let nativeBufferPrototype = (require('buffer'): any).Buffer.prototype; |
There was a problem hiding this comment.
I don't understand what this is doing.
There was a problem hiding this comment.
Hm. This is defined in native C code. This is related to the setupBufferJS thing below. The runtime we're currently running in has already called setupBufferJS, read from a mutated object and that's how it gets a handle on this object.
https://github.com/nodejs/node/blob/v7.9.0/src/node_buffer.cc#L1197-L1230
We can't do the same thing at this point. Luckily this same object is reachable another way through another module which I found here.
| let utf8Slice = new NativeFunctionValue(realm, "Buffer.prototype.utf8Slice", "utf8Slice", 0, (context, args) => { | ||
| let self = getBufferFromArg(realm, context); | ||
| let decodedArgs = args.map((arg, i) => { | ||
| return ToInteger(realm, arg); |
There was a problem hiding this comment.
This seems like a good place to follow the arrow with an expression rather than a block with a return.
| let decodedArgs = args.map((arg, i) => { | ||
| return ToInteger(realm, arg); | ||
| }); | ||
| let utf8String = nativeBufferPrototype.utf8Slice.apply(self, decodedArgs); |
There was a problem hiding this comment.
I'm a bit surprised by this. The utf8Slice I can find does not expect a this parameter.
There was a problem hiding this comment.
It's defined here: https://github.com/nodejs/node/blob/v7.9.0/src/node_buffer.cc#L1209
| let self = getBufferFromArg(realm, context); | ||
| let decodedArgs = args.map((arg, i) => { | ||
| if (i === 0) { | ||
| return getBufferFromArg(realm, arg); |
There was a problem hiding this comment.
This seems inconsistent with utf8Slice and I can't think of a good reason why it should be.
There was a problem hiding this comment.
utf8Slice creates a string from a Buffer. No side-effects. The rest of the arguments are all integers if they're defined.
copy copies bytes from this Buffer by mutating a target Buffer. The rest of the arguments are integers if defined. The return value is an integer.
https://github.com/nodejs/node/blob/v7.9.0/src/node_buffer.cc#L524-L557
| return ToInteger(realm, arg); | ||
| } | ||
| }); | ||
| let bytesCopied = nativeBufferPrototype.copy.apply(self, decodedArgs); |
There was a problem hiding this comment.
If you unpacked args with a pattern, you might get away with calling copy directly.
| throw new Error("TODO"); | ||
| }); | ||
|
|
||
| Set(realm, obj, "setupBufferJS", setupBufferJS, true); |
There was a problem hiding this comment.
This would be more readable if moved to just after the definition of setupBufferJS.
hermanventer
left a comment
There was a problem hiding this comment.
I only really skimmed over this since it is quite large and somewhat strange to me. It seems self contained enough that I'm not overly concerned that it will break things, so I'm going to let you go ahead with this.
|
|
||
| declare var process: any; | ||
|
|
||
| function getBufferFromArg(realm: Realm, value: Value): Uint8Array { |
There was a problem hiding this comment.
This seems like a duplicate of the one in buffer.js.
| return realm.intrinsics.undefined; | ||
| }); | ||
| obj.defineNativeMethod("internalModuleStat", 0, (context, args) => { | ||
| const fileName = ToString(realm, args[0]); |
There was a problem hiding this comment.
Rather use a pattern with more descriptive names.
| enumerable: true, | ||
| }); | ||
|
|
||
| // TODO |
There was a problem hiding this comment.
This comment can be a bit more descriptive. When your future self comes back it he might appreciate a hint as to what your present self had in mind.
| declare var process: any; | ||
|
|
||
| // Takes a value from the host realm and create it into a Prepack Realm. | ||
| // This seems like it could be a useful primitive elsewhere. |
There was a problem hiding this comment.
Perhaps move this to Realm.js.
| enumerable: true, | ||
| configurable: true | ||
| }); | ||
| // TODO |
| return realm.intrinsics.undefined; | ||
| }); | ||
| TTYPrototype.defineNativeMethod("writeUtf8String", 0, (context, args) => { | ||
| // let req = args[0]; |
| let fd = ToInteger(realm, args[0]); | ||
| return new StringValue(realm, nativeTTYWrap.guessHandleType(fd)); | ||
| // TODO: Make this abstract so that changing the pipe at runtime is | ||
| // possible. Currently this cause an introspection error. |
| ); | ||
| return realm.createAbstract(types, values, [], buildNode, undefined, `(process.binding('tty_wrap').isTTY(${fd}))`); | ||
| }); | ||
| // TODO |
| } | ||
|
|
||
| export function prepackNodeCLISync(filename: string, options: Options = defaultOptions) { | ||
| if (process.version !== "v7.9.0") { |
There was a problem hiding this comment.
This seems rather specific. Wouldn't later versions work as well?
There was a problem hiding this comment.
Not at the moment. The strategy is very fragile because I only model the minimal needed. Which is why this whole thing is so hacky. The ideal would be to have more of an automatic pass-through or making more things abstract.
There was a problem hiding this comment.
To be clear, I'm modeling private APIs here. They do change between minor versions. The rationale for picking this strategy over modeling public APIs is outlined in the PR comment.
Expose `process` on intrinsics. We won't expose it on the global through the normal route. Instead, we'll let Node's on script set it up for us.
This lets us run from within a module scope for convenience.
This is just a sufficient amount of hacks to get the base line running. Needs more implementations and more of this needs to be automated.
I enable this mode only on CI for now. Not to `yarn test`. Since it is locked to a particular version of Node so we don't want to force people to run it locally.
The sync API doesn't accept a callback.
|
I've noticed this may still throw errors when attempting to load built-in modules like "zlib". |
|
@jdalton I think it'll be expected that many things won't work out of the box until the ecosystem shifts towards supporting "heap snapshot". E.g. Any hanging open socket at the end won't work 100%. We'll have to keep chasing the minimal set needed. Ideally Prepack can help minimize the things that can and can't be. I'll take a look at why zlib doesn't work. Keep em coming. :) |
|
@sebmarkbage Cool, thanks! |
This adds a compatibility option for
node-cli. The goal of this is to Prepack a whole Node.js CLI environment including Node's module system. This is useful for an end application but not so useful for a small library yet.This takes a bit different approach than compiling a single bundle. Instead of running the target bundle in the Prepack realm, it actually runs Node.js's JavaScript code inside the Prepack realm. This code is embedded within the runtime and we can get to it through
process.binding('natives')when we run Prepack.The script file will be passed as a
process.argv.Node's CLI code will then pick that up and start loading modules from the file system. The idea is that reads from
fswhile Prepacking will be considered constant. That way all the relevant modules will be automatically picked up and executed in the Prepack realm.Finally, we can snap shot the entire Node system.
This PR takes the approach of building intrinsics only for the native code in Node.js (which are all on the
processobject). An alternative solution would be to build intrinsics from the Node.js public APIs. The tradeoff is that the public API is much bigger surface area but moves slower, where as the native APIs are considered more private and therefore moves faster but have smaller surface area.My guess is that it will be pretty easy to keep up with the native APIs but not the JS APIs so I picked that solution. It guarantees that the JS parts are 100% compatible with the Node version you're running.
This is still a WIP. I have more intrinsics to implement before this works.