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

Replace broken Svelte 2 HMR with the one from rixo #156

Merged
merged 89 commits into from
Jan 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
45f83ef
remove hard dep on svelte
rixo Jul 28, 2019
23fa354
style: remove trailing space
rixo Jul 28, 2019
eda88c6
extract makeHot from index.js
rixo Jul 28, 2019
5b568c8
add support for HMR with svelte 3
rixo Jul 29, 2019
dcf722e
Merge remote-tracking branch 'upstream/master' into hmr
rixo Jul 29, 2019
9dd5eec
add support for HMR with svelte-native
rixo Jul 29, 2019
66b3061
introduce cmp.$$.replace instead of capture_state & inject_state
rixo Jul 29, 2019
87b4192
better name for $$.replace
rixo Jul 29, 2019
76ec3b8
fix restore props
rixo Jul 29, 2019
c4d9f09
style: autofix
rixo Jul 29, 2019
21e0276
update native support
halfnelson Jul 30, 2019
733852f
Merge pull request #3 from halfnelson/hmr-native-fix
rixo Jul 30, 2019
b2c1d29
remove global polution, now that it isn't needed anymore
rixo Jul 30, 2019
81cfdd2
style: remove trailing space
rixo Aug 1, 2019
189a246
refactor: extract svelte resolution to its own module
rixo Aug 1, 2019
74636fb
resolve svelte from application entrypoint - fixes #109
rixo Aug 1, 2019
0089526
add support for overriding svelte location by env variable
rixo Aug 1, 2019
e106329
Merge branch 'fix-link'
rixo Aug 6, 2019
7787618
refactor: move makeHot to resolveSvelte (because it depends on svelte…
rixo Aug 6, 2019
46db2fd
Merge branch 'hmr' into hmr-next
rixo Aug 6, 2019
3122272
filter removed props using compilation infos
rixo Aug 6, 2019
5fbd73b
fix hmr support for context
rixo Aug 22, 2019
75bc4a3
better debug name for file with multiple extensions
rixo Aug 22, 2019
638aa78
Merge branch 'hmr' into hmr-next
rixo Aug 22, 2019
67e98a8
fix target / anchor in onMount when rendered to different ones (native)
rixo Sep 19, 2019
9026759
fix support for native, & TNS 6
rixo Sep 19, 2019
e69f405
backport fixes from rollup plugin
rixo Sep 28, 2019
d6e4a1a
mock $capture_state to support preserving local state
rixo Sep 28, 2019
91cfef7
respect noPreserveState option
rixo Sep 28, 2019
0e5b209
implement full reload on failure
rixo Sep 28, 2019
4c322c2
simpler error recuperation (not broken also)
rixo Sep 28, 2019
8d5701a
exclude store subscriptions from preserved local state
rixo Sep 28, 2019
1ae9f83
use svelte-hmr
rixo Sep 29, 2019
3f54b18
add missing dep
rixo Sep 30, 2019
fadd9b4
upgrade to last svelte-hmr, with error management
rixo Sep 30, 2019
4e6f38a
use last svelte-hmr
rixo Oct 1, 2019
765d64d
fix svelte-hmr incorrectly registered as a dev dep
rixo Oct 2, 2019
2204a8f
upgrade to last version of svelte-hmr
rixo Oct 25, 2019
dfbdb6f
cleaner hot-api adapter (also, fix svhs tests)
rixo Oct 25, 2019
251588e
run accept handlers serially to prevent race conditions
rixo Nov 5, 2019
3bc4671
add info message when svelte loc is overriden by env
rixo Nov 5, 2019
3278148
fix async accept handlers race condition
rixo Nov 5, 2019
2be1bd7
add webpack specific HMR done message
rixo Nov 7, 2019
4a20c99
adapt for next version of svelte-hmr
rixo Nov 7, 2019
ed4e5ed
bump svelte-hmr
rixo Nov 15, 2019
e715674
make README hot
rixo Nov 15, 2019
66fe7ac
fix tests
rixo Nov 15, 2019
7929ed5
v0.0.1-0
rixo Nov 15, 2019
73b11bb
v0.0.1-1
rixo Nov 15, 2019
40068b8
fix links
rixo Nov 22, 2019
1059907
v0.1.0
rixo Nov 22, 2019
962a0be
Update README.md
rixo Nov 27, 2019
ada239e
better warning
rixo Nov 27, 2019
bdb5c58
bump svelte-hmr for svelte 3.16+ support
rixo Dec 28, 2019
6caf00d
Merge branch 'master' of github.com:rixo/svelte-loader-hot
rixo Dec 28, 2019
7783129
more affirmative
rixo Dec 28, 2019
e2d82d9
v0.1.1-0
rixo Dec 28, 2019
8b31c10
v0.1.1
rixo Dec 28, 2019
b2d291c
bump svelte-hmr
rixo Jan 26, 2020
472668f
v0.1.2
rixo Jan 26, 2020
0c1ccaa
upgrade svelte-hmr (preservation of local state, reactive blocks, acc…
rixo Feb 27, 2020
60b7ad9
v0.2.0
rixo Feb 27, 2020
ca2e6bd
update README
rixo Feb 27, 2020
ecaa242
v0.2.1
rixo Feb 27, 2020
7367022
bump svelte-hmr (restore auto accept accessors & named exports -- fix…
rixo Feb 28, 2020
f148343
v0.3.0
rixo Feb 28, 2020
0d7fb49
Bump acorn from 6.3.0 to 6.4.1
dependabot[bot] Mar 15, 2020
e2be8af
Merge pull request #2 from rixo/dependabot/npm_and_yarn/acorn-6.4.1
rixo Mar 15, 2020
5d444ba
bump svelte-hmr to next
rixo Apr 4, 2020
25e3244
v0.3.1-0
rixo Apr 4, 2020
df80f24
Bump lodash from 4.17.15 to 4.17.19
dependabot[bot] Jul 18, 2020
4071bfc
Merge pull request #5 from rixo/dependabot/npm_and_yarn/lodash-4.17.19
rixo Jul 21, 2020
1351638
upgrade to last svelte-hmr (fixes support for svelte >= 3.24.1)
rixo Aug 24, 2020
42550eb
v0.3.1-1
rixo Aug 24, 2020
cd29428
v0.3.1
rixo Aug 24, 2020
e01f36a
Fix minor typo
rendall Aug 27, 2020
f88b4c6
Merge pull request #9 from rendall/patch-1
rixo Aug 27, 2020
93feeb4
document that dev must be true for HMR
rixo Oct 5, 2020
8b7d9bc
Merge branch 'master' of svelte-loader-hot into merge-rixo-hmr
non25 Jan 15, 2021
b5afe79
Drop Svelte 2 from HMR
non25 Jan 15, 2021
7e65739
Fix HMR test
non25 Jan 15, 2021
db9ff99
Move posixify back, remove unnecessary svelte-resolve
non25 Jan 15, 2021
780c961
Change backticks to single quotes
non25 Jan 15, 2021
931dcc9
Use matching imports in index.js
non25 Jan 16, 2021
1ff2a35
Merge README.md from rixo/svelte-loader#svelte-hmr
non25 Jan 16, 2021
ca550ea
Small changes that regards enabling emitCss and hotReload
non25 Jan 16, 2021
7925e76
Options: disable hotReload by default, remove shared
non25 Jan 16, 2021
0f1c49b
Revert hotReload
non25 Jan 16, 2021
bfb3b47
Options: remove externalDependencies
non25 Jan 16, 2021
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
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that @rixo's branch does not have the below options specified in pluginOptions

	externalDependencies: true,
	hotReload: true,
	hotOptions: true,
	shared: true,

I think shared does not exist and perhaps we don't want to enable the others by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get this from?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only disable hotReload by default and remove shared. The other two doesn't do anything in true/false manner.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the only thing it's used for is to determine if it's a compiler option:

if (!pluginOptions[option]) compileOptions[option] = options[option];

Since none of these are compiler options we should probably remove them all

Copy link
Contributor Author

@non25 non25 Jan 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to avoid making changes that are out of the scope of this PR.
Code could be improved, but in the other PR.
This place is hard to spot new comments on. 😕

Copy link
Member

@benmccann benmccann Jan 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, when I said "all" I meant the four options I originally suggested removing: externalDependencies, hotReload, hotOptions, shared

There shouldn't be any reason to pass those to the compiler since none of those four are valid compiler options: https://svelte.dev/docs#svelte_compile

Copy link
Contributor Author

@non25 non25 Jan 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, when I said "all" I meant the four options I originally suggested removing: externalDependencies, hotReload, hotOptions, shared

Doing this breaks tests and compilation. These options are PREVENTED from going to the compiler due to the pluginOptions object and code above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. Sorry I had that backwards. It's a bit confusingly written. In rollup-plugin-svelte we moved them to be under compilerOptions. We should probably do that here too, but that can be a separate change. Just removing shared and leaving the rest as-is would make sense

Copy link
Contributor Author

@non25 non25 Jan 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is done.

Yeah, that made me scratch my head for several minutes too. 😄

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