From f9f9fe4ea994f226d9ce34446e319b9bbef736bd Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Fri, 13 Nov 2026 21:13:35 -0600 Subject: [PATCH] Give each .rx module its own stylesheet; find a cold dependency's program Under vite dev, a \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 || "cold".includes(filter)) { // a dependency asked for before its entry, as Vitest's `fsModuleCache` leaves it + const { default: rx } = await import(pathToFileURL(path.join(pkg, "vite.mjs")).href); + const root = project("cold", "shop/main.rx"), catalog = path.join(root, "src/shop/catalog.rx"); + const ctx = (id, moduleId) => ({ id, moduleId, root, ssr: false, solid: { generate: "dom", hydratable: false }, addWatchFile() {} }); + try { + // a commented-out import is not an edge: read as one, it would make main.rx look imported and leave no entry + writeFileSync(catalog, readFileSync(catalog, "utf8") + `\n// import { x } from "./main.rx"\n`); + const warm = rx(); warm.compile("", ctx(path.join(root, "src/shop/main.rx"), path.join(root, "src/shop/main.rx"))); + const want = warm.compile("", ctx(catalog, `${catalog}?rx-dep`)).code; + const got = rx().compile("", ctx(catalog, `${catalog}?rx-dep`)).code; + if (got !== want) throw new Error("the dependency compiled cold differs from the one compiled after its entry"); + writeFileSync(catalog, readFileSync(catalog, "utf8") + "\nexport bad = missingName\n"); + let report = ""; + try { rx().compile("", ctx(catalog, `${catalog}?rx-dep`)); } catch (e) { report = e.message; } + if (!/catalog\.rx:\d+(:\d+)?: error /.test(report)) throw new Error(`a cold dependency that does not compile reports ${JSON.stringify(report.slice(0, 200))}`); + pass++; console.log("✓ cold dependency: compiled from the program that holds it, or that program's diagnostics"); + } catch (e) { fail++; console.log(`✗ cold dependency: ${e.message.split("\n")[0]}`); } +} await browser.close(); console.log(`\n${pass} pass, ${fail} fail (vite ${vite.version}, @solidjs/vite-plugin ${JSON.parse(readFileSync(path.join(pluginDir, "package.json"), "utf8")).version})`); process.exit(fail ? 1 : 0); diff --git a/compiler/vite.mjs b/compiler/vite.mjs index d65161b4..ed6f0275 100644 --- a/compiler/vite.mjs +++ b/compiler/vite.mjs @@ -3,8 +3,8 @@ // names it explicitly. // // The crate (its Node-API build, `src/node.rs`: what `rxc app` does) already hands each module to Solid's compiler -// (spec/11 CP-1), so a `.rx` module comes back as Solid's output for the environment's `generate`, with its source map onto the `.rx` and, on a -// program's entry, the program's stylesheet. Solid's plugin then takes it the way it takes lowered `.tsrx`: primitive +// (spec/11 CP-1), so a `.rx` module comes back as Solid's output for the environment's `generate`, with its source map onto the `.rx` and +// its own stylesheet. Solid's plugin then takes it the way it takes lowered `.tsrx`: primitive // names, `lazy()`, the HMR refresh pass, server functions (12 SF-7a), the stylesheet as a CSS module. // // Entries find themselves. JavaScript imports only a program's entry (its default export mounts the app); an `.rx` @@ -13,7 +13,7 @@ // changes. The crate's output imports its dependencies as `./x.rx?rx-dep`, and a dependency is served from the build // of a program that contains it. A module several programs share is one module: its instance is keyed by its factory // (11 CP-4b `module`), not by its place in a program. -import { existsSync, statSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -35,6 +35,29 @@ const isDep = moduleId => new URLSearchParams(moduleId.split("?")[1] ?? "").has( // `import __m0 from "./catalog.rx";` → `"./catalog.rx?rx-dep"` (one line each: the module's map is by line, CP-1c) const tagDeps = code => code.replace(/^(import \w+ from ")(\.{1,2}\/[^"?]*\.rx)(";?)$/gm, `$1$2?${DEP}$3`); +const RX_IMPORT = /^import\b[^"']*?\bfrom\s+(["'])(\.{1,2}\/[^"'?]*\.rx)\1/gm; +/** The `.rx` files under `root` from which `file` is reached by imports: first those no other `.rx` file imports. */ +function entriesOf(file, root) { + const importers = new Map(); + const walk = dir => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + if (e.name === "node_modules" || e.name.startsWith(".")) continue; + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (e.name.endsWith(".rx")) for (const [, , spec] of readFileSync(p, "utf8").matchAll(RX_IMPORT)) { + const dep = path.resolve(path.dirname(p), spec); + importers.set(dep, [...(importers.get(dep) ?? []), p]); + } + } + }; + walk(root); + const seen = new Set([file]); + for (const f of seen) for (const i of importers.get(f) ?? []) seen.add(i); + seen.delete(file); + const top = f => !importers.has(f); + return [...seen].sort().sort((a, b) => top(b) - top(a)); +} + /** @returns {import("@solidjs/vite-plugin").Frontend} */ export default function rx() { const builds = new Map(); // entry\0target\0moduleName\0spa → { entry, target, moduleName, spa, out, stamps } @@ -50,28 +73,38 @@ export default function rx() { builds.set(key, b); return b; } + const built = (file, target, moduleName, spa) => + [...builds.values()].filter(b => b.target === target && b.moduleName === moduleName && b.spa === spa && b.out.modules?.[file]); /** A dependency: from a build of a program that has it, rebuilt if that program changed. */ - function containing(file, target, moduleName, spa) { - const has = [...builds.values()].filter(b => b.target === target && b.moduleName === moduleName && b.spa === spa && b.out.modules?.[file]); + function containing(file, target, moduleName, spa, root) { + const has = built(file, target, moduleName, spa); const b = has.find(fresh) ?? has[0]; - if (!b) throw new Error(`${file} is imported as an .rx dependency, but no program built so far contains it`); - return build(b.entry, target, moduleName, spa); + if (b) return build(b.entry, target, moduleName, spa); + // the entry's compile was skipped (Vitest's `fsModuleCache` served it), so find the program from the files + let failed; + for (const entry of entriesOf(file, root)) { + const found = build(entry, target, moduleName, spa); + if (found.out.modules?.[file]) return found; + if (!failed && found.out.diagnostics) failed = found; // the program holds it but does not compile: report why + } + if (failed) return failed; + throw new Error(`${file} is imported as an .rx dependency, but no program under ${root} contains it`); } return { - compile(_source, { id, moduleId, solid, addWatchFile }) { + compile(_source, { id, moduleId, root, solid, addWatchFile }) { const file = path.resolve(id); const target = solid.generate === "ssr" ? "ssr" : solid.generate === "universal" ? "universal" : "dom"; const moduleName = solid.moduleName ?? "@solidjs/web"; // 08 CN-3d: a client-only build (no SSR, nothing hydrates) has nothing to show on a first load nothing holds const spa = target === "dom" && solid.hydratable === false; const dep = isDep(moduleId); - const { entry, out } = dep ? containing(file, target, moduleName, spa) : build(file, target, moduleName, spa); + const { out } = dep ? containing(file, target, moduleName, spa, path.resolve(root ?? process.cwd())) : build(file, target, moduleName, spa); if (out.diagnostics) throw new Error(report(out.diagnostics)); if (out.error) throw new Error(out.error); const m = out.modules[file]; for (const p of Object.keys(out.modules)) if (p !== file) addWatchFile(p); - // CSS-4: the program's stylesheet, linked once, from its entry - return { code: tagDeps(m.code), map: m.map, css: !dep && file === entry ? out.css : "" }; + // CSS-4: each module links its own stylesheet, so a module several programs share links it once + return { code: tagDeps(m.code), map: m.map, css: m.css ?? "" }; }, }; }