Skip to content

Commit 9814758

Browse files
authored
fixed: hypergraph typesync codegen is now now generating app properly (#259)
Nik approved. Next steps: tidy up the codegen resolving location, however it is currently functional without conflicts :)
1 parent 23b0956 commit 9814758

11 files changed

Lines changed: 2198 additions & 231 deletions

File tree

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Graph Framework
1+
# Hypergraph Framework
22

33
## Development
44

@@ -22,6 +22,18 @@ pnpm dev
2222
# in another tab
2323
cd apps/server
2424
pnpm dev
25+
# in another tab
26+
cd apps/typesync
27+
pnpm run dev:client
28+
```
29+
30+
You can also run Typesync after building:
31+
32+
```sh
33+
# Build all packages and apps first
34+
pnpm build
35+
# Then start Typesync
36+
hypergraph typesync
2537
```
2638

2739
Any time you make changes to the schema, you will need to run the following commands:
@@ -40,6 +52,30 @@ cd apps/next-example
4052
pnpm dev
4153
```
4254

55+
### Scaffolding a new Hypergraph application
56+
57+
```sh
58+
# 1. Launch TypeSync (if it isn't already running)
59+
hypergraph typesync
60+
61+
# 2. In the browser UI click **Generate App**, choose an app name (e.g. `my-app`).
62+
# When the toast says "Application my-app generated at ./my-app" the scaffold
63+
# is complete and all dependencies are already installed.
64+
65+
# 3. Run the app
66+
cd my-app
67+
pnpm dev
68+
```
69+
70+
That's it – the generator automatically
71+
72+
* adds the app to `pnpm-workspace.yaml`,
73+
* runs `pnpm install` inside the new folder, *and*
74+
* re-installs at the workspace root so everything stays in sync.
75+
76+
You can immediately start hacking in [`src/routes`](my-app/src/routes) and the
77+
Vite dev server will hot-reload your changes.
78+
4379
## Upgrading Dependencies
4480

4581
```sh

apps/next-example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "next dev --turbopack",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint"
9+
"lint": "next lint",
10+
"prebuild": "pnpm --workspace-concurrency 1 --filter @graphprotocol/hypergraph run build && pnpm --workspace-concurrency 1 --filter @graphprotocol/hypergraph-react run build"
1011
},
1112
"type": "module",
1213
"dependencies": {

apps/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"dev": "bun run --watch ./src/index.ts",
88
"prisma": "prisma",
9+
"prebuild": "prisma generate",
910
"build": "tsup"
1011
},
1112
"dependencies": {

apps/typesync/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,19 @@ hypergraph typesync --open --browser firefox
3737
- `port` [OPTIONAL, default = 3000] port to run the application on
3838
- example: `hypergraph typesync --port 3001`
3939
- `browser` [OPTION, default 'browser'] browser to open the app in, if the `--open` flag is passed
40-
- example: `hypergraph typesync --open --browser firefox`
40+
- example: `hypergraph typesync --open --browser firefox`
41+
42+
## Generating & running a new app
43+
44+
1. Start TypeSync:
45+
```bash
46+
hypergraph typesync
47+
```
48+
2. In the UI click **Generate App** and choose a name (e.g. `awesome-app`). When the toast shows the path, the scaffold is ready and all dependencies are already installed.
49+
3. Run it:
50+
```bash
51+
cd awesome-app
52+
pnpm dev
53+
```
54+
55+
No additional `pnpm install` is necessary – the generator takes care of adding the app to the workspace and installing its dependencies for you.

apps/typesync/src/Generator.ts

Lines changed: 143 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { execSync } from 'node:child_process';
2+
import * as fsSync from 'node:fs';
3+
import * as nodePath from 'node:path';
14
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem';
25
import * as FileSystem from '@effect/platform/FileSystem';
36
import * as Path from '@effect/platform/Path';
@@ -24,11 +27,27 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
2427
codegen(app: Domain.InsertAppSchema) {
2528
return Effect.gen(function* () {
2629
// check directory
27-
/** @todo solve directory pathing */
28-
let directory = app.directory;
29-
if (!directory) {
30-
directory = `./${app.name}`;
31-
}
30+
/**
31+
* Decide where to place the new application.
32+
* If the caller explicitly provides `app.directory` we respect it.
33+
* Otherwise, we always create the application inside the repository-root
34+
* `apps` folder so it shows up next to `connect`, `events`, etc.
35+
*/
36+
37+
// 1. Locate the repo root by walking up until we find `pnpm-workspace.yaml`
38+
const findRepoRoot = (start: string): string => {
39+
let dir = start;
40+
while (true) {
41+
if (fsSync.existsSync(nodePath.join(dir, 'pnpm-workspace.yaml'))) return dir;
42+
const parent = nodePath.dirname(dir);
43+
if (parent === dir) return start; // Fallback if we can't find it
44+
dir = parent;
45+
}
46+
};
47+
48+
const repoRoot = findRepoRoot(process.cwd());
49+
50+
const directory = app.directory?.length ? app.directory : nodePath.join(repoRoot, 'apps', app.name);
3251
const directoryExists = yield* fs.exists(directory);
3352
if (directoryExists) {
3453
// directory already exists, fail
@@ -52,16 +71,69 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
5271
]);
5372
// create the src directory inside
5473
yield* fs.makeDirectory(path.join(directory, 'src'));
74+
yield* fs.makeDirectory(path.join(directory, 'src', 'routes'));
5575

5676
// create the src files
5777
yield* Effect.all([
5878
fs.writeFileString(path.join(directory, 'src', 'index.css'), indexcss),
5979
fs.writeFileString(path.join(directory, 'src', 'main.tsx'), mainTsx),
60-
fs.writeFileString(path.join(directory, 'src', 'App.tsx'), appTsx),
6180
fs.writeFileString(path.join(directory, 'src', 'vite-env.d.ts'), vitEnvDTs),
6281
fs.writeFileString(path.join(directory, 'src', 'schema.ts'), buildSchemaFile(app)),
82+
fs.writeFileString(path.join(directory, 'src', 'routes', '__root.tsx'), rootRouteTsx),
83+
fs.writeFileString(path.join(directory, 'src', 'routes', 'index.tsx'), indexRouteTsx),
6384
]);
6485

86+
// -----------------------------
87+
// Post-generation helpers
88+
// 1. Add the new directory to pnpm-workspace.yaml
89+
// 2. Run `pnpm install` inside the new directory so deps are ready
90+
// 3. Run `pnpm install` at repo root to update lockfile/hoist
91+
// -----------------------------
92+
93+
const workspaceFile = nodePath.join(repoRoot, 'pnpm-workspace.yaml');
94+
const workspaceExists = yield* fs.exists(workspaceFile);
95+
if (workspaceExists) {
96+
const current = yield* fs.readFileString(workspaceFile);
97+
const lines = current.split('\n');
98+
99+
const relPackagePath = nodePath.relative(repoRoot, directory);
100+
const newPackageLine = ` - ${relPackagePath}`;
101+
const alreadyExists = lines.some((line) => line.trim() === newPackageLine.trim());
102+
103+
if (!alreadyExists) {
104+
const packagesLineIndex = lines.findIndex((line) => line.startsWith('packages:'));
105+
106+
if (packagesLineIndex !== -1) {
107+
let lastPackageLineIndex = packagesLineIndex;
108+
for (let i = packagesLineIndex + 1; i < lines.length; i++) {
109+
if (lines[i].trim().startsWith('- ')) {
110+
lastPackageLineIndex = i;
111+
} else if (lines[i].trim() !== '') {
112+
break;
113+
}
114+
}
115+
lines.splice(lastPackageLineIndex + 1, 0, newPackageLine);
116+
const updated = lines.join('\n');
117+
yield* fs.writeFileString(workspaceFile, updated);
118+
}
119+
}
120+
}
121+
122+
// helper to run a shell command synchronously (cross-platform)
123+
const run = (cmd: string, cwd?: string) =>
124+
Effect.sync(() => {
125+
try {
126+
execSync(cmd, { stdio: 'inherit', cwd });
127+
} catch {
128+
throw new Error(`command failed (${cmd})`);
129+
}
130+
});
131+
132+
// install deps within the new app folder
133+
yield* run('pnpm install', directory);
134+
// update lockfile/hoist at repo root
135+
yield* run('pnpm install');
136+
65137
return { directory };
66138
});
67139
},
@@ -343,7 +415,11 @@ const prettierrc = {
343415
singleQuote: true,
344416
printWidth: 120,
345417
};
346-
const prettierignore = 'dist/';
418+
const prettierignore = `
419+
# Ignore artifacts:
420+
build
421+
dist
422+
`;
347423

348424
// --------------------
349425
// vite.config.ts
@@ -423,33 +499,75 @@ dist-ssr
423499
// src/
424500
// --------------------
425501

426-
const indexcss = `@import "tailwindcss";`;
502+
const indexcss = `
503+
@tailwind base;
504+
@tailwind components;
505+
@tailwind utilities;
506+
`;
427507

428-
const vitEnvDTs = `/// <reference types="vite/client" />`;
508+
const vitEnvDTs = `/// <reference types="vite/client" />
509+
`;
429510

430-
const appTsx = `export default function App() {
431-
return (
432-
<div className="flex flex-col gap-y-8 h-full items-center justify-center py-16">
433-
<h1>Vite + React + Hypergraph starter</h1>
511+
const mainTsx = `import React from 'react';
512+
import ReactDOM from 'react-dom/client';
513+
import { RouterProvider, createRouter } from '@tanstack/react-router';
514+
import './index.css';
434515
435-
<p>Import schema from '@/schema'</p>
436-
</div>
437-
)
516+
// Import the generated route tree
517+
import { routeTree } from './routeTree.gen';
518+
519+
// Create a new router instance
520+
const router = createRouter({ routeTree });
521+
522+
// Register the router instance for type safety
523+
declare module '@tanstack/react-router' {
524+
interface Register {
525+
router: typeof router;
526+
}
527+
}
528+
529+
// Render the app
530+
const rootElement = document.getElementById('root');
531+
if (rootElement && !rootElement.innerHTML) {
532+
const root = ReactDOM.createRoot(rootElement);
533+
root.render(
534+
<React.StrictMode>
535+
<RouterProvider router={router} />
536+
</React.StrictMode>
537+
);
438538
}
439539
`;
440540

441-
const mainTsx = `import { StrictMode } from 'react'
442-
import { createRoot } from 'react-dom/client'
541+
const rootRouteTsx = `import { createRootRoute, Outlet } from '@tanstack/react-router';
542+
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
543+
544+
export const Route = createRootRoute({
545+
component: () => (
546+
<>
547+
<div className="min-h-screen bg-gray-900 text-white p-4">
548+
<h1 className="text-2xl font-bold mb-4">My Hypergraph App</h1>
549+
<Outlet />
550+
</div>
551+
<TanStackRouterDevtools />
552+
</>
553+
),
554+
});
555+
`;
443556

444-
import './index.css'
557+
const indexRouteTsx = `import { createFileRoute } from '@tanstack/react-router';
445558
446-
import App from './App.tsx'
559+
export const Route = createFileRoute('/')({
560+
component: Index,
561+
});
447562
448-
createRoot(document.getElementById('root')!).render(
449-
<StrictMode>
450-
<App />
451-
</StrictMode>,
452-
)
563+
function Index() {
564+
return (
565+
<div className="p-2">
566+
<h3 className="text-xl">Welcome Home!</h3>
567+
<p className="mt-2">This is your new application generated by Typesync.</p>
568+
</div>
569+
);
570+
}
453571
`;
454572

455573
// --------------------

apps/typesync/src/Server.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
/** Defines the static file routes for serving the client dist directory with the built vite/react app */
22

3+
import { dirname, resolve } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
36
import * as HttpMiddleware from '@effect/platform/HttpMiddleware';
47
import * as HttpRouter from '@effect/platform/HttpRouter';
58
import * as HttpServer from '@effect/platform/HttpServer';
@@ -12,13 +15,16 @@ import * as Struct from 'effect/Struct';
1215

1316
import * as Api from './Api.js';
1417

18+
const __dirname = dirname(fileURLToPath(import.meta.url));
19+
const clientDist = resolve(__dirname, '..', 'client', 'dist');
20+
1521
const FilesRouter = Effect.gen(function* () {
1622
const path = yield* Path.Path;
1723

1824
return HttpRouter.empty.pipe(
1925
HttpRouter.get(
2026
'/',
21-
HttpServerResponse.file(path.resolve('client', 'dist', 'index.html')).pipe(
27+
HttpServerResponse.file(path.join(clientDist, 'index.html')).pipe(
2228
Effect.orElse(() => HttpServerResponse.empty({ status: 404 })),
2329
),
2430
),
@@ -31,7 +37,7 @@ const FilesRouter = Effect.gen(function* () {
3137
return HttpServerResponse.empty({ status: 404 });
3238
}
3339

34-
const assets = path.resolve('client', 'dist', 'assets');
40+
const assets = path.join(clientDist, 'assets');
3541
const normalized = path.normalize(path.join(assets, ...file.value.split('/')));
3642
if (!normalized.startsWith(assets)) {
3743
return HttpServerResponse.empty({ status: 404 });

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
"type": "module",
44
"packageManager": "pnpm@10.6.2",
55
"scripts": {
6-
"clean": "node scripts/clean.mjs",
7-
"build": "tsc -b tsconfig.build.json && pnpm --recursive --parallel --filter \"./packages/*\" run build",
6+
"clean": "rm -rf .turbo && rm -rf node_modules && pnpm --recursive --filter \"./packages/*\" exec rm -rf dist && pnpm --recursive --filter \"./packages/*\" exec rm -rf .turbo && pnpm --recursive --filter \"./packages/*\" exec rm -rf tsconfig.tsbuildinfo && pnpm --recursive --filter \"./apps/*\" exec rm -rf dist && pnpm --recursive --filter \"./apps/*\" exec rm -rf .turbo && pnpm --recursive --filter \"./apps/*\" exec rm -rf tsconfig.tsbuildinfo",
7+
"dev": "pnpm --recursive --parallel --filter \"./apps/*\" run dev",
8+
"build": "pnpm --recursive --filter \"./packages/*\" run build && pnpm --recursive --parallel --filter \"./apps/*\" run build",
89
"test": "vitest",
910
"lint": "biome check",
1011
"lint:fix": "biome check --write --unsafe",
11-
"check": "tsc --noEmit"
12+
"check": "tsc --noEmit",
13+
"db:migrate:dev": "pnpm --filter server db:migrate:dev",
14+
"db:studio": "pnpm --filter server db:studio",
15+
"graph": "pnpm --filter server-logic-ts graph"
1216
},
1317
"devDependencies": {
1418
"@babel/cli": "^7.27.2",

packages/hypergraph-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"types": "./dist/index.d.ts",
2121
"sideEffects": [],
2222
"scripts": {
23-
"build": "tsc -b tsconfig.build.json && babel dist --plugins annotate-pure-calls --out-dir dist --source-maps && node ../../scripts/package.mjs",
23+
"build": "tsc -b --force tsconfig.build.json && babel dist --plugins annotate-pure-calls --out-dir dist --source-maps && node ../../scripts/package.mjs",
2424
"test": "vitest"
2525
},
2626
"peerDependencies": {

0 commit comments

Comments
 (0)