id | title |
---|---|
modules |
Modules |
In which we describe the module system and metadata input.
In order to provide a directory or zipfile to Hermes,
it must contain a metadata.json
file at the root.
The metadata.json
file must be a JSON object, with the following fields:
segments
- An object with string keys representing integers, which are the segments IDs. The IDs are generally sequential but there can be gaps (ex. having IDs "6" and "8" and no "7"). ID zero has special meaning, this is the "main segment" that gets loaded on startup (ex. when React Native starts).
- Each value must be an array containing file paths
of the files to place into that segment, expressed relative to the root of
the ZIP file (ex.
subdir/foo.js
). File paths may include./
at the start, but they are not required to. - The first element of
segments["0"]
is the first modulerequire
d at run time.
resolutionTable
(Optional)- An object for which the keys are relative file names
(the same file names as in
segments
). - Values are objects which map from strings given to
require()
to their actual resolved file path relative to the directory or zip file root. These relative paths should be the same as insegments
, with leading./
.
- An object for which the keys are relative file names
(the same file names as in
{
"segments": {
"0": [
"./cjs-subdir-main.js",
"cjs-subdir-2.js",
"bar/cjs-subdir-bar.js",
"foo/cjs-subdir-foo.js"
]
},
"resolutionTable": {
"./cjs-subdir-main.js": {
"foo": "./foo/cjs-subdir-foo.js"
},
"./foo/cjs-subdir-foo.js": {
"bar": "./bar/cjs-subdir-bar.js"
}
}
}
Modules can require each other using the require
function provided as a local
variable by the runtime. The argument to require
must be an absolute path
which root is the root of the ZIP file or input directory. For example:
const Foo = require('/subdir/foo.js');
Foo.doSmth();
Each JavaScript file can optionally provide a corresponding source map, which is
the name of the source file with the suffix .map
. For example subdir/foo.js
might provide a subdir/foo.js.map
file.
Module mode is currently activated via passing -commonjs
to Hermes while compiling.
Hermes can then be given a directory and a metadata.json
file,
or a simple list of files with the first being the entry point.
First, each file is parsed just like any other JS file.
Then, a call to hermes::wrapCJSModule
puts each file in its own "module function",
which binds exports, require, module
as parameters.
IR generation relies on this wrapping of the AST.
Every CJS module file generates IR into a shared hermes::Module
(it's called M
in generateIRForSourcesAsCJSModules
).
Each of these generated CJS modules are now hermes::Function *
s.
These are registered in the cjsModules_
field using the hermes::Module::addCJSModule
function,
which stores information in the CJSModule
struct regarding filename, id, and IR function.
This allows lookup of the CJS modules either via hermes::Function *
or by string literal (file path).
If the caller of the Hermes CLI passes -static-require
,
then we attempt to resolve all require
calls at compilation time.
This occurs in ResolveStaticRequire.cpp
, which is able to resolve files if they were povided
by the user at invocation time and if all require
calls only take string literals as arguments.
If every require
call is able to be resolved, every one of these require
calls is replaced
with a call to HermesBuiltin_requireFast
with an ID for the CJS module,
and that function does no string work and is therefore very fast.
After all CJS modules have generated IR and all the require
s have been resolved,
we generate one or more bytecode files from our IR::Module M
.
This requires two special bits of logic in hbc::generateBytecodeModule
.
- We add a mapping from a CJS module to the HBC function ID.
This allows us to actually run
require
when JS execution demands it. Ifrequire
s have been resolved, we add to thecjsModulesStatic_
table in the HBC file, else we add to to thecjsModules_
table in the HBC file (which maps from strings instead of IDs). - To accommodate bundle splitting, we also pass a
SegmentRange
to the function. This allows us to only compile the functions which are needed by the CJS modules in the segment. We then set acjsModuleOffset_
field in the HBC file, which allows us to know how far into the complete set of CJS modules this segment is. Because every HBC file contains a contiguous set of CJS modules, each of which are unique, it is not possible for multiple segments to contain the same CJS module.
Having built 1 or more HBC files in the compiler, we can now execute them.
The vm::Domain
data structure is used for keeping track of all HBC files which were compiled together.
In particular, the Domain
owns a CJS module table.
This table is an array of CJS modules, indexed by the CJS module's ID.
If require
s were not resolved at compile time, there's also a mapping from file path to that ID.
The first HBC file to be loaded must have a cjsModuleOffset_
of 0
.
CJSModule 0
is the entry point, and will be executed first.
Loading of other segments is then done via a loadSegment
call in the runtime;
the user can call it via the ConsoleHost loadSegment
or more commonly via the Hermes API.
loadSegment
does need a requireContext
, which allows us to determine which Domain
to actually
load the new CJS modules from the segment into.
This is registered into require.context
(recall that require
was bound as a param in all CJS modules).
Finally, we can require
.
When require
is called, one of two things happens:
- If we DID resolve all static
require
s, then calls torequire
were turned into Hermes builtin calls, so the actual value ofrequire
s (the parameter) is not used. We simply callHermesBuiltin_requireFast
via theCallBuiltin
instruction. - If we DID NOT resolve all static
require
s, we call therequire
function. The runtime will have set up that function to also pass along the base path (to allow relativerequire
), sorequire
is now a BoundFunction (as if we'd donerequire = require.bind(currentPath)
). Note that this is transparent to the application developer, and doesn't change the wayrequire
is actually called in JS source. In either case, we dorunRequireCall
as implemented inrequire.cpp
. It performs any necessary checks, calls the target CJS module, and caches and readsexports
back.
Hermes also partially supports ECMAScript modules.
Currently, the interop of ESM and CJS modules is defined simply by turning import
into require
calls.
This precludes full correctness of the ESM module system, which requires live bindings;
when a value is changed in the imported module, the local binding to that value must also be updated.
Tests for the current emitted code can be found in test/hermes/esm/
.