diff --git a/app/.yarnrc.yml b/app/.yarnrc.yml
index 2f81962..4e08a33 100644
--- a/app/.yarnrc.yml
+++ b/app/.yarnrc.yml
@@ -6,6 +6,7 @@ nodeLinker: node-modules
# allow-list and disables lifecycle scripts by default; the build needs both on.
approvedGitRepositories:
- "https://github.com/ComlineProject/simulator.git"
+ - "https://github.com/ComlineProject/examples.git"
enableScripts: true
# That WASM build isn't byte-reproducible (wasm-opt runs only where it's
diff --git a/app/index.html b/app/index.html
index 75b53cf..f9c9261 100644
--- a/app/index.html
+++ b/app/index.html
@@ -23,6 +23,9 @@
diff --git a/app/package.json b/app/package.json
index bf92022..8ab8137 100644
--- a/app/package.json
+++ b/app/package.json
@@ -8,7 +8,7 @@
"dev": "yarn wasm && vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
- "test": "tsc -p tsconfig.test.json && node --import tsx --test \"src/sim/**/*.test.ts\""
+ "test": "tsc -p tsconfig.test.json && node --import tsx --test \"src/**/*.test.ts\""
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.0",
@@ -22,6 +22,7 @@
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.34.0",
"@lezer/highlight": "^1.2.3",
+ "comline-examples": "git+https://github.com/ComlineProject/examples.git#f63894988ea2cc5db98ff97cc0fbdfceb5af6bb7",
"comline-simulator": "git+https://github.com/ComlineProject/simulator.git#eff27ce9b077ec411c299743b328a24089387215"
},
"devDependencies": {
diff --git a/app/src/examples.test.ts b/app/src/examples.test.ts
new file mode 100644
index 0000000..0e1fb6b
--- /dev/null
+++ b/app/src/examples.test.ts
@@ -0,0 +1,38 @@
+/// Every example schema (from the `comline-examples` git dependency) compiles
+/// against *this* build of the editor wasm — so a `comline-core` change that
+/// breaks one is caught here, not in the playground UI.
+
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { strict as assert } from "node:assert";
+import { test } from "node:test";
+
+import examples from "comline-examples";
+
+const initWasm = (await import("./wasm/comline_playground_wasm.js")).default;
+const { compile_project } = await import("./wasm/comline_playground_wasm.js");
+await initWasm(
+ readFileSync(fileURLToPath(new URL("./wasm/comline_playground_wasm_bg.wasm", import.meta.url))),
+);
+
+interface Example {
+ id: string;
+ entry: string;
+ files: { name: string; source: string }[];
+}
+
+for (const ex of examples as Example[]) {
+ test(`example "${ex.id}" compiles with no errors`, () => {
+ assert.ok(
+ ex.files.some((f) => f.name === ex.entry),
+ `entry "${ex.entry}" is one of the files`,
+ );
+ const res = compile_project(
+ ex.files.map((f) => ({ path: f.name, source: f.source })),
+ ) as { diagnostics?: { severity?: unknown; message?: string }[] };
+ const errors = (res.diagnostics ?? []).filter(
+ (d) => d.severity === "error" || d.severity === 1 || d.severity === "Error",
+ );
+ assert.equal(errors.length, 0, `diagnostics: ${JSON.stringify(errors)}`);
+ });
+}
diff --git a/app/src/main.ts b/app/src/main.ts
index f42e365..0e41329 100644
--- a/app/src/main.ts
+++ b/app/src/main.ts
@@ -23,36 +23,19 @@ import type {
import { createSim, type ProjectShape, type SimView } from "./sim/index.ts";
import initSimWasm from "comline-simulator";
import simWasmUrl from "comline-simulator/pkg/comline_simulator_bg.wasm?url";
+import examplesData from "comline-examples";
-// ── sample: two files, one `use`ing the other ────────────────────────────
-const SAMPLE_FILES: { name: string; doc: string }[] = [
- {
- name: "chat.ids",
- doc: `use types::Message
-
-error Rejected {
- message = "rejected: {self.reason}"
- reason: string
-}
-
-@framing = "jsonrpc"
-protocol Chat {
- /// Request/response with a raised error.
- function send(text: string) -> Message ! Rejected;
- /// Fire-and-forget.
- function note(text: string);
-}
-`,
- },
- {
- name: "types.ids",
- doc: `struct Message {
- body: string
- seq: u64
+// ── examples ────────────────────────────────────────────────────────────
+// `.ids` projects from ComlineProject/examples (a git dependency; bundled to
+// one JSON in its `prepare`). The first is what a fresh visit opens.
+interface Example {
+ id: string;
+ title: string;
+ blurb: string;
+ entry: string;
+ files: { name: string; source: string }[];
}
-`,
- },
-];
+const EXAMPLES = examplesData as Example[];
// ── virtual file set ────────────────────────────────────────────────────
interface SchemaFile {
@@ -71,6 +54,23 @@ const nextId = () => `f${++uid}`;
const activeFile = () => files.find((f) => f.id === activeId)!;
const project = (): FileInput[] => files.map((f) => ({ path: f.name, source: f.doc }));
+// cleared when an example is loaded, set on the first edit — gates the
+// "replace your schema?" confirm in the Examples picker.
+let dirty = false;
+
+/** Replace the whole file set with an example's files. */
+function setFiles(ex: Example) {
+ files = ex.files.map((f) => ({
+ id: nextId(),
+ name: f.name,
+ doc: f.source,
+ state: makeState(f.source, ctx),
+ }));
+ openIds = files.map((f) => f.id);
+ activeId = (files.find((f) => f.name === ex.entry) ?? files[0]).id;
+ dirty = false;
+}
+
// ── DOM ─────────────────────────────────────────────────────────────────
const $ = (sel: string) => document.querySelector(sel) as T;
const statusEl = $("#status");
@@ -156,6 +156,7 @@ const ctx: EditorContext = {
const f = activeFile();
if (!f) return; // the blank scratch buffer shown when no file is open
f.doc = d;
+ dirty = true;
scheduleRefresh();
},
};
@@ -769,13 +770,33 @@ function wireCollapse(toggleSel: string) {
wireCollapse("#files-toggle");
wireCollapse("#problems-toggle");
+// ── examples picker ────────────────────────────────────────────────────
+const examplesEl = $("#examples");
+for (const ex of EXAMPLES) {
+ const o = document.createElement("option");
+ o.value = ex.id;
+ o.textContent = ex.title;
+ o.title = ex.blurb;
+ examplesEl.append(o);
+}
+examplesEl.addEventListener("change", () => {
+ const ex = EXAMPLES.find((e) => e.id === examplesEl.value);
+ examplesEl.value = "";
+ if (!ex) return;
+ if (dirty && !window.confirm(`Replace the current schema with the "${ex.title}" example?`)) return;
+ setFiles(ex);
+ view.setState(activeFile().state);
+ editorEl.classList.remove("is-hidden");
+ editorEmptyEl.classList.add("is-hidden");
+ renderTabs();
+ renderFileTree();
+ statusEl.textContent = "compiling…";
+ void refresh();
+});
+
// ── boot ───────────────────────────────────────────────────────────────
-for (const s of SAMPLE_FILES) {
- files.push({ id: nextId(), name: s.name, doc: s.doc, state: makeState(s.doc, ctx) });
-}
-openIds = files.map((f) => f.id);
-activeId = files[0].id;
-view = mountEditor(editorEl, files[0].state);
+setFiles(EXAMPLES[0]);
+view = mountEditor(editorEl, activeFile().state);
renderTabs();
renderFileTree();
diff --git a/app/src/style.css b/app/src/style.css
index 57dee87..e67def6 100644
--- a/app/src/style.css
+++ b/app/src/style.css
@@ -84,7 +84,8 @@ header h1 {
background: var(--bg-2);
}
-#target {
+#target,
+#examples {
font: inherit;
font-size: 0.75rem;
color: var(--fg);
@@ -94,6 +95,9 @@ header h1 {
padding: 0.15rem 0.4rem;
cursor: pointer;
}
+#examples {
+ color: var(--muted);
+}
.status {
margin-left: auto;
diff --git a/app/tsconfig.json b/app/tsconfig.json
index 49bea6e..4080bb6 100644
--- a/app/tsconfig.json
+++ b/app/tsconfig.json
@@ -4,6 +4,7 @@
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
"lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
"strict": true,
"noUnusedLocals": true,
diff --git a/app/tsconfig.test.json b/app/tsconfig.test.json
index 1fc472e..e657511 100644
--- a/app/tsconfig.test.json
+++ b/app/tsconfig.test.json
@@ -4,6 +4,6 @@
"types": ["node"],
"noEmit": true
},
- "include": ["src/sim"],
+ "include": ["src/sim", "src/examples.test.ts"],
"exclude": []
}
diff --git a/app/yarn.lock b/app/yarn.lock
index 33c5948..7f9b8f7 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -755,6 +755,13 @@ __metadata:
languageName: node
linkType: hard
+"comline-examples@git+https://github.com/ComlineProject/examples.git#f63894988ea2cc5db98ff97cc0fbdfceb5af6bb7":
+ version: 0.0.0
+ resolution: "comline-examples@https://github.com/ComlineProject/examples.git#commit=f63894988ea2cc5db98ff97cc0fbdfceb5af6bb7"
+ checksum: 10c0/8659fdb337f52f30bf55f4c0eb185b0937e6ea819a078ba6c07ba3bc3e00b89f8349e337e596e8d8b9410041fd538e0523bc0d7e8487e8cabb1b010e2ac02f0e
+ languageName: node
+ linkType: hard
+
"comline-playground@workspace:.":
version: 0.0.0-use.local
resolution: "comline-playground@workspace:."
@@ -771,6 +778,7 @@ __metadata:
"@codemirror/view": "npm:^6.34.0"
"@lezer/highlight": "npm:^1.2.3"
"@types/node": "npm:^20.19.43"
+ comline-examples: "git+https://github.com/ComlineProject/examples.git#f63894988ea2cc5db98ff97cc0fbdfceb5af6bb7"
comline-simulator: "git+https://github.com/ComlineProject/simulator.git#eff27ce9b077ec411c299743b328a24089387215"
linkedom: "npm:^0.18.13"
tsx: "npm:^4.23.13"