1+ import { execSync } from 'node:child_process' ;
2+ import * as fsSync from 'node:fs' ;
3+ import * as nodePath from 'node:path' ;
14import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem' ;
25import * as FileSystem from '@effect/platform/FileSystem' ;
36import * 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// --------------------
0 commit comments