Skip to content

Commit

Permalink
Adding panel hot loaders to reload jade, styl, controller without pag…
Browse files Browse the repository at this point in the history
…e refresh (#26)

* Add css-reload-client

* Add eslint to devDependencies package

* Add jade loader

* comment

* module.id as string is not a requirement anymore

* Adding jade style controller hot loaders

* updateStyle does a passthrough for extract text plugin

* Re-export after hot accept

* Re-export on hot accept

* Adding helpers and cleaning up code for consistency

* Fix typo

* Fix helpers

* We need to go deeper

* Rename to template loader

* Webpack has really bad side effects sharing noopLoader

* Add [HMR Panel]  prefix

* Make a sub-package called panel-hot

* Use update-panel-helpers to consolidate panel update logic

* Auto-detect isDevServerHot

* use proxy fn so `import template from './index.jade` works as is

* 0.1.0-rc.2 Much smarter hot reloading capabilities

* Add loader-utils to package.json no panel-hot module

* add package-lock.json

* rc version for hot loaders

* PR feedback #1

* PR Feedback #2

* Use findPanelElemsByName

* findPanelElemsByTagName

* expand getElemName to handle 'template' and 'styles?'

* Some documentation for hot reloaders

* customElements.define

* 0.14.0

* 13 I mean

* package-lock
  • Loading branch information
nojvek authored Jan 20, 2018
1 parent 4717fbe commit fcb5cca
Show file tree
Hide file tree
Showing 9 changed files with 2,942 additions and 1,344 deletions.
100 changes: 100 additions & 0 deletions hot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Using panel hot loaders with webpack

## Sample webpack config

```js
const {HOT} = process.env;
const isDevServer = process.argv.some(s => s.includes(`webpack-dev-server`));

const webpackConfig = {
entry {
app: '.src/index.js'
},
output: {
filename: '[name].js',
},
loaders: {
rules :[
{ test: /\.jade$/, use: [
{ loader: `panel/hot/template-loader`},
{ loader: `babel-loader`, options: {
presets: [`es2015`],
}},
{ loader: `virtual-jade-loader`, options: {
vdom: `snabbdom`,
runtime: `var h = require("panel").h;`,
}},
]},
{ test: /\.styl$/, use: [
{ loader: `panel/hot/style-loader`},
{ loader: `css-loader`},
{ loader: `stylus-loader`},
]},
{ test: /\.js$/, use: [
{ loader: `babel-loader`, options: {
presets: [`es2015`],
}},
]},
]
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
]
resolveLoader: {
alias: {
'panel-controller': `panel/hot/controller-loader`,
},
},
};

if (isDevServer && HOT) {
webpackConfig.devServer.hot = true;
webpackConfig.plugins.push(new webpack.NamedModulesPlugin());
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
}
```

Panel hot loaders only activate if `webpackConfig.devServer.hot === true` otherwise they do a no-op and behave like they are non-existent


## Inside a panel component

```js
import {Component} from 'panel';
// Ensure controller uses a `export default class SampleController`
import SampleController from 'panel-controller!./controller';
import template from './index.jade';
import './index.styl';

customElements.define('sample-component', class SampleComponent extends Component {
get config() {
return {
template,
};
}

constructor() {
super(...arguments);
this.controller = new SampleController({store: this});
}
}
```
Or using a ControlledComponent
```js
import {ControlledComponent} from 'panel';
import SampleController from 'panel-controller!./controller';
import template from './index.jade';
import './index.styl';

customElements.define('sample-component', class SampleComponent extends ControlledComponent {
get config() {
return {
template,
// new instance of class is used after hot mode replacement when element is created
controller: new SampleController(),
};
}
}
```
28 changes: 28 additions & 0 deletions hot/controller-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-env commonjs */
const loaderUtils = require(`loader-utils`);
const helpers = require(`./loader-helpers`);

// Used in non-HMR mode, do nothing
module.exports = source => source;

module.exports.pitch = function(remainingReq) {
if (!helpers.isDevServerHot(this.options)) {
return;
}

const moduleId = loaderUtils.stringifyRequest(this, `!!${remainingReq}`);
const elemName = helpers.getElemName(this.resourcePath);

return `
module.hot.accept(${moduleId}, () => {
const newExports = module.exports = require(${moduleId});
Object.assign(oldExports, newExports);
const updatePanelElems = require('panel/hot/update-panel-elems');
updatePanelElems('${elemName}', (elem) => {
Object.setPrototypeOf(elem.controller, newExports.default.prototype);
return true;
});
});
const oldExports = module.exports = require(${moduleId});
`.trim().replace(/^ {4}/gm, ``);
};
28 changes: 28 additions & 0 deletions hot/loader-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-env commonjs */
const path = require(`path`);

// Retrieve elemName for hot injection from path convention
//
// elem name patterns look like this
// ./src/.../${elemName}/index.<ext>
// ./src/.../${elemName}/template.<ext>
// ./src/.../${elemName}/style.<ext> or styles.<ext>
// ./src/.../${elemName}/controller.<ext>
// OR ./src/.../${elemName}.<ext>
//
// this means multiple element definitions in a single file won't work

module.exports.getElemName = function(resourcePath) {
const pathInfo = path.parse(resourcePath);
let elemName = pathInfo.name;
if (/^(index|template|styles?|controller)$/.test(pathInfo.name)) {
const pathParts = resourcePath.split(`/`);
elemName = pathParts[pathParts.length - 2];
}

return elemName;
};

module.exports.isDevServerHot = function(webpackOpts) {
return webpackOpts.devServer && webpackOpts.devServer.hot;
};
34 changes: 34 additions & 0 deletions hot/style-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-env commonjs */
const loaderUtils = require(`loader-utils`);
const helpers = require(`./loader-helpers`);

// Used in non-HMR mode, do nothing
module.exports = source => source;

module.exports.pitch = function(remainingReq) {
if (!helpers.isDevServerHot(this.options)) {
return;
}

const moduleId = loaderUtils.stringifyRequest(this, `!!${remainingReq}`);
const resourcePath = this.resourcePath;
const elemName = helpers.getElemName(resourcePath);

return `
module.hot.accept(${moduleId}, () => {
const newStyle = module.exports = require(${moduleId});
const updatePanelElems = require('panel/hot/update-panel-elems');
const updateCount = updatePanelElems('${elemName}', elem => {
if (elem.getConfig('useShadowDom')) {
elem.el.querySelector('style').textContent = newStyle.toString();
return true;
}
});
if (!updateCount) {
const updateStyle = require('panel/hot/update-style');
updateStyle(newStyle.toString(), ${JSON.stringify(resourcePath)});
}
});
module.exports = require(${moduleId});
`.trim().replace(/^ {4}/gm, ``);
};
25 changes: 25 additions & 0 deletions hot/template-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-env commonjs */
const loaderUtils = require(`loader-utils`);
const helpers = require(`./loader-helpers`);

// Used in non-HMR mode, do nothing
module.exports = source => source;

module.exports.pitch = function(remainingReq) {
if (!helpers.isDevServerHot(this.options)) {
return;
}

const moduleId = loaderUtils.stringifyRequest(this, `!!` + remainingReq);
const elemName = helpers.getElemName(this.resourcePath);

return `
let template = require(${moduleId});
module.hot.accept(${moduleId}, () => {
template = require(${moduleId});
const updatePanelElems = require('panel/hot/update-panel-elems');
updatePanelElems('${elemName}', elem => true);
});
module.exports = function() {return template.apply(this, arguments)};
`.trim().replace(/^ {4}/gm, ``);
};
41 changes: 41 additions & 0 deletions hot/update-panel-elems.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-env commonjs */

// 'That's not enough, we need to go deeper'
// NOTE: document.querySelectorAll(`body /deep/ ${elemName}`) is deprecated, so we use recursion
function findPanelElemsByTagName(rootElem, elemName) {
const results = [];

for (const elem of rootElem.querySelectorAll(`*`)) {
if (elem.panelID && elem.tagName.toLowerCase() === elemName) {
results.push(elem);
}
if (elem.shadowRoot) {
for (const shadowElem of findPanelElemsByTagName(elem.shadowRoot, elemName)) {
results.push(shadowElem);
}
}
}

return results;
}

module.exports = function updatePanelElems(elemName, updateFn) {
let numUpdated = 0;
const elems = findPanelElemsByTagName(document.body, elemName);

for (const elem of elems) {
if (updateFn.call(null, elem)) {
const update = elem._update || elem.update;
numUpdated += update.apply(elem) ? 1 : 0;
}
}

if (numUpdated > 0) {
console.info(`[HMR Panel] Updated ${elems.length} ${elemName} elems`);
} else if (!elems.length) {
console.warn(`[HMR Panel] No ${elemName} elems found`);
console.warn(`[HMR Panel] Exepect file path to be '.../<elemName>/index.js' or '.../<elemName>.js'`);
}

return numUpdated;
};
14 changes: 14 additions & 0 deletions hot/update-style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-env commonjs */
module.exports = function updateStyle(newCss, styleId) {
let elem = document.getElementById(styleId);
if (elem) {
elem.textContent = newCss;
console.info(`[HMR Panel] Updated ${styleId}`);
} else {
elem = document.createElement(`style`);
elem.setAttribute(`type`, `text/css`);
elem.id = styleId;
elem.textContent = newCss;
document.head.appendChild(elem);
}
};
Loading

0 comments on commit fcb5cca

Please sign in to comment.