"change"
event can occur several times before the end of the recording (unfortunately, there is no such event). Therefore, in order to avoid potentially problematic situations with an invalid patch, we have to include an additional check of the validity of the code (trite check for data in the file and syntax errors). This part seems to be clear. Well, move on? { "bundle": BundleName <String>, // bundle- "patch": Patch <String> // }
{ "bundle": BundleName <String>, // bundle-, "error": Error <String> // }
{ "message": "Connected to browserify-patch-server", "sources": sources <Array>, // bundle- }
{ 1: [function (require, module, exports) { module.exports = 'DEP'; }, {}], 2: [function (require, module, exports) { require('./dep'); module.exports = 'ENTRY'; }, {"./dep": 1}] }
require
function and the module
and exports
objects. In our case, the usual require
will not be enough: we need to encapsulate the logic of working with the patch (we are not going to write it with our hands in each module)! The easiest, if not the only way to do this is to overload the require
. This is what I am doing in this file : function isReloadable(name) { // @todo Replace this sketch by normal one return name.indexOf('react') === -1; } module.exports = function makeOverrideRequire(scope, req) { return function overrideRequire(name) { if (!isReloadable(name)) { if (name === 'react') { return scope.React; } else if (name === 'react-dom') { return scope.ReactDOM; } } else { scope.modules = scope.modules || {}; scope.modules[name] = req(name); return scope.modules[name]; } }; };
scope
, which above the stack refers to window
. Also, the makeOverrideRequire
function uses req
, which is nothing but the original require
function. As you can see, all modules are proxied to scope.modules in order to be able to access them at any time (perhaps I will find use for this in the future. If not, I will abolish). Also, as can be seen from the code above, I check if the react
module is om or react-dom
. In this case, I simply return the link to the object from the scopa (if you use different versions of React, this will lead us to errors when working with hot-loader-api, because the service getRootInstances
will point to another object). var moment = require('moment'); var Logdown = require('logdown'); var diff = require('diff'); var system = new Logdown({ prefix: '[BDS:SYSTEM]', }); var error = new Logdown({ prefix: '[BDS:ERROR]', }); var message = new Logdown({ prefix: '[BDS:MSG]', }); var size = 0; var port = 8081; var patched; var timestamp; var data; /** * Convert bytes to kb + round it to xx.xx mask * @param {Number} bytes * @return {Number} */ function bytesToKb(bytes) { return Math.round((bytes / 1024) * 100) / 100; } module.exports = function injectWebSocket(scope, options) { if (scope.ws) return; if (options.port) port = options.port; scope.ws = new WebSocket('ws://localhost:' + port); scope.ws.onmessage = function onMessage(res) { timestamp = '['+ moment().format('HH:mm:ss') + ']'; data = JSON.parse(res.data); /** * Check for errors * @param {String} data.error */ if (data.error) { var errObj = data.error.match(/console.error\("(.+)"\)/)[1].split(': '); var errType = errObj[0]; var errFile = errObj[1]; var errMsg = errObj[2].match(/(.+) while parsing file/)[1]; error.error(timestamp + ' Bundle *' + data.bundle + '* is corrupted:' + '\n\n ' + errFile + '\n\t ' + errMsg + '\n'); } /** * Setup initial bundles * @param {String} data.sources */ if (data.sources) { scope.bundles = data.sources; scope.bundles.forEach(function iterateBundles(bundle) { system.log(timestamp + ' Initial bundle size: *' + bytesToKb(bundle.content.length) + 'kb*'); }); } /** * Apply patch to initial bundle * @param {Diff} data.patch */ if (data.patch) { console.groupCollapsed(timestamp, 'Patch for', data.bundle); system.log('Received patch for *' + data.bundle + '* (' + bytesToKb(data.patch.length) + 'kb)'); var source = scope.bundles.filter(function filterBundle(bundle) { return bundle.file === data.bundle; })[0].content; system.log('Patch content:\n\n', data.patch, '\n\n'); try { patched = diff.applyPatch(source, data.patch); } catch (e) { return error.error('Patch failed. Can\'t apply last patch to source: ' + e); } Function('return ' + patched)(); scope.bundles.forEach(function iterateBundles(bundle) { if (bundle.file === data.bundle) { bundle.content = patched; } }); system.log('Applied patch to *' + data.bundle + '*'); console.groupEnd(); } /** * Some other info messages * @param {String} data.message */ if (data.message) { message.log(timestamp + ' ' + data.message); } }; };
diff.applyPatch(source, data.patch)
. As a result of calling this function, we get the patched source, which later in the code is beautifully called through Function
. module.exports = function injectReactDeps(scope) { scope.React = require('react'); scope.ReactMount = require('react/lib/ReactMount'); scope.makeHot = require('react-hot-api')( function getRootInstances() { return scope.ReactMount._instancesByReactRootID; } ); };
react-hot-api
from Daniel Abramov aka gaearon beats . This library replaces the export of our modules (read components) and when they are changed, it “patches” their prototypes. It works like a clock, but with a number of limitations: in the “patch” process, all the variables of the loop that are detached from the react component will be lost. There are also a number of restrictions on working with the state of the components: you cannot change the initial state of the elements - this requires a reboot. const through = require('through2'); const pjson = require('../package.json'); /** * Resolve path to library file * @param {String} file * @return {String} */ function pathTo(file) { return pjson.name + '/src/' + file; } /** * Initialize react live patch * @description Inject React & WS, create namespace * @param {Object} options * @return {String} */ function initialize(options) { return '\n' + 'const options = JSON.parse(\'' + JSON.stringify(options) + '\');\n' + 'const scope = window.__hmr = (window.__hmr || {});\n' + '(function() {\n' + 'if (typeof window === \'undefined\') return;\n' + 'if (!scope.initialized) {\n' + 'require("' + pathTo('injectReactDeps') + '")(scope, options);\n' + 'require("' + pathTo('injectWebSocket') + '")(scope, options);' + 'scope.initialized = true;\n' + '}\n' + '})();\n'; } /** * Override require to proxy react/component require * @return {String} */ function overrideRequire() { return '\n' + 'require = require("' + pathTo('overrideRequire') + '")' + '(scope, require);'; } /** * Decorate every component module by `react-hot-api` makeHot method * @return {String} */ function overrideExports() { return '\n' + ';(function() {\n' + 'if (module.exports.name || module.exports.displayName) {\n' + 'module.exports = scope.makeHot(module.exports);\n' + '}\n' + '})();\n'; } module.exports = function applyReactHotAPI(file, options) { var content = []; return through( function transform(part, enc, next) { content.push(part); next(); }, function finish(done) { content = content.join(''); const bundle = initialize(options) + overrideRequire() + content + overrideExports(); this.push(bundle); done(); } ); };
git clone https://github.com/Kureev/browserify-react-live.git cd browserify-react-live/examples/01\ -\ Basic npm i && npm start
this.setState({ counter: this.state.counter + 1 });
this.setState({ counter: this.state.counter + 2 });
Source: https://habr.com/ru/post/264175/
All Articles