Skip to content
Open
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
4 changes: 2 additions & 2 deletions compiler/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn target(name: &str) -> Target {
}
}

/// `{entry, css, modules: {path: {code, map}}}`, or `{diagnostics: [...]}` / `{error}`. `entry` is an absolute path.
/// `{entry, css, modules: {path: {code, map, css}}}` (the program's stylesheet, and each module's own), or `{diagnostics: [...]}` / `{error}`. `entry` is an absolute path.
/// `spa`: a client-only build, checked under 08 CN-3d.
pub fn app(entry: &str, target: Target, module_name: &str, spa: bool) -> Value {
// module keys are POSIX paths (`modules::join_path`); Windows takes `D:/a/b.rx` as it takes `D:\a\b.rx`
Expand All @@ -49,7 +49,7 @@ pub fn app(entry: &str, target: Target, module_name: &str, spa: bool) -> Value {
let path = &out.paths[name];
let text = read_disk(path).unwrap_or_default();
let l = lower_view(module, path, target, module_name, Some((path, &text))).map_err(|e| json!({ "error": e }))?;
modules.insert(path.clone(), json!({ "code": l.code, "map": l.map }));
modules.insert(path.clone(), json!({ "code": l.code, "map": l.map, "css": out.module_css[name] }));
}
Ok(json!({ "entry": set.entry, "css": out.css, "modules": modules }))
})()
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ pub struct Compiled {
pub entry: String,
/// CSS-4: the modules' stylesheets, beside their JavaScript
pub css: String,
/// CSS-4: each module's own stylesheet, by its output name
pub module_css: IndexMap<String, String>,
}

/// 09 MO-1: one ES module per `.rx` module. A dependency's module is a factory over the app (`__rt.module` runs it once per
Expand All @@ -197,8 +199,10 @@ pub fn compile_linked(set: &ModuleSet, extra: &[Vec<Stmt>], by_source: bool) ->
g.by_source = by_source;
let mut files = IndexMap::new();
let mut paths = IndexMap::new();
let mut module_css = IndexMap::new();
for path in &set.order {
let p = &set.programs[path];
let css0 = g.css.len();
g.begin_module(path, &p.decls);
let mut deps: Vec<&str> = vec![];
for d in &p.decls {
Expand Down Expand Up @@ -232,10 +236,11 @@ pub fn compile_linked(set: &ModuleSet, extra: &[Vec<Stmt>], by_source: bool) ->
fbody.append(&mut inner);
body.push(S::ExportDefault(Func { name: Some("module".into()), params: vec![P::Id("__rt".into())], directives: vec![], body: fbody, is_async: false, generator: false }));
}
module_css.insert(g.out(path), g.css[css0..].join("\n"));
files.insert(g.out(path), body);
paths.insert(g.out(path), path.clone());
}
Ok(Compiled { files, paths, entry: g.out(&set.entry), css: g.css.join("\n") })
Ok(Compiled { files, paths, entry: g.out(&set.entry), css: g.css.join("\n"), module_css })
}

struct Gen<'p> {
Expand Down
44 changes: 43 additions & 1 deletion compiler/test/vite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,51 @@ if (!filter || "shared".includes(filter)) { // two programs on one page, shari
if (!filter || "todo".includes(filter)) { // the dev server: modules transformed on request
const dir = project("todo-dev", "todo.rx");
const server = await vite.createServer(config(dir, "todo.rx")); await server.listen(0);
await drive("todo (dev)", `http://127.0.0.1:${server.httpServer.address().port}/`, todo);
await drive("todo (dev)", server.resolvedUrls.local[0], todo);
await server.close();
}
if (!filter || "styles".includes(filter)) { // CSS-4 under the dev server: a dependency's <style> edit reaches the page
const dir = project("styles-dev", "styled/main.rx");
mkdirSync(path.join(dir, "src/styled"));
writeFileSync(path.join(dir, "src/styled/main.rx"), `import { Badge } from "./badge.rx"\n\nexport default component Main() {\n <main><Badge /></main>\n}\n`);
const badge = path.join(dir, "src/styled/badge.rx");
const style = color => `export component Badge() {\n <style>\n .badge { color: ${color} }\n </style>\n <p class="badge">badge</p>\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);
55 changes: 44 additions & 11 deletions compiler/vite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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";
Expand All @@ -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 }
Expand All @@ -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 ?? "" };
},
};
}
Loading