sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
79.68k stars 4.22k forks source link

HTML white space entities  /  not rendered correctly #7112

Open janosh opened 3 years ago

janosh commented 3 years ago

Describe the bug

Putting   or   at the start of lines seems to be mishandled in the latest version of kit

"devDependencies": {
  "@sveltejs/kit": "^1.0.0-next.190",
  "svelte": "^3.42.6",
},

Reproduction

npm init svelte@next test-html-ents

Pick the skeleton project, install deps, start dev server, then paste

<p>
    &emsp;&emsp;
    {#each [1, 2, 3] as idx}
        <span>{idx}</span>
    {/each}
</p>

into src/routes/index.svelte. The white space that should be inserted by &emsp;&emsp; in front of the numbers only briefly flashes on reloading the page.

Logs

If you then delete one &emsp;, the compiler warns

[vite-plugin-svelte] ignoring compiler output js change for /repos/test-html-ents/src/routes/index.svelte as it is equal to previous output after normalization

System Info

System:
    OS: macOS 11.6
    CPU: (16) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 475.04 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 17.0.1 - /usr/local/bin/node
    Yarn: 1.22.17 - /usr/local/bin/yarn
    npm: 8.1.0 - /usr/local/bin/npm
  Browsers:
    Brave Browser: 95.1.31.87
    Safari: 15.0
  npmPackages:
    @sveltejs/adapter-static: ^1.0.0-next.21 => 1.0.0-next.21 
    @sveltejs/kit: ^1.0.0-next.190 => 1.0.0-next.190 
    svelte: ^3.42.6 => 3.44.0

Severity

annoyance

dominikg commented 3 years ago

The warning is emitted by vite-plugin-svelte when it detects that a file change results in similar compiler output to inform the user of a skipped hmr update.

The output comparison omits add_location() calls as the arguments change when you edit whitespace. In this case it seems that the svelte compiler itself strips the &emsp; value, so when you remove one of the 2, an add_location call changes and causes the warning you posted.

https://github.com/sveltejs/vite-plugin-svelte/blob/51a1449a88e469ab7bb9ae438ebdb742240481e6/packages/vite-plugin-svelte/src/handle-hot-update.ts#L106

It seems the svelte compiler removes &emsp; if it is not accompanied by additional non-whitespace characters within the same textnode. you can try it in the svelte repl: https://svelte.dev/repl . <p>x;&emsp;<span>y</span></p> shows the emsp, <p>&emsp;<span>y</span></p> doesn't.

This may even be correct according to spec. https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace I havn't been able to find definitions on how emsp should be treated. https://stackoverflow.com/questions/42463838/html-emsp-entity-exact-behavior is an old question regarding it but not really answered.

janosh commented 3 years ago

This may even be correct according to spec.

Even if (or especially if) the spec says not to render white space entities without following non-whitespace content, it shouldn't be briefly flashing in kit on reloads, right?

dominikg commented 3 years ago

Could be a difference in SSR rendering/hydration. I'll try to summon someone with deeper svelte compiler knowledge.

baseballyama commented 2 years ago

I found 2 things about it.

Summary

1. Differences of compiled file between SSR and hydration.

Should we update white space processing for SSR mode in the compiler?

2. Why briefly flashing in kit on reloads is not happen

This is expected behavior I think.

Details

1. Differences of compiled file between SSR and hydration.

Regarding compiler, SSR mode just render what a developer writes. https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/render_ssr/handlers/Text.ts But for hydration, there is whitespace normalization process. https://github.com/sveltejs/svelte/blob/4d4f959f1643f45702c71c01dff13f43f64fef77/src/compiler/compile/render_dom/wrappers/Text.ts#L28-L41

And when Kit creates entry HTML, it compiles as SSR mode. But bundled index.svelte is created as hydration mode. That's why you can see spaces briefly flashes at the beginning because entry HTML has compiled HTML by SSR mode which doesn't have whitespace processing.

I think already we followed whitespace processing for hydration. But for SSR is not like that. Should we follow this? I think we should follow because an initial HTML should be the same between SSR and hydration.

entry HTML ```js

   123

```
bundled index.svelte ```js import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/routes/index.svelte");/* C:/Users/baseballyama/Desktop/git/svelte-contributes/kit-2693/my-app/src/routes/index.svelte generated by Svelte v3.44.1 */ import { SvelteComponentDev, add_location, append_hydration_dev, children, claim_element, claim_space, claim_text, destroy_each, detach_dev, dispatch_dev, element, init, insert_hydration_dev, noop, safe_not_equal, space, text, validate_each_argument, validate_slots } from "/node_modules/.vite/svelte_internal.js?v=54e66b1a"; const file = "C:/Users/baseballyama/Desktop/git/svelte-contributes/kit-2693/my-app/src/routes/index.svelte"; function get_each_context(ctx, list, i) { const child_ctx = ctx.slice(); child_ctx[0] = list[i]; return child_ctx; } // (3:1) {#each [1, 2, 3] as idx} function create_each_block(ctx) { let span; let t; const block = { c: function create() { span = element("span"); t = text(/*idx*/ ctx[0]); this.h(); }, l: function claim(nodes) { span = claim_element(nodes, "SPAN", {}); var span_nodes = children(span); t = claim_text(span_nodes, /*idx*/ ctx[0]); span_nodes.forEach(detach_dev); this.h(); }, h: function hydrate() { add_location(span, file, 3, 2, 46); }, m: function mount(target, anchor) { insert_hydration_dev(target, span, anchor); append_hydration_dev(span, t); }, p: noop, d: function destroy(detaching) { if (detaching) detach_dev(span); } }; dispatch_dev("SvelteRegisterBlock", { block, id: create_each_block.name, type: "each", source: "(3:1) {#each [1, 2, 3] as idx}", ctx }); return block; } function create_fragment(ctx) { let p; let t; let each_value = [1, 2, 3]; validate_each_argument(each_value); let each_blocks = []; for (let i = 0; i < 3; i += 1) { each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i)); } const block = { c: function create() { p = element("p"); t = space(); for (let i = 0; i < 3; i += 1) { each_blocks[i].c(); } this.h(); }, l: function claim(nodes) { p = claim_element(nodes, "P", {}); var p_nodes = children(p); t = claim_space(p_nodes); for (let i = 0; i < 3; i += 1) { each_blocks[i].l(p_nodes); } p_nodes.forEach(detach_dev); this.h(); }, h: function hydrate() { add_location(p, file, 0, 0, 0); }, m: function mount(target, anchor) { insert_hydration_dev(target, p, anchor); append_hydration_dev(p, t); for (let i = 0; i < 3; i += 1) { each_blocks[i].m(p, null); } }, p: noop, i: noop, o: noop, d: function destroy(detaching) { if (detaching) detach_dev(p); destroy_each(each_blocks, detaching); } }; dispatch_dev("SvelteRegisterBlock", { block, id: create_fragment.name, type: "component", source: "", ctx }); return block; } function instance($$self, $$props) { let { $$slots: slots = {}, $$scope } = $$props; validate_slots('Routes', slots, []); const writable_props = []; Object.keys($$props).forEach(key => { if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') console.warn(` was created with unknown prop '${key}'`); }); return []; } class Routes extends SvelteComponentDev { constructor(options) { super(options); init(this, options, instance, create_fragment, safe_not_equal, {}); dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "Routes", options, id: create_fragment.name }); } } import * as ___SVELTE_HMR_HOT_API from '/node_modules/svelte-hmr/runtime/hot-api-esm.js';import { adapter as ___SVELTE_HMR_HOT_API_PROXY_ADAPTER } from '/node_modules/svelte-hmr/runtime/proxy-adapter-dom.js';if (import.meta && import.meta.hot) { if (false) import.meta.hot.accept(); Routes = ___SVELTE_HMR_HOT_API.applyHmr({ m: import.meta, id: "C:/Users/baseballyama/Desktop/git/svelte-contributes/kit-2693/my-app/src/routes/index.svelte", hotOptions: {"preserveLocalState":false,"noPreserveStateKey":["@hmr:reset","@!hmr"],"preserveAllLocalStateKey":"@hmr:keep-all","preserveLocalStateKey":"@hmr:keep","noReload":false,"optimistic":true,"acceptNamedExports":true,"acceptAccessors":true,"injectCss":false,"cssEjectDelay":100,"native":false,"importAdapterName":"___SVELTE_HMR_HOT_API_PROXY_ADAPTER","noOverlay":true}, Component: Routes, ProxyAdapter: ___SVELTE_HMR_HOT_API_PROXY_ADAPTER, acceptable: true, preserveLocalState: false, emitCss: true, }); } export default Routes; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O29CQUdTLEdBQUc7Ozs7OztzQ0FBSCxHQUFHOzs7Ozs7OztHQUFWLG9CQUFpQjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7bUJBRFYsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQUZoQixvQkFLRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7IiwibmFtZXMiOltdLCJzb3VyY2VzIjpbImluZGV4LnN2ZWx0ZSJdLCJzb3VyY2VzQ29udGVudCI6WyI8cD5cblx0JmVtc3A7JmVtc3A7XG5cdHsjZWFjaCBbMSwgMiwgM10gYXMgaWR4fVxuXHRcdDxzcGFuPntpZHh9PC9zcGFuPlxuXHR7L2VhY2h9XG48L3A+Il19 ```

2. Why briefly flashing in kit on reloads is not happen

In the case of HMR, entry HTML is not updated. It just updates bundled index.svelte. I think this behavior is ok in terms of developer experience.

I mentioned that bundled index.svelte is created by hydration mode. Therefore if you change only spaces, hydration mode will normalize spaces, and bundled index.svelte will not change. Then both entry HTML and bundled index.svelte are not changed and briefly flashing is not happen. Therefore I think this is expected behavior.