-
Notifications
You must be signed in to change notification settings - Fork 0
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
Allow deferring import and evaluation to runtime #131
Comments
Actually, bundling externals would not be too hard to implement. For
The runtime mentioned above would execute function created above from file and return const __commonJS = (callback, module) => () => {
if (!module) {
module = {exports: {}};
callback(module.exports, module);
}
return module.exports;
}; Then: // `wrapped` is function created by from file code
const requireFoo = __commonJS( wrapped ); By using function scopes, Livepack's existing mechanisms for dealing with circularity can be utilized to deal with circular requires. Only tricky part is that any assignments, to inject circular values into scope, would need to be executed before An option to |
Alternative solution - replace at serialize time// src/index.js
import { createElement } from 'react';
export default function App() {
return createElement( 'div', {}, 'Hello!' );
}
// build.js
import { serialize, deferredImport } from 'livepack';
import React from 'react';
import App from './src/index.js';
const deferredReact = deferredImport('react');
const js = serialize( App, {
replace: [
[ React, deferredReact ],
[ React.createElement, deferredReact.createElement ]
]
} ); The first replacement tells Livepack to serialize
const o = { x: 1 };
const js = serialize( o, {
replace: [
[ o, { y: 2 } ]
]
} );
// js === '{y:2}'
The // src/react-shim.js
import React from 'react';
const { createElement, createContext: _createContext } = React;
const contexts = [];
const createContext = (value) => {
const context = _createContext(value);
contexts.push( { context, value } );
return context;
};
// Deal with `import * as React from './react-shim.js'`
import * as reactShim from './react-shim.js';
function _getReplacements() {
const deferredReact = deferredImport('react');
return [
[ reactShim, deferredReact ],
[ createElement, deferredReact.createElement ],
...contexts.map(
( { context, value } ) => [ context, deferredReact.createContext(value) ]
)
];
}
export { createElement, createContext, _getReplacements }; Then serialization: // build.js
import { serialize, deferredImport } from 'livepack';
import { _getReplacements } from './src/react-shim.js';
import App from './src/index.js';
const js = serialize( App, {
replace: _getReplacements()
} ) Disadvantages of this approach:
Advantages:
The latter is the really big gain.
|
Question: When bundling a deferred import, should code from React contains How about a module like this: import { last } from 'lodash';
const isBrowser = typeof window !== 'undefined';
export default [ isBrowser, last ]; This module needs to be a deferred import due to use of Two options: Option 1There are two "realms":
The two realms would have no crossover. In the example above, Option 2Resolve This does assume that objects shared between realms are static/stateless. Otherwise, changes made to the object in one realm could clash with the other. |
The problem
Currently Livepack is not really useable for client-side code.
The main issue is client-side libraries use of globals.
This comes in 2 varieties:
window
,document
Symbol
For example, some libraries contain top-level code like:
React contains the following top-level code:
And React DOM includes top-level code:
If this code is run on the server before serializing:
window
anddocument
do not exist socanUseDOM
isfalse
.WeakMap
andSymbol
do exist on Node, but may not in the browser.When bundled by Livepack, based on the evaluation of code as it was on the server, the app may not run correctly in the browser.
An additional problem is that Livepack's output from serializing ReactDOM is about 50% larger than ReactDOM's original code.
Ideal solution
Livepack could identify globals in code. Such values would be considered "uncertain". Any other values which were calculated based on uncertain values would also be considered uncertain. Any "uncertain" values would not be serialized as is, but instead the code which created them would be included in the output to run at runtime. All other values would be serialized as usual.
This would be very complicated to implement. Tracing "uncertainty" throughout program flow would be complex. There's also the problem of dealing with the case where the code run to produce "uncertain" values has side-effects.
I'm not sure this problem is entirely solvable. Prepack got tripped up on this kind of problem (see this comment).
If it is possible, this would be the best solution as it would require no intervention from the user, and would maintain the advantages of Livepack's code splitting and tree-shaking.
Simpler solution
A solution which is less ergonomic but more easily implemented in the short term is to allow the user to flag some values to be imported or evaluated at run time.
Three potential APIs:
deferImport()
deferImport( React, 'react' )
would cause Livepack to serializeReact
as an import statement rather than serializing it as a value in the usual way.A 3rd argument would specify the import name:
defer()
defer()
runs a function in Node at build time, but Livepack will serialize the returned value as the function provided immediately executed.defer()
could be used for React's static methods which are typically used at top level e.g.React.createContext()
.React.createContext( { count: 0 } )
returns an object:If React is being imported at runtime, this object will not work with the imported React. The Symbols in the object may not equal the Symbols used in the imported React.
defer()
would solve this by runningcreateContext()
at runtime:You could create a re-usable React wrapper which is evaluated at runtime:
NB
useState
anduseEffect
do not need to be deferred as they're only used inside functions, not top level.deferred()
Sugar for
defer()
where value being deferred is a function. These two are equivalent:deferred( fn )
would defer evaluation of values returned by the function. Implementation:Specify deferred imports at serialization time
Livepack would need to hook
require()
to record the values returned for all calls torequire()
. Whenserialize()
is called withdeferModules
option, Livepack would look up the value thatrequire('react')
returned, and serialize it asimport React from 'react'
.In most cases, you'd also want the whole tree of React's object properties crawled and also serialized as imports. So
const c = require('react').createElement
is serialized asimport react from 'react'; const c = react.createElement
.This is a bit trickier to implement.
Has advantage of allowing user to call
serialize()
twice with different options to create (1) client-side build with deferred import/evaluation and (2) self-contained server-side build (for server-side rendering) without deferred evaluation. Deferred evaluation is only required for client-side builds, as runtime and build-time environments should be the same for server-side code.Bunding externals
As described above, the Livepack build would include "externals". The bundle would no longer include all code from the app - there are
import
statements referring tonode_modules
. So Livepack's output would need bundling with e.g. Snowpack / Webpack / ESBuild to produce a final self-contained bundle.That's probably OK for starters, but it'd be better if Livepack traced the dependencies of any deferred modules and added them to the bundle too. This is into the realm of "normal" bundlers though, so no doubt quite a bit of effort.
The text was updated successfully, but these errors were encountered: