Skip to content
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

Hot loading support #40

Merged
merged 11 commits into from
Mar 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,84 @@ module.exports = {

This should create an additional `styles.css.map` file.

### Hot Reload

Hot reloading is turned off by default, you can turn it on using the `hotReload` option as shown below:

```javascript
...
module: {
rules: [
...
{
test: /\.(html|svelte)$/,
exclude: /node_modules/,
use: 'svelte-loader',
options: {
hotReload: true
}
}
...
]
}
...
```

#### Hot reload rules and caveats:

- `_rerender` and `_register` are reserved method names, please don't use them in `methods:{...}`
- Turning `dev` mode on (`dev:true`) is **not** necessary.
- Modifying the HTML (template) part of your component will replace and re-render the changes in place. Current local state of the component will also be preserved (this can be turned off per component see [Stop preserving state](#stop-preserving-state)).
- When modifying the `<script>` part of your component, instances will be replaced and re-rendered in place too.
However if your component has lifecycle methods that produce global side-effects, you might need to reload the whole page.
- If you are using `svelte/store`, a full reload is required if you modify `store` properties


Components will **not** be hot reloaded in the following situations:
1. `process.env.NODE_ENV === 'production'`
2. Webpack is minifying code
3. Webpack's `target` is `node` (i.e SSR components)
4. `generate` option has a value of `ssr`

#### Stop preserving state

Sometimes it might be necessary for some components to avoid state preservation on hot reload.

This can be configured on a per-component basis by adding a property `noPreserveState = true` to the component's constructor using the `setup()` method. For example:
```js
export default {
setup(comp){
comp.noPreserveState = true;
},
data(){return {...}},
oncreate(){...}
}
```

Or, on a global basis by adding `{noPreserveState: true}` to `hotOptions`. For example:
```js
{
test: /\.(html|svelte)$/,
exclude: /node_modules/,
use: [
{
loader: 'svelte-loader',
options: {
hotReload: true,
hotOptions: {
noPreserveState: true
}
}
}
]
}
```

**Please Note:** If you are using `svelte/store`, `noPreserveState` has no effect on `store` properties. Neither locally, nor globally.




## License

MIT
39 changes: 38 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
const { basename, extname, posix } = require('path');
const { basename, extname, posix, relative } = require('path');
const { compile, preprocess } = require('svelte');
const { getOptions } = require('loader-utils');
const { statSync, utimesSync, writeFileSync } = require('fs');
const { tmpdir } = require('os');

function makeHot(id, code, hotOptions) {
const options = JSON.stringify(hotOptions);
const replacement = `

let proxyComponent = $2;

if (module.hot) {

const { configure, register, reload } = require('svelte-loader/lib/hot-api');

module.hot.accept();

if (!module.hot.data) {
// initial load
configure(${options});
proxyComponent = register(${id}, $2);
} else {
// hot update
reload(${id}, proxyComponent);
}
}

export default proxyComponent;
`;

return code.replace(/(export default ([^;]*));/, replacement);
}

function posixify(file) {
return file.replace(/[/\\]/g, '/');
}
Expand All @@ -27,6 +55,9 @@ module.exports = function(source, map) {
const options = Object.assign({}, this.options, getOptions(this));
const callback = this.async();

const isServer = this.target === 'node' || (options.generate && options.generate == 'ssr');
const isProduction = this.minimize || process.env.NODE_ENV === 'production';

options.filename = this.resourcePath;
options.format = this.version === 1 ? options.format || 'cjs' : 'es';
options.shared =
Expand All @@ -51,6 +82,12 @@ module.exports = function(source, map) {
utimesSync(tmpFile, new Date(atime.getTime() - 99999), new Date(mtime.getTime() - 99999));
}

if (options.hotReload && !isProduction && !isServer) {
const hotOptions = Object.assign({}, options.hotOptions);
const id = JSON.stringify(relative(process.cwd(), options.filename));
code = makeHot(id, code, hotOptions);
}

callback(null, code, map);
}, err => callback(err)).catch(err => {
// wrap error to provide correct
Expand Down
40 changes: 40 additions & 0 deletions lib/hot-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Registry, configure as configureProxy, createProxy } from 'svelte-dev-helper';

let hotOptions = {
noPreserveState: false
};

export function configure(options) {
hotOptions = Object.assign(hotOptions, options);
configureProxy(hotOptions);
}

export function register(id, component) {

//store original component in registry
Registry.set(id, {
rollback: null,
component,
instances: []
});

return createProxy(id);
}

export function reload(id, component) {

const record = Registry.get(id);

//keep reference to previous version to enable rollback
record.rollback = record.component;

//replace component in registry with newly loaded component
record.component = component;

Registry.set(id, record);

//re-render the proxies
record.instances.slice().forEach(function(instance) {
instance && instance._rerender();
});
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"all": "npm run lint && npm run test",
"test": "mocha --harmony --full-trace --check-leaks",
"lint": "eslint index.js test/**/*.js"
"lint": "eslint index.js lib/*.js test/**/*.js"
},
"keywords": [
"svelte",
Expand All @@ -16,7 +16,8 @@
],
"dependencies": {
"loader-utils": "^1.1.0",
"tmp": "0.0.31"
"tmp": "0.0.31",
"svelte-dev-helper": "^1.1.2"
},
"devDependencies": {
"chai": "^3.5.0",
Expand Down
64 changes: 64 additions & 0 deletions test/loader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,70 @@ describe('loader', () => {

});
});

describe('hotReload', () => {
it(
'should configure hotReload=false (default)',
testLoader(
'test/fixtures/good.html',
function(err, code, map) {
expect(err).not.to.exist;

expect(code).not.to.contain('module.hot.accept();');
},
{}
)
);

it(
'should configure hotReload=true',
testLoader(
'test/fixtures/good.html',
function(err, code, map) {
expect(err).not.to.exist;

expect(code).to.contain('module.hot.accept();');
expect(code).not.to.contain('configure({"noPreserveState":true});');
},
{ hotReload: true }
)
);

it(
'should configure hotReload=true & hotOptions',
testLoader(
'test/fixtures/good.html',
function(err, code, map) {
expect(err).not.to.exist;

expect(code).to.contain('module.hot.accept();');
expect(code).to.contain('configure({"noPreserveState":true});');
},
{
hotReload: true,
hotOptions: {
noPreserveState: true
}
}
)
);

it(
'should ignore hotReload when generate=ssr',
testLoader(
'test/fixtures/good.html',
function(err, code, map) {
expect(err).not.to.exist;

expect(code).not.to.contain('module.hot.accept();');
},
{
hotReload: true,
generate:'ssr'
}
)
);
});
});
});

Expand Down