@noego/app 0.0.1

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.
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+
4
+ import { createBuildContext } from '../build/context.js';
5
+ import { runCommand } from '../utils/command.js';
6
+
7
+ export async function runServe(config) {
8
+ const context = createBuildContext(config);
9
+ const { logger } = context;
10
+
11
+ const runner = await resolveRunner(context);
12
+ logger.info(`Starting dev server using ${runner.kind}`);
13
+
14
+ if (!config.dev.watch) {
15
+ const args = [...runner.args, config.server.entry.absolute];
16
+ await runCommand(process.execPath, args, {
17
+ cwd: config.rootDir,
18
+ env: { NODE_ENV: 'development' },
19
+ logger
20
+ });
21
+ return;
22
+ }
23
+
24
+ const watcher = await createWatcher(context);
25
+ let frontend = null;
26
+ if (config.dev.splitServe) {
27
+ frontend = await startFrontend(context).catch((e) => {
28
+ logger.info(`Frontend split-serve disabled: ${e?.message || e}`);
29
+ return null;
30
+ });
31
+ }
32
+ await runWithRestart(context, runner, watcher, frontend);
33
+ }
34
+
35
+ async function resolveRunner(context) {
36
+ const { requireFromRoot } = context;
37
+
38
+ // Prefer tsx (handles ESM/CJS seamlessly and integrates well with Vite/dev loaders)
39
+ try {
40
+ const tsxCli = requireFromRoot.resolve('tsx/dist/cli.js');
41
+ const args = [tsxCli];
42
+ // If Forge dev loader is available, attach it (keeps current dev SSR behavior)
43
+ try {
44
+ // Use export path if available; if resolution fails, we skip attaching a loader
45
+ requireFromRoot.resolve('@noego/forge/loader');
46
+ args.push('--loader', '@noego/forge/loader');
47
+ } catch {}
48
+ return { kind: 'tsx', args };
49
+ } catch {}
50
+
51
+ // Fallback: ts-node (transpile-only)
52
+ try {
53
+ const tsnode = requireFromRoot.resolve('ts-node/register/transpile-only');
54
+ return { kind: 'ts-node', args: ['-r', tsnode] };
55
+ } catch {}
56
+
57
+ // Last resort: plain node (will likely fail on .ts). Provide a helpful error.
58
+ const err = new Error(
59
+ 'No dev TypeScript runner found. Please install "tsx" (preferred) or "ts-node" in your project.'
60
+ );
61
+ err.code = 'CLI_USAGE';
62
+ throw err;
63
+ }
64
+
65
+ async function createWatcher(context) {
66
+ const { config, requireFromRoot, logger } = context;
67
+ const chokidar = requireFromRoot('chokidar');
68
+
69
+ // Build watch list
70
+ const patterns = new Set();
71
+
72
+ const addPattern = (value) => {
73
+ if (!value) return;
74
+ patterns.add(value);
75
+ };
76
+
77
+ const resolveGlob = (entry) => {
78
+ if (!entry) return null;
79
+ if (typeof entry === 'string') {
80
+ return path.isAbsolute(entry) ? entry : path.join(config.rootDir, entry);
81
+ }
82
+ if (entry.isAbsolute) {
83
+ return entry.pattern;
84
+ }
85
+ if (entry.cwd) {
86
+ return path.join(entry.cwd, entry.pattern);
87
+ }
88
+ return entry.pattern;
89
+ };
90
+
91
+ // User-supplied patterns
92
+ for (const entry of config.dev.watchPaths) {
93
+ const pattern = resolveGlob(entry);
94
+ if (pattern) addPattern(pattern);
95
+ }
96
+
97
+ // Server-side defaults
98
+ addPattern(path.join(config.server.controllersDir, '**/*.{ts,js}'));
99
+ addPattern(path.join(config.server.middlewareDir, '**/*.{ts,js}'));
100
+ addPattern(config.server.openapiFile);
101
+ addPattern(path.join(path.dirname(config.server.openapiFile), 'openapi/**/*.yaml'));
102
+ for (const entry of config.server.sqlGlobs) {
103
+ const pattern = resolveGlob(entry);
104
+ if (pattern) addPattern(pattern);
105
+ }
106
+
107
+ // UI OpenAPI changes should restart (routes/manifest)
108
+ addPattern(config.ui.openapiFile);
109
+ addPattern(path.join(path.dirname(config.ui.openapiFile), 'openapi/**/*.yaml'));
110
+
111
+ // Ignore Svelte/client files to let Vite HMR handle them
112
+ const uiIgnores = [
113
+ path.join(config.ui.rootDir, '**/*.svelte'),
114
+ path.join(config.ui.rootDir, '**/*.{ts,tsx,css,scss,html}')
115
+ ];
116
+
117
+ logger.info('Watching for changes to restart server...');
118
+ const watcher = chokidar.watch(Array.from(patterns), {
119
+ ignoreInitial: true,
120
+ ignored: uiIgnores,
121
+ cwd: config.rootDir
122
+ });
123
+ return watcher;
124
+ }
125
+
126
+ async function runWithRestart(context, runner, watcher, frontendProc) {
127
+ const { config, logger } = context;
128
+
129
+ let child = null;
130
+ const start = () => {
131
+ const args = [...runner.args, config.server.entry.absolute];
132
+ child = spawn(process.execPath, args, {
133
+ cwd: config.rootDir,
134
+ env: { ...process.env, NODE_ENV: 'development' },
135
+ stdio: 'inherit'
136
+ });
137
+ child.on('exit', (code) => {
138
+ if (code !== null && code !== 0) {
139
+ logger.info(`Server exited with code ${code}`);
140
+ }
141
+ });
142
+ };
143
+
144
+ const stop = () =>
145
+ new Promise((resolve) => {
146
+ if (!child) return resolve();
147
+ const to = setTimeout(resolve, 2000);
148
+ child.once('exit', () => {
149
+ clearTimeout(to);
150
+ resolve();
151
+ });
152
+ try {
153
+ child.kill('SIGTERM');
154
+ } catch {
155
+ resolve();
156
+ }
157
+ });
158
+
159
+ let pending = false;
160
+ const scheduleRestart = async (reason, file) => {
161
+ if (pending) return;
162
+ pending = true;
163
+ logger.info(`Change detected (${reason}): ${file}. Restarting...`);
164
+ await stop();
165
+ start();
166
+ pending = false;
167
+ };
168
+
169
+ watcher
170
+ .on('add', (p) => scheduleRestart('add', p))
171
+ .on('change', (p) => scheduleRestart('change', p))
172
+ .on('unlink', (p) => scheduleRestart('unlink', p));
173
+
174
+ // Start initial server
175
+ start();
176
+
177
+ // Keep process alive until SIGINT/SIGTERM
178
+ const shutdown = async () => {
179
+ await stop();
180
+ await watcher.close();
181
+ if (frontendProc) {
182
+ try { frontendProc.kill('SIGTERM'); } catch {}
183
+ }
184
+ process.exit(0);
185
+ };
186
+ process.on('SIGINT', shutdown);
187
+ process.on('SIGTERM', shutdown);
188
+ }
189
+
190
+ async function startFrontend(context) {
191
+ const { config, requireFromRoot, logger } = context;
192
+ const cmd = (config.dev.frontendCmd || 'vite').toLowerCase();
193
+ if (cmd !== 'vite') {
194
+ throw new Error(`Unsupported --frontend-cmd '${cmd}'. Only 'vite' is supported currently.`);
195
+ }
196
+ // Resolve vite bin and spawn
197
+ let viteBin;
198
+ try {
199
+ viteBin = requireFromRoot.resolve('vite/bin/vite.js');
200
+ } catch {
201
+ throw new Error('vite not found in project. Install it or omit --split-serve.');
202
+ }
203
+ logger.info('Starting frontend dev server (vite) in separate process');
204
+ const child = spawn(process.execPath, [viteBin], {
205
+ cwd: config.ui.rootDir,
206
+ env: { ...process.env, NODE_ENV: 'development' },
207
+ stdio: 'inherit'
208
+ });
209
+ child.on('exit', (code) => {
210
+ logger.info(`Frontend dev server exited with code ${code}`);
211
+ });
212
+ return child;
213
+ }