Skip to content

Commit 7b81c00

Browse files
authored
Replace broken Svelte 2 HMR with rixo's (#156)
1 parent bbfff1f commit 7b81c00

File tree

6 files changed

+170
-123
lines changed

6 files changed

+170
-123
lines changed

README.md

+58-49
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,12 @@ This should create an additional `styles.css.map` file.
145145

146146
### Hot Reload
147147

148-
Hot reloading is turned off by default, you can turn it on using the `hotReload` option as shown below:
148+
This loader supports component-level HMR via the community supported [svelte-hmr](https://github.com/rixo/svelte-hmr) package. This package serves as a testbed and early access for Svelte HMR, while we figure out how to best include HMR support in the compiler itself (which is tricky to do without unfairly favoring any particular dev tooling). Feedback, suggestion, or help to move HMR forward is welcomed at [svelte-hmr](https://github.com/rixo/svelte-hmr/issues) (for now).
149+
150+
Configure inside your `webpack.config.js`:
149151

150152
```javascript
153+
module.exports = {
151154
...
152155
module: {
153156
rules: [
@@ -158,68 +161,74 @@ Hot reloading is turned off by default, you can turn it on using the `hotReload`
158161
use: {
159162
loader: 'svelte-loader',
160163
options: {
161-
hotReload: true
164+
// NOTE Svelte's dev mode MUST be enabled for HMR to work
165+
// -- in a real config, you'd probably set it to false for prod build,
166+
// based on a env variable or so
167+
dev: true,
168+
169+
// NOTE emitCss: true is currently not supported with HMR
170+
// Enable it for production to output separate css file
171+
emitCss: false,
172+
// Enable HMR only for dev mode
173+
hotReload: true, // Default: false
174+
// Extra HMR options
175+
hotOptions: {
176+
// Prevent preserving local component state
177+
noPreserveState: false,
178+
179+
// If this string appears anywhere in your component's code, then local
180+
// state won't be preserved, even when noPreserveState is false
181+
noPreserveStateKey: '@!hmr',
182+
183+
// Prevent doing a full reload on next HMR update after fatal error
184+
noReload: false,
185+
186+
// Try to recover after runtime errors in component init
187+
optimistic: false,
188+
189+
// --- Advanced ---
190+
191+
// Prevent adding an HMR accept handler to components with
192+
// accessors option to true, or to components with named exports
193+
// (from <script context="module">). This have the effect of
194+
// recreating the consumer of those components, instead of the
195+
// component themselves, on HMR updates. This might be needed to
196+
// reflect changes to accessors / named exports in the parents,
197+
// depending on how you use them.
198+
acceptAccessors: true,
199+
acceptNamedExports: true,
200+
}
162201
}
163202
}
164203
}
165204
...
166205
]
167-
}
168-
...
206+
},
207+
plugins: [
208+
new webpack.HotModuleReplacementPlugin(),
209+
...
210+
]
211+
}
169212
```
170213

171-
#### Hot reload rules and caveats:
172-
173-
- `_rerender` and `_register` are reserved method names, please don't use them in `methods:{...}`
174-
- Turning `dev` mode on (`dev:true`) is **not** necessary.
175-
- 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)).
176-
- When modifying the `<script>` part of your component, instances will be replaced and re-rendered in place too.
177-
However if your component has lifecycle methods that produce global side-effects, you might need to reload the whole page.
178-
- If you are using `svelte/store`, a full reload is required if you modify `store` properties
179-
214+
You also need to add the [HotModuleReplacementPlugin](https://webpack.js.org/plugins/hot-module-replacement-plugin/). There are multiple ways to achieve this.
180215

181-
Components will **not** be hot reloaded in the following situations:
182-
1. `process.env.NODE_ENV === 'production'`
183-
2. Webpack is minifying code
184-
3. Webpack's `target` is `node` (i.e SSR components)
185-
4. `generate` option has a value of `ssr`
216+
If you're using webpack-dev-server, you can just pass it the [`hot` option](https://webpack.js.org/configuration/dev-server/#devserverhot) to add the plugin automatically.
186217

187-
#### Stop preserving state
218+
Otherwise, you can add it to your webpack config directly:
188219

189-
Sometimes it might be necessary for some components to avoid state preservation on hot reload.
190-
191-
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:
192220
```js
193-
export default {
194-
setup(comp){
195-
comp.noPreserveState = true;
196-
},
197-
data(){return {...}},
198-
oncreate(){...}
199-
}
200-
```
221+
const webpack = require('webpack');
201222

202-
Or, on a global basis by adding `{noPreserveState: true}` to `hotOptions`. For example:
203-
```js
204-
{
205-
test: /\.(html|svelte)$/,
206-
exclude: /node_modules/,
207-
use: [
208-
{
209-
loader: 'svelte-loader',
210-
options: {
211-
hotReload: true,
212-
hotOptions: {
213-
noPreserveState: true
214-
}
215-
}
216-
}
217-
]
218-
}
223+
module.exports = {
224+
...
225+
plugins: [
226+
new webpack.HotModuleReplacementPlugin(),
227+
...
228+
]
229+
}
219230
```
220231

221-
**Please Note:** If you are using `svelte/store`, `noPreserveState` has no effect on `store` properties. Neither locally, nor globally.
222-
223232
#### External Dependencies
224233

225234
If you rely on any external dependencies (files required in a preprocessor for example) you might want to watch these files for changes and re-run svelte compile.

index.js

+4-31
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,21 @@
11
const { relative } = require('path');
22
const { getOptions } = require('loader-utils');
3-
4-
const hotApi = require.resolve('./lib/hot-api.js');
5-
3+
const { makeHot } = require('./lib/make-hot.js');
64
const { compile, preprocess } = require('svelte/compiler');
75

86
const pluginOptions = {
9-
externalDependencies: true,
107
hotReload: true,
118
hotOptions: true,
129
preprocess: true,
1310
emitCss: true,
1411

1512
// legacy
1613
onwarn: true,
17-
shared: true,
1814
style: true,
1915
script: true,
2016
markup: true
2117
};
2218

23-
function makeHot(id, code, hotOptions) {
24-
const options = JSON.stringify(hotOptions);
25-
const replacement = `
26-
if (module.hot) {
27-
const { configure, register, reload } = require('${posixify(hotApi)}');
28-
29-
module.hot.accept();
30-
31-
if (!module.hot.data) {
32-
// initial load
33-
configure(${options});
34-
$2 = register(${id}, $2);
35-
} else {
36-
// hot update
37-
$2 = reload(${id}, $2);
38-
}
39-
}
40-
41-
export default $2;
42-
`;
43-
44-
return code.replace(/(export default ([^;]*));/, () => replacement);
45-
}
46-
4719
function posixify(file) {
4820
return file.replace(/[/\\]/g, '/');
4921
}
@@ -120,7 +92,8 @@ module.exports = function(source, map) {
12092
}
12193
}
12294

123-
let { js, css, warnings } = normalize(compile(processed.toString(), compileOptions));
95+
const compiled = compile(processed.toString(), compileOptions);
96+
let { js, css, warnings } = normalize(compiled);
12497

12598
warnings.forEach(
12699
options.onwarn
@@ -131,7 +104,7 @@ module.exports = function(source, map) {
131104
if (options.hotReload && !isProduction && !isServer) {
132105
const hotOptions = Object.assign({}, options.hotOptions);
133106
const id = JSON.stringify(relative(process.cwd(), compileOptions.filename));
134-
js.code = makeHot(id, js.code, hotOptions);
107+
js.code = makeHot(id, js.code, hotOptions, compiled, source, compileOptions);
135108
}
136109

137110
if (options.emitCss && css.code) {

lib/hot-api.js

+87-35
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,103 @@
1-
import { Registry, configure as configureProxy, createProxy } from 'svelte-dev-helper';
1+
import { makeApplyHmr } from 'svelte-hmr/runtime';
22

3-
let hotOptions = {
4-
noPreserveState: false
5-
};
3+
// eslint-disable-next-line no-undef
4+
const g = typeof window !== 'undefined' ? window : global;
65

7-
export function configure(options) {
8-
hotOptions = Object.assign(hotOptions, options);
9-
configureProxy(hotOptions);
10-
}
6+
const globalKey =
7+
typeof Symbol !== 'undefined'
8+
? Symbol('SVELTE_LOADER_HOT')
9+
: '__SVELTE_LOADER_HOT';
1110

12-
export function register(id, component) {
11+
if (!g[globalKey]) {
12+
// do updating refs counting to know when a full update has been applied
13+
let updatingCount = 0;
1314

14-
//store original component in registry
15-
Registry.set(id, {
16-
rollback: null,
17-
component,
18-
instances: []
19-
});
15+
const notifyStart = () => {
16+
updatingCount++;
17+
};
2018

21-
//create the proxy itself
22-
const proxy = createProxy(id);
19+
const notifyError = reload => err => {
20+
const errString = (err && err.stack) || err;
21+
// eslint-disable-next-line no-console
22+
console.error(
23+
'[HMR] Failed to accept update (nollup compat mode)',
24+
errString
25+
);
26+
reload();
27+
notifyEnd();
28+
};
2329

24-
//patch the registry record with proxy constructor
25-
const record = Registry.get(id);
26-
record.proxy = proxy;
27-
Registry.set(id, record);
30+
const notifyEnd = () => {
31+
updatingCount--;
32+
if (updatingCount === 0) {
33+
// NOTE this message is important for timing in tests
34+
// eslint-disable-next-line no-console
35+
console.log('[HMR:Svelte] Up to date');
36+
}
37+
};
2838

29-
return proxy;
39+
g[globalKey] = {
40+
hotStates: {},
41+
notifyStart,
42+
notifyError,
43+
notifyEnd,
44+
};
3045
}
3146

32-
export function reload(id, component) {
47+
const runAcceptHandlers = acceptHandlers => {
48+
const queue = [...acceptHandlers];
49+
const next = () => {
50+
const cur = queue.shift();
51+
if (cur) {
52+
return cur(null).then(next);
53+
} else {
54+
return Promise.resolve(null);
55+
}
56+
};
57+
return next();
58+
};
3359

34-
const record = Registry.get(id);
60+
export const applyHmr = makeApplyHmr(args => {
61+
const { notifyStart, notifyError, notifyEnd } = g[globalKey];
62+
const { m, reload } = args;
3563

36-
//keep reference to previous version to enable rollback
37-
record.rollback = record.component;
64+
let acceptHandlers = (m.hot.data && m.hot.data.acceptHandlers) || [];
65+
let nextAcceptHandlers = [];
3866

39-
//replace component in registry with newly loaded component
40-
record.component = component;
67+
m.hot.dispose(data => {
68+
data.acceptHandlers = nextAcceptHandlers;
69+
});
4170

42-
Registry.set(id, record);
71+
const dispose = (...args) => m.hot.dispose(...args);
4372

44-
//re-render the proxy instances
45-
record.instances.slice().forEach(function(instance) {
46-
instance && instance._rerender();
73+
const accept = handler => {
74+
if (nextAcceptHandlers.length === 0) {
75+
m.hot.accept();
76+
}
77+
nextAcceptHandlers.push(handler);
78+
};
79+
80+
const check = status => {
81+
if (status === 'ready') {
82+
notifyStart();
83+
} else if (status === 'idle') {
84+
runAcceptHandlers(acceptHandlers)
85+
.then(notifyEnd)
86+
.catch(notifyError(reload));
87+
}
88+
};
89+
90+
m.hot.addStatusHandler(check);
91+
92+
m.hot.dispose(() => {
93+
m.hot.removeStatusHandler(check);
4794
});
4895

49-
//return the original proxy constructor that was `register()`-ed
50-
return record.proxy;
51-
}
96+
const hot = {
97+
data: m.hot.data,
98+
dispose,
99+
accept,
100+
};
101+
102+
return Object.assign({}, args, { hot });
103+
});

lib/make-hot.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const { walk } = require('svelte/compiler');
2+
const { createMakeHot } = require('svelte-hmr');
3+
4+
const hotApi = require.resolve('./hot-api.js');
5+
6+
const makeHot = createMakeHot({
7+
walk,
8+
meta: 'module',
9+
hotApi,
10+
});
11+
12+
module.exports.makeHot = makeHot;

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
],
1717
"dependencies": {
1818
"loader-utils": "^1.1.0",
19-
"svelte-dev-helper": "^1.1.9"
19+
"svelte-dev-helper": "^1.1.9",
20+
"svelte-hmr": "^0.12.2"
2021
},
2122
"devDependencies": {
2223
"chai": "^4.1.2",

0 commit comments

Comments
 (0)