Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
900da7a
Move springboard dev to single-port ModuleRunner
Feb 25, 2026
24883d9
Add midi_files_module to @jamtools/core package exports
Feb 26, 2026
4e02efa
Refactor io_module to use separate .browser.ts and .node.ts files
Feb 26, 2026
e0366d8
Use package.json export conditions for platform-specific io_dependencies
Feb 27, 2026
5786193
Fix ES module import reassignment in io_module for Vite dev mode
Feb 27, 2026
48723f5
Fix @tonejs/midi import to use default import with type import
Feb 27, 2026
51fe57e
Fix dev plugin stream and module-graph typings
Mar 22, 2026
c6b22d8
Add e2e dev-route HMR test for registerServerModule
Mar 22, 2026
ac6c546
docs: clarify registerServerModule contract
Mar 25, 2026
76d916a
apps/vite-test: add start script and configurable port
Mar 25, 2026
b2a694f
Committed the two requested files as `apps/vite-test: add start scrip…
Mar 25, 2026
1543cb5
apps/vite-test: add debug log for module load
Mar 25, 2026
2335c8e
fix: use single-port dev server in vite plugin
Mar 25, 2026
b3b9628
fix: install vite dev middleware before spa fallback
Mar 25, 2026
6a8c1c4
refactor: remove unused vite plugin modules
Mar 25, 2026
c44e8be
Committed the deletions as `refactor: remove unused vite plugin modul…
Mar 25, 2026
98cb178
refactor: store generated vite files in node_modules
Mar 25, 2026
8fb45c6
edit scripts
Mar 25, 2026
1616cba
fix: serve dev web entry through vite transform
Mar 25, 2026
b7b6ea0
fix: stop forcing localhost npm registry in ci
May 12, 2026
94e21c4
Merge branch 'preview-pr-70-vk-8100-fix-vite-dev-nod' into vk/e1e4-co…
Jun 6, 2026
6db3543
Improve Springboard agent docs
Jun 6, 2026
32c43a1
Fix springboard reset preserving registered modules
Jun 6, 2026
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
88 changes: 88 additions & 0 deletions .springboard/node-dev-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import process from 'node:process';

import crosswsNode from 'crossws/adapters/node';

import { initApp } from 'springboard/server/hono_app';
import { makeWebsocketServerCoreDependenciesWithSqlite } from 'springboard/platforms/node/services/ws_server_core_dependencies';
import { LocalJsonNodeKVStoreService } from 'springboard/platforms/node/services/node_kvstore_service';
import { CoreDependencies, Springboard } from 'springboard/core';
import {
springboard,
clearRegisteredModules,
clearRegisteredClassModules,
clearRegisteredSplashScreen,
} from 'springboard/core/engine/register';
import { resetServerRegistry } from 'springboard/server/register';

export type DevServerHandle = {
fetch: (request: Request) => Promise<Response>;
ws: ReturnType<typeof crosswsNode>;
dispose: () => Promise<void>;
};

springboard.reset();
clearRegisteredModules();
clearRegisteredClassModules();
clearRegisteredSplashScreen();
resetServerRegistry();

await import('../src/server-entry.ts');

export async function createDevServer(): Promise<DevServerHandle> {
const nodeKvDeps = await makeWebsocketServerCoreDependenciesWithSqlite();
const useWebSocketsForRpc = import.meta.env.VITE_USE_WEBSOCKETS_FOR_RPC === 'true';

let wsNode: ReturnType<typeof crosswsNode>;

const { app, serverAppDependencies, injectResources, createWebSocketHooks } = initApp({
broadcastMessage: (message) => {
return wsNode.publish('event', message);
},
remoteKV: nodeKvDeps.kvStoreFromKysely,
userAgentKV: new LocalJsonNodeKVStoreService('userAgent'),
enableStaticRoutes: false,
});

app.notFound((c) => {
c.header('x-springboard-fallback', '1');
return c.text('', 404);
});

wsNode = crosswsNode({
hooks: createWebSocketHooks(useWebSocketsForRpc),
});

const coreDeps: CoreDependencies = {
log: console.log,
showError: console.error,
storage: serverAppDependencies.storage,
isMaestro: () => true,
rpc: serverAppDependencies.rpc,
};

Object.assign(coreDeps, serverAppDependencies);

const engine = new Springboard(coreDeps, {});

injectResources({
engine,
serveStaticFile: async (c, _fileName, headers) => {
Object.entries(headers).forEach(([key, value]) => {
c.header(key, value);
});
c.status(404);
return c.text('Not found');
},
getEnvValue: (name) => process.env[name],
});

await engine.initialize();

return {
fetch: app.fetch,
ws: wsNode,
dispose: async () => {
wsNode.closeAll();
},
};
}
163 changes: 163 additions & 0 deletions .springboard/node-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import process from 'node:process';
import path from 'node:path';

import { serve } from '@hono/node-server';
import crosswsNode from 'crossws/adapters/node';
import type { Server } from 'node:http';

import { initApp } from 'springboard/server/hono_app';
import { makeWebsocketServerCoreDependenciesWithSqlite } from 'springboard/platforms/node/services/ws_server_core_dependencies';
import { LocalJsonNodeKVStoreService } from 'springboard/platforms/node/services/node_kvstore_service';
import { CoreDependencies, Springboard } from 'springboard/core';
import '../src/server-entry.ts';

/**
* Node.js server entrypoint with HMR support
*
* This file is generated by the Springboard Vite plugin and serves as the
* entry point for the Node.js dev server. It:
*
* 1. Imports the user's application entry (which registers modules)
* 2. Exports start/stop functions for lifecycle management
* 3. Supports HMR via import.meta.hot.dispose()
*/

let server: Server | null = null;
let engine: Springboard | null = null;

/**
* Start the node server
*/
export async function start() {
// If server is already running, stop it first
if (server) {
await stop();
}

try {
const webappFolder = process.env.WEBAPP_FOLDER || './dist';
const webappDistFolder = webappFolder;

const nodeKvDeps = await makeWebsocketServerCoreDependenciesWithSqlite();
const useWebSocketsForRpc = import.meta.env.VITE_USE_WEBSOCKETS_FOR_RPC === 'true';

let wsNode: ReturnType<typeof crosswsNode>;

const { app, serverAppDependencies, injectResources, createWebSocketHooks } = initApp({
broadcastMessage: (message) => {
return wsNode.publish('event', message);
},
remoteKV: nodeKvDeps.kvStoreFromKysely,
userAgentKV: new LocalJsonNodeKVStoreService('userAgent'),
});

wsNode = crosswsNode({
hooks: createWebSocketHooks(useWebSocketsForRpc),
});

// Use configured port (ignores process.env.PORT to avoid conflicts)
const port = 1337;

// Start the HTTP server
server = serve({
fetch: app.fetch,
port,
}, (info) => {
console.log(`Server listening on http://localhost:${info.port}`);
});

server.on('upgrade', (request, socket, head) => {
const url = new URL(request.url || '', `http://${request.headers.host}`);
if (url.pathname === '/ws') {
wsNode.handleUpgrade(request, socket, head);
} else {
socket.end('HTTP/1.1 404 Not Found\r\n\r\n');
}
});

const coreDeps: CoreDependencies = {
log: console.log,
showError: console.error,
storage: serverAppDependencies.storage,
isMaestro: () => true,
rpc: serverAppDependencies.rpc,
};

Object.assign(coreDeps, serverAppDependencies);

const extraDeps = {}; // TODO: remove this extraDeps thing from the framework

engine = new Springboard(coreDeps, extraDeps);

injectResources({
engine,
serveStaticFile: async (c, fileName, headers) => {
try {
const fullPath = `${webappDistFolder}/${fileName}`;
const fs = await import('node:fs');
const data = await fs.promises.readFile(fullPath, 'utf-8');
c.status(200);

if (headers) {
Object.entries(headers).forEach(([key, value]) => {
c.header(key, value);
});
}

return c.body(data);
} catch (error) {
console.error('Error serving file:', error);
c.status(404);
return c.text('404 Not found');
}
},
getEnvValue: name => process.env[name],
});

await engine.initialize();
console.log('Node application started successfully');
} catch (error) {
console.error('Failed to start node server:', error);
throw error;
}
}

/**
* Stop the node server
*/
export async function stop() {
if (!server) {
return;
}

return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Server close timeout'));
}, 5000);

server!.close((err) => {
clearTimeout(timeout);
if (err) {
reject(err);
} else {
console.log('Server stopped successfully');
server = null;
engine = null; // TODO: add explicit shutdown once the engine exposes it
resolve();
}
});
});
}

// HMR support: clean up before module reload
if (import.meta.hot) {
import.meta.hot.dispose(async () => {
console.log('[HMR] Stopping server before reload...');
await stop();
});
}

// Auto-start in production builds (not in dev mode)
if (!import.meta.env.DEV) {
start().catch(console.error);
}
6 changes: 4 additions & 2 deletions apps/vite-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
"build:esbuild:watch": "tsx esbuild.ts --watch",
"build": "npm run build:web && npm run build:node",
"build-sb-and-app": "cd ../../packages/springboard && npm run build && cd vite-plugin && npm run build && cd ../../../apps/vite-test && npm run build",
"build-sb-and-dev": "cd ../../packages/springboard && npm run build && cd vite-plugin && npm run build && cd ../../../apps/vite-test && npm run dev",
"build:web": "SPRINGBOARD_PLATFORM=web vite build",
"build:node": "SPRINGBOARD_PLATFORM=node vite build --outDir dist/node",
"dev": "vite",
"check-types": "tsc --noEmit"
"dev": "vite --host 0.0.0.0",
"check-types": "tsc --noEmit",
"start": "node dist/node/node-entry.mjs"
},
"engines": {
"node": ">=20.0.0"
Expand Down
7 changes: 7 additions & 0 deletions apps/vite-test/src/tic_tac_toe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import springboard from 'springboard';

// @platform "node"
console.log('only in node');

import {serverRegistry} from 'springboard/server/register'
serverRegistry.registerServerModule(api => {
api.hono.get('/yeah', async (req) => {
return new Response('Oh yeah');
});
});
// @platform end

// @platform "browser"
Expand Down
2 changes: 1 addition & 1 deletion apps/vite-test/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default defineConfig({
springboard({
entry: './src/tic_tac_toe.tsx',
// platforms: ['browser', 'node'],
nodeServerPort: 3001,
nodeServerPort: (process.env.PORT && parseInt(process.env.PORT)) || 3001,
})
],
define: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"debug-node": "npm run debug --prefix apps/jamtools/node",
"test": "turbo run test",
"test:watch": "turbo run test:watch",
"pretest:e2e": "pnpm --filter @springboard/vite-plugin build",
"pretest:e2e": "pnpm --filter springboard build && pnpm --filter @springboard/vite-plugin build",
"test:e2e": "vitest run tests/e2e",
"test:e2e:watch": "vitest watch tests/e2e",
"pretest:integration": "pnpm --filter @springboard/vite-plugin build",
Expand Down
16 changes: 16 additions & 0 deletions packages/jamtools/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@
"types": "./dist/modules/midi_files/midi_file_parser/midi_file_parser.d.ts",
"import": "./dist/modules/midi_files/midi_file_parser/midi_file_parser.js"
},
"./modules/midi_files/midi_files_module": {
"types": "./dist/modules/midi_files/midi_files_module.d.ts",
"import": "./dist/modules/midi_files/midi_files_module.js"
},
"./modules/io/io_dependencies": {
"types": "./dist/modules/io/io_dependencies_types.d.ts",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Export io_dependencies types from declaration with value

The ./modules/io/io_dependencies export maps types to io_dependencies_types.d.ts, which defines only type aliases and does not declare the createIoDependencies value export that the runtime files provide. TypeScript consumers importing createIoDependencies from this subpath will get missing-export type errors even though the JS export exists.

Useful? React with 👍 / 👎.

"node": {
"import": "./dist/modules/io/io_dependencies.node.js"
},
"browser": {
"import": "./dist/modules/io/io_dependencies.browser.js"
},
"default": {
"import": "./dist/modules/io/io_dependencies.js"
}
},
"./modules/macro_module/registered_macro_types": {
"types": "./dist/modules/macro_module/registered_macro_types.d.ts",
"import": "./dist/modules/macro_module/registered_macro_types.js"
Expand Down
12 changes: 12 additions & 0 deletions packages/jamtools/core/src/modules/io/io_dependencies.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {BrowserQwertyService} from '@jamtools/core/services/browser/browser_qwerty_service';
import {BrowserMidiService} from '@jamtools/core/services/browser/browser_midi_service';
import type {IoDeps} from './io_dependencies_types';

export const createIoDependencies = async (): Promise<IoDeps> => {
const qwerty = new BrowserQwertyService(document);
const midi = new BrowserMidiService();
return {
qwerty,
midi,
};
};
21 changes: 21 additions & 0 deletions packages/jamtools/core/src/modules/io/io_dependencies.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {NodeQwertyService} from '@jamtools/core/services/node/node_qwerty_service';
import {NodeMidiService} from '@jamtools/core/services/node/node_midi_service';
import {MockMidiService} from '@jamtools/core/test/services/mock_midi_service';
import {MockQwertyService} from '@jamtools/core/test/services/mock_qwerty_service';
import type {IoDeps} from './io_dependencies_types';

export const createIoDependencies = async (): Promise<IoDeps> => {
if (process.env.DISABLE_IO === 'true') {
return {
qwerty: new MockQwertyService(),
midi: new MockMidiService(),
};
}

const qwerty = new NodeQwertyService();
const midi = new NodeMidiService();
return {
qwerty,
midi,
};
};
11 changes: 11 additions & 0 deletions packages/jamtools/core/src/modules/io/io_dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {MockMidiService} from '@jamtools/core/test/services/mock_midi_service';
import {MockQwertyService} from '@jamtools/core/test/services/mock_qwerty_service';
import type {IoDeps} from './io_dependencies_types';

// Default implementation for testing
export const createIoDependencies = async (): Promise<IoDeps> => {
return {
qwerty: new MockQwertyService(),
midi: new MockMidiService(),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {MidiService, QwertyService} from '@jamtools/core/types/io_types';

export type IoDeps = {
midi: MidiService;
qwerty: QwertyService;
}

export type CreateIoDependencies = () => Promise<IoDeps>;
Loading
Loading