Skip to content

Commit

Permalink
Replace broken Svelte 2 HMR with rixo's (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
non25 committed Jan 16, 2021
1 parent bbfff1f commit 7b81c00
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 123 deletions.
107 changes: 58 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,12 @@ 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:
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).

Configure inside your `webpack.config.js`:

```javascript
module.exports = {
...
module: {
rules: [
Expand All @@ -158,68 +161,74 @@ Hot reloading is turned off by default, you can turn it on using the `hotReload`
use: {
loader: 'svelte-loader',
options: {
hotReload: true
// NOTE Svelte's dev mode MUST be enabled for HMR to work
// -- in a real config, you'd probably set it to false for prod build,
// based on a env variable or so
dev: true,

// NOTE emitCss: true is currently not supported with HMR
// Enable it for production to output separate css file
emitCss: false,
// Enable HMR only for dev mode
hotReload: true, // Default: false
// Extra HMR options
hotOptions: {
// Prevent preserving local component state
noPreserveState: false,

// If this string appears anywhere in your component's code, then local
// state won't be preserved, even when noPreserveState is false
noPreserveStateKey: '@!hmr',

// Prevent doing a full reload on next HMR update after fatal error
noReload: false,

// Try to recover after runtime errors in component init
optimistic: false,

// --- Advanced ---

// Prevent adding an HMR accept handler to components with
// accessors option to true, or to components with named exports
// (from <script context="module">). This have the effect of
// recreating the consumer of those components, instead of the
// component themselves, on HMR updates. This might be needed to
// reflect changes to accessors / named exports in the parents,
// depending on how you use them.
acceptAccessors: true,
acceptNamedExports: true,
}
}
}
}
...
]
}
...
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
...
]
}
```

#### 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

You also need to add the [HotModuleReplacementPlugin](https://webpack.js.org/plugins/hot-module-replacement-plugin/). There are multiple ways to achieve this.

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`
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.

#### Stop preserving state
Otherwise, you can add it to your webpack config directly:

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(){...}
}
```
const webpack = require('webpack');

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
}
}
}
]
}
module.exports = {
...
plugins: [
new webpack.HotModuleReplacementPlugin(),
...
]
}
```

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

#### External Dependencies

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.
Expand Down
35 changes: 4 additions & 31 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,21 @@
const { relative } = require('path');
const { getOptions } = require('loader-utils');

const hotApi = require.resolve('./lib/hot-api.js');

const { makeHot } = require('./lib/make-hot.js');
const { compile, preprocess } = require('svelte/compiler');

const pluginOptions = {
externalDependencies: true,
hotReload: true,
hotOptions: true,
preprocess: true,
emitCss: true,

// legacy
onwarn: true,
shared: true,
style: true,
script: true,
markup: true
};

function makeHot(id, code, hotOptions) {
const options = JSON.stringify(hotOptions);
const replacement = `
if (module.hot) {
const { configure, register, reload } = require('${posixify(hotApi)}');
module.hot.accept();
if (!module.hot.data) {
// initial load
configure(${options});
$2 = register(${id}, $2);
} else {
// hot update
$2 = reload(${id}, $2);
}
}
export default $2;
`;

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

function posixify(file) {
return file.replace(/[/\\]/g, '/');
}
Expand Down Expand Up @@ -120,7 +92,8 @@ module.exports = function(source, map) {
}
}

let { js, css, warnings } = normalize(compile(processed.toString(), compileOptions));
const compiled = compile(processed.toString(), compileOptions);
let { js, css, warnings } = normalize(compiled);

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

if (options.emitCss && css.code) {
Expand Down
122 changes: 87 additions & 35 deletions lib/hot-api.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,103 @@
import { Registry, configure as configureProxy, createProxy } from 'svelte-dev-helper';
import { makeApplyHmr } from 'svelte-hmr/runtime';

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

export function configure(options) {
hotOptions = Object.assign(hotOptions, options);
configureProxy(hotOptions);
}
const globalKey =
typeof Symbol !== 'undefined'
? Symbol('SVELTE_LOADER_HOT')
: '__SVELTE_LOADER_HOT';

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

//store original component in registry
Registry.set(id, {
rollback: null,
component,
instances: []
});
const notifyStart = () => {
updatingCount++;
};

//create the proxy itself
const proxy = createProxy(id);
const notifyError = reload => err => {
const errString = (err && err.stack) || err;
// eslint-disable-next-line no-console
console.error(
'[HMR] Failed to accept update (nollup compat mode)',
errString
);
reload();
notifyEnd();
};

//patch the registry record with proxy constructor
const record = Registry.get(id);
record.proxy = proxy;
Registry.set(id, record);
const notifyEnd = () => {
updatingCount--;
if (updatingCount === 0) {
// NOTE this message is important for timing in tests
// eslint-disable-next-line no-console
console.log('[HMR:Svelte] Up to date');
}
};

return proxy;
g[globalKey] = {
hotStates: {},
notifyStart,
notifyError,
notifyEnd,
};
}

export function reload(id, component) {
const runAcceptHandlers = acceptHandlers => {
const queue = [...acceptHandlers];
const next = () => {
const cur = queue.shift();
if (cur) {
return cur(null).then(next);
} else {
return Promise.resolve(null);
}
};
return next();
};

const record = Registry.get(id);
export const applyHmr = makeApplyHmr(args => {
const { notifyStart, notifyError, notifyEnd } = g[globalKey];
const { m, reload } = args;

//keep reference to previous version to enable rollback
record.rollback = record.component;
let acceptHandlers = (m.hot.data && m.hot.data.acceptHandlers) || [];
let nextAcceptHandlers = [];

//replace component in registry with newly loaded component
record.component = component;
m.hot.dispose(data => {
data.acceptHandlers = nextAcceptHandlers;
});

Registry.set(id, record);
const dispose = (...args) => m.hot.dispose(...args);

//re-render the proxy instances
record.instances.slice().forEach(function(instance) {
instance && instance._rerender();
const accept = handler => {
if (nextAcceptHandlers.length === 0) {
m.hot.accept();
}
nextAcceptHandlers.push(handler);
};

const check = status => {
if (status === 'ready') {
notifyStart();
} else if (status === 'idle') {
runAcceptHandlers(acceptHandlers)
.then(notifyEnd)
.catch(notifyError(reload));
}
};

m.hot.addStatusHandler(check);

m.hot.dispose(() => {
m.hot.removeStatusHandler(check);
});

//return the original proxy constructor that was `register()`-ed
return record.proxy;
}
const hot = {
data: m.hot.data,
dispose,
accept,
};

return Object.assign({}, args, { hot });
});
12 changes: 12 additions & 0 deletions lib/make-hot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { walk } = require('svelte/compiler');
const { createMakeHot } = require('svelte-hmr');

const hotApi = require.resolve('./hot-api.js');

const makeHot = createMakeHot({
walk,
meta: 'module',
hotApi,
});

module.exports.makeHot = makeHot;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
],
"dependencies": {
"loader-utils": "^1.1.0",
"svelte-dev-helper": "^1.1.9"
"svelte-dev-helper": "^1.1.9",
"svelte-hmr": "^0.12.2"
},
"devDependencies": {
"chai": "^4.1.2",
Expand Down
Loading

0 comments on commit 7b81c00

Please sign in to comment.