|
| 1 | +import * as path from 'path' |
| 2 | +import * as vm from 'vm' |
| 3 | + |
| 4 | +import { isPlainObject } from 'lodash' |
| 5 | +import * as _resolve from 'resolve' |
| 6 | + |
| 7 | +import { UserContext } from '../util' |
| 8 | + |
| 9 | +// tslint:disable-next-line no-var-requires |
| 10 | +const NativeModule = require('module') |
| 11 | + |
| 12 | +function createSandbox(context?) { |
| 13 | + const sandbox = { |
| 14 | + Buffer, |
| 15 | + console, |
| 16 | + process, |
| 17 | + setTimeout, |
| 18 | + setInterval, |
| 19 | + setImmediate, |
| 20 | + clearTimeout, |
| 21 | + clearInterval, |
| 22 | + clearImmediate, |
| 23 | + __REACT_SSR_CONTEXT__: context, |
| 24 | + } as any |
| 25 | + sandbox.global = sandbox |
| 26 | + return sandbox |
| 27 | +} |
| 28 | + |
| 29 | +function compileModule(files, basedir, runInNewContext) { |
| 30 | + const compiledScripts = {} |
| 31 | + const resolvedModules = {} |
| 32 | + |
| 33 | + function getCompiledScript(filename) { |
| 34 | + if (compiledScripts[filename]) { |
| 35 | + return compiledScripts[filename] |
| 36 | + } |
| 37 | + const code = files[filename] |
| 38 | + const wrapper = NativeModule.wrap(code) |
| 39 | + const script = new vm.Script(wrapper, { |
| 40 | + filename, |
| 41 | + displayErrors: true, |
| 42 | + }) |
| 43 | + compiledScripts[filename] = script |
| 44 | + return script |
| 45 | + } |
| 46 | + |
| 47 | + function evaluateModule(filename, sandbox, evaluatedFiles = {}) { |
| 48 | + if (evaluatedFiles[filename]) { |
| 49 | + return evaluatedFiles[filename] |
| 50 | + } |
| 51 | + |
| 52 | + const script = getCompiledScript(filename) |
| 53 | + const compiledWrapper = |
| 54 | + runInNewContext === false |
| 55 | + ? script.runInThisContext() |
| 56 | + : script.runInNewContext(sandbox) |
| 57 | + const m = { exports: {} } as any |
| 58 | + const r = file => { |
| 59 | + file = path.posix.join('.', file) |
| 60 | + if (files[file]) { |
| 61 | + return evaluateModule(file, sandbox, evaluatedFiles) |
| 62 | + } else if (basedir) { |
| 63 | + return require(resolvedModules[file] || |
| 64 | + (resolvedModules[file] = _resolve.sync(file, { basedir }))) |
| 65 | + } else { |
| 66 | + return require(file) |
| 67 | + } |
| 68 | + } |
| 69 | + compiledWrapper.call(m.exports, m.exports, r, m) |
| 70 | + |
| 71 | + const res = Object.prototype.hasOwnProperty.call(m.exports, 'default') |
| 72 | + ? m.exports.default |
| 73 | + : m.exports |
| 74 | + evaluatedFiles[filename] = res |
| 75 | + return res |
| 76 | + } |
| 77 | + return evaluateModule |
| 78 | +} |
| 79 | + |
| 80 | +function deepClone(val) { |
| 81 | + if (isPlainObject(val)) { |
| 82 | + const res = {} |
| 83 | + // tslint:disable-next-line forin |
| 84 | + for (const key in val) { |
| 85 | + res[key] = deepClone(val[key]) |
| 86 | + } |
| 87 | + return res |
| 88 | + } else if (Array.isArray(val)) { |
| 89 | + return val.slice() |
| 90 | + } else { |
| 91 | + return val |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +export function createBundleRunner(entry, files, basedir, runInNewContext) { |
| 96 | + const evaluate = compileModule(files, basedir, runInNewContext) |
| 97 | + if (runInNewContext !== false && runInNewContext !== 'once') { |
| 98 | + // new context mode: creates a fresh context and re-evaluate the bundle |
| 99 | + // on each render. Ensures entire application state is fresh for each |
| 100 | + // render, but incurs extra evaluation cost. |
| 101 | + return (userContext: UserContext = {}) => |
| 102 | + new Promise(resolve => { |
| 103 | + userContext._registeredComponents = new Set() |
| 104 | + const res = evaluate(entry, createSandbox(userContext)) |
| 105 | + resolve(typeof res === 'function' ? res(userContext) : res) |
| 106 | + }) |
| 107 | + } else { |
| 108 | + // direct mode: instead of re-evaluating the whole bundle on |
| 109 | + // each render, it simply calls the exported function. This avoids the |
| 110 | + // module evaluation costs but requires the source code to be structured |
| 111 | + // slightly differently. |
| 112 | + let runner // lazy creation so that errors can be caught by user |
| 113 | + let initialContext |
| 114 | + return (userContext: UserContext = {}) => |
| 115 | + new Promise(resolve => { |
| 116 | + if (!runner) { |
| 117 | + const sandbox = runInNewContext === 'once' ? createSandbox() : global |
| 118 | + // the initial context is only used for collecting possible non-component |
| 119 | + // styles injected by react-style-loader. |
| 120 | + initialContext = sandbox.__REACT_SSR_CONTEXT__ = {} |
| 121 | + runner = evaluate(entry, sandbox) |
| 122 | + // On subsequent renders, __REACT_SSR_CONTEXT__ will not be available |
| 123 | + // to prevent cross-request pollution. |
| 124 | + delete sandbox.__REACT_SSR_CONTEXT__ |
| 125 | + if (typeof runner !== 'function') { |
| 126 | + throw new Error( |
| 127 | + 'bundle export should be a function when using ' + |
| 128 | + '{ runInNewContext: false }.', |
| 129 | + ) |
| 130 | + } |
| 131 | + } |
| 132 | + userContext._registeredComponents = new Set() |
| 133 | + |
| 134 | + // react-style-loader styles imported outside of component lifecycle hooks |
| 135 | + if (initialContext._styles) { |
| 136 | + userContext._styles = deepClone(initialContext._styles) |
| 137 | + // #6353 ensure "styles" is exposed even if no styles are injected |
| 138 | + // in component lifecycles. |
| 139 | + // the renderStyles fn is exposed by react-style-loader >= 3.0.3 |
| 140 | + const renderStyles = initialContext._renderStyles |
| 141 | + if (renderStyles) { |
| 142 | + Object.defineProperty(userContext, 'styles', { |
| 143 | + enumerable: true, |
| 144 | + get() { |
| 145 | + return renderStyles(userContext._styles) |
| 146 | + }, |
| 147 | + }) |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + resolve(runner(userContext)) |
| 152 | + }) |
| 153 | + } |
| 154 | +} |
0 commit comments