Skip to content
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
44 changes: 30 additions & 14 deletions electron/previewClient/morphAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,28 +180,44 @@ export function loadStyles(page: Document): void {
// to the page reaches the browser as a reload, which this patch replaces, and
// nothing else would ever bring the page's new CSS: the canvas kept the
// stylesheet the page loaded with, whatever the style panel wrote. The
// rendering just fetched holds every dev stylesheet as the server compiled it
// for this edit, so a live one whose text differs takes the new text. One
// whose text is the same is left alone: a rewritten stylesheet restarts every
// animation it defines, on every keystroke of a text edit.
export function refreshDevStyles(page: Document): void {
const fresh = new Map<string, string>();
const served = page.querySelectorAll('style[data-vite-dev-id]');
for (let i = 0; i < served.length; i++) {
const id = served[i]?.getAttribute('data-vite-dev-id');
if (id !== null && id !== undefined) {
fresh.set(id, served[i]?.textContent ?? '');
}
}
// rendering just fetched holds that stylesheet as the server compiled it for
// this edit, so a live one takes the text the server has just changed. One the
// server renders as it did before is left alone, for two reasons. A rewritten
// stylesheet restarts every animation it defines, on every keystroke of a text
// edit. And the server's text is not always the newest: a stylesheet that
// reaches the page through a CSS \`@import\` is rendered from the server's
// first compile for as long as it runs, while Vite hot-swaps the live one on
// every write. Comparing against the live text put that first compile back on
// each patch, and the canvas dropped every style edit made to such a file.
export function refreshDevStyles(previous: Document, next: Document): void {
const before = devStyleTexts(previous);
const fresh = devStyleTexts(next);
const live = document.querySelectorAll('style[data-vite-dev-id]');
for (let i = 0; i < live.length; i++) {
const style = live[i];
const text = fresh.get(style?.getAttribute('data-vite-dev-id') ?? '');
const id = style?.getAttribute('data-vite-dev-id') ?? '';
const text = fresh.get(id);
if (style === undefined || text === undefined) {
continue; // Not in this rendering: Vite's own, or a component that left.
}
if (before.get(id) === text) {
continue; // The server did not change it: the live text is Vite's to keep.
}
if (style.textContent !== text) {
style.textContent = text;
}
}
}

// Each dev stylesheet of a server rendering, by the id Vite gave it.
function devStyleTexts(page: Document): Map<string, string> {
const texts = new Map<string, string>();
const served = page.querySelectorAll('style[data-vite-dev-id]');
for (let i = 0; i < served.length; i++) {
const id = served[i]?.getAttribute('data-vite-dev-id');
if (id !== null && id !== undefined) {
texts.set(id, served[i]?.textContent ?? '');
}
}
return texts;
}
3 changes: 2 additions & 1 deletion electron/previewClient/morphClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,12 @@ async function update(): Promise<void> {
patchAttrs(liveRoot, previousRoot, nextRoot);
patchChildren(document.head, previousDocument.head, next.clean.head);
patchChildren(document.body, previousDocument.body, next.clean.body);
const renderedBefore = previousDocument;
previousDocument = next.clean;
// After the patch, so a component that has just appeared is styled by the
// time anything measures it — and running by the time anything clicks it.
loadStyles(next.clean);
refreshDevStyles(next.clean);
refreshDevStyles(renderedBefore, next.clean);
runScripts(added);
const liveBody = document.body;
const serverBody = next.withAnchors.body;
Expand Down
62 changes: 49 additions & 13 deletions test/electron/previewClient/morph.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,17 @@ const LIVE_TABS = (labels, active) =>
// A page's <style> is inlined by the server, so its module was never imported
// and Vite answers an edit to the page with a reload, not a stylesheet update.
// The patch replaces that reload, so it is the only thing that can bring the new
// CSS: a live dev stylesheet takes the text the server rendered this time. One
// whose text did not change is not written at all — a rewritten stylesheet
// restarts every animation it defines — and Vite's own stylesheets, which the
// server's rendering does not carry, are left to Vite.
// CSS: a live dev stylesheet takes the text the server has just changed. One
// the server renders as it did before is not written at all — a rewritten
// stylesheet restarts every animation it defines — and Vite's own stylesheets,
// which the server's rendering does not carry, are left to Vite.
//
// "As it did before" is the server's previous rendering, not the live text. A
// stylesheet reached through a CSS `@import` (global.css importing
// utilities.css) is rendered from the server's first compile for as long as it
// runs, while Vite hot-swaps the live one on every write. Compared against the
// live text, each patch put that first compile back: a padding dragged in the
// style panel reached the file and never the canvas.
{
const head = dom.window.document.head;
const devStyle = (id, css) => {
Expand All @@ -569,21 +576,28 @@ const LIVE_TABS = (labels, active) =>
const PAGE = '/src/pages/index.astro?astro&type=style&index=0&lang.css';
const CARD = '/src/components/Card.astro?astro&type=style&index=0&lang.css';
const OWN = '/src/styles/vite-only.css';
const SHEET = '/src/styles/global.css';
const page = devStyle(PAGE, '.box { color: red; }');
const card = devStyle(CARD, '.card { gap: 1rem; }');
const own = devStyle(OWN, '.vite { margin: 0; }');
head.append(page, card, own);
const served = new dom.window.DOMParser().parseFromString(
'<!doctype html><html><head>' +
`<style data-vite-dev-id="${PAGE}">.box { color: blue; }</style>` +
`<style data-vite-dev-id="${CARD}">.card { gap: 1rem; }</style>` +
'</head><body></body></html>',
'text/html',
);
// Vite has hot-swapped this one since the server first compiled it.
const sheet = devStyle(SHEET, '.pad { padding: 3rem; }');
head.append(page, card, own, sheet);
const rendering = (pageCss) =>
new dom.window.DOMParser().parseFromString(
'<!doctype html><html><head>' +
`<style data-vite-dev-id="${PAGE}">${pageCss}</style>` +
`<style data-vite-dev-id="${CARD}">.card { gap: 1rem; }</style>` +
`<style data-vite-dev-id="${SHEET}">.pad { padding: 1rem; }</style>` +
'</head><body></body></html>',
'text/html',
);
const before = rendering('.box { color: red; }');
const served = rendering('.box { color: blue; }');
const writes = [];
const observer = new dom.window.MutationObserver((records) => writes.push(...records));
observer.observe(head, { childList: true, subtree: true, characterData: true });
refreshDevStyles(served);
refreshDevStyles(before, served);
writes.push(...observer.takeRecords());
observer.disconnect();
check(
Expand All @@ -600,6 +614,27 @@ const LIVE_TABS = (labels, active) =>
'a stylesheet the rendering does not carry is left to Vite',
own.textContent === '.vite { margin: 0; }',
);
check(
'a stylesheet the server renders stale keeps the text Vite gave it',
sheet.textContent === '.pad { padding: 3rem; }',
sheet.textContent,
);
// A stylesheet new to the server's rendering has no earlier text to agree with.
const arrived = devStyle(CARD, '.card { gap: 0; }');
head.append(arrived);
const without = new dom.window.DOMParser().parseFromString(
'<!doctype html><html><head></head><body></body></html>',
'text/html',
);
card.remove();
refreshDevStyles(without, served);
check(
'a stylesheet the previous rendering lacked takes the server text',
arrived.textContent === '.card { gap: 1rem; }',
arrived.textContent,
);
arrived.remove();
head.append(card);
check(
'exactly one stylesheet was written',
new Set(
Expand All @@ -612,6 +647,7 @@ const LIVE_TABS = (labels, active) =>
page.remove();
card.remove();
own.remove();
sheet.remove();
}

if (failures.length) {
Expand Down
Loading