@noego/app 0.0.6 → 0.0.7

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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(cat:*)",
5
5
  "Bash(curl:*)",
6
- "Bash(noego dev)"
6
+ "Bash(noego dev)",
7
+ "WebSearch"
7
8
  ],
8
9
  "deny": [],
9
10
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/app",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Production build tool for Dinner/Forge apps.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,10 @@
14
14
  "./client": {
15
15
  "import": "./src/client.js",
16
16
  "types": "./types/client.d.ts"
17
+ },
18
+ "./config": {
19
+ "import": "./src/runtime/config.js",
20
+ "types": "./types/config.d.ts"
17
21
  }
18
22
  },
19
23
  "scripts": {
@@ -296,34 +296,39 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
296
296
  const chokidar = requireFromRoot('chokidar');
297
297
  const picomatch = (await import('picomatch')).default;
298
298
 
299
- const resolvePattern = (pattern) => {
300
- if (path.isAbsolute(pattern)) return pattern;
301
- return path.resolve(root, pattern);
299
+ // Keep patterns relative for chokidar to work properly with globs
300
+ // Absolute patterns with globs don't work well, especially in chokidar v4
301
+ const normalizePattern = (pattern) => {
302
+ // If pattern is absolute, make it relative to root
303
+ if (path.isAbsolute(pattern)) {
304
+ return path.relative(root, pattern);
305
+ }
306
+ return pattern;
302
307
  };
303
-
308
+
304
309
  // Collect watch patterns
305
310
  const backendPatterns = [];
306
311
  const frontendPatterns = [];
307
312
  const sharedPatterns = [];
308
-
313
+
309
314
  // App watch patterns (restart both)
310
315
  if (yamlConfig.app?.watch) {
311
316
  for (const pattern of yamlConfig.app.watch) {
312
- sharedPatterns.push(resolvePattern(pattern));
317
+ sharedPatterns.push(normalizePattern(pattern));
313
318
  }
314
319
  }
315
-
320
+
316
321
  // Server watch patterns (restart backend only)
317
322
  if (yamlConfig.server?.watch) {
318
323
  for (const pattern of yamlConfig.server.watch) {
319
- backendPatterns.push(resolvePattern(pattern));
324
+ backendPatterns.push(normalizePattern(pattern));
320
325
  }
321
326
  }
322
-
327
+
323
328
  // Client watch patterns (restart frontend only)
324
329
  if (yamlConfig.client?.watch) {
325
330
  for (const pattern of yamlConfig.client.watch) {
326
- frontendPatterns.push(resolvePattern(pattern));
331
+ frontendPatterns.push(normalizePattern(pattern));
327
332
  }
328
333
  }
329
334
 
@@ -426,25 +431,27 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
426
431
  const handleFileChange = async (reason, file) => {
427
432
  if (pending) return;
428
433
  pending = true;
429
-
430
- const absPath = path.isAbsolute(file) ? file : path.resolve(root, file);
431
-
434
+
435
+ // With cwd set, chokidar reports relative paths
436
+ // Our patterns are now relative, so match against the relative path
437
+ const relativePath = path.isAbsolute(file) ? path.relative(root, file) : file;
438
+
432
439
  // Determine which service(s) to restart
433
440
  let restartBackend = false;
434
441
  let restartFrontend = false;
435
-
436
- if (sharedMatcher && sharedMatcher(absPath)) {
442
+
443
+ if (sharedMatcher && sharedMatcher(relativePath)) {
437
444
  // Shared file changed - restart both
438
- logger.info(`Shared file changed (${reason}): ${file}`);
445
+ logger.info(`Shared file changed (${reason}): ${relativePath}`);
439
446
  restartBackend = true;
440
447
  restartFrontend = true;
441
- } else if (backendMatcher && backendMatcher(absPath)) {
448
+ } else if (backendMatcher && backendMatcher(relativePath)) {
442
449
  // Backend file changed
443
- logger.info(`Backend file changed (${reason}): ${file}`);
450
+ logger.info(`Backend file changed (${reason}): ${relativePath}`);
444
451
  restartBackend = true;
445
- } else if (frontendMatcher && frontendMatcher(absPath)) {
452
+ } else if (frontendMatcher && frontendMatcher(relativePath)) {
446
453
  // Frontend file changed
447
- logger.info(`Frontend file changed (${reason}): ${file}`);
454
+ logger.info(`Frontend file changed (${reason}): ${relativePath}`);
448
455
  restartFrontend = true;
449
456
  }
450
457
 
@@ -462,15 +469,34 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
462
469
  };
463
470
 
464
471
  // Create watcher
472
+ // Use cwd with relative patterns for proper glob support
465
473
  const watcher = chokidar.watch(allPatterns, {
466
- ignoreInitial: true,
467
- cwd: root
474
+ cwd: root,
475
+ ignoreInitial: true
468
476
  });
469
-
477
+
478
+ // Debug: Log what patterns we're actually watching
479
+ logger.info('Watch patterns:', allPatterns);
480
+
470
481
  watcher
471
- .on('add', (p) => handleFileChange('add', p))
472
- .on('change', (p) => handleFileChange('change', p))
473
- .on('unlink', (p) => handleFileChange('unlink', p));
482
+ .on('add', (p) => {
483
+ logger.debug(`File add event: ${p}`);
484
+ handleFileChange('add', p);
485
+ })
486
+ .on('change', (p) => {
487
+ logger.debug(`File change event: ${p}`);
488
+ handleFileChange('change', p);
489
+ })
490
+ .on('unlink', (p) => {
491
+ logger.debug(`File unlink event: ${p}`);
492
+ handleFileChange('unlink', p);
493
+ })
494
+ .on('ready', () => {
495
+ logger.info('File watcher is ready');
496
+ })
497
+ .on('error', (error) => {
498
+ logger.error('Watcher error:', error);
499
+ });
474
500
 
475
501
  // Start initial processes
476
502
  startBackend();
@@ -518,48 +544,47 @@ async function createWatcher(context, yamlConfig, configFilePath) {
518
544
  const chokidar = requireFromRoot('chokidar');
519
545
 
520
546
  const patterns = new Set();
521
-
522
- const resolvePattern = (pattern) => {
523
- if (path.isAbsolute(pattern)) return pattern;
524
- return path.resolve(root, pattern);
547
+
548
+ // Keep patterns relative for chokidar to work properly with globs
549
+ const normalizePattern = (pattern) => {
550
+ // If pattern is absolute, make it relative to root
551
+ if (path.isAbsolute(pattern)) {
552
+ return path.relative(root, pattern);
553
+ }
554
+ return pattern;
525
555
  };
526
-
527
- // User-supplied watch paths
528
- if (yamlConfig.dev?.watchPaths) {
529
- for (const pattern of yamlConfig.dev.watchPaths) {
530
- patterns.add(resolvePattern(pattern));
556
+
557
+ // App watch patterns
558
+ if (yamlConfig.app?.watch) {
559
+ for (const pattern of yamlConfig.app.watch) {
560
+ patterns.add(normalizePattern(pattern));
531
561
  }
532
562
  }
533
-
534
- // Default watch patterns from config
535
- if (yamlConfig.server?.controllers) {
536
- patterns.add(resolvePattern(`${yamlConfig.server.controllers}/**/*.{ts,js}`));
537
- }
538
- if (yamlConfig.server?.middleware) {
539
- patterns.add(resolvePattern(`${yamlConfig.server.middleware}/**/*.{ts,js}`));
540
- }
541
- if (yamlConfig.server?.openapi) {
542
- patterns.add(resolvePattern(yamlConfig.server.openapi));
543
- const openapiDir = path.dirname(resolvePattern(yamlConfig.server.openapi));
544
- patterns.add(path.join(openapiDir, 'openapi/**/*.yaml'));
545
- }
546
- if (yamlConfig.client?.openapi) {
547
- patterns.add(resolvePattern(yamlConfig.client.openapi));
548
- const openapiDir = path.dirname(resolvePattern(yamlConfig.client.openapi));
549
- patterns.add(path.join(openapiDir, 'openapi/**/*.yaml'));
563
+
564
+ // Server watch patterns
565
+ if (yamlConfig.server?.watch) {
566
+ for (const pattern of yamlConfig.server.watch) {
567
+ patterns.add(normalizePattern(pattern));
568
+ }
550
569
  }
551
- if (yamlConfig.assets) {
552
- for (const asset of yamlConfig.assets) {
553
- if (asset.includes('*.sql')) {
554
- patterns.add(resolvePattern(asset));
555
- }
570
+
571
+ // Client watch patterns
572
+ if (yamlConfig.client?.watch) {
573
+ for (const pattern of yamlConfig.client.watch) {
574
+ patterns.add(normalizePattern(pattern));
556
575
  }
557
576
  }
558
-
577
+
578
+ if (patterns.size === 0) {
579
+ logger.warn('No watch patterns configured. Watching disabled.');
580
+ return null;
581
+ }
582
+
559
583
  logger.info('Watching for changes to restart server...');
584
+ // Use cwd with relative patterns for proper glob support
560
585
  const watcher = chokidar.watch(Array.from(patterns), {
561
- ignoreInitial: true,
562
- cwd: root
586
+ cwd: root,
587
+ ignoreInitial: true
563
588
  });
564
589
  return watcher;
565
590
  }
@@ -604,19 +629,23 @@ async function runWithRestart(context, tsxExecutable, tsxArgs, env, watcher, log
604
629
  start();
605
630
  pending = false;
606
631
  };
607
-
608
- watcher
609
- .on('add', (p) => scheduleRestart('add', p))
610
- .on('change', (p) => scheduleRestart('change', p))
611
- .on('unlink', (p) => scheduleRestart('unlink', p));
612
-
632
+
633
+ if (watcher) {
634
+ watcher
635
+ .on('add', (p) => scheduleRestart('add', p))
636
+ .on('change', (p) => scheduleRestart('change', p))
637
+ .on('unlink', (p) => scheduleRestart('unlink', p));
638
+ }
639
+
613
640
  // Start initial server
614
641
  start();
615
-
642
+
616
643
  // Keep process alive until SIGINT/SIGTERM
617
644
  const shutdown = async () => {
618
645
  await stop();
619
- await watcher.close();
646
+ if (watcher) {
647
+ await watcher.close();
648
+ }
620
649
  process.exit(0);
621
650
  };
622
651
  process.on('SIGINT', shutdown);
@@ -74,39 +74,52 @@ async function createWatcher(context) {
74
74
  patterns.add(value);
75
75
  };
76
76
 
77
- const resolveGlob = (entry) => {
77
+ // Keep patterns relative for chokidar to work properly with globs
78
+ const normalizePattern = (entry) => {
78
79
  if (!entry) return null;
79
80
  if (typeof entry === 'string') {
80
- return path.isAbsolute(entry) ? entry : path.join(config.rootDir, entry);
81
+ // If pattern is absolute, make it relative to rootDir
82
+ if (path.isAbsolute(entry)) {
83
+ return path.relative(config.rootDir, entry);
84
+ }
85
+ return entry;
81
86
  }
87
+ // Handle object-style patterns
82
88
  if (entry.isAbsolute) {
83
- return entry.pattern;
89
+ return path.relative(config.rootDir, entry.pattern);
84
90
  }
85
91
  if (entry.cwd) {
86
- return path.join(entry.cwd, entry.pattern);
92
+ const absolute = path.join(entry.cwd, entry.pattern);
93
+ return path.relative(config.rootDir, absolute);
87
94
  }
88
95
  return entry.pattern;
89
96
  };
90
97
 
91
- // User-supplied patterns
92
- for (const entry of config.dev.watchPaths) {
93
- const pattern = resolveGlob(entry);
94
- if (pattern) addPattern(pattern);
98
+ // App watch patterns - restart server
99
+ if (config.app?.watch) {
100
+ for (const entry of config.app.watch) {
101
+ const pattern = normalizePattern(entry);
102
+ if (pattern) addPattern(pattern);
103
+ }
95
104
  }
96
105
 
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);
106
+ // Server watch patterns - restart server
107
+ if (config.server?.watch) {
108
+ for (const entry of config.server.watch) {
109
+ const pattern = normalizePattern(entry);
110
+ if (pattern) addPattern(pattern);
111
+ }
105
112
  }
106
113
 
107
- // UI OpenAPI changes should restart (routes/manifest)
108
- addPattern(config.ui.openapiFile);
109
- addPattern(path.join(path.dirname(config.ui.openapiFile), 'openapi/**/*.yaml'));
114
+ // Client watch patterns - restart server (in non-split mode)
115
+ // Check both ui.watch (from buildConfig) and client.watch (from spread)
116
+ const clientWatch = config.ui?.watch || config.client?.watch;
117
+ if (clientWatch) {
118
+ for (const entry of clientWatch) {
119
+ const pattern = normalizePattern(entry);
120
+ if (pattern) addPattern(pattern);
121
+ }
122
+ }
110
123
 
111
124
  // Ignore Svelte/client files to let Vite HMR handle them
112
125
  const uiIgnores = [
@@ -114,11 +127,17 @@ async function createWatcher(context) {
114
127
  path.join(config.ui.rootDir, '**/*.{ts,tsx,css,scss,html}')
115
128
  ];
116
129
 
130
+ if (patterns.size === 0) {
131
+ logger.warn('No watch patterns configured. Watching disabled.');
132
+ return null;
133
+ }
134
+
117
135
  logger.info('Watching for changes to restart server...');
136
+ // Use cwd with relative patterns for proper glob support
118
137
  const watcher = chokidar.watch(Array.from(patterns), {
138
+ cwd: config.rootDir,
119
139
  ignoreInitial: true,
120
- ignored: uiIgnores,
121
- cwd: config.rootDir
140
+ ignored: uiIgnores
122
141
  });
123
142
  return watcher;
124
143
  }
@@ -166,10 +185,12 @@ async function runWithRestart(context, runner, watcher, frontendProc) {
166
185
  pending = false;
167
186
  };
168
187
 
169
- watcher
170
- .on('add', (p) => scheduleRestart('add', p))
171
- .on('change', (p) => scheduleRestart('change', p))
172
- .on('unlink', (p) => scheduleRestart('unlink', p));
188
+ if (watcher) {
189
+ watcher
190
+ .on('add', (p) => scheduleRestart('add', p))
191
+ .on('change', (p) => scheduleRestart('change', p))
192
+ .on('unlink', (p) => scheduleRestart('unlink', p));
193
+ }
173
194
 
174
195
  // Start initial server
175
196
  start();
@@ -177,7 +198,9 @@ async function runWithRestart(context, runner, watcher, frontendProc) {
177
198
  // Keep process alive until SIGINT/SIGTERM
178
199
  const shutdown = async () => {
179
200
  await stop();
180
- await watcher.close();
201
+ if (watcher) {
202
+ await watcher.close();
203
+ }
181
204
  if (frontendProc) {
182
205
  try { frontendProc.kill('SIGTERM'); } catch {}
183
206
  }
package/src/config.js CHANGED
@@ -73,7 +73,8 @@ export async function loadBuildConfig(cliOptions = {}, { cwd = process.cwd() } =
73
73
  controllersDir: config.server.controllers_abs,
74
74
  middlewareDir: config.server.middleware_abs,
75
75
  openapiFile: config.server.openapi_abs,
76
- sqlGlobs: serverSqlGlobs
76
+ sqlGlobs: serverSqlGlobs,
77
+ watch: config.server.watch // Preserve watch property
77
78
  } : null,
78
79
  ui: config.client ? {
79
80
  rootDir: uiRootDir,
@@ -85,7 +86,8 @@ export async function loadBuildConfig(cliOptions = {}, { cwd = process.cwd() } =
85
86
  options: {}, // This seems to be deprecated
86
87
  openapiFile: config.client.openapi_abs,
87
88
  assets: assetSpecs,
88
- clientExclude: clientExcludeSpecs
89
+ clientExclude: clientExcludeSpecs,
90
+ watch: config.client.watch // Preserve watch property
89
91
  } : null,
90
92
  assets: assetSpecs,
91
93
  vite: {
@@ -0,0 +1,82 @@
1
+ import path from 'node:path';
2
+ import { loadConfig, findConfigFile, loadConfigFromEnv } from './config-loader.js';
3
+ import {setContext} from "../client";
4
+
5
+
6
+
7
+
8
+ /**
9
+ * Loads and applies the resolved project configuration to the application's runtime context.
10
+ *
11
+ * @param {(config:any) => any} getAppFunction - A function that returns the application instance (e.g., Express app).
12
+ * @returns {Promise<object>} - The fully resolved and normalized configuration object.
13
+ */
14
+ export async function buildConfig(getAppFunction) {
15
+ // Retrieve the application instance from the provided function.
16
+ /** @type {any} */
17
+ const app = getAppFunction();
18
+ // Load and resolve the configuration via getConfig (auto-detects config file or env).
19
+ const { config } = await getConfig();
20
+ // Attach the loaded config to the runtime context for downstream usage.
21
+ setContext(app, config);
22
+ // Return the config for consumers (generates env and internal path invariants).
23
+ return {config,app};
24
+ }
25
+
26
+ /**
27
+ * Automatically load configuration from environment variable or config file.
28
+ *
29
+ * This function:
30
+ * 1. First checks NOEGO_CONFIGURATION env var (uses loadConfigFromEnv if present)
31
+ * 2. Otherwise searches for config file starting from rootDir (or process.cwd())
32
+ * and walks up directories until a config file is found
33
+ * 3. Sets all environment variables automatically
34
+ * 4. Returns the fully resolved config object
35
+ *
36
+ * @param {string} [rootDir] - Optional starting directory for config file search. Defaults to process.cwd()
37
+ * @returns {Promise<{root: string, config: object, configFilePath: string|null}>}
38
+ */
39
+ export async function getConfig(rootDir) {
40
+ // First priority: check NOEGO_CONFIGURATION env var
41
+ if (process.env.NOEGO_CONFIGURATION) {
42
+ return loadConfigFromEnv();
43
+ }
44
+
45
+ // Second priority: find and load config file
46
+ const startDir = rootDir ? path.resolve(rootDir) : process.cwd();
47
+
48
+
49
+ // Walk up directories to find config file
50
+ let currentDir = startDir;
51
+ let lastDir = '';
52
+
53
+ while (currentDir !== lastDir) {
54
+ try {
55
+ const configFilePath = await findConfigFile(currentDir);
56
+ // Found config file, load it
57
+ // Pass rootDir if provided, otherwise let loadConfig determine root from config file
58
+ return await loadConfig(configFilePath, rootDir);
59
+ } catch (error) {
60
+ // Config file not found in this directory, try parent
61
+ lastDir = currentDir;
62
+ currentDir = path.dirname(currentDir);
63
+
64
+ // Stop if we've reached filesystem root
65
+ if (currentDir === lastDir) {
66
+ throw new Error(
67
+ `No configuration file found starting from ${startDir}.\n` +
68
+ `Please create hammer.config.yml (or a similar named file) in your project root.`
69
+ );
70
+ }
71
+ }
72
+ }
73
+
74
+
75
+
76
+ // Should never reach here, but just in case
77
+ throw new Error(
78
+ `No configuration file found starting from ${startDir}.\n` +
79
+ `Please create hammer.config.yml (or a similar named file) in your project root.`
80
+ );
81
+ }
82
+
@@ -0,0 +1,23 @@
1
+ import type { Express } from 'express';
2
+
3
+ /**
4
+ * Result returned by buildConfig
5
+ */
6
+ export interface BuildConfigResult {
7
+ config: Record<string, unknown>;
8
+ app: Express;
9
+ }
10
+
11
+ /**
12
+ * Loads and applies the resolved project configuration to the application's runtime context.
13
+ *
14
+ * This function:
15
+ * 1. Retrieves the application instance from the provided function
16
+ * 2. Loads configuration via getConfig (auto-detects config file or env)
17
+ * 3. Attaches the loaded config to the runtime context via setContext
18
+ * 4. Returns both the config object and app instance for consumers
19
+ *
20
+ * @param getAppFunction - A function that returns the application instance (e.g., Express app)
21
+ * @returns Promise resolving to an object containing both the config and app
22
+ */
23
+ export function buildConfig(getAppFunction: (config:any) => Express): Promise<BuildConfigResult>;