@geekmidas/cli 1.10.35 → 1.10.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/{config-B62g483e.mjs → config-Bbu1JKvf.mjs} +2 -2
- package/dist/{config-B62g483e.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
- package/dist/{config-Cuo8vFsp.cjs → config-DujV8DbR.cjs} +2 -2
- package/dist/{config-Cuo8vFsp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
- package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
- package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
- package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
- package/dist/{index-Dt_dZ7K4.d.mts → index-BBvGMG_A.d.mts} +162 -68
- package/dist/index-BBvGMG_A.d.mts.map +1 -0
- package/dist/{index-C3t5VL4R.d.cts → index-Dz2a7xQU.d.cts} +162 -68
- package/dist/index-Dz2a7xQU.d.cts.map +1 -0
- package/dist/index.cjs +1366 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1366 -149
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-B_rJjImN.cjs → openapi-0JUzU5Tv.cjs} +72 -15
- package/dist/openapi-0JUzU5Tv.cjs.map +1 -0
- package/dist/{openapi-CflxypuN.mjs → openapi-C5AycKTR.mjs} +73 -16
- package/dist/openapi-C5AycKTR.mjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +7 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +7 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
- package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
- package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
- package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
- package/dist/workspace/index.cjs +4 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-CGYykWfn.cjs → workspace-CQTT7HJQ.cjs} +107 -27
- package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
- package/dist/{workspace-Bi4X7Yzy.mjs → workspace-D-37Ylqo.mjs} +90 -28
- package/dist/workspace-D-37Ylqo.mjs.map +1 -0
- package/docs/dev-server-resilience-design.md +189 -0
- package/package.json +4 -4
- package/src/__tests__/openapi.spec.ts +99 -9
- package/src/build/__tests__/workspace-build.spec.ts +7 -7
- package/src/build/index.ts +25 -9
- package/src/credentials/__tests__/helpers.ts +1 -1
- package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
- package/src/deploy/__tests__/domain.spec.ts +12 -12
- package/src/deploy/__tests__/env-resolver.spec.ts +29 -1
- package/src/deploy/__tests__/index.spec.ts +14 -14
- package/src/deploy/__tests__/sniffer.spec.ts +9 -9
- package/src/deploy/domain.ts +19 -11
- package/src/deploy/env-resolver.ts +5 -10
- package/src/deploy/index.ts +25 -8
- package/src/deploy/sniffer.ts +11 -5
- package/src/deploy/types.ts +1 -1
- package/src/dev/__tests__/index.spec.ts +11 -11
- package/src/dev/index.ts +43 -9
- package/src/docker/__tests__/compose.spec.ts +4 -4
- package/src/docker/compose.ts +5 -3
- package/src/docker/index.ts +34 -6
- package/src/docker/templates.ts +138 -0
- package/src/generators/OpenApiTsGenerator.ts +22 -0
- package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
- package/src/index.ts +6 -2
- package/src/init/__tests__/generators.spec.ts +147 -0
- package/src/init/__tests__/init.spec.ts +1 -1
- package/src/init/generators/mobile-expo.ts +576 -0
- package/src/init/generators/monorepo.ts +42 -11
- package/src/init/generators/web-tanstack.ts +348 -0
- package/src/init/index.ts +40 -3
- package/src/init/templates/index.ts +31 -0
- package/src/init/versions.ts +2 -2
- package/src/openapi.ts +110 -23
- package/src/secrets/__tests__/reconcile.spec.ts +1 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
- package/src/setup/fullstack-secrets.ts +7 -1
- package/src/setup/index.ts +2 -1
- package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
- package/src/test/__tests__/web.spec.ts +2 -2
- package/src/workspace/__tests__/client-generator.spec.ts +6 -6
- package/src/workspace/__tests__/index.spec.ts +96 -4
- package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
- package/src/workspace/__tests__/schema.spec.ts +27 -30
- package/src/workspace/__tests__/type-inference.spec.ts +5 -5
- package/src/workspace/client-generator.ts +10 -7
- package/src/workspace/index.ts +17 -3
- package/src/workspace/publicEnv.ts +57 -0
- package/src/workspace/schema.ts +126 -27
- package/src/workspace/types.ts +40 -64
- package/dist/index-C3t5VL4R.d.cts.map +0 -1
- package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
- package/dist/openapi-B_rJjImN.cjs.map +0 -1
- package/dist/openapi-CflxypuN.mjs.map +0 -1
- package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
- package/dist/workspace-CGYykWfn.cjs.map +0 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type { GeneratedFile, TemplateOptions } from '../templates/index.js';
|
|
2
|
+
import { GEEKMIDAS_VERSIONS } from '../versions.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate TanStack Start web app files for the fullstack template.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the Next.js scaffold (apps/web) but on Vite + TanStack Start +
|
|
8
|
+
* TanStack Router with the `VITE_` env-var convention.
|
|
9
|
+
*/
|
|
10
|
+
export function generateTanStackWebFiles(
|
|
11
|
+
options: TemplateOptions,
|
|
12
|
+
): GeneratedFile[] {
|
|
13
|
+
if (!options.monorepo || options.template !== 'fullstack') {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const packageName = `@${options.name}/web`;
|
|
18
|
+
const apiPackage = `@${options.name}/api`;
|
|
19
|
+
const modelsPackage = `@${options.name}/models`;
|
|
20
|
+
const uiPackage = `@${options.name}/ui`;
|
|
21
|
+
|
|
22
|
+
const packageJson = {
|
|
23
|
+
name: packageName,
|
|
24
|
+
version: '0.0.1',
|
|
25
|
+
private: true,
|
|
26
|
+
type: 'module',
|
|
27
|
+
scripts: {
|
|
28
|
+
dev: 'gkm exec -- vite dev',
|
|
29
|
+
build: 'gkm exec -- vite build',
|
|
30
|
+
start: 'node .output/server/index.mjs',
|
|
31
|
+
typecheck: 'tsc --noEmit',
|
|
32
|
+
},
|
|
33
|
+
dependencies: {
|
|
34
|
+
[apiPackage]: 'workspace:*',
|
|
35
|
+
[modelsPackage]: 'workspace:*',
|
|
36
|
+
[uiPackage]: 'workspace:*',
|
|
37
|
+
'@geekmidas/client': GEEKMIDAS_VERSIONS['@geekmidas/client'],
|
|
38
|
+
'@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
|
|
39
|
+
'@tanstack/react-query': '~5.80.0',
|
|
40
|
+
'@tanstack/react-router': '^1.87.0',
|
|
41
|
+
'@tanstack/react-start': '^1.87.0',
|
|
42
|
+
'better-auth': '~1.2.0',
|
|
43
|
+
react: '~19.2.0',
|
|
44
|
+
'react-dom': '~19.2.0',
|
|
45
|
+
},
|
|
46
|
+
devDependencies: {
|
|
47
|
+
'@geekmidas/cli': GEEKMIDAS_VERSIONS['@geekmidas/cli'],
|
|
48
|
+
'@tailwindcss/vite': '^4.0.0',
|
|
49
|
+
'@tanstack/router-plugin': '^1.87.0',
|
|
50
|
+
'@types/node': '~22.0.0',
|
|
51
|
+
'@types/react': '~19.0.0',
|
|
52
|
+
'@types/react-dom': '~19.0.0',
|
|
53
|
+
'@vitejs/plugin-react': '^4.3.4',
|
|
54
|
+
tailwindcss: '^4.0.0',
|
|
55
|
+
tsx: '~4.20.0',
|
|
56
|
+
typescript: '~5.8.2',
|
|
57
|
+
vite: '^7.0.0',
|
|
58
|
+
'vite-tsconfig-paths': '~5.1.0',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const viteConfig = `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
63
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
64
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
|
65
|
+
import viteReact from '@vitejs/plugin-react';
|
|
66
|
+
import { defineConfig } from 'vite';
|
|
67
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
server: {
|
|
71
|
+
port: Number(Credentials.PORT ?? 3001),
|
|
72
|
+
},
|
|
73
|
+
// VITE_* vars are inlined automatically; no manual loadEnv needed.
|
|
74
|
+
envPrefix: 'VITE_',
|
|
75
|
+
plugins: [
|
|
76
|
+
tsconfigPaths({ root: import.meta.dirname }),
|
|
77
|
+
tailwindcss(),
|
|
78
|
+
tanstackStart(),
|
|
79
|
+
viteReact(),
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const tsConfig = {
|
|
85
|
+
extends: '../../tsconfig.json',
|
|
86
|
+
compilerOptions: {
|
|
87
|
+
lib: ['dom', 'dom.iterable', 'ES2022'],
|
|
88
|
+
allowJs: true,
|
|
89
|
+
skipLibCheck: true,
|
|
90
|
+
strict: true,
|
|
91
|
+
noEmit: true,
|
|
92
|
+
allowImportingTsExtensions: true,
|
|
93
|
+
esModuleInterop: true,
|
|
94
|
+
module: 'ESNext',
|
|
95
|
+
moduleResolution: 'bundler',
|
|
96
|
+
resolveJsonModule: true,
|
|
97
|
+
isolatedModules: true,
|
|
98
|
+
jsx: 'preserve',
|
|
99
|
+
incremental: true,
|
|
100
|
+
baseUrl: '.',
|
|
101
|
+
types: ['vite/client'],
|
|
102
|
+
paths: {
|
|
103
|
+
'~/*': ['./src/*', '../../packages/ui/src/*'],
|
|
104
|
+
[modelsPackage]: ['../../packages/models/src'],
|
|
105
|
+
[`${modelsPackage}/*`]: ['../../packages/models/src/*'],
|
|
106
|
+
[uiPackage]: ['../../packages/ui/src'],
|
|
107
|
+
[`${uiPackage}/*`]: ['../../packages/ui/src/*'],
|
|
108
|
+
[`${apiPackage}/client`]: ['../../apps/api/.gkm/openapi.ts'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
include: ['**/*.ts', '**/*.tsx', 'src/routeTree.gen.ts'],
|
|
112
|
+
exclude: ['node_modules', '.output', '.vinxi', 'dist'],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
116
|
+
|
|
117
|
+
function makeQueryClient() {
|
|
118
|
+
return new QueryClient({
|
|
119
|
+
defaultOptions: {
|
|
120
|
+
queries: {
|
|
121
|
+
staleTime: 60 * 1000,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let browserQueryClient: QueryClient | undefined = undefined;
|
|
128
|
+
|
|
129
|
+
export function getQueryClient() {
|
|
130
|
+
if (typeof window === 'undefined') {
|
|
131
|
+
return makeQueryClient();
|
|
132
|
+
}
|
|
133
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
|
134
|
+
return browserQueryClient;
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const clientConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
139
|
+
|
|
140
|
+
// Client config - VITE_* vars are inlined at build time.
|
|
141
|
+
const envParser = new EnvironmentParser({
|
|
142
|
+
VITE_API_URL: import.meta.env.VITE_API_URL,
|
|
143
|
+
VITE_AUTH_URL: import.meta.env.VITE_AUTH_URL,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const clientConfig = envParser
|
|
147
|
+
.create((get) => ({
|
|
148
|
+
apiUrl: get('VITE_API_URL').string(),
|
|
149
|
+
authUrl: get('VITE_AUTH_URL').string(),
|
|
150
|
+
}))
|
|
151
|
+
.parse();
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
const serverConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
155
|
+
|
|
156
|
+
// Server config - all env vars (server-side only, not exposed to browser).
|
|
157
|
+
// Access these only in TanStack Start server functions / server routes.
|
|
158
|
+
const envParser = new EnvironmentParser({ ...process.env });
|
|
159
|
+
|
|
160
|
+
export const serverConfig = envParser
|
|
161
|
+
.create((_get) => ({
|
|
162
|
+
// Add server-only secrets here
|
|
163
|
+
// Example: stripeSecretKey: _get('STRIPE_SECRET_KEY').string(),
|
|
164
|
+
}))
|
|
165
|
+
.parse();
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const authClientTs = `import { magicLinkClient } from 'better-auth/client/plugins';
|
|
169
|
+
import { createAuthClient } from 'better-auth/react';
|
|
170
|
+
import { clientConfig } from '~/config/client.ts';
|
|
171
|
+
|
|
172
|
+
export const authClient = createAuthClient({
|
|
173
|
+
baseURL: clientConfig.authUrl,
|
|
174
|
+
plugins: [magicLinkClient()],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const apiIndexTs = `import { createApi } from '${apiPackage}/client';
|
|
181
|
+
import { clientConfig } from '~/config/client.ts';
|
|
182
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
183
|
+
|
|
184
|
+
export function createAppApi(options?: { headers?: Record<string, string> }) {
|
|
185
|
+
return createApi({
|
|
186
|
+
baseURL: clientConfig.apiUrl,
|
|
187
|
+
queryClient: getQueryClient(),
|
|
188
|
+
headers: options?.headers,
|
|
189
|
+
onRequest: (config) => ({ ...config, credentials: 'include' }),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const api = createAppApi();
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
const routerTs = `import { createRouter } from '@tanstack/react-router';
|
|
197
|
+
import { routeTree } from './routeTree.gen.ts';
|
|
198
|
+
|
|
199
|
+
export function getRouter() {
|
|
200
|
+
return createRouter({
|
|
201
|
+
routeTree,
|
|
202
|
+
scrollRestoration: true,
|
|
203
|
+
defaultPreload: 'intent',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
declare module '@tanstack/react-router' {
|
|
208
|
+
interface Register {
|
|
209
|
+
router: ReturnType<typeof getRouter>;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
// Placeholder routeTree.gen — TanStack's plugin regenerates this on dev.
|
|
215
|
+
const routeTreeGen = `// This file is auto-generated by @tanstack/router-plugin.
|
|
216
|
+
// It will be overwritten on the next \`vite dev\` run.
|
|
217
|
+
import { rootRouteWithContext } from '@tanstack/react-router';
|
|
218
|
+
|
|
219
|
+
export const routeTree = rootRouteWithContext()({
|
|
220
|
+
// Routes are populated by the plugin from src/routes/**.tsx
|
|
221
|
+
} as never);
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const rootRouteTsx = `/// <reference types="vite/client" />
|
|
225
|
+
import '~/styles/globals.css';
|
|
226
|
+
|
|
227
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
228
|
+
import {
|
|
229
|
+
HeadContent,
|
|
230
|
+
Outlet,
|
|
231
|
+
Scripts,
|
|
232
|
+
createRootRoute,
|
|
233
|
+
} from '@tanstack/react-router';
|
|
234
|
+
import type { ReactNode } from 'react';
|
|
235
|
+
import { getQueryClient } from '~/lib/query-client.ts';
|
|
236
|
+
|
|
237
|
+
export const Route = createRootRoute({
|
|
238
|
+
head: () => ({
|
|
239
|
+
meta: [
|
|
240
|
+
{ charSet: 'utf-8' },
|
|
241
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
242
|
+
{ title: '${options.name}' },
|
|
243
|
+
],
|
|
244
|
+
}),
|
|
245
|
+
component: RootComponent,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
function RootComponent() {
|
|
249
|
+
const queryClient = getQueryClient();
|
|
250
|
+
return (
|
|
251
|
+
<RootDocument>
|
|
252
|
+
<QueryClientProvider client={queryClient}>
|
|
253
|
+
<Outlet />
|
|
254
|
+
</QueryClientProvider>
|
|
255
|
+
</RootDocument>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function RootDocument({ children }: { children: ReactNode }) {
|
|
260
|
+
return (
|
|
261
|
+
<html lang="en">
|
|
262
|
+
<head>
|
|
263
|
+
<HeadContent />
|
|
264
|
+
</head>
|
|
265
|
+
<body>
|
|
266
|
+
{children}
|
|
267
|
+
<Scripts />
|
|
268
|
+
</body>
|
|
269
|
+
</html>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
const indexRouteTsx = `import { createFileRoute } from '@tanstack/react-router';
|
|
275
|
+
import { api } from '~/api/index.ts';
|
|
276
|
+
|
|
277
|
+
export const Route = createFileRoute('/')({
|
|
278
|
+
component: HomeComponent,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
function HomeComponent() {
|
|
282
|
+
const { data: health } = api.useQuery('GET /health', {});
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<main className="min-h-screen bg-background p-8">
|
|
286
|
+
<div className="mx-auto max-w-4xl space-y-8">
|
|
287
|
+
<div className="space-y-2">
|
|
288
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
289
|
+
Welcome to ${options.name}
|
|
290
|
+
</h1>
|
|
291
|
+
<p className="text-muted-foreground">
|
|
292
|
+
Your TanStack Start app is ready.
|
|
293
|
+
</p>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<section className="rounded-lg border p-6">
|
|
297
|
+
<h2 className="text-xl font-semibold">API Status</h2>
|
|
298
|
+
{health ? (
|
|
299
|
+
<pre className="mt-4 rounded bg-muted p-4 text-sm">
|
|
300
|
+
{JSON.stringify(health, null, 2)}
|
|
301
|
+
</pre>
|
|
302
|
+
) : (
|
|
303
|
+
<p className="mt-2 text-muted-foreground">Connecting…</p>
|
|
304
|
+
)}
|
|
305
|
+
</section>
|
|
306
|
+
</div>
|
|
307
|
+
</main>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
`;
|
|
311
|
+
|
|
312
|
+
const globalsCss = `@import '${uiPackage}/styles';
|
|
313
|
+
|
|
314
|
+
@source "../..";
|
|
315
|
+
@source "../../../../packages/ui/src";
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
const gitignore = `.output/
|
|
319
|
+
.vinxi/
|
|
320
|
+
node_modules/
|
|
321
|
+
.env.local
|
|
322
|
+
*.log
|
|
323
|
+
src/routeTree.gen.ts
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
return [
|
|
327
|
+
{
|
|
328
|
+
path: 'apps/web/package.json',
|
|
329
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`,
|
|
330
|
+
},
|
|
331
|
+
{ path: 'apps/web/vite.config.ts', content: viteConfig },
|
|
332
|
+
{
|
|
333
|
+
path: 'apps/web/tsconfig.json',
|
|
334
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`,
|
|
335
|
+
},
|
|
336
|
+
{ path: 'apps/web/src/lib/query-client.ts', content: queryClientTs },
|
|
337
|
+
{ path: 'apps/web/src/lib/auth-client.ts', content: authClientTs },
|
|
338
|
+
{ path: 'apps/web/src/config/client.ts', content: clientConfigTs },
|
|
339
|
+
{ path: 'apps/web/src/config/server.ts', content: serverConfigTs },
|
|
340
|
+
{ path: 'apps/web/src/api/index.ts', content: apiIndexTs },
|
|
341
|
+
{ path: 'apps/web/src/router.tsx', content: routerTs },
|
|
342
|
+
{ path: 'apps/web/src/routeTree.gen.ts', content: routeTreeGen },
|
|
343
|
+
{ path: 'apps/web/src/routes/__root.tsx', content: rootRouteTsx },
|
|
344
|
+
{ path: 'apps/web/src/routes/index.tsx', content: indexRouteTsx },
|
|
345
|
+
{ path: 'apps/web/src/styles/globals.css', content: globalsCss },
|
|
346
|
+
{ path: 'apps/web/.gitignore', content: gitignore },
|
|
347
|
+
];
|
|
348
|
+
}
|
package/src/init/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
generateDockerFiles,
|
|
18
18
|
} from './generators/docker.js';
|
|
19
19
|
import { generateEnvFiles } from './generators/env.js';
|
|
20
|
+
import { generateExpoAppFiles } from './generators/mobile-expo.js';
|
|
20
21
|
import { generateModelsPackage } from './generators/models.js';
|
|
21
22
|
import { generateMonorepoFiles } from './generators/monorepo.js';
|
|
22
23
|
import { generatePackageJson } from './generators/package.js';
|
|
@@ -24,10 +25,13 @@ import { generateSourceFiles } from './generators/source.js';
|
|
|
24
25
|
import { generateTestFiles } from './generators/test.js';
|
|
25
26
|
import { generateUiPackageFiles } from './generators/ui.js';
|
|
26
27
|
import { generateWebAppFiles } from './generators/web.js';
|
|
28
|
+
import { generateTanStackWebFiles } from './generators/web-tanstack.js';
|
|
27
29
|
import {
|
|
28
30
|
type DeployTarget,
|
|
29
31
|
deployTargetChoices,
|
|
30
32
|
eventsBackendChoices,
|
|
33
|
+
type FullstackFrontendFramework,
|
|
34
|
+
frontendFrameworkChoices,
|
|
31
35
|
getTemplate,
|
|
32
36
|
isFullstackTemplate,
|
|
33
37
|
loggerTypeChoices,
|
|
@@ -154,6 +158,18 @@ export async function initCommand(
|
|
|
154
158
|
choices: routesStructureChoices,
|
|
155
159
|
initial: 0,
|
|
156
160
|
},
|
|
161
|
+
{
|
|
162
|
+
// Only prompt for frontend framework on the fullstack template.
|
|
163
|
+
type: (_prev, values) =>
|
|
164
|
+
!options.yes &&
|
|
165
|
+
(options.template === 'fullstack' || values.template === 'fullstack')
|
|
166
|
+
? 'select'
|
|
167
|
+
: null,
|
|
168
|
+
name: 'frontendFramework',
|
|
169
|
+
message: 'Frontend framework:',
|
|
170
|
+
choices: frontendFrameworkChoices,
|
|
171
|
+
initial: 0,
|
|
172
|
+
},
|
|
157
173
|
],
|
|
158
174
|
{ onCancel },
|
|
159
175
|
);
|
|
@@ -221,6 +237,11 @@ export async function initCommand(
|
|
|
221
237
|
: (answers.deployTarget ?? 'dokploy');
|
|
222
238
|
|
|
223
239
|
const database = services.db;
|
|
240
|
+
const frontendFramework: FullstackFrontendFramework | undefined = isFullstack
|
|
241
|
+
? options.yes
|
|
242
|
+
? 'nextjs'
|
|
243
|
+
: (answers.frontendFramework ?? 'nextjs')
|
|
244
|
+
: undefined;
|
|
224
245
|
const templateOptions: TemplateOptions = {
|
|
225
246
|
name,
|
|
226
247
|
template,
|
|
@@ -236,6 +257,7 @@ export async function initCommand(
|
|
|
236
257
|
packageManager: pkgManager,
|
|
237
258
|
deployTarget,
|
|
238
259
|
services,
|
|
260
|
+
frontendFramework,
|
|
239
261
|
};
|
|
240
262
|
|
|
241
263
|
const targetDir = join(cwd, name);
|
|
@@ -293,8 +315,14 @@ export async function initCommand(
|
|
|
293
315
|
]
|
|
294
316
|
: [];
|
|
295
317
|
|
|
296
|
-
// Collect
|
|
297
|
-
const webAppFiles = isFullstack
|
|
318
|
+
// Collect frontend app files for fullstack template, dispatched by framework
|
|
319
|
+
const webAppFiles = isFullstack
|
|
320
|
+
? frontendFramework === 'tanstack-start'
|
|
321
|
+
? generateTanStackWebFiles(templateOptions)
|
|
322
|
+
: frontendFramework === 'expo'
|
|
323
|
+
? generateExpoAppFiles(templateOptions)
|
|
324
|
+
: generateWebAppFiles(templateOptions)
|
|
325
|
+
: [];
|
|
298
326
|
|
|
299
327
|
// Collect auth app files for fullstack template
|
|
300
328
|
const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
|
|
@@ -473,7 +501,16 @@ function printNextSteps(
|
|
|
473
501
|
console.log(` │ ├── api/ # Backend API`);
|
|
474
502
|
if (isFullstackTemplate(options.template)) {
|
|
475
503
|
console.log(` │ ├── auth/ # Auth service (better-auth)`);
|
|
476
|
-
|
|
504
|
+
switch (options.frontendFramework) {
|
|
505
|
+
case 'tanstack-start':
|
|
506
|
+
console.log(` │ └── web/ # TanStack Start frontend`);
|
|
507
|
+
break;
|
|
508
|
+
case 'expo':
|
|
509
|
+
console.log(` │ └── app/ # Expo (React Native) app`);
|
|
510
|
+
break;
|
|
511
|
+
default:
|
|
512
|
+
console.log(` │ └── web/ # Next.js frontend`);
|
|
513
|
+
}
|
|
477
514
|
}
|
|
478
515
|
console.log(` ├── packages/`);
|
|
479
516
|
console.log(` │ ├── models/ # Shared Zod schemas`);
|
|
@@ -47,6 +47,14 @@ export interface ServicesSelection {
|
|
|
47
47
|
events?: import('../../types.js').EventsBackend;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Frontend framework for the fullstack template's client app.
|
|
52
|
+
* Limited to the three with first-class scaffolds; the wider
|
|
53
|
+
* `FrontendFramework` type also covers `vite` and `remix` which
|
|
54
|
+
* users can configure manually after init.
|
|
55
|
+
*/
|
|
56
|
+
export type FullstackFrontendFramework = 'nextjs' | 'tanstack-start' | 'expo';
|
|
57
|
+
|
|
50
58
|
/**
|
|
51
59
|
* Options collected from user prompts
|
|
52
60
|
*/
|
|
@@ -67,6 +75,8 @@ export interface TemplateOptions {
|
|
|
67
75
|
deployTarget: DeployTarget;
|
|
68
76
|
/** Services selection */
|
|
69
77
|
services: ServicesSelection;
|
|
78
|
+
/** Frontend framework (fullstack template only) */
|
|
79
|
+
frontendFramework?: FullstackFrontendFramework;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
/**
|
|
@@ -156,6 +166,27 @@ export const allTemplateChoices = [
|
|
|
156
166
|
},
|
|
157
167
|
];
|
|
158
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Frontend framework choices for the fullstack template prompt.
|
|
171
|
+
*/
|
|
172
|
+
export const frontendFrameworkChoices = [
|
|
173
|
+
{
|
|
174
|
+
title: 'Next.js',
|
|
175
|
+
value: 'nextjs' as FullstackFrontendFramework,
|
|
176
|
+
description: 'React server components, app router (default)',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
title: 'TanStack Start',
|
|
180
|
+
value: 'tanstack-start' as FullstackFrontendFramework,
|
|
181
|
+
description: 'File-based router on Vite + Nitro',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: 'Expo',
|
|
185
|
+
value: 'expo' as FullstackFrontendFramework,
|
|
186
|
+
description: 'React Native mobile app (iOS + Android)',
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
159
190
|
/**
|
|
160
191
|
* Logger type choices for prompts
|
|
161
192
|
*/
|
package/src/init/versions.ts
CHANGED
|
@@ -35,9 +35,9 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
35
35
|
'@geekmidas/constructs': '~3.0.9',
|
|
36
36
|
'@geekmidas/db': '~1.0.1',
|
|
37
37
|
'@geekmidas/emailkit': '~1.0.0',
|
|
38
|
-
'@geekmidas/envkit': '~1.0.
|
|
38
|
+
'@geekmidas/envkit': '~1.0.6',
|
|
39
39
|
'@geekmidas/errors': '~1.0.0',
|
|
40
|
-
'@geekmidas/events': '~1.1.
|
|
40
|
+
'@geekmidas/events': '~1.1.1',
|
|
41
41
|
'@geekmidas/logger': '~1.0.1',
|
|
42
42
|
'@geekmidas/rate-limit': '~2.0.0',
|
|
43
43
|
'@geekmidas/schema': '~1.0.0',
|
package/src/openapi.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
3
4
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { loadWorkspaceConfig } from './config.js';
|
|
6
8
|
import { EndpointGenerator } from './generators/EndpointGenerator.js';
|
|
7
9
|
import { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';
|
|
8
10
|
import type { GkmConfig, OpenApiConfig } from './types.js';
|
|
9
11
|
import { normalizeRoutes } from './workspace/client-generator.js';
|
|
12
|
+
import type { NormalizedAppConfig } from './workspace/types.js';
|
|
10
13
|
|
|
11
14
|
interface OpenAPIOptions {
|
|
12
15
|
cwd?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Workspace mode: generate for a single named backend app. When set,
|
|
18
|
+
* generation runs in-process against the current CWD. When unset,
|
|
19
|
+
* workspace mode spawns one subprocess per backend app so each gets a
|
|
20
|
+
* fresh tsx that loads the app's own tsconfig (path aliases included).
|
|
21
|
+
*/
|
|
22
|
+
app?: string;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
/**
|
|
@@ -132,37 +142,114 @@ export async function openapiCommand(
|
|
|
132
142
|
return;
|
|
133
143
|
}
|
|
134
144
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
openapi: app.openapi,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Change to app directory for generation
|
|
151
|
-
const originalCwd = process.cwd();
|
|
152
|
-
process.chdir(appPath);
|
|
153
|
-
|
|
154
|
-
const result = await generateOpenApi(gkmConfig, { silent: true });
|
|
155
|
-
|
|
156
|
-
process.chdir(originalCwd);
|
|
157
|
-
|
|
145
|
+
// Single-app mode: generate in-process. The caller is expected to
|
|
146
|
+
// have CWD set to the app's directory (e.g., when invoked as a
|
|
147
|
+
// subprocess), so tsx picks up the app's tsconfig path aliases.
|
|
148
|
+
if (options.app) {
|
|
149
|
+
const entry = backendApps.find(([name]) => name === options.app);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`App "${options.app}" not found or has OpenAPI disabled`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const [appName, app] = entry;
|
|
156
|
+
const result = await generateOpenApiForApp(workspaceRoot, appName, app);
|
|
158
157
|
if (result) {
|
|
159
158
|
logger.log(
|
|
160
159
|
`📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,
|
|
161
160
|
);
|
|
162
161
|
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Multi-app mode: spawn a subprocess per backend app. Each
|
|
166
|
+
// subprocess starts with CWD at the app's directory so that
|
|
167
|
+
// tsx's tsconfig discovery picks up the app's `paths` aliases
|
|
168
|
+
// (e.g., `~/*`) instead of the workspace root's tsconfig.
|
|
169
|
+
for (const [appName, app] of backendApps) {
|
|
170
|
+
if (app.type !== 'backend' || !app.routes) continue;
|
|
171
|
+
const appPath = join(workspaceRoot, app.path);
|
|
172
|
+
await runOpenApiInSubprocess(appPath, appName);
|
|
163
173
|
}
|
|
164
174
|
}
|
|
165
175
|
} catch (error) {
|
|
166
176
|
throw new Error(`OpenAPI generation failed: ${(error as Error).message}`);
|
|
167
177
|
}
|
|
168
178
|
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate OpenAPI for a single named app within a workspace.
|
|
182
|
+
* Runs in-process. The caller is responsible for ensuring `process.cwd()`
|
|
183
|
+
* is the app's directory so tsx loads the app's tsconfig path aliases.
|
|
184
|
+
*/
|
|
185
|
+
async function generateOpenApiForApp(
|
|
186
|
+
workspaceRoot: string,
|
|
187
|
+
_appName: string,
|
|
188
|
+
app: NormalizedAppConfig,
|
|
189
|
+
): Promise<{ outputPath: string; endpointCount: number } | null> {
|
|
190
|
+
if (app.type !== 'backend' || !app.routes) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const appPath = join(workspaceRoot, app.path);
|
|
195
|
+
const routes = normalizeRoutes(app.routes);
|
|
196
|
+
const routesGlob = routes.map((r) => join(appPath, r));
|
|
197
|
+
|
|
198
|
+
const gkmConfig: GkmConfig = {
|
|
199
|
+
routes: routesGlob,
|
|
200
|
+
envParser: app.envParser || '',
|
|
201
|
+
logger: app.logger || '',
|
|
202
|
+
openapi: app.openapi,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return generateOpenApi(gkmConfig, { silent: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve the gkm bin path. Tests can override via `GKM_BIN_PATH` env var
|
|
210
|
+
* to avoid depending on the built dist.
|
|
211
|
+
*/
|
|
212
|
+
function resolveGkmBinPath(): string {
|
|
213
|
+
if (process.env.GKM_BIN_PATH) {
|
|
214
|
+
return process.env.GKM_BIN_PATH;
|
|
215
|
+
}
|
|
216
|
+
return fileURLToPath(new URL('../bin/gkm.mjs', import.meta.url));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Spawn a subprocess that runs `gkm openapi --app <name>` with `cwd` set to
|
|
221
|
+
* the app's directory. Inherits `NODE_OPTIONS` (which already contains
|
|
222
|
+
* `--import tsx`), so the child gets a fresh tsx instance whose tsconfig
|
|
223
|
+
* discovery picks up the app's tsconfig path aliases.
|
|
224
|
+
*/
|
|
225
|
+
async function runOpenApiInSubprocess(
|
|
226
|
+
appCwd: string,
|
|
227
|
+
appName: string,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const binPath = resolveGkmBinPath();
|
|
230
|
+
|
|
231
|
+
await new Promise<void>((resolve, reject) => {
|
|
232
|
+
const child = spawn(
|
|
233
|
+
process.execPath,
|
|
234
|
+
[binPath, 'openapi', '--app', appName],
|
|
235
|
+
{
|
|
236
|
+
cwd: appCwd,
|
|
237
|
+
stdio: 'inherit',
|
|
238
|
+
env: process.env,
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
child.on('error', reject);
|
|
243
|
+
child.on('close', (code) => {
|
|
244
|
+
if (code === 0) {
|
|
245
|
+
resolve();
|
|
246
|
+
} else {
|
|
247
|
+
reject(
|
|
248
|
+
new Error(
|
|
249
|
+
`OpenAPI generation for app "${appName}" exited with code ${code}`,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
@@ -29,7 +29,7 @@ function createWorkspace(
|
|
|
29
29
|
dependencies: [],
|
|
30
30
|
},
|
|
31
31
|
web: {
|
|
32
|
-
type: '
|
|
32
|
+
type: 'web',
|
|
33
33
|
port: 3002,
|
|
34
34
|
root: '/tmp/test-project/apps/web',
|
|
35
35
|
packageName: '@test/web',
|
|
@@ -333,7 +333,7 @@ describe('generateFullstackCustomSecrets', () => {
|
|
|
333
333
|
dependencies: [],
|
|
334
334
|
},
|
|
335
335
|
web: {
|
|
336
|
-
type: '
|
|
336
|
+
type: 'web',
|
|
337
337
|
port: 3001,
|
|
338
338
|
root: '/tmp/test-project/apps/web',
|
|
339
339
|
path: 'apps/web',
|