@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.
- package/AGENTS.md +457 -0
- package/bin/app.js +5 -0
- package/docs/design.md +107 -0
- package/package.json +24 -0
- package/src/args.js +180 -0
- package/src/build/bootstrap.js +43 -0
- package/src/build/client-modules.js +9 -0
- package/src/build/client.js +206 -0
- package/src/build/context.js +16 -0
- package/src/build/fix-imports.js +99 -0
- package/src/build/helpers.js +29 -0
- package/src/build/html.js +83 -0
- package/src/build/openapi.js +249 -0
- package/src/build/plugins/client-exclude.js +90 -0
- package/src/build/runtime-manifest.js +64 -0
- package/src/build/server.js +294 -0
- package/src/build/ssr.js +257 -0
- package/src/build/ui-common.js +188 -0
- package/src/build/vite.js +45 -0
- package/src/cli.js +72 -0
- package/src/commands/build.js +59 -0
- package/src/commands/preview.js +33 -0
- package/src/commands/serve.js +213 -0
- package/src/config.js +584 -0
- package/src/logger.js +16 -0
- package/src/utils/command.js +23 -0
|
@@ -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
|
+
}
|