Skip to content

Instantly share code, notes, and snippets.

@Lightnet
Last active April 19, 2026 06:57
Show Gist options
  • Select an option

  • Save Lightnet/60fef1821aefdbceaa8d6ae3c681f360 to your computer and use it in GitHub Desktop.

Select an option

Save Lightnet/60fef1821aefdbceaa8d6ae3c681f360 to your computer and use it in GitHub Desktop.
spacetimedb batch into single build typescript.
// reducer: scheduleReminder
// table: person
// view: my_player

// lifecycle: init
export const init = spacetimedb.init(_ctx => {
  console.log("====::: INIT SPACETIMEDB :::====");
});

// lifecycle: onConnect
export const onConnect = spacetimedb.clientConnected(ctx => {
  console.log("connect")
});

// lifecycle: onDisconnect
export const onDisconnect = spacetimedb.clientDisconnected(ctx => {
  console.log("disconnect")
});

tag filer.

import fs from 'fs';
import path from 'path';
const SRC_DIR = path.join(process.cwd(), 'src');
const OUTPUT_FILE = path.join(process.cwd(), 'src', 'index.ts');
const IGNORE_LIST = ['module.ts','index.ts', 'index.js', '.git', 'node_modules', 'dist'];
function shouldIgnore(filePath: string): boolean {
if (filePath === OUTPUT_FILE) return true;
const relative = path.relative(SRC_DIR, filePath);
return IGNORE_LIST.some(ignore => relative.includes(ignore));
}
function collectFiles(dir: string, files: string[] = []): string[] {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
if (shouldIgnore(fullPath)) continue;
if (fs.statSync(fullPath).isDirectory()) {
collectFiles(fullPath, files);
} else if (item.endsWith('.ts') && !item.startsWith('index')) {
files.push(fullPath);
}
}
return files;
}
function getTag(filePath: string): 'table' | 'reducer' | 'procedure' | 'view' | 'lifecycle' | 'other' {
const content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
if (content.includes('// table:')) return 'table';
if (content.includes('// reducer:')) return 'reducer';
if (content.includes('// procedure:')) return 'procedure';
if (content.includes('// view:')) return 'view';
if (content.includes('// init') ||
content.includes('// onconnect') ||
content.includes('// ondisconnect') ||
content.includes('// lifecycle')) return 'lifecycle';
return 'other';
}
async function build() {
console.log('πŸ” Collecting source files...');
const allFiles = collectFiles(SRC_DIR);
const tables = allFiles.filter(f => getTag(f) === 'table');
const lifecycles = allFiles.filter(f => getTag(f) === 'lifecycle');
const reducers = allFiles.filter(f => getTag(f) === 'reducer');
const procedures = allFiles.filter(f => getTag(f) === 'procedure');
const views = allFiles.filter(f => getTag(f) === 'view');
const others = allFiles.filter(f => getTag(f) === 'other');
console.log(`Found ${allFiles.length} files β†’ Tables: ${tables.length}`);
let output = `// =============================================\n`;
output += `// AUTO-GENERATED by build.ts - DO NOT EDIT THIS FILE!\n`;
output += `// Generated: ${new Date().toISOString()}\n`;
output += `// =============================================\n\n`;
output += `import { schema, table, t } from 'spacetimedb/server';\n`;
output += `import { ScheduleAt } from 'spacetimedb';\n\n`;
// 1. TABLES FIRST (without 'export')
for (const file of tables) {
let content = fs.readFileSync(file, 'utf-8')
.replace(/^import .*$/gm, '')
.replace(/^export\s+const\s+/gm, 'const ') // ← Remove 'export' from tables
.replace(/^\/\/\s*(table|reducer|procedure|view|lifecycle):?.*$/gm, '')
.replace(/^\s*[\r\n]+/gm, '')
.trim();
if (content) {
output += `// === Table from: ${path.relative(SRC_DIR, file)} ===\n`;
output += content + '\n\n';
}
}
// 2. Collect table names for schema()
const tableNames: string[] = [];
tables.forEach(file => {
const content = fs.readFileSync(file, 'utf-8');
const match = content.match(/\/\/\s*table:\s*(\w+)/i);
if (match && match[1]) {
tableNames.push(match[1].trim());
}
});
// 3. SCHEMA() β€” AFTER TABLES
output += `// === SCHEMA (must come before reducers, procedures, views, etc.) ===\n`;
output += `const spacetimedb = schema({\n`;
tableNames.forEach((name, i) => {
output += ` ${name}${i < tableNames.length - 1 ? ',' : ''}\n`;
});
output += `});\n\n`;
output += `export default spacetimedb;\n\n`;
// 4. LIFECYCLE + REDUCERS + PROCEDURES + VIEWS (AFTER SCHEMA)
output += `// === LIFECYCLE HOOKS, REDUCERS, PROCEDURES & VIEWS ===\n\n`;
const postSchemaFiles = [...lifecycles, ...reducers, ...procedures, ...views, ...others];
for (const file of postSchemaFiles) {
let content = fs.readFileSync(file, 'utf-8')
.replace(/^import .*$/gm, '')
.replace(/^\/\/\s*(table|reducer|procedure|view|lifecycle):?.*$/gm, '')
.replace(/^\s*[\r\n]+/gm, '')
.trim();
if (content) {
const tag = getTag(file);
const displayTag = tag === 'lifecycle' ? 'Lifecycle' : tag.charAt(0).toUpperCase() + tag.slice(1);
output += `// === ${displayTag} from: ${path.relative(SRC_DIR, file)} ===\n`;
output += content + '\n\n';
}
}
fs.writeFileSync(OUTPUT_FILE, output, 'utf-8');
console.log(`βœ… Build completed successfully!`);
console.log(` Tables : ${tables.length}`);
console.log(` Lifecycle : ${lifecycles.length}`);
console.log(` Reducers : ${reducers.length}`);
console.log(` Procedures : ${procedures.length}`);
console.log(` Views : ${views.length}`);
}
build().catch((err) => {
console.error('❌ Build failed:', err.message);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment