Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c66a5b3
Write the pagination as a TypeScript LitElement
panarch Oct 24, 2026
5abad99
Write the confirm dialogs as TypeScript LitElements
panarch Oct 24, 2026
8cfc3a0
Write the shared components as TypeScript LitElements
panarch Oct 24, 2026
e2069e9
Write Review, Git, and GitHub as TypeScript LitElements
panarch Oct 25, 2026
13072c7
Write Settings and Notes as TypeScript LitElements
panarch Oct 25, 2026
368efad
Write the Task conversation as TypeScript LitElements
panarch Oct 25, 2026
025b655
Write the Composer and New Task as TypeScript LitElements
panarch Oct 25, 2026
310a944
Write the Task list as TypeScript LitElements
panarch Oct 25, 2026
5f6a45c
Write Task Detail as TypeScript LitElements
panarch Oct 25, 2026
576fc94
Write the App Shell and workspace as TypeScript LitElements
panarch Oct 26, 2026
5200092
Write the non-visual frontend modules in TypeScript
panarch Oct 26, 2026
203e6e5
Declare Action Hints and keyboard contexts in Lit templates
panarch Oct 26, 2026
ac1d60e
Read Action Hints from what Lit last drew
panarch Oct 26, 2026
3b01318
Start the app shell from app.ts
panarch Oct 27, 2026
48f8c08
Draw on Lit's update where drawing at once served nothing
panarch Oct 27, 2026
f42004f
Build the frontend with tsc alone
panarch Oct 27, 2026
1aff191
Declare Lit properties and registration with standard decorators
panarch Oct 27, 2026
dcda669
Type custom event details in the modules that own them
panarch Oct 27, 2026
b992add
Check that frontend functions return on every path
panarch Oct 27, 2026
601cc17
Merge remote-tracking branch 'origin/main' into typescript-lit-element
panarch Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/workflows/browser-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ jobs:
- name: Restore Cargo cache
uses: Swatinem/rust-cache@v2

- name: Install Node.js 22
- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 24
cache: npm
cache-dependency-path: frontend/package-lock.json

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/documentation-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
ref: ${{ github.sha }}
persist-credentials: false

- name: Install Node.js 22
- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 24

- name: Test documentation contracts
run: node --test docs/tests/*.test.mjs
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,25 @@ jobs:
ref: ${{ github.sha }}
persist-credentials: false

- name: Install Node.js 22
- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 24
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci
working-directory: frontend

- name: Check frontend types
run: npm run typecheck
working-directory: frontend

- name: Lint frontend TypeScript
run: npm run lint
working-directory: frontend

- name: Test frontend units
run: npm run test:unit
working-directory: frontend
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/macos-packaging-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
ref: ${{ github.sha }}
persist-credentials: false

- name: Install Node.js 22
- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 24

# These verify shell scripts, workflow definitions, and documentation, so
# they need no macOS host. The Swift programs and the packaging-metadata
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/macos-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,18 @@ jobs:
# Packaging requires a clean main worktree on this runner.
git checkout -B main "${REQUESTED_SHA}"

- name: Install Node.js 22
- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 24
package-manager-cache: false

# The server embeds Lit and tslib from frontend/node_modules and
# compiles its TypeScript modules with the TypeScript installed there.
- name: Install frontend build dependencies
run: npm ci --omit=dev
working-directory: frontend

- name: Bump the release version locally
if: startsWith(inputs.action, 'release-')
id: bump
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/repository-tooling-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
ref: ${{ github.sha }}
persist-credentials: false

- name: Install Node.js 22
- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 24

- name: Test repository tooling
run: node --test scripts/tests/*.test.mjs
13 changes: 13 additions & 0 deletions .github/workflows/rust-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ jobs:
- name: Restore Cargo cache
uses: Swatinem/rust-cache@v2

- name: Install Node.js 24
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: frontend/package-lock.json

# The server embeds Lit and tslib from frontend/node_modules and
# compiles its TypeScript modules with the TypeScript installed there.
- name: Install frontend build dependencies
run: npm ci --omit=dev
working-directory: frontend

- name: Check formatting
run: cargo fmt --check

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/target
/frontend/node_modules
/frontend/tests/unit-build
/playwright-report
/test-results
/frontend/tests/e2e/*-snapshots
Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ the changed boundary, and record what was and was not verified.
The supported contributor baseline is:

- the Rust toolchain pinned by `rust-toolchain.toml`;
- Node.js 22 and npm;
- Node.js 24 or newer and npm;
- Git;
- CMake and the Xcode Command Line Tools on macOS;
- the authenticated standalone Codex CLI for Codex live work;
Expand All @@ -29,6 +29,9 @@ xcode-select --install

The npm package lives in `frontend/`, so JavaScript and browser commands run
from that directory while Rust commands stay at the repository root. The
server embeds Lit from `frontend/node_modules` and runs Node.js to build the
frontend's TypeScript modules, so run `npm ci` there before the first Cargo
build in a checkout or worktree, and keep Node.js on `PATH` while building. The
ordinary browser suite also needs Playwright's Chromium build:

```sh
Expand Down
3 changes: 3 additions & 0 deletions caffold/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ whisper-rs = { version = "0.16.0", features = ["tracing_backend"] }
[target.'cfg(target_os = "macos")'.dependencies]
whisper-rs = { version = "0.16.0", features = ["metal"] }

[build-dependencies]
serde_json = "1.0.140"

[dev-dependencies]
tempfile = "3.20.0"
tokio = { version = "1.45.1", features = ["test-util"] }
132 changes: 129 additions & 3 deletions caffold/build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde_json::Value;
use std::{
env, fs,
path::PathBuf,
process::Command,
path::{Path, PathBuf},
process::{Command, Stdio},
time::{SystemTime, UNIX_EPOCH},
};

Expand All @@ -10,6 +11,23 @@ fn main() {
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=Cargo.toml");

let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("Cargo OUT_DIR must be set"));

for package in ["lit", "tslib", "typescript"] {
if let Err(problem) = check_installed_package(package) {
println!("cargo::error={problem}; run `npm ci` in frontend/ before building");
return;
}
}
let frontend = out_dir.join("frontend");
if let Err(problem) = compile_frontend(&frontend) {
for line in problem.lines() {
println!("cargo::error={line}");
}
return;
}
write_source_map_table(&out_dir, &frontend);

let commit =
git_output(&["rev-parse", "--short=8", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
let dirty = git_output(&["status", "--porcelain", "--untracked-files=no"])
Expand All @@ -28,14 +46,122 @@ fn main() {
let module = format!(
"export const BUILD_INFO = Object.freeze({{\n id: {build_id:?},\n label: {build_id:?},\n version: {version:?},\n commit: {commit_label:?},\n number: {build_number},\n}});\n"
);
let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("Cargo OUT_DIR must be set"));
fs::write(out_dir.join("build-info.js"), module).expect("build info module must be written");

println!("cargo:rustc-env=CAFFOLD_BUILD_ID={build_id}");
println!("cargo:rustc-env=CAFFOLD_BUILD_LABEL={build_id}");
println!("cargo:rustc-env=CAFFOLD_BUILD_NUMBER={build_number}");
}

// The static asset table embeds Lit and tslib from frontend/node_modules, and
// the TypeScript there compiles the frontend modules; each must be the version
// frontend/package.json names.
fn check_installed_package(name: &str) -> Result<(), String> {
let wanted = read_json("../frontend/package.json")
.and_then(|package| package["dependencies"][name].as_str().map(str::to_owned))
.ok_or_else(|| format!("frontend/package.json does not name a {name} version"))?;
let installed = read_json(&format!("../frontend/node_modules/{name}/package.json"))
.and_then(|package| package["version"].as_str().map(str::to_owned));
match installed {
Some(installed) if installed == wanted => Ok(()),
Some(installed) => Err(format!(
"frontend/node_modules holds {name} {installed}, but frontend/package.json needs {wanted}"
)),
None => Err(format!(
"frontend/node_modules does not hold {name} {wanted}"
)),
}
}

// The static asset table embeds each frontend TypeScript module as the
// JavaScript and source map tsc writes to the same path under `output`. The
// page modules and the service worker are typed for different globals, so
// each has its own project; a type error in either stops the build.
fn compile_frontend(output: &Path) -> Result<(), String> {
if output.exists() {
fs::remove_dir_all(output)
.map_err(|error| format!("{} could not be cleared: {error}", output.display()))?;
}
let compilers = ["tsconfig.json", "tsconfig.worker.json"].map(|project| {
let child = Command::new("node")
.arg("../frontend/node_modules/typescript/bin/tsc")
.arg("--project")
.arg(format!("../frontend/{project}"))
.args(["--noEmit", "false", "--outDir"])
.arg(output)
.stdout(Stdio::piped())
.spawn();
(project, child)
});
let mut problems = Vec::new();
for (project, child) in compilers {
let compiled = child
.and_then(|child| child.wait_with_output())
.map_err(|error| {
format!(
"Node.js must be on PATH to compile the frontend's TypeScript modules: {error}"
)
})?;
if !compiled.status.success() {
problems.push(format!(
"tsc could not compile frontend/{project} ({})",
compiled.status
));
problems.extend(
String::from_utf8_lossy(&compiled.stdout)
.lines()
.map(str::to_owned),
);
}
}
if problems.is_empty() {
Ok(())
} else {
Err(problems.join("\n"))
}
}

// The static asset table answers `<module>.js.map` from this table of every
// source map tsc wrote.
fn write_source_map_table(out_dir: &Path, frontend: &Path) {
let mut maps = Vec::new();
collect_source_maps(frontend, frontend, &mut maps);
maps.sort();
let arms: String = maps
.iter()
.map(|map| {
let file = format!("/frontend/{map}");
format!(
" {map:?} => Some(include_bytes!(concat!(env!(\"OUT_DIR\"), {file:?}))),\n"
)
})
.collect();
let table = format!(
"fn compiled_source_map(path: &str) -> Option<&'static [u8]> {{\n match path {{\n{arms} _ => None,\n }}\n}}\n"
);
fs::write(out_dir.join("source_maps.rs"), table).expect("source map table must be written");
}

fn collect_source_maps(root: &Path, directory: &Path, maps: &mut Vec<String>) {
for entry in fs::read_dir(directory).expect("compiled frontend must be readable") {
let path = entry
.expect("compiled frontend entry must be readable")
.path();
if path.is_dir() {
collect_source_maps(root, &path, maps);
} else if path.to_string_lossy().ends_with(".js.map") {
let relative = path
.strip_prefix(root)
.expect("map must be under the compiled frontend");
maps.push(relative.to_string_lossy().into_owned());
}
}
}

fn read_json(path: &str) -> Option<Value> {
serde_json::from_str(&fs::read_to_string(path).ok()?).ok()
}

fn git_output(args: &[&str]) -> Option<String> {
let output = Command::new("git").args(args).output().ok()?;
output
Expand Down
8 changes: 6 additions & 2 deletions caffold/src/app/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,12 @@ mod tests {
let first = render_service_worker(source, "first-build").unwrap();
let second = render_service_worker(source, "second\"\\\nbuild").unwrap();

assert!(first.starts_with("const CACHE_NAME = \"caffold-shell-first-build\";"));
assert!(second.starts_with("const CACHE_NAME = \"caffold-shell-second\\\"\\\\\\nbuild\";"));
assert!(
first.starts_with("\"use strict\";\nconst CACHE_NAME = \"caffold-shell-first-build\";")
);
assert!(second.starts_with(
"\"use strict\";\nconst CACHE_NAME = \"caffold-shell-second\\\"\\\\\\nbuild\";"
));
assert_ne!(first, second);
assert!(!first.contains("__CAFFOLD_BUILD_ID__"));
assert!(!second.contains("__CAFFOLD_BUILD_ID__"));
Expand Down
2 changes: 1 addition & 1 deletion caffold/src/fs/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ fn is_long_line(line: &str) -> bool {
}

/// The six colors the viewers have. Their numbers are the order in which
/// `frontend/components/file-viewer/syntax-spans.js` names its classes.
/// `frontend/components/file-viewer/syntax-spans.ts` names its classes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SyntaxColor {
Comment = 0,
Expand Down
Loading
Loading