@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
package/src/config.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import deepmerge from 'deepmerge';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILENAMES = [
|
|
8
|
+
// Primary App config filenames
|
|
9
|
+
'noego.yaml',
|
|
10
|
+
'noego.yml',
|
|
11
|
+
'noego.config.yaml',
|
|
12
|
+
'noego.config.yml',
|
|
13
|
+
'noego.config.json',
|
|
14
|
+
// Common App config filenames
|
|
15
|
+
'app.yaml',
|
|
16
|
+
'app.yml',
|
|
17
|
+
'app.config.yaml',
|
|
18
|
+
'app.config.yml',
|
|
19
|
+
'app.config.json',
|
|
20
|
+
// Back-compat with legacy Hammer config filenames
|
|
21
|
+
'hammer.yaml',
|
|
22
|
+
'hammer.yml',
|
|
23
|
+
'hammer.config.yaml',
|
|
24
|
+
'hammer.config.yml',
|
|
25
|
+
'hammer.config.json'
|
|
26
|
+
];
|
|
27
|
+
const OVERRIDE_FILE_KEY = '__appFromFile';
|
|
28
|
+
|
|
29
|
+
const DEFAULTS = {
|
|
30
|
+
mode: 'production',
|
|
31
|
+
outDir: 'dist',
|
|
32
|
+
assets: ['ui/resources/**', 'ui/styles/**', 'database/**'],
|
|
33
|
+
server: {
|
|
34
|
+
entry: 'index.ts',
|
|
35
|
+
rootDir: '.',
|
|
36
|
+
controllers: 'server/controller',
|
|
37
|
+
middleware: 'middleware',
|
|
38
|
+
openapi: 'server/stitch.yaml',
|
|
39
|
+
sqlGlobs: []
|
|
40
|
+
},
|
|
41
|
+
ui: {
|
|
42
|
+
page: 'ui/index.html',
|
|
43
|
+
rootDir: null,
|
|
44
|
+
openapi: 'ui/stitch.yaml',
|
|
45
|
+
assets: [],
|
|
46
|
+
clientExclude: ['server/**', 'middleware/**'],
|
|
47
|
+
options: 'ui/options.ts'
|
|
48
|
+
},
|
|
49
|
+
vite: {
|
|
50
|
+
client: {
|
|
51
|
+
configFile: null,
|
|
52
|
+
override: null
|
|
53
|
+
},
|
|
54
|
+
ssr: {
|
|
55
|
+
configFile: null,
|
|
56
|
+
override: null
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
dev: {
|
|
60
|
+
watch: false,
|
|
61
|
+
watchPaths: [],
|
|
62
|
+
splitServe: false,
|
|
63
|
+
frontendCmd: 'vite'
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export async function loadBuildConfig(cliOptions = {}, { cwd = process.cwd() } = {}) {
|
|
68
|
+
const rootDir = resolvePath(cliOptions.root ?? '.', cwd);
|
|
69
|
+
const configFilePath = await findConfigFile(rootDir, cliOptions.configFile);
|
|
70
|
+
|
|
71
|
+
const fileRaw =
|
|
72
|
+
configFilePath != null ? await loadConfigFile(configFilePath) : {};
|
|
73
|
+
const fileNormalized =
|
|
74
|
+
configFilePath != null
|
|
75
|
+
? normalizeExternalConfig(fileRaw, { baseDir: path.dirname(configFilePath) })
|
|
76
|
+
: {};
|
|
77
|
+
|
|
78
|
+
const cliNormalized = normalizeCliOptions(cliOptions);
|
|
79
|
+
|
|
80
|
+
const mergedRaw = deepmerge.all(
|
|
81
|
+
[DEFAULTS, fileNormalized, cliNormalized],
|
|
82
|
+
{
|
|
83
|
+
arrayMerge: (_target, source) => Array.isArray(source) ? source : []
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const overrides = await resolveOverrides(mergedRaw);
|
|
88
|
+
const finalConfig = finalizeConfig(overrides, { rootDir });
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...finalConfig,
|
|
92
|
+
sources: {
|
|
93
|
+
rootDir,
|
|
94
|
+
configFile: configFilePath ?? null
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function findConfigFile(rootDir, explicit) {
|
|
100
|
+
if (explicit) {
|
|
101
|
+
const candidate = resolvePath(explicit, rootDir);
|
|
102
|
+
await assertFile(candidate, `Config file not found: ${explicit}`);
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const fileName of CONFIG_FILENAMES) {
|
|
107
|
+
const candidate = path.join(rootDir, fileName);
|
|
108
|
+
if (await pathExists(candidate)) {
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function loadConfigFile(filePath) {
|
|
116
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
117
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
118
|
+
return loadYamlConfig(filePath);
|
|
119
|
+
}
|
|
120
|
+
if (ext === '.json') {
|
|
121
|
+
return loadJsonConfig(filePath);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Unsupported App config extension for ${filePath}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function loadYamlConfig(filePath) {
|
|
127
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
128
|
+
const parsed = YAML.parse(content);
|
|
129
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
130
|
+
throw new Error(`Invalid YAML configuration in ${filePath}: expected an object`);
|
|
131
|
+
}
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function loadJsonConfig(filePath) {
|
|
136
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(content);
|
|
139
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
140
|
+
throw new Error('expected an object');
|
|
141
|
+
}
|
|
142
|
+
return parsed;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(`Invalid JSON configuration in ${filePath}: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeExternalConfig(raw, { baseDir }) {
|
|
149
|
+
const result = {};
|
|
150
|
+
|
|
151
|
+
if (raw.mode != null) {
|
|
152
|
+
result.mode = raw.mode;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (raw.outDir != null) {
|
|
156
|
+
result.outDir = resolveMaybePath(raw.outDir, baseDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Top-level page shortcut
|
|
160
|
+
if (raw.page != null) {
|
|
161
|
+
result.ui = result.ui || {};
|
|
162
|
+
result.ui.page = resolveMaybePath(raw.page, baseDir);
|
|
163
|
+
}
|
|
164
|
+
if (raw.options != null) {
|
|
165
|
+
result.ui = result.ui || {};
|
|
166
|
+
result.ui.options = resolveMaybePath(raw.options, baseDir);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (raw.assets != null) {
|
|
170
|
+
result.assets = normalizeList(raw.assets).map((glob) =>
|
|
171
|
+
resolveMaybePath(glob, baseDir)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (raw.server) {
|
|
176
|
+
result.server = {};
|
|
177
|
+
if (raw.server.entry != null) {
|
|
178
|
+
result.server.entry = resolveMaybePath(raw.server.entry, baseDir);
|
|
179
|
+
}
|
|
180
|
+
if (raw.server.rootDir != null) {
|
|
181
|
+
result.server.rootDir = resolveMaybePath(raw.server.rootDir, baseDir);
|
|
182
|
+
}
|
|
183
|
+
if (raw.server.controllers != null) {
|
|
184
|
+
result.server.controllers = resolveMaybePath(raw.server.controllers, baseDir);
|
|
185
|
+
}
|
|
186
|
+
if (raw.server.middleware != null) {
|
|
187
|
+
result.server.middleware = resolveMaybePath(raw.server.middleware, baseDir);
|
|
188
|
+
}
|
|
189
|
+
if (raw.server.openapi != null) {
|
|
190
|
+
result.server.openapi = resolveMaybePath(raw.server.openapi, baseDir);
|
|
191
|
+
}
|
|
192
|
+
if (raw.server.sqlGlobs != null) {
|
|
193
|
+
result.server.sqlGlobs = normalizeList(raw.server.sqlGlobs).map((glob) =>
|
|
194
|
+
resolveMaybePath(glob, baseDir)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (raw.ui) {
|
|
200
|
+
result.ui = {};
|
|
201
|
+
if (raw.ui.page != null) {
|
|
202
|
+
result.ui.page = resolveMaybePath(raw.ui.page, baseDir);
|
|
203
|
+
}
|
|
204
|
+
if (raw.ui.options != null) {
|
|
205
|
+
result.ui.options = resolveMaybePath(raw.ui.options, baseDir);
|
|
206
|
+
}
|
|
207
|
+
if (raw.ui.rootDir != null) {
|
|
208
|
+
result.ui.rootDir = resolveMaybePath(raw.ui.rootDir, baseDir);
|
|
209
|
+
}
|
|
210
|
+
if (raw.ui.openapi != null) {
|
|
211
|
+
result.ui.openapi = resolveMaybePath(raw.ui.openapi, baseDir);
|
|
212
|
+
}
|
|
213
|
+
if (raw.ui.assets != null) {
|
|
214
|
+
result.ui.assets = normalizeList(raw.ui.assets).map((glob) =>
|
|
215
|
+
resolveMaybePath(glob, baseDir)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (raw.ui.clientExclude != null) {
|
|
219
|
+
result.ui.clientExclude = normalizeList(raw.ui.clientExclude).map((glob) =>
|
|
220
|
+
resolveMaybePath(glob, baseDir)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (raw.vite) {
|
|
226
|
+
result.vite = {};
|
|
227
|
+
if (raw.vite.client) {
|
|
228
|
+
result.vite.client = {};
|
|
229
|
+
if (raw.vite.client.configFile != null) {
|
|
230
|
+
result.vite.client.configFile = resolveMaybePath(raw.vite.client.configFile, baseDir);
|
|
231
|
+
}
|
|
232
|
+
if (raw.vite.client.override != null) {
|
|
233
|
+
result.vite.client.override = wrapOverride(raw.vite.client.override, baseDir);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (raw.vite.ssr) {
|
|
237
|
+
result.vite.ssr = {};
|
|
238
|
+
if (raw.vite.ssr.configFile != null) {
|
|
239
|
+
result.vite.ssr.configFile = resolveMaybePath(raw.vite.ssr.configFile, baseDir);
|
|
240
|
+
}
|
|
241
|
+
if (raw.vite.ssr.override != null) {
|
|
242
|
+
result.vite.ssr.override = wrapOverride(raw.vite.ssr.override, baseDir);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (raw.dev) {
|
|
248
|
+
result.dev = {};
|
|
249
|
+
if (raw.dev.watch != null) {
|
|
250
|
+
result.dev.watch = Boolean(raw.dev.watch);
|
|
251
|
+
}
|
|
252
|
+
if (raw.dev.watchPaths != null) {
|
|
253
|
+
result.dev.watchPaths = normalizeList(raw.dev.watchPaths).map((glob) =>
|
|
254
|
+
resolveMaybePath(glob, baseDir)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (raw.dev.splitServe != null) {
|
|
258
|
+
result.dev.splitServe = Boolean(raw.dev.splitServe);
|
|
259
|
+
}
|
|
260
|
+
if (raw.dev.frontendCmd != null) {
|
|
261
|
+
result.dev.frontendCmd = raw.dev.frontendCmd;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeCliOptions(options) {
|
|
269
|
+
const result = {};
|
|
270
|
+
|
|
271
|
+
if (options.out != null) {
|
|
272
|
+
result.outDir = options.out;
|
|
273
|
+
}
|
|
274
|
+
if (options.mode != null) {
|
|
275
|
+
result.mode = options.mode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (options.server != null || options.serverRoot != null || options.controllers != null || options.middleware != null || options.openapi != null || options.sqlGlob != null) {
|
|
279
|
+
result.server = {};
|
|
280
|
+
if (options.server != null) result.server.entry = options.server;
|
|
281
|
+
if (options.serverRoot != null) result.server.rootDir = options.serverRoot;
|
|
282
|
+
if (options.controllers != null) result.server.controllers = options.controllers;
|
|
283
|
+
if (options.middleware != null) result.server.middleware = options.middleware;
|
|
284
|
+
if (options.openapi != null) result.server.openapi = options.openapi;
|
|
285
|
+
if (options.sqlGlob != null) result.server.sqlGlobs = normalizeList(options.sqlGlob);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (options.page != null || options.uiRoot != null || options.uiOpenapi != null || options.uiOptions != null || options.clientExclude != null) {
|
|
289
|
+
result.ui = {};
|
|
290
|
+
if (options.page != null) result.ui.page = options.page;
|
|
291
|
+
if (options.uiRoot != null) result.ui.rootDir = options.uiRoot;
|
|
292
|
+
if (options.uiOpenapi != null) result.ui.openapi = options.uiOpenapi;
|
|
293
|
+
if (options.uiOptions != null) result.ui.options = options.uiOptions;
|
|
294
|
+
if (options.clientExclude != null) result.ui.clientExclude = normalizeList(options.clientExclude);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (options.assets != null) {
|
|
298
|
+
result.assets = normalizeList(options.assets);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (options.clientViteConfig != null || options.clientViteOverride != null || options.ssrViteConfig != null || options.ssrViteOverride != null) {
|
|
302
|
+
result.vite = {};
|
|
303
|
+
if (options.clientViteConfig != null || options.clientViteOverride != null) {
|
|
304
|
+
result.vite.client = {};
|
|
305
|
+
if (options.clientViteConfig != null) {
|
|
306
|
+
result.vite.client.configFile = options.clientViteConfig;
|
|
307
|
+
}
|
|
308
|
+
if (options.clientViteOverride != null) {
|
|
309
|
+
result.vite.client.override = parseJsonOption(options.clientViteOverride, '--client-vite-override');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (options.ssrViteConfig != null || options.ssrViteOverride != null) {
|
|
313
|
+
result.vite.ssr = {};
|
|
314
|
+
if (options.ssrViteConfig != null) {
|
|
315
|
+
result.vite.ssr.configFile = options.ssrViteConfig;
|
|
316
|
+
}
|
|
317
|
+
if (options.ssrViteOverride != null) {
|
|
318
|
+
result.vite.ssr.override = parseJsonOption(options.ssrViteOverride, '--ssr-vite-override');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (options.watch != null || options.watchPath != null || options.splitServe != null || options.frontendCmd != null) {
|
|
324
|
+
result.dev = {};
|
|
325
|
+
if (options.watch != null) result.dev.watch = Boolean(options.watch);
|
|
326
|
+
if (options.watchPath != null) result.dev.watchPaths = normalizeList(options.watchPath);
|
|
327
|
+
if (options.splitServe != null) result.dev.splitServe = Boolean(options.splitServe);
|
|
328
|
+
if (options.frontendCmd != null) result.dev.frontendCmd = options.frontendCmd;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function resolveOverrides(raw) {
|
|
335
|
+
const resolved = structuredClone(raw);
|
|
336
|
+
|
|
337
|
+
const clientOverride = raw?.vite?.client?.override;
|
|
338
|
+
if (clientOverride && typeof clientOverride === 'object' && OVERRIDE_FILE_KEY in clientOverride) {
|
|
339
|
+
const loaded = await readJsonFile(clientOverride[OVERRIDE_FILE_KEY]);
|
|
340
|
+
resolved.vite.client.override = loaded;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const ssrOverride = raw?.vite?.ssr?.override;
|
|
344
|
+
if (ssrOverride && typeof ssrOverride === 'object' && OVERRIDE_FILE_KEY in ssrOverride) {
|
|
345
|
+
const loaded = await readJsonFile(ssrOverride[OVERRIDE_FILE_KEY]);
|
|
346
|
+
resolved.vite.ssr.override = loaded;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return resolved;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function finalizeConfig(raw, { rootDir }) {
|
|
353
|
+
const outDir = resolvePath(raw.outDir ?? DEFAULTS.outDir, rootDir);
|
|
354
|
+
|
|
355
|
+
const serverRootDir = resolvePath(raw.server?.rootDir ?? DEFAULTS.server.rootDir ?? '.', rootDir);
|
|
356
|
+
const serverEntryRaw = raw.server?.entry ?? DEFAULTS.server.entry;
|
|
357
|
+
const serverEntryAbs = resolveWithBase(serverEntryRaw, serverRootDir);
|
|
358
|
+
|
|
359
|
+
const controllersDir = resolvePath(raw.server?.controllers ?? DEFAULTS.server.controllers, rootDir);
|
|
360
|
+
const middlewareDir = resolvePath(raw.server?.middleware ?? DEFAULTS.server.middleware, rootDir);
|
|
361
|
+
const openapiFile = resolvePath(raw.server?.openapi ?? DEFAULTS.server.openapi, rootDir);
|
|
362
|
+
const sqlGlobs = normalizeGlobList(raw.server?.sqlGlobs ?? DEFAULTS.server.sqlGlobs, rootDir);
|
|
363
|
+
|
|
364
|
+
const pageRaw = raw.ui?.page ?? DEFAULTS.ui.page;
|
|
365
|
+
const pageAbs = resolveWithBase(pageRaw, rootDir);
|
|
366
|
+
const uiRootDir = resolvePath(raw.ui?.rootDir ?? path.dirname(pageAbs), rootDir);
|
|
367
|
+
const optionsRaw = raw.ui?.options ?? DEFAULTS.ui.options;
|
|
368
|
+
const optionsAbs = resolveWithBase(optionsRaw, rootDir);
|
|
369
|
+
const uiOpenapiFile = resolvePath(raw.ui?.openapi ?? DEFAULTS.ui.openapi, rootDir);
|
|
370
|
+
const assetSources = collectAssetPatterns({
|
|
371
|
+
topLevel: raw.assets,
|
|
372
|
+
topLevelDefault: DEFAULTS.assets,
|
|
373
|
+
uiLevel: raw.ui?.assets,
|
|
374
|
+
uiDefault: DEFAULTS.ui.assets
|
|
375
|
+
});
|
|
376
|
+
const assetGlobs = normalizeGlobList(assetSources, rootDir);
|
|
377
|
+
const clientExclude = normalizeGlobList(raw.ui?.clientExclude ?? DEFAULTS.ui.clientExclude, rootDir);
|
|
378
|
+
|
|
379
|
+
const uiRelRoot = path.relative(rootDir, uiRootDir);
|
|
380
|
+
const layout = {
|
|
381
|
+
outDir,
|
|
382
|
+
serverOutDir: path.join(outDir, 'server'),
|
|
383
|
+
// Place client bundle under .app
|
|
384
|
+
clientOutDir: path.join(outDir, '.app', 'assets'),
|
|
385
|
+
// Mirror UI outputs under the same relative root (no hardcoded 'ui')
|
|
386
|
+
uiOutDir: path.join(outDir, uiRelRoot),
|
|
387
|
+
// SSR precompile staging (build-only); will be overlaid into uiOutDir
|
|
388
|
+
ssrOutDir: path.join(outDir, '.app', 'ssr'),
|
|
389
|
+
middlewareOutDir: path.join(outDir, 'middleware'),
|
|
390
|
+
tempDir: path.join(outDir, '.app'),
|
|
391
|
+
tsOutDir: path.join(outDir, '.app', 'tsc')
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
mode: raw.mode ?? DEFAULTS.mode,
|
|
396
|
+
rootDir,
|
|
397
|
+
outDir,
|
|
398
|
+
// publicPaths removed; client base is fixed to '/.app/client'
|
|
399
|
+
layout,
|
|
400
|
+
server: {
|
|
401
|
+
rootDir: serverRootDir,
|
|
402
|
+
entry: {
|
|
403
|
+
raw: serverEntryRaw,
|
|
404
|
+
absolute: serverEntryAbs,
|
|
405
|
+
relativeToRoot: path.relative(rootDir, serverEntryAbs),
|
|
406
|
+
relativeToServerRoot: path.relative(serverRootDir, serverEntryAbs)
|
|
407
|
+
},
|
|
408
|
+
controllersDir,
|
|
409
|
+
middlewareDir,
|
|
410
|
+
openapiFile,
|
|
411
|
+
sqlGlobs
|
|
412
|
+
},
|
|
413
|
+
ui: {
|
|
414
|
+
rootDir: uiRootDir,
|
|
415
|
+
page: {
|
|
416
|
+
raw: pageRaw,
|
|
417
|
+
absolute: pageAbs,
|
|
418
|
+
relativeToRoot: path.relative(rootDir, pageAbs)
|
|
419
|
+
},
|
|
420
|
+
options: {
|
|
421
|
+
raw: optionsRaw,
|
|
422
|
+
absolute: optionsAbs,
|
|
423
|
+
relativeToRoot: path.relative(rootDir, optionsAbs),
|
|
424
|
+
relativeToUiRoot: path.relative(uiRootDir, optionsAbs)
|
|
425
|
+
},
|
|
426
|
+
openapiFile: uiOpenapiFile,
|
|
427
|
+
assets: assetGlobs,
|
|
428
|
+
clientExclude
|
|
429
|
+
},
|
|
430
|
+
assets: assetGlobs,
|
|
431
|
+
vite: {
|
|
432
|
+
client: {
|
|
433
|
+
configFile: raw?.vite?.client?.configFile
|
|
434
|
+
? resolveMaybePath(raw.vite.client.configFile, rootDir)
|
|
435
|
+
: null,
|
|
436
|
+
override: raw?.vite?.client?.override ?? null
|
|
437
|
+
},
|
|
438
|
+
ssr: {
|
|
439
|
+
configFile: raw?.vite?.ssr?.configFile
|
|
440
|
+
? resolveMaybePath(raw.vite.ssr.configFile, rootDir)
|
|
441
|
+
: null,
|
|
442
|
+
override: raw?.vite?.ssr?.override ?? null
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
dev: {
|
|
446
|
+
watch: Boolean(raw?.dev?.watch ?? DEFAULTS.dev.watch),
|
|
447
|
+
watchPaths: normalizeGlobList(raw?.dev?.watchPaths ?? DEFAULTS.dev.watchPaths, rootDir),
|
|
448
|
+
splitServe: Boolean(raw?.dev?.splitServe ?? DEFAULTS.dev.splitServe),
|
|
449
|
+
frontendCmd: raw?.dev?.frontendCmd ?? DEFAULTS.dev.frontendCmd
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function collectAssetPatterns({ topLevel, topLevelDefault, uiLevel, uiDefault }) {
|
|
455
|
+
const patterns = new Set();
|
|
456
|
+
|
|
457
|
+
const addAll = (value) => {
|
|
458
|
+
if (value == null) return;
|
|
459
|
+
for (const entry of normalizeList(value)) {
|
|
460
|
+
if (entry == null || entry === '') continue;
|
|
461
|
+
patterns.add(entry);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (topLevel != null) {
|
|
466
|
+
addAll(topLevel);
|
|
467
|
+
} else {
|
|
468
|
+
addAll(topLevelDefault);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (uiLevel != null) {
|
|
472
|
+
addAll(uiLevel);
|
|
473
|
+
} else {
|
|
474
|
+
addAll(uiDefault);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return Array.from(patterns);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function normalizeGlobList(values, baseDir) {
|
|
481
|
+
return normalizeList(values).map((pattern) => ({
|
|
482
|
+
pattern,
|
|
483
|
+
isAbsolute: path.isAbsolute(pattern),
|
|
484
|
+
cwd: path.isAbsolute(pattern) ? undefined : baseDir,
|
|
485
|
+
absolute: path.isAbsolute(pattern) ? pattern : path.resolve(baseDir, pattern)
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function wrapOverride(value, baseDir) {
|
|
490
|
+
if (value == null) return null;
|
|
491
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
if (typeof value === 'string') {
|
|
495
|
+
const trimmed = value.trim();
|
|
496
|
+
try {
|
|
497
|
+
return JSON.parse(trimmed);
|
|
498
|
+
} catch {
|
|
499
|
+
const resolved = resolveMaybePath(trimmed, baseDir);
|
|
500
|
+
return { [OVERRIDE_FILE_KEY]: resolved };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
throw new Error('Unsupported override configuration value');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function normalizeList(value) {
|
|
507
|
+
if (Array.isArray(value)) {
|
|
508
|
+
return value.flatMap((item) => normalizeList(item));
|
|
509
|
+
}
|
|
510
|
+
if (typeof value === 'string') {
|
|
511
|
+
if (value.includes(',')) {
|
|
512
|
+
return value
|
|
513
|
+
.split(',')
|
|
514
|
+
.map((part) => part.trim())
|
|
515
|
+
.filter(Boolean);
|
|
516
|
+
}
|
|
517
|
+
return value.trim() ? [value.trim()] : [];
|
|
518
|
+
}
|
|
519
|
+
if (value == null) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
return [String(value)];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function resolveMaybePath(value, baseDir) {
|
|
526
|
+
if (value == null || value === '') {
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
if (path.isAbsolute(value)) {
|
|
530
|
+
return path.normalize(value);
|
|
531
|
+
}
|
|
532
|
+
return path.resolve(baseDir, value);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function resolveWithBase(value, baseDir) {
|
|
536
|
+
if (path.isAbsolute(value)) {
|
|
537
|
+
return path.normalize(value);
|
|
538
|
+
}
|
|
539
|
+
return path.resolve(baseDir, value);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function resolvePath(input, baseDir) {
|
|
543
|
+
if (input == null || input === '') {
|
|
544
|
+
return path.resolve(baseDir);
|
|
545
|
+
}
|
|
546
|
+
if (path.isAbsolute(input)) {
|
|
547
|
+
return path.normalize(input);
|
|
548
|
+
}
|
|
549
|
+
return path.resolve(baseDir, input);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// No public base normalization; client assets served from '/.app/client'
|
|
553
|
+
|
|
554
|
+
async function pathExists(target) {
|
|
555
|
+
try {
|
|
556
|
+
await fs.access(target);
|
|
557
|
+
return true;
|
|
558
|
+
} catch {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function assertFile(filePath, message) {
|
|
564
|
+
if (!(await pathExists(filePath))) {
|
|
565
|
+
throw new Error(message);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function readJsonFile(filePath) {
|
|
570
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
571
|
+
try {
|
|
572
|
+
return JSON.parse(content);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw new Error(`Failed to parse JSON override at ${filePath}: ${error.message}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function parseJsonOption(value, flagName) {
|
|
579
|
+
try {
|
|
580
|
+
return JSON.parse(value);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
throw new Error(`Invalid JSON passed to ${flagName}: ${error.message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
export function createLogger({ verbose = false } = {}) {
|
|
4
|
+
const debugEnabled = verbose || Boolean(process.env.APP_DEBUG || process.env.HAMMER_DEBUG);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
info: (...args) => console.log('[app]', ...args),
|
|
8
|
+
warn: (...args) => console.warn('[app]', ...args),
|
|
9
|
+
error: (...args) => console.error('[app]', ...args),
|
|
10
|
+
debug: (...args) => {
|
|
11
|
+
if (debugEnabled) {
|
|
12
|
+
console.log('[app:debug]', ...args);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function runCommand(command, args = [], { cwd, env, logger } = {}) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
cwd,
|
|
7
|
+
env: { ...process.env, ...env },
|
|
8
|
+
stdio: 'inherit',
|
|
9
|
+
shell: false
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
child.on('error', reject);
|
|
13
|
+
child.on('close', (code) => {
|
|
14
|
+
if (code === 0) {
|
|
15
|
+
resolve();
|
|
16
|
+
} else {
|
|
17
|
+
const error = new Error(`Command "${command} ${args.join(' ')}" exited with code ${code}`);
|
|
18
|
+
error.code = 'COMMAND_FAILED';
|
|
19
|
+
reject(error);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|