Skip to content
Open
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
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.17.0
v22.20.0
185 changes: 185 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Jam Tools is a music performance system built on Springboard, a full-stack JavaScript framework. Springboard emphasizes realtime communication via WebSockets/JSON-RPC, side effect imports with dependency injection, and multi-platform deployment from a single codebase.

## Repository Structure

This is a pnpm monorepo with two main package families:

- **`packages/springboard/`**: The Springboard framework itself
- `core/`: Core framework (modules, engine, services, hooks, types)
- `cli/`: CLI tool for building and running apps (`sb` command)
- `server/`: Server-side runtime (Hono-based WebSocket server)
- `platforms/`: Platform-specific implementations (browser, node, partykit, tauri, react-native)
- `data_storage/`: Key-value store implementations (SQLite/Kysely-based)
- `plugins/`: Framework plugins

- **`packages/jamtools/`**: Jam Tools application code
- `core/`: MIDI, music-related core functionality
- `features/`: Feature modules

- **`apps/`**: Application entry points
- `jamtools/`: Main Jam Tools application
- `small_apps/`: Test and example applications

## Common Commands

### Development
```bash
pnpm dev # Run dev server for main app
pnpm dev-without-node # Run dev excluding Node.js app
npm run dev-dev --prefix packages/springboard/cli # CLI dev mode
```

### Building
```bash
pnpm build # Build all packages
pnpm build-saas # Production build for SaaS deployment
turbo run build # Build using Turbo
```

### Testing
```bash
pnpm test # Run all tests
pnpm test:watch # Run tests in watch mode
vitest --run # Run tests in a specific package
```

Individual package tests:
```bash
cd packages/springboard/core && pnpm test
cd packages/jamtools/core && pnpm test:watch
```

### Linting and Type Checking
```bash
pnpm lint # Lint all packages
pnpm fix # Auto-fix lint issues
pnpm check-types # Type check all packages
turbo run check-types # Type check using Turbo
```

### CI Pipeline
```bash
pnpm ci # Run full CI: lint, check-types, build, test
```

### Springboard CLI
```bash
npx tsx packages/springboard/cli/src/cli.ts dev <entrypoint>
npx tsx packages/springboard/cli/src/cli.ts build <entrypoint> --platforms <platform>
```

Platforms: `browser`, `browser_offline`, `desktop` (Tauri), `partykit`, `all`

## Architecture

### Module System

Springboard uses a module registration pattern. Modules are registered via side effect imports:

```typescript
import springboard from 'springboard';

springboard.registerModule('ModuleName', {}, async (moduleAPI) => {
// Module initialization logic
// Use moduleAPI to register routes, actions, states, etc.
});
```

**Key APIs:**
- `moduleAPI.registerRoute(path, options, component)` - Register React Router routes
- `moduleAPI.statesAPI` - Create shared/server/userAgent state pieces
- `moduleAPI.registerAction(name, callback)` - Register RPC actions
- `moduleAPI.registerNavigationItem(config)` - Add navigation items

### State Management

Springboard provides three types of state:

1. **Shared State** (`SharedStateService`): Synchronized across all clients and server
2. **Server State** (`ServerStateService`): Server-only state, read-only from clients
3. **User Agent State**: Client-local persistent state

States are managed through supervisors:
- `SharedStateSupervisor` - For shared state pieces
- `ServerStateSupervisor` - For server state pieces
- `UserAgentStateSupervisor` - For client-local state

### RPC Communication

Communication between client and server uses JSON-RPC over WebSockets. The framework provides:
- `Rpc` interface for calling/broadcasting/registering RPC methods
- Automatic reconnection handling
- Mode selection: `remote` (client-server) or `local` (same process)

### Build System

The CLI (`packages/springboard/cli`) uses esbuild with custom plugins:

- **`esbuild_plugin_platform_inject`**: Conditionally includes code based on `@platform` directives
- **`esbuild_plugin_html_generate`**: Generates HTML entry files
- **`esbuild_plugin_partykit_config`**: Creates PartyKit configuration

Platform-specific code blocks:
```typescript
// @platform "browser"
// Browser-only code
// @platform end

// @platform "node"
// Node-only code
// @platform end
```

### Multi-Platform Support

Single codebase deploys to multiple platforms:
- **Browser (online/offline)**: WebSocket-connected or standalone
- **Node**: Server-side runtime
- **Tauri**: Native desktop (maestro + webview bundles)
- **PartyKit**: Edge deployment
- **React Native**: Mobile (experimental)

### Testing

Tests use Vitest with:
- **Workspace configuration**: `vitest.workspace.ts` defines test projects
- **Per-package testing**: Each package has its own `vite.config.ts`
- **jsdom environment**: For React component testing
- **60s timeout**: `testTimeout: 1000 * 60`

### TypeScript Configuration

Each package has its own `tsconfig.json` (118 total). Root `tsconfig.json` provides shared configuration. Type checking is done per-package with `tsc --noEmit`.

## Development Workflow

1. **Install dependencies**: `pnpm install` (runs postinstall hook for springboard-cli setup)
2. **Start development**: `pnpm dev` or use CLI directly with `npx tsx`
3. **Make changes**: Edit source files (framework watches for changes in dev mode)
4. **Run tests**: `pnpm test` in specific package or root
5. **Type check**: `pnpm check-types`
6. **Lint**: `pnpm fix` to auto-fix issues
7. **Build**: `pnpm build` when ready for production

## Key Files

- `packages/springboard/core/engine/register.ts` - Module registration system
- `packages/springboard/core/engine/module_api.ts` - ModuleAPI implementation
- `packages/springboard/core/types/module_types.ts` - Core type definitions
- `packages/springboard/core/services/states/shared_state_service.ts` - State management
- `packages/springboard/cli/src/build.ts` - Build configuration and platform definitions
- `packages/springboard/server/src/hono_app.ts` - Server implementation
- `turbo.json` - Turborepo task configuration
- `pnpm-workspace.yaml` - Workspace package definitions

## Current Branch Context

Branch: `server-state` (PR typically targets `main`)

Recent work involves server state caching and KV storage improvements.
2 changes: 1 addition & 1 deletion apps/jamtools/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import '@springboardjs/shoelace/shoelace_imports';
import {ShoelaceApplicationShell} from '@springboardjs/shoelace/components/shoelace_application_shell';

springboard.registerModule('UIMain', {}, async (moduleAPI) => {
moduleAPI.registerApplicationShell(ShoelaceApplicationShell);
moduleAPI.ui.registerApplicationShell(ShoelaceApplicationShell);
});
// @platform end
5 changes: 4 additions & 1 deletion apps/jamtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"name": "jamtools",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
"scripts": {
"check-types": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
Expand All @@ -17,6 +19,7 @@
"@springboardjs/shoelace": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:",
"springboard-cli": "workspace:*"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import React, { useEffect } from 'react';
import springboard from 'springboard';

springboard.registerModule('Main', {}, async (moduleAPI) => {
moduleAPI.registerRoute('/', {}, () => {
moduleAPI.ui.registerRoute('/', {}, () => {
useEffect(() => {
fetch('/hello')
.then(res => res.json())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,20 @@ const CustomSplashScreen = () => {
springboard.registerSplashScreen(CustomSplashScreen);

springboard.registerModule('AppWithSplashScreen', {}, async (moduleAPI) => {
const messageState = await moduleAPI.statesAPI.createPersistentState<string>('message', 'Hello from the app with custom splash screen!');
const states = await moduleAPI.shared.createSharedStates({
message: 'Hello from the app with custom splash screen!',
});
const messageState = states.message;

await new Promise(r => setTimeout(r, 5000)); // fake waiting time

const actions = moduleAPI.createActions({
const actions = moduleAPI.shared.createSharedActions({
updateMessage: async (args: {newMessage: string}) => {
messageState.setState(args.newMessage);
},
});

moduleAPI.registerRoute('/', {}, () => {
moduleAPI.ui.registerRoute('/', {}, () => {
return (
<AppWithSplashScreenComponent
message={messageState.useState()}
Expand Down
5 changes: 4 additions & 1 deletion apps/small_apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
"version": "0.0.1-autogenerated",
"main": "index.js",
"scripts": {
"check-types": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"springboard": "workspace:*",
"@jamtools/core": "workspace:*",
"@jamtools/features": "workspace:*",
"@springboardjs/platforms-browser": "workspace:*",
"@springboardjs/platforms-node": "workspace:*",
"better-sqlite3": "^11.3.0",
"react": "catalog:",
"react-dom": "catalog:",
"springboard": "workspace:*",
"springboard-cli": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import springboard from 'springboard';

// Test @platform directive comment removal and line preservation

springboard.registerModule('platform_directives_test', {}, async (moduleAPI) => {
// Line 7 - This comment should be preserved

// @platform "node"
const nodeFeature = {
fs: 'filesystem',
secret: 'node-only-secret',
data: 'node-platform-data'
};
console.log('Node platform code - should be removed in browser build');
// @platform end
// Line 17 - This should stay at line 17 even after platform block removal

// @platform "browser"
const browserFeature = {
dom: 'document',
feature: 'browser-only-feature',
api: 'browser-web-api'
};
console.log('Browser platform code - should be removed in node build');
// @platform end
// Line 27 - Shared code marker

// Shared code that should always exist in all builds
const sharedCode = 'always-present';
const anotherShared = 'also-shared';

console.log('This is shared code that appears in all platforms');

// @platform "server"
const serverContext = {
database: 'postgres',
secret: 'server-context-secret'
};
console.log('Server context code - should appear in node AND cf-workers builds');
// @platform end
// Line 42 - After server context block

moduleAPI.ui.registerRoute('/', {}, () => {
return (
<div>
<h1>Platform Directives Test</h1>
<p>Shared: {sharedCode}</p>
</div>
);
});
});
65 changes: 65 additions & 0 deletions apps/small_apps/run_on_test/run_on_test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import springboard from 'springboard';

// Test springboard.runOn() transformation

springboard.registerModule('run_on_test', {}, async (moduleAPI) => {
// Test 1: runOn with node platform
const nodeDeps = springboard.runOn('node', () => {
console.log('Running on node');
return {
platform: 'node',
secret: 'node-only-secret',
};
});

// Test 2: runOn with browser platform
const browserDeps = springboard.runOn('browser', () => {
console.log('Running on browser');
return {
platform: 'browser',
feature: 'browser-only-feature',
};
});

// Test 3: runOn with async callback
const asyncDeps = await springboard.runOn('node', async () => {
console.log('Running async on node');
return {
asyncData: 'node-async-data',
};
});

// Test 4: runOn with fallback pattern
const deps = springboard.runOn('node', () => {
return {midi: 'node-midi-service'};
}) ?? springboard.runOn('browser', () => {
return {audio: 'browser-audio-service'};
});

const RunOnTestUI: React.FC = () => {
return (
<div style={{padding: '20px', fontFamily: 'monospace'}}>
<h1>springboard.runOn() Test</h1>
<div style={{marginTop: '20px'}}>
<h2>Expected Behavior:</h2>
<ul>
<li><strong>Node Build:</strong> nodeDeps and asyncDeps should have values, browserDeps should be null</li>
<li><strong>Browser Build:</strong> browserDeps should have values, nodeDeps and asyncDeps should be null</li>
</ul>
</div>
<div style={{marginTop: '20px'}}>
<h2>Values:</h2>
<pre>nodeDeps: {JSON.stringify(nodeDeps, null, 2)}</pre>
<pre>browserDeps: {JSON.stringify(browserDeps, null, 2)}</pre>
<pre>asyncDeps: {JSON.stringify(asyncDeps, null, 2)}</pre>
<pre>deps: {JSON.stringify(deps, null, 2)}</pre>
</div>
</div>
);
};

moduleAPI.ui.registerRoute('/', {}, () => <RunOnTestUI />);

return {};
});
Loading
Loading