-
Notifications
You must be signed in to change notification settings - Fork 0
FE-545: Local-first storage; npx distribution #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,9 @@ | |
| }, | ||
| "license": "(MIT OR Apache-2.0)", | ||
| "type": "module", | ||
| "bin": { | ||
| "brunch": "./src/server/cli.ts" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. package.json:16 sets Severity: high 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| }, | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "check": "npm run fmt:check && npm run lint && npm run typecheck", | ||
|
|
@@ -56,6 +59,7 @@ | |
| "md-pen": "^1.2.0", | ||
| "motion": "^12.38.0", | ||
| "nanoid": "^5.1.7", | ||
| "open": "^11.0.0", | ||
| "radix-ui": "^1.4.3", | ||
| "react": "^19.2.4", | ||
| "react-dom": "^19.2.4", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { launch } from './launcher.js'; | ||
|
|
||
| launch(process.cwd()).catch((error) => { | ||
| console.error('Failed to start brunch:', error); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,15 @@ | ||
| import { createApp } from './app.js'; | ||
| import { resolveBrunchProject } from './project.js'; | ||
|
|
||
| const PORT = process.env.PORT || 3000; | ||
| const DB_PATH = process.env.BRUNCH_DB || './brunch.db'; | ||
| const DB_PATH = process.env.BRUNCH_DB; | ||
|
|
||
| const { app } = createApp(DB_PATH); | ||
| // In dev mode, use BRUNCH_DB env var if set, otherwise resolve .brunch/ project | ||
| const dbPath = DB_PATH ?? resolveBrunchProject(process.cwd()).dbPath; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/server/index.ts:8 uses Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
|
|
||
| const { app } = createApp(dbPath); | ||
|
|
||
| app.listen(PORT, () => { | ||
| console.log(`Brunch server listening on http://localhost:${PORT}`); | ||
| console.log(`Database: ${DB_PATH}`); | ||
| console.log(`Database: ${dbPath}`); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| import request from 'supertest'; | ||
| import { afterEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { createApp } from './app.js'; | ||
| import { resolveBrunchProject } from './project.js'; | ||
|
|
||
| describe('launcher integration', () => { | ||
| const tempDirs: string[] = []; | ||
|
|
||
| const makeTempDir = () => { | ||
| const dir = mkdtempSync(join(tmpdir(), 'brunch-launcher-')); | ||
| tempDirs.push(dir); | ||
| return dir; | ||
| }; | ||
|
|
||
| afterEach(() => { | ||
| for (const dir of tempDirs.splice(0)) { | ||
| rmSync(dir, { force: true, recursive: true }); | ||
| } | ||
| }); | ||
|
|
||
| it('serves API from a .brunch/ project directory', async () => { | ||
| const cwd = makeTempDir(); | ||
| const project = resolveBrunchProject(cwd); | ||
| const { app } = createApp(project.dbPath); | ||
|
|
||
| const res = await request(app).get('/api/projects').expect(200); | ||
| expect(Array.isArray(res.body)).toBe(true); | ||
| }); | ||
|
|
||
| it('serves static files when dist/ exists alongside the API', async () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/server/launcher.test.ts:35 is named as if it verifies static serving, but it never creates a Severity: low 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| const cwd = makeTempDir(); | ||
| const project = resolveBrunchProject(cwd); | ||
| const { app } = createApp(project.dbPath); | ||
|
|
||
| // createLauncher mounts express.static(distDir) as a fallback | ||
| // For this test, we verify the API works and that the launcher | ||
| // function can mount static files. Full static serving is tested | ||
| // via the launcher function. | ||
| const res = await request(app).get('/api/projects').expect(200); | ||
| expect(res.body).toBeDefined(); | ||
| }); | ||
|
|
||
| it('resolves drizzle migrations when cwd differs from package root', () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/server/launcher.test.ts:48 doesn’t change Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| // The key risk: migrations path is relative to import.meta.url, not cwd | ||
| const cwd = makeTempDir(); | ||
| const project = resolveBrunchProject(cwd); | ||
|
|
||
| // This would throw if migrations can't be found | ||
| expect(() => createApp(project.dbPath)).not.toThrow(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { existsSync } from 'node:fs'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| import express from 'express'; | ||
|
|
||
| import { createApp } from './app.js'; | ||
| import { resolveBrunchProject } from './project.js'; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const DIST_DIR = join(__dirname, '..', '..', 'dist'); | ||
|
|
||
| export async function launch(cwd: string): Promise<void> { | ||
| const project = resolveBrunchProject(cwd); | ||
| console.log(`.brunch/ directory: ${project.root}`); | ||
|
|
||
| const { app } = createApp(project.dbPath); | ||
|
|
||
| // Serve built Vite assets as static files (production mode) | ||
| if (existsSync(DIST_DIR)) { | ||
| app.use(express.static(DIST_DIR)); | ||
|
|
||
| // SPA fallback: serve index.html for all non-API routes | ||
| app.get('*', (_req, res) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/server/launcher.ts:24 installs a catch-all Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| res.sendFile(join(DIST_DIR, 'index.html')); | ||
| }); | ||
| } | ||
|
|
||
| const port = Number(process.env.PORT) || 3000; | ||
|
|
||
| await new Promise<void>((resolve) => { | ||
| app.listen(port, () => { | ||
| console.log(`Brunch running at http://localhost:${port}`); | ||
| resolve(); | ||
| }); | ||
| }); | ||
|
|
||
| // Open browser | ||
| try { | ||
| const { default: open } = await import('open'); | ||
| await open(`http://localhost:${port}`); | ||
| } catch { | ||
| console.log(`Open http://localhost:${port} in your browser`); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'; | ||
| import { homedir, tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| import { afterEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { findBrunchProject, initBrunchProject, resolveBrunchProject } from './project.js'; | ||
|
|
||
| describe('project resolution', () => { | ||
| const tempDirs: string[] = []; | ||
|
|
||
| const makeTempDir = () => { | ||
| const dir = mkdtempSync(join(tmpdir(), 'brunch-test-')); | ||
| tempDirs.push(dir); | ||
| return dir; | ||
| }; | ||
|
|
||
| afterEach(() => { | ||
| for (const dir of tempDirs.splice(0)) { | ||
| rmSync(dir, { force: true, recursive: true }); | ||
| } | ||
| }); | ||
|
|
||
| // ── findBrunchProject ─────────────────────────────────────────── | ||
|
|
||
| it('returns BrunchProject when .brunch/ exists in cwd', () => { | ||
| const cwd = makeTempDir(); | ||
| mkdirSync(join(cwd, '.brunch')); | ||
|
|
||
| const project = findBrunchProject(cwd); | ||
|
|
||
| expect(project).not.toBeNull(); | ||
| expect(project!.root).toBe(join(cwd, '.brunch')); | ||
| expect(project!.dbPath).toBe(join(cwd, '.brunch', 'brunch.db')); | ||
| expect(project!.cwd).toBe(cwd); | ||
| }); | ||
|
|
||
| it('finds .brunch/ in a parent directory (walk-up)', () => { | ||
| const root = makeTempDir(); | ||
| mkdirSync(join(root, '.brunch')); | ||
| const child = join(root, 'packages', 'frontend'); | ||
| mkdirSync(child, { recursive: true }); | ||
|
|
||
| const project = findBrunchProject(child); | ||
|
|
||
| expect(project).not.toBeNull(); | ||
| expect(project!.root).toBe(join(root, '.brunch')); | ||
| expect(project!.cwd).toBe(root); | ||
| }); | ||
|
|
||
| it('returns null when no .brunch/ exists up to the walk-up limit', () => { | ||
| const root = makeTempDir(); | ||
| // Create a deep path but no .brunch/ anywhere | ||
| const deep = join(root, 'a', 'b', 'c', 'd', 'e', 'f'); | ||
| mkdirSync(deep, { recursive: true }); | ||
|
|
||
| const project = findBrunchProject(deep); | ||
|
|
||
| expect(project).toBeNull(); | ||
| }); | ||
|
|
||
| it('does not walk above the filesystem root or home directory', () => { | ||
| // Walking from a tmp dir should never find .brunch/ above tmp | ||
| const cwd = makeTempDir(); | ||
| const project = findBrunchProject(cwd); | ||
| expect(project).toBeNull(); | ||
| }); | ||
|
|
||
| // ── initBrunchProject ─────────────────────────────────────────── | ||
|
|
||
| it('creates .brunch/ directory with correct BrunchProject shape', () => { | ||
| const cwd = makeTempDir(); | ||
|
|
||
| const project = initBrunchProject(cwd); | ||
|
|
||
| expect(existsSync(project.root)).toBe(true); | ||
| expect(project.root).toBe(join(cwd, '.brunch')); | ||
| expect(project.dbPath).toBe(join(cwd, '.brunch', 'brunch.db')); | ||
| expect(project.cwd).toBe(cwd); | ||
| }); | ||
|
|
||
| it('throws when .brunch/ already exists', () => { | ||
| const cwd = makeTempDir(); | ||
| mkdirSync(join(cwd, '.brunch')); | ||
|
|
||
| expect(() => initBrunchProject(cwd)).toThrow(); | ||
| }); | ||
|
|
||
| // ── resolveBrunchProject ──────────────────────────────────────── | ||
|
|
||
| it('creates .brunch/ when none found', () => { | ||
| const cwd = makeTempDir(); | ||
|
|
||
| const project = resolveBrunchProject(cwd); | ||
|
|
||
| expect(existsSync(project.root)).toBe(true); | ||
| expect(project.cwd).toBe(cwd); | ||
| }); | ||
|
|
||
| it('finds existing .brunch/ without creating a new one', () => { | ||
| const root = makeTempDir(); | ||
| mkdirSync(join(root, '.brunch')); | ||
| const child = join(root, 'src'); | ||
| mkdirSync(child); | ||
|
|
||
| const project = resolveBrunchProject(child); | ||
|
|
||
| expect(project.root).toBe(join(root, '.brunch')); | ||
| expect(project.cwd).toBe(root); | ||
| // Should not create a second .brunch/ in the child | ||
| expect(existsSync(join(child, '.brunch'))).toBe(false); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
memory/PLAN.md:159 (process) — the PR title doesn’t match the repo PR title convention (
FE-XXX: ...). See (Rule: AGENTS.md).Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.