diff --git a/package.json b/package.json
index f2adae3..66064ea 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"build:core": "esbuild src/ts/ribbit-core.ts --bundle --format=iife --global-name=ribbit --sourcemap --outfile=dist/ribbit/ribbit-core.js",
"build:core-min": "esbuild src/ts/ribbit-core.ts --bundle --format=iife --global-name=ribbit --minify --outfile=dist/ribbit/ribbit-core.min.js",
"build:css": "cp src/static/ribbit-core.css dist/ribbit/ && cp -r src/static/themes dist/ribbit/",
+ "dev": "npm run build && node test/integration/dev-server.js",
"test": "npm run build && jest --verbose",
"test:integration": "npm run build && node test/integration/test.js && node test/integration/test_wysiwyg.js",
"test:coverage": "npm run build && jest --coverage"
diff --git a/test/integration/dev-server.js b/test/integration/dev-server.js
new file mode 100644
index 0000000..e0f5796
--- /dev/null
+++ b/test/integration/dev-server.js
@@ -0,0 +1,106 @@
+/**
+ * Development server with livereload.
+ *
+ * Serves the test page and ribbit dist files. Watches src/ for
+ * changes, rebuilds automatically, and notifies connected browsers
+ * to reload via a simple EventSource stream.
+ *
+ * Run: npm run dev
+ */
+const { createServer } = require('./server');
+const { execSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = 8080;
+const WATCH_DIRS = [
+ path.join(__dirname, '..', '..', 'src'),
+ path.join(__dirname, '..', '..', 'test', 'integration'),
+];
+const DEBOUNCE_MS = 300;
+
+const server = createServer(PORT);
+const reloadClients = [];
+
+// Patch the server to add the livereload endpoint
+const originalServer = require('http').createServer;
+const httpServer = server._server || (() => {
+ // Access the internal server by starting and intercepting
+ let captured = null;
+ const origListen = require('http').Server.prototype.listen;
+ require('http').Server.prototype.listen = function (...args) {
+ captured = this;
+ return origListen.apply(this, args);
+ };
+ server.start();
+ require('http').Server.prototype.listen = origListen;
+ return captured;
+})();
+
+// Simpler approach: create a standalone livereload server
+const reloadServer = require('http').createServer((request, response) => {
+ response.writeHead(200, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ 'Access-Control-Allow-Origin': '*',
+ });
+ reloadClients.push(response);
+ request.on('close', () => {
+ const index = reloadClients.indexOf(response);
+ if (index >= 0) {
+ reloadClients.splice(index, 1);
+ }
+ });
+});
+
+function notifyReload() {
+ for (const client of reloadClients) {
+ client.write('data: reload\n\n');
+ }
+}
+
+function rebuild() {
+ try {
+ console.log('\nšØ Rebuilding...');
+ execSync('npm run build:js && npm run build:css', {
+ cwd: path.join(__dirname, '..', '..'),
+ stdio: 'pipe',
+ });
+ console.log('ā
Build complete');
+ notifyReload();
+ } catch (error) {
+ console.error('ā Build failed:', error.stderr?.toString().slice(0, 500));
+ }
+}
+
+let debounceTimer = null;
+function onFileChange(filename) {
+ if (debounceTimer) {
+ clearTimeout(debounceTimer);
+ }
+ console.log(`š Changed: ${filename}`);
+ debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
+}
+
+// Watch source directories
+for (const directory of WATCH_DIRS) {
+ if (fs.existsSync(directory)) {
+ fs.watch(directory, { recursive: true }, (eventType, filename) => {
+ if (filename && !filename.includes('node_modules')) {
+ onFileChange(filename);
+ }
+ });
+ }
+}
+
+server.start().then(() => {
+ reloadServer.listen(PORT + 1, () => {
+ console.log(`\nšø Ribbit dev server`);
+ console.log(` Editor: http://localhost:${PORT}`);
+ console.log(` Livereload: http://localhost:${PORT + 1} (EventSource)`);
+ console.log(` Watching: src/, test/integration/`);
+ console.log(`\n Add this to the page to enable livereload:`);
+ console.log(` \n`);
+ });
+});
diff --git a/test/integration/index.html b/test/integration/index.html
index 29f9769..72e15d8 100644
--- a/test/integration/index.html
+++ b/test/integration/index.html
@@ -42,5 +42,10 @@
editor.run();
window.__ribbitEditor = editor;
+