Skip to content
Draft
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
1 change: 1 addition & 0 deletions compiler/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions compiler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ serde_json = { version = "1", features = ["preserve_order"] }
solidjs-compiler = { git = "https://github.com/solidjs/solid", rev = "2105ffb8ca2fced55f5fac0763177fc93f97c2c2", default-features = false }
oxc_allocator = "0.144"
oxc_ast = "0.144"
oxc_codegen = "0.144"
oxc_span = "0.144"
oxc_sourcemap = "8.1"
oxc_syntax = "0.144"
Expand Down
22 changes: 16 additions & 6 deletions compiler/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! compiler for `target`, each importing its dependencies by `.rx` path, with the program's stylesheet. What `rxc app`
//! prints and the Node-API build (`src/node.rs`) returns to `@reactivescript/compiler/vite`.
use crate::check::check_modules;
use crate::emit::{compile_linked, lower_view, Target};
use crate::emit::{compile_linked, lower_view, view_jsx, Target};
use crate::modules::load_modules;
use serde_json::{json, Map, Value};
use std::path::Path;
Expand All @@ -26,16 +26,22 @@ pub fn target(name: &str) -> Target {
/// `{entry, css, modules: {path: {code, map}}}`, 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 {
app_with(entry, target, module_name, spa, false, &read_disk)
}

/// `app` over `read` in place of the disk: the WebAssembly build's host, or an editor's open files. `jsx`: each module
/// also carries the JSX Solid's compiler was handed (`view_jsx`).
pub fn app_with(entry: &str, target: Target, module_name: &str, spa: bool, jsx: bool, read: &dyn Fn(&str) -> Option<String>) -> Value {
// module keys are POSIX paths (`modules::join_path`); Windows takes `D:/a/b.rx` as it takes `D:\a\b.rx`
let entry = &entry.replace('\\', "/");
(|| {
if read_disk(entry).is_none() {
if read(entry).is_none() {
return Err(json!({ "error": format!("{entry}: no such file") }));
}
let set = load_modules(entry, &read_disk).map_err(|e| {
let set = load_modules(entry, read).map_err(|e| {
// a parse error is a diagnostic like the checker's, with its code (PARSE, or the habit it names)
let file = e.2.clone().unwrap_or_else(|| entry.to_string());
let mut d = crate::serve::parse_error_in(&file, &e, read_disk(&file).as_deref());
let mut d = crate::serve::parse_error_in(&file, &e, read(&file).as_deref());
d["msg"] = json!(crate::serve::parse_message(&e));
json!({ "diagnostics": [d] })
})?;
Expand All @@ -47,9 +53,13 @@ pub fn app(entry: &str, target: Target, module_name: &str, spa: bool) -> Value {
let mut modules = Map::new();
for (name, module) in &out.files {
let path = &out.paths[name];
let text = read_disk(path).unwrap_or_default();
let text = read(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 }));
let mut m = json!({ "code": l.code, "map": l.map });
if jsx {
m["jsx"] = json!(view_jsx(module));
}
modules.insert(path.clone(), m);
}
Ok(json!({ "entry": set.entry, "css": out.css, "modules": modules }))
})()
Expand Down
7 changes: 7 additions & 0 deletions compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,13 @@ pub struct Lowered {
pub map: Option<String>,
}

/// A module as the JSX Solid's compiler is handed, printed: what `lower_view` compiles, before templates.
pub fn view_jsx(module: &[S]) -> String {
let alloc = oxc_allocator::Allocator::default();
let program = crate::oxc::Build::new(&alloc).program(module);
oxc_codegen::Codegen::new().build(&program).code
}

/// Each module's JSX → JavaScript for `target` (`ref/compile/jsx.ts`): dom and ssr with hydration; universal over the
/// renderer `module_name`. With `rx` (the module's `.rx` path and text), the output carries a source map onto it. CP-1c:
/// Solid's positions are the program's layout; each maps to the `.rx` line it was lowered from (`oxc::Build::lines`), at
Expand Down
12 changes: 12 additions & 0 deletions compiler/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,11 +1216,16 @@ pub fn parse_block(c: &mut Cursor) -> R<Vec<Stmt>> {
c.ws(true);
let mut out = vec![];
while !c.starts_with("}") {
let at = c.i;
out.push(parse_stmt(c)?);
out.append(&mut c.pending);
c.ws(true);
c.eat(";");
c.ws(true);
// an editor's parse reads a missing statement as an empty one, so at the end of input nothing moves
if c.i == at {
break;
}
}
c.expect("}")?;
Ok(out)
Expand Down Expand Up @@ -2256,6 +2261,13 @@ mod recovering {
assert!(parse_program(src).is_err());
}

#[test]
fn an_unclosed_block_at_the_end_of_input_stops() {
let src = "export default component App() {\n <button onClick={() => { c";
let (_, errs) = parse_program_recovering(src);
assert!(!errs.is_empty());
}

#[test]
fn function_types_in_props_and_type_aliases() {
let src = "type Pick = (id: number) => void\ncomponent Row(props: { id: number; onPick: (id: number) => void; onDrop: Pick }) {\n source n: number := peek(props.id)\n <li onClick={() => { props.onPick(n) }}>{n}</li>\n}\nfn twice(x: number): number => x * 2\nexport default component App() {\n <Row id={twice(1)} onPick={id => { log(id) }} onDrop={id => { log(id) }} />\n}\n";
Expand Down
23 changes: 20 additions & 3 deletions compiler/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
use serde_json::{json, Value};
use std::collections::HashMap;

/// One request: `analyze` (`{file, overlays, prelude}`) or `prelude`.
/// One request: `analyze` (`{file, overlays, prelude, project}`), `app` (`{entry, overlays, target, moduleName, spa, jsx}`) or `prelude`.
pub fn request(method: &str, params: &Value, disk: &dyn Fn(&str) -> Option<String>) -> Value {
match method {
"analyze" => analyze(params, disk),
"prelude" => json!(crate::project::PRELUDE),
"app" => app(params, disk),
_ => json!({ "error": "unknown method" }),
}
}
Expand Down Expand Up @@ -60,12 +61,17 @@ pub fn utf16_index(s: &str) -> Vec<u32> {
idx
}

/// A request's `overlays`: the editor's open files, read before the disk.
fn overlays(params: &Value) -> HashMap<String, String> {
params["overlays"].as_object().map(|o| o.iter().filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect()).unwrap_or_default()
}

/// `rxc serve`, one request: `{file, overlays: {path: text}, prelude}` — the file loaded (and its imports) as an editor
/// has them, recovering (CP-8e); its projection with mappings in UTF-16 offsets, and its diagnostics: the parse's, else
/// the checker's.
pub fn analyze(params: &Value, disk: &dyn Fn(&str) -> Option<String>) -> Value {
let file = params["file"].as_str().unwrap_or_default().to_string();
let overlays: HashMap<String, String> = params["overlays"].as_object().map(|o| o.iter().filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect()).unwrap_or_default();
let overlays = overlays(params);
let read = |p: &str| overlays.get(p).cloned().or_else(|| disk(p));
let set = match crate::modules::load_modules_recovering(&file, &read) {
Ok(s) => s,
Expand All @@ -77,7 +83,9 @@ pub fn analyze(params: &Value, disk: &dyn Fn(&str) -> Option<String>) -> Value {
let src = set.sources.get(&file).cloned().unwrap_or_default();
let prelude = params["prelude"].as_str();
let mut out = json!({ "code": null, "mappings": [] });
if let Some(p) = crate::project::project_modules_with(&set, prelude).into_iter().find(|p| p.rx == file) {
// `project: false`: diagnostics only, for a caller that does not show the TSX projection
let project = params["project"].as_bool().unwrap_or(true);
if let Some(p) = project.then(|| crate::project::project_modules_with(&set, prelude).into_iter().find(|p| p.rx == file)).flatten() {
let (gi, si) = (utf16_index(&p.code), utf16_index(&src));
let maps: Vec<[u32; 3]> = p.mappings.iter().map(|m| [gi[m.gen as usize], si[m.src as usize], si[(m.src + m.len) as usize] - si[m.src as usize]]).collect();
let u = |i: &[u32], a: u32, n: u32| [i[a as usize], i[(a + n) as usize] - i[a as usize]];
Expand All @@ -93,3 +101,12 @@ pub fn analyze(params: &Value, disk: &dyn Fn(&str) -> Option<String>) -> Value {
out
}


/// `rxc app` over the editor's files: what `app.rs` returns for `entry`, reading `overlays` before the disk.
pub fn app(params: &Value, disk: &dyn Fn(&str) -> Option<String>) -> Value {
let overlays = overlays(params);
let read = |p: &str| overlays.get(p).cloned().or_else(|| disk(p));
let target = crate::app::target(params["target"].as_str().unwrap_or("dom"));
let module_name = params["moduleName"].as_str().unwrap_or("@solidjs/web");
crate::app::app_with(params["entry"].as_str().unwrap_or_default(), target, module_name, params["spa"].as_bool().unwrap_or(false), params["jsx"].as_bool().unwrap_or(false), &read)
}
1 change: 1 addition & 0 deletions site/.assetsignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
wrangler.toml
.assetsignore
.wrangler
playground/
4 changes: 4 additions & 0 deletions site/playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
test-results
public/rxc.wasm
35 changes: 35 additions & 0 deletions site/playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ReactiveScript Playground

![The playground: .rx on the left, the same program as Solid 2.0 primitives on the right](docs/screenshot.png)

Type `.rx` on the left and see it as Solid 2.0 on the right: the program written with Solid's own primitives
(`createSignal`, `createMemo`, `createOptimistic`, `action`, `<Errored>`, `<Loading>`) and the checker's diagnostics
(each linked to `guide/diagnostics.md#<code>`). The program is kept in the URL hash, so a link reopens it.

The Solid view is the JSX the crate hands to Solid's compiler, with each `__rt` helper that is a Solid primitive
underneath written as that primitive and the mangled names (`v$id`, `w$id`) made plain (`src/solidify.ts`), then
formatted with Prettier. It is a reading aid, not the code that runs.

## How it compiles in the browser

`public/rxc.wasm` is the crate (`compiler/` in this repository) built for `wasm32-unknown-unknown`, the same build the
language server loads (`compiler/src/wasm.rs`: `rx_alloc`, `rx_call`, `rx_free`, and a host `env.rx_read`). The page
answers `rx_read` from the editor's files in memory, so there is no server and no filesystem. Each keystroke sends
`analyze` (diagnostics) for every `.rx` file and `app` with `jsx: true` for `main.rx`.

The `app` request is new: `app::app_with` takes a reader in place of the disk, `serve.rs` routes `app` to it, and with
`jsx: true` each module also carries the JSX printed before Solid's compiler runs (`emit::view_jsx`, over
`oxc_codegen`). `pnpm wasm` rebuilds `../../compiler` and copies the result into `public/` (it is not checked in).

The playground is not part of the deployed site: `site/.assetsignore` leaves `playground/` out of the static upload.
Publishing it would need a build step (`pnpm wasm && pnpm build`) and its `dist/` served under `/playground/`.

## Run

```sh
. ~/.local/share/solid-toolchain/env.sh
corepack pnpm@11.9.0 install
corepack pnpm@11.9.0 wasm # needs Rust with the wasm32-unknown-unknown target
corepack pnpm@11.9.0 dev # vite --host on port 3020
corepack pnpm@11.9.0 test # Playwright against the running dev server
```
Binary file added site/playground/docs/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions site/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/logo.svg" type="image/svg+xml" />
<link rel="preload" href="/rxc.wasm" as="fetch" type="application/wasm" crossorigin />
<meta name="color-scheme" content="dark" />
<link rel="stylesheet" href="/src/style.css" />
<title>ReactiveScript Playground</title>
<meta name="description" content="Type ReactiveScript and see the Solid 2.0 it compiles to." />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions site/playground/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "reactivescript-playground",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host --port 3020",
"build": "vite build",
"preview": "vite preview --host --port 3020",
"wasm": "sh scripts/build-wasm.sh",
"test": "node tests/playground.check.mjs"
},
"dependencies": {
"@solidjs/web": "2.0.0-rc.21",
"highlight.js": "^11.11.1",
"prettier": "^3.10.1",
"solid-js": "2.0.0-rc.21"
},
"devDependencies": {
"@solidjs/vite-plugin": "3.0.0-next.57",
"playwright-core": "^1.56.0",
"typescript": "^5.9.3",
"vite": "^8.3.8"
}
}
Loading
Loading