diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ea93c..a2fb8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +**New** + +- Under `vite dev`, an edit inside a dependency's components re-renders them in place instead of reloading the page: + the module's top-level sources and every other component's state are kept. An edit to the module's top level or to + an entry still reloads. The runtime's new `@reactivescript/runtime/hot` entry does the swap; it needs a + `@solidjs/vite-plugin` with the front end's `hotUpdate` hook. + **Fixed** - `affects` on a keyed or structured source that takes a guess threw `INVALID_AFFECTS_TARGET` in a dev build, so the diff --git a/compiler/README.md b/compiler/README.md index 17769fb..94a2bdd 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -88,8 +88,14 @@ What happens to a `.rx` module in Vite: The app mounts a program with `createApp` from `@reactivescript/runtime` (`../runtime`, CP-4a), as the examples do. -Not yet: hot updates of `.rx` modules are untested; server rendering in Vite -needs a request-scoped app for server functions to run in. +Hot updates: with a Solid plugin that has the front end's `hotUpdate` hook, +an edit inside a dependency's components (markup, logic, `\n
badge
\n}\n`; + writeFileSync(badge, style("rgb(0, 0, 255)")); + const server = await vite.createServer(config(dir, "styled/main.rx")); await server.listen(0); + const url = server.resolvedUrls.local[0]; + const color = page => page.$eval(".badge", e => getComputedStyle(e).color); + const until = async (page, want, what) => { + for (let i = 0; i < 40 && await color(page).catch(() => "") !== want; i++) await page.waitForTimeout(100); + const got = await color(page).catch(e => e.message.split("\n")[0]); if (got !== want) throw new Error(`${what}: color ${got}, not ${want}`); + }; + await drive("styles (dev)", url, async page => { + await until(page, "rgb(0, 0, 255)", "first load"); + writeFileSync(badge, style("rgb(255, 0, 0)")); + await until(page, "rgb(255, 0, 0)", "after the edit"); + await page.reload(); await until(page, "rgb(255, 0, 0)", "after a reload"); + const fresh = await browser.newPage(); + try { await fresh.goto(url); await until(fresh, "rgb(255, 0, 0)", "in a new page"); } finally { await fresh.close(); } + }); + await server.close(); +} +if (!filter || "hot".includes(filter)) { // a dependency's component edit swaps in place; a top-level or entry edit reloads + const dir = project("hot-dev", "hot/main.rx"); + mkdirSync(path.join(dir, "src/hot")); + const main = path.join(dir, "src/hot/main.rx"), panel = path.join(dir, "src/hot/panel.rx"); + writeFileSync(main, `import { Panel } from "./panel.rx"\nimport { Other } from "./other.rx"\nimport { Tick } from "./tick.rx"\n\nexport default component Main() {\n{first}
\n}\n`); + const lib = path.join(dir, "src/hot/lib.mjs"); + writeFileSync(lib, `export const word = () => "one";\n`); + const source = ({ top = "", label = "total", color = "rgb(0, 0, 255)", step = 1 } = {}) => + `import pure { word } from "./lib"\n\n${top}source total := 0\n\nexport component Panel() {\n said = word()\n \n \n{said}
\n}\n`; + writeFileSync(panel, source()); + const server = await vite.createServer(config(dir, "hot/main.rx")); await server.listen(0); + await drive("hot (dev)", server.resolvedUrls.local[0], async page => { + const seen = async () => ({ + panel: (await page.textContent(".panel").catch(() => "")).trim(), other: (await page.textContent(".other").catch(() => "")).trim(), + color: await page.$eval(".panel", e => getComputedStyle(e).color).catch(() => ""), kept: await page.evaluate(() => window.__kept === 1).catch(() => false), + said: (await page.textContent(".said").catch(() => "")).trim(), + }); + const until = async (what, want) => { + let got; + for (let i = 0; i < 50; i++) { got = await seen(); if (Object.entries(want).every(([k, v]) => got[k] === v)) return; await page.waitForTimeout(100); } + throw new Error(`${what}: ${JSON.stringify(got)}, not ${JSON.stringify(want)}`); + }; + for (let i = 0; i < 3; i++) await page.click(".panel"); + for (let i = 0; i < 2; i++) await page.click(".other"); + await page.evaluate(() => { window.__kept = 1; }); + await until("before the edits", { panel: "total 3", other: "other 2" }); + // the module-level source and the other module's component state survive; the page is not reloaded + writeFileSync(panel, source({ label: "sum" })); + await until("after a markup edit", { panel: "sum 3", other: "other 2", kept: true }); + writeFileSync(panel, source({ label: "sum", color: "rgb(255, 0, 0)" })); + await until("after a