@noego/app 0.0.3 → 0.0.4

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.
@@ -1,17 +1,9 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Read(//Users/shavauhngabay/dev/noego/dinner/**)",
5
- "Read(//Users/shavauhngabay/dev/ego/sqlstack/**)",
6
- "Read(//Users/shavauhngabay/dev/ego/forge/**)",
7
- "Read(//Users/shavauhngabay/dev/noblelaw/ui/**)",
8
- "Read(//Users/shavauhngabay/dev/noblelaw/**)",
9
- "mcp__chrome-devtools__take_screenshot",
10
- "Bash(npm run build:ui:ssr:*)",
11
- "mcp__chrome-devtools__navigate_page",
12
- "Bash(node dist/hammer.js:*)",
13
- "Bash(tee:*)",
14
- "mcp__chrome-devtools__list_console_messages"
4
+ "Bash(cat:*)",
5
+ "Bash(curl:*)",
6
+ "Bash(noego dev)"
15
7
  ],
16
8
  "deny": [],
17
9
  "ask": []
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@noego/app",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Production build tool for Dinner/Forge apps.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "app": "./bin/app.js",
8
8
  "noego": "./bin/app.js"
9
9
  },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./src/index.js"
13
+ },
14
+ "./client": {
15
+ "import": "./src/client.js",
16
+ "types": "./types/client.d.ts"
17
+ }
18
+ },
10
19
  "scripts": {
11
20
  "build": "node ./bin/app.js build"
12
21
  },
@@ -20,5 +29,10 @@
20
29
  "picomatch": "^2.3.1",
21
30
  "yaml": "^2.6.0"
22
31
  },
32
+ "peerDependencies": {
33
+ "@noego/dinner": "*",
34
+ "@noego/forge": "*",
35
+ "express": "*"
36
+ },
23
37
  "devDependencies": {}
24
38
  }
package/src/args.js CHANGED
@@ -125,6 +125,7 @@ function formatFlag(key) {
125
125
  export function printHelpAndExit({ stdout = process.stdout } = {}) {
126
126
  stdout.write(`Usage:
127
127
  app build [options]
128
+ app dev [options]
128
129
  app serve [options]
129
130
  app preview [options]
130
131
 
@@ -2,29 +2,123 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  /**
5
- * Generates a bootstrap wrapper (hammer.js) that sets up the runtime environment
5
+ * Generates a bootstrap wrapper (no_ego.js) that sets up the runtime environment
6
6
  * before importing the compiled application entry.
7
7
  *
8
8
  * This ensures:
9
+ * - NOEGO_CONFIGURATION is set with full resolved config
9
10
  * - FORGE_ROOT is set to the dist directory (for consistent asset resolution)
10
- * - Application works regardless of invocation directory (node dist/hammer.js vs cd dist && node hammer.js)
11
+ * - Application works regardless of invocation directory (node dist/no_ego.js vs cd dist && node no_ego.js)
11
12
  * - Environment-agnostic (staging, QA, production all work identically)
12
13
  */
13
14
  export async function generateBootstrap(context) {
14
15
  const { config, logger } = context;
15
16
 
17
+ // Build the config object that will be serialized to NOEGO_CONFIGURATION
18
+ // Paths are relative to project root, mirrored in dist (so dist/server/server.js mirrors src/server/server.ts)
19
+ const runtimeConfig = {
20
+ root: '__dirname', // Will be replaced in template with actual __dirname (dist directory)
21
+ mode: 'production',
22
+ outDir: config.outDir ? path.basename(config.outDir) : 'dist',
23
+ outDir_abs: '__dirname', // Will be replaced
24
+ app: {
25
+ boot: config.app?.boot,
26
+ boot_abs: config.app?.boot ? './' + path.relative(config.rootDir, config.app.boot_abs).replace(/\.ts$/, '.js') : null
27
+ },
28
+ server: config.server ? {
29
+ main: config.server.entry?.raw,
30
+ main_abs: config.server.entry?.absolute ? './' + path.relative(config.rootDir, config.server.entry.absolute).replace(/\.ts$/, '.js') : null,
31
+ controllers: config.server?.controllers,
32
+ controllers_abs: config.server?.controllersDir ? './' + path.relative(config.rootDir, config.server.controllersDir) : null,
33
+ controllers_base_path: config.server?.controllersDir ? './' + path.relative(config.rootDir, config.server.controllersDir) : null,
34
+ middleware: config.server?.middleware,
35
+ middleware_abs: config.server?.middlewareDir ? './' + path.relative(config.rootDir, config.server.middlewareDir) : null,
36
+ middleware_path: config.server?.middlewareDir ? './' + path.relative(config.rootDir, config.server.middlewareDir) : null,
37
+ openapi: config.server?.openapi,
38
+ openapi_abs: config.server?.openapiFile ? './' + path.relative(config.rootDir, config.server.openapiFile) : null,
39
+ openapi_path: config.server?.openapiFile ? './' + path.relative(config.rootDir, config.server.openapiFile) : null
40
+ } : undefined,
41
+ client: config.ui ? {
42
+ main: config.client?.main,
43
+ main_abs: config.client?.main_abs ? './' + path.relative(config.rootDir, config.client.main_abs).replace(/\.ts$/, '.js') : null,
44
+ shell: config.client?.shell,
45
+ shell_abs: config.client?.shell_abs ? './' + path.relative(config.rootDir, config.client.shell_abs) : null,
46
+ shell_path: config.client?.shell,
47
+ openapi: config.client?.openapi,
48
+ openapi_abs: config.client?.openapi_abs ? './' + path.relative(config.rootDir, config.client.openapi_abs) : null,
49
+ openapi_path: config.client?.openapi,
50
+ component_dir: config.client?.component_dir,
51
+ componentDir_abs: config.client?.componentDir_abs ? './' + path.relative(config.rootDir, config.client.componentDir_abs) : null,
52
+ assetDirs: config.client?.assetDirs || []
53
+ } : undefined,
54
+ dev: {
55
+ port: config.dev?.port || 3000,
56
+ backendPort: config.dev?.backendPort || 3001
57
+ },
58
+ assets: []
59
+ };
60
+
16
61
  // Compute relative path from outDir to compiled entry
17
62
  // The entry is copied to outDir root by syncRootEntry in server.js
18
- const entryRel = path.relative(
19
- config.rootDir,
20
- config.server.entry.absolute
21
- ).replace(/\.ts$/, '.js');
63
+ const entryRel = config.server?.entry?.absolute
64
+ ? path.relative(config.rootDir, config.server.entry.absolute).replace(/\.ts$/, '.js')
65
+ : 'server/index.js';
66
+
67
+ // Serialize config, then replace __dirname placeholders
68
+ let configJson = JSON.stringify(runtimeConfig, null, 2);
69
+ configJson = configJson.replace(/"__dirname"/g, '__dirname');
22
70
 
23
71
  const template = `import path from 'path';
24
72
  import { fileURLToPath } from 'url';
25
73
 
26
74
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
75
 
76
+ // Set full configuration in environment
77
+ const runtimeConfig = ${configJson};
78
+
79
+ // Resolve all relative paths to absolute based on __dirname
80
+ if (runtimeConfig.app?.boot_abs) {
81
+ runtimeConfig.app.boot_abs = path.resolve(__dirname, runtimeConfig.app.boot_abs);
82
+ }
83
+ if (runtimeConfig.server) {
84
+ if (runtimeConfig.server.main_abs) {
85
+ runtimeConfig.server.main_abs = path.resolve(__dirname, runtimeConfig.server.main_abs);
86
+ }
87
+ if (runtimeConfig.server.controllers_abs) {
88
+ runtimeConfig.server.controllers_abs = path.resolve(__dirname, runtimeConfig.server.controllers_abs);
89
+ runtimeConfig.server.controllers_base_path = runtimeConfig.server.controllers_abs;
90
+ }
91
+ if (runtimeConfig.server.middleware_abs) {
92
+ runtimeConfig.server.middleware_abs = path.resolve(__dirname, runtimeConfig.server.middleware_abs);
93
+ runtimeConfig.server.middleware_path = runtimeConfig.server.middleware_abs;
94
+ }
95
+ if (runtimeConfig.server.openapi_abs) {
96
+ runtimeConfig.server.openapi_abs = path.resolve(__dirname, runtimeConfig.server.openapi_abs);
97
+ runtimeConfig.server.openapi_path = runtimeConfig.server.openapi_abs;
98
+ }
99
+ }
100
+ if (runtimeConfig.client) {
101
+ if (runtimeConfig.client.main_abs) {
102
+ runtimeConfig.client.main_abs = path.resolve(__dirname, runtimeConfig.client.main_abs);
103
+ }
104
+ if (runtimeConfig.client.shell_abs) {
105
+ runtimeConfig.client.shell_abs = path.resolve(__dirname, runtimeConfig.client.shell_abs);
106
+ }
107
+ if (runtimeConfig.client.openapi_abs) {
108
+ runtimeConfig.client.openapi_abs = path.resolve(__dirname, runtimeConfig.client.openapi_abs);
109
+ }
110
+ if (runtimeConfig.client.componentDir_abs) {
111
+ runtimeConfig.client.componentDir_abs = path.resolve(__dirname, runtimeConfig.client.componentDir_abs);
112
+ }
113
+ // Resolve assetDirs paths
114
+ if (runtimeConfig.client.assetDirs) {
115
+ runtimeConfig.client.assetDirs = runtimeConfig.client.assetDirs.map(assetDir => ({
116
+ ...assetDir,
117
+ absolutePath: path.resolve(__dirname, assetDir.absolutePath)
118
+ }));
119
+ }
120
+ }
121
+
28
122
  // Set Forge root explicitly (highest priority in resolveRuntimeRoot)
29
123
  process.env.FORGE_ROOT = __dirname;
30
124
 
@@ -35,8 +129,20 @@ process.env.NOEGO_BUILT = 'true';
35
129
  // Indicate Dinner is running in the built environment
36
130
  process.env.DINNER_SERVED = 'true';
37
131
 
38
- // Import and execute the application
39
- await import('./${entryRel}');
132
+ // Force production mode in config (not via NODE_ENV)
133
+ runtimeConfig.mode = 'production';
134
+
135
+ // Store config in env for @noego/app/client.js to read
136
+ process.env.NOEGO_CONFIGURATION = JSON.stringify(runtimeConfig);
137
+
138
+ // Import and run the runtime directly
139
+ import { runCombinedServices } from '@noego/app';
140
+
141
+ // Determine what services to run
142
+ const hasBackend = !!runtimeConfig.server?.main_abs;
143
+ const hasFrontend = !!runtimeConfig.client?.main_abs;
144
+
145
+ await runCombinedServices(runtimeConfig, { hasBackend, hasFrontend });
40
146
  `;
41
147
 
42
148
  const targetPath = path.join(config.outDir, 'no_ego.js');
@@ -3,8 +3,8 @@ import { createRequire } from 'node:module';
3
3
 
4
4
  import { createLogger } from '../logger.js';
5
5
 
6
- export function createBuildContext(config, options = {}) {
7
- const logger = options.logger ?? createLogger(options);
6
+ export function createBuildContext(config) {
7
+ const logger = createLogger();
8
8
  const requireFromRoot = createRequire(path.join(config.rootDir, 'package.json'));
9
9
 
10
10
  return {
@@ -1,6 +1,16 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
+ export function resolvePath(input, baseDir) {
5
+ if (input == null || input === '') {
6
+ return path.resolve(baseDir);
7
+ }
8
+ if (path.isAbsolute(input)) {
9
+ return path.normalize(input);
10
+ }
11
+ return path.resolve(baseDir, input);
12
+ }
13
+
4
14
  export async function resolveConfigFile(preferred, fallback) {
5
15
  if (preferred) {
6
16
  const absolutePreferred = path.resolve(preferred);
@@ -16,9 +16,11 @@ const HTTP_METHODS = new Set([
16
16
  ]);
17
17
 
18
18
  export async function discoverProject(context) {
19
+ const serverOpenApiPath = context.config.server?.openapiFile;
20
+ const uiOpenApiPath = context.config.ui?.openapiFile;
19
21
  const [serverDoc, uiDoc] = await Promise.all([
20
- loadOpenApiDocumentIfPresent(context, context.config.server.openapiFile),
21
- loadOpenApiDocumentIfPresent(context, context.config.ui.openapiFile)
22
+ loadOpenApiDocumentIfPresent(context, serverOpenApiPath),
23
+ loadOpenApiDocumentIfPresent(context, uiOpenApiPath)
22
24
  ]);
23
25
 
24
26
  const serverRoutes = collectRoutes(serverDoc);
@@ -114,6 +116,9 @@ async function loadOpenApiDocumentIfPresent(context, filePath) {
114
116
  }
115
117
 
116
118
  function collectRoutes(document) {
119
+ if (!document) {
120
+ return [];
121
+ }
117
122
  const routes = [];
118
123
  const inheritedMiddleware = ensureArray(document?.paths?.['x-middleware']);
119
124
 
@@ -123,7 +128,7 @@ function collectRoutes(document) {
123
128
  return routes;
124
129
  }
125
130
 
126
- function parseModules(document, inheritedMiddleware = []) {
131
+ function parseModules(document = {}, inheritedMiddleware = []) {
127
132
  const modulesConfig = document.modules || document.module;
128
133
  if (!modulesConfig) return [];
129
134
 
@@ -151,7 +156,7 @@ function parseModules(document, inheritedMiddleware = []) {
151
156
  return routes;
152
157
  }
153
158
 
154
- function parsePaths(document, inheritedMiddleware = []) {
159
+ function parsePaths(document = {}, inheritedMiddleware = []) {
155
160
  const paths = document.paths;
156
161
  if (!paths || typeof paths !== 'object') {
157
162
  return [];
@@ -10,41 +10,31 @@ export async function writeRuntimeManifest(context, artifacts) {
10
10
  mode: config.mode,
11
11
  rootDir: config.rootDir,
12
12
  outDir: config.outDir,
13
- server: {
14
- entry: config.server.entry.relativeToRoot,
15
- controllersDir: path.relative(config.rootDir, config.server.controllersDir),
16
- middlewareDir: path.relative(config.rootDir, config.server.middlewareDir),
17
- openapi: path.relative(config.rootDir, config.server.openapiFile),
18
- sqlGlobs: config.server.sqlGlobs.map((spec) => spec.pattern)
19
- },
20
- ui: {
21
- page: config.ui.page.relativeToRoot,
22
- openapi: path.relative(config.rootDir, config.ui.openapiFile),
23
- assets: config.assets.map((spec) => spec.pattern),
24
- clientExclude: config.ui.clientExclude.map((spec) => spec.pattern)
25
- },
26
- discovery: {
27
- server: {
28
- controllers: artifacts.discovery.server.controllers,
29
- middleware: artifacts.discovery.server.middleware
30
- },
31
- ui: {
32
- views: artifacts.discovery.ui.views,
33
- layouts: artifacts.discovery.ui.layouts,
34
- middleware: artifacts.discovery.ui.middleware
35
- }
36
- },
37
- client: {
38
- manifest: relativeToOut(config, artifacts.client.manifestPath),
39
- entry: artifacts.client.entryGraph.entry?.file
13
+ server: config.server ? {
14
+ entry: config.server.entry?.relativeToRoot ?? null,
15
+ controllersDir: config.server.controllersDir ? path.relative(config.rootDir, config.server.controllersDir) : null,
16
+ middlewareDir: config.server.middlewareDir ? path.relative(config.rootDir, config.server.middlewareDir) : null,
17
+ openapi: config.server.openapiFile ? path.relative(config.rootDir, config.server.openapiFile) : null,
18
+ sqlGlobs: Array.isArray(config.server.sqlGlobs) ? config.server.sqlGlobs.map((spec) => spec.pattern) : []
19
+ } : null,
20
+ ui: config.ui ? {
21
+ page: config.ui.page?.relativeToRoot ?? null,
22
+ openapi: config.ui.openapiFile ? path.relative(config.rootDir, config.ui.openapiFile) : null,
23
+ assets: Array.isArray(config.assets) ? config.assets.map((spec) => spec.pattern) : [],
24
+ clientExclude: Array.isArray(config.ui.clientExclude) ? config.ui.clientExclude.map((spec) => spec.pattern) : []
25
+ } : null,
26
+ discovery: artifacts.discovery ?? null,
27
+ client: artifacts.client ? {
28
+ manifest: artifacts.client.manifestPath ? relativeToOut(config, artifacts.client.manifestPath) : null,
29
+ entry: artifacts.client.entryGraph?.entry?.file
40
30
  ? `/assets/${toPosix(artifacts.client.entryGraph.entry.file)}`
41
31
  : null
42
- },
43
- ssr: {
32
+ } : null,
33
+ ssr: artifacts.ssr ? {
44
34
  manifest: artifacts.ssr.manifestPath
45
35
  ? relativeToOut(config, artifacts.ssr.manifestPath)
46
36
  : null
47
- }
37
+ } : null
48
38
  };
49
39
 
50
40
  await fs.writeFile(targetPath, JSON.stringify(payload, null, 2));
@@ -52,10 +42,12 @@ export async function writeRuntimeManifest(context, artifacts) {
52
42
  }
53
43
 
54
44
  function relativeToRoot(config, absolutePath) {
45
+ if (!absolutePath) return null;
55
46
  return path.relative(config.rootDir, absolutePath);
56
47
  }
57
48
 
58
49
  function relativeToOut(config, absolutePath) {
50
+ if (!absolutePath) return null;
59
51
  return path.relative(config.outDir, absolutePath);
60
52
  }
61
53
 
@@ -13,6 +13,7 @@ export async function buildServer(context, discovery) {
13
13
  await reorganizeServerOutputs(context);
14
14
  await mirrorUiModules(context);
15
15
  await syncRootEntry(context);
16
+ await syncAppBoot(context);
16
17
  await copyServerAssets(context);
17
18
  await fixImportExtensions(context.config.outDir);
18
19
  }
@@ -70,7 +71,7 @@ async function reorganizeServerOutputs(context) {
70
71
  }
71
72
 
72
73
  const compiledEntryRel = replaceExtension(
73
- path.relative(config.rootDir, config.server.entry.absolute),
74
+ path.relative(config.server.rootDir, config.server.entry.absolute),
74
75
  '.js'
75
76
  );
76
77
  const compiledEntryPath = path.join(serverOutDir, compiledEntryRel);
@@ -172,15 +173,44 @@ async function mirrorUiModules(context) {
172
173
 
173
174
  async function syncRootEntry(context) {
174
175
  const { config } = context;
175
- const compiledEntryRel = replaceExtension(
176
+ const compiledEntryRelFromServerRoot = replaceExtension(
177
+ path.relative(config.server.rootDir, config.server.entry.absolute),
178
+ '.js'
179
+ );
180
+ const compiledEntryRelFromProjectRoot = replaceExtension(
176
181
  path.relative(config.rootDir, config.server.entry.absolute),
177
182
  '.js'
178
183
  );
179
- const sourcePath = path.join(config.layout.serverOutDir, compiledEntryRel);
184
+ const sourcePath = path.join(config.layout.serverOutDir, compiledEntryRelFromServerRoot);
180
185
  if (!(await pathExists(sourcePath))) {
181
186
  return;
182
187
  }
183
- const destinationPath = path.join(config.outDir, compiledEntryRel);
188
+ const destinationPath = path.join(config.outDir, compiledEntryRelFromProjectRoot);
189
+ await ensureParentDir(destinationPath);
190
+ await fs.copyFile(sourcePath, destinationPath);
191
+ await copyIfExists(`${sourcePath}.map`, `${destinationPath}.map`);
192
+ }
193
+
194
+ async function syncAppBoot(context) {
195
+ const { config } = context;
196
+
197
+ // Copy app.boot (e.g., index.ts -> dist/index.js) to maintain project structure
198
+ if (!config.app?.boot_abs) {
199
+ return;
200
+ }
201
+
202
+ const compiledBootRelFromProjectRoot = replaceExtension(
203
+ path.relative(config.rootDir, config.app.boot_abs),
204
+ '.js'
205
+ );
206
+
207
+ // The compiled boot file should be in the tsOutDir, mirroring project structure
208
+ const sourcePath = path.join(config.layout.tsOutDir, compiledBootRelFromProjectRoot);
209
+ if (!(await pathExists(sourcePath))) {
210
+ return;
211
+ }
212
+
213
+ const destinationPath = path.join(config.outDir, compiledBootRelFromProjectRoot);
184
214
  await ensureParentDir(destinationPath);
185
215
  await fs.copyFile(sourcePath, destinationPath);
186
216
  await copyIfExists(`${sourcePath}.map`, `${destinationPath}.map`);
package/src/cli.js CHANGED
@@ -7,10 +7,11 @@ import { loadBuildConfig } from './config.js';
7
7
  import { runBuild } from './commands/build.js';
8
8
  import { runServe } from './commands/serve.js';
9
9
  import { runPreview } from './commands/preview.js';
10
+ import { runDev } from './commands/dev.js';
10
11
 
11
- async function main() {
12
+ export async function runCli(argv = process.argv.slice(2)) {
12
13
  try {
13
- const { command, options } = parseCliArgs(process.argv.slice(2));
14
+ const { command, options } = parseCliArgs(argv);
14
15
 
15
16
  if (options.help || !command) {
16
17
  printHelpAndExit();
@@ -24,8 +25,7 @@ async function main() {
24
25
 
25
26
  switch (command) {
26
27
  case 'build': {
27
- const config = await loadBuildConfig(options, { cwd: process.cwd() });
28
- await runBuild(config);
28
+ await runBuild(options);
29
29
  break;
30
30
  }
31
31
  case 'serve': {
@@ -33,6 +33,11 @@ async function main() {
33
33
  await runServe(config);
34
34
  break;
35
35
  }
36
+ case 'dev': {
37
+ const config = await loadBuildConfig(options, { cwd: process.cwd() });
38
+ await runDev(config);
39
+ break;
40
+ }
36
41
  case 'preview': {
37
42
  const config = await loadBuildConfig(options, { cwd: process.cwd() });
38
43
  await runPreview(config);
@@ -69,4 +74,4 @@ function cliError(message) {
69
74
  return error;
70
75
  }
71
76
 
72
- await main();
77
+ await runCli();
package/src/client.js ADDED
@@ -0,0 +1,141 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+
4
+ // Runtime context - set by the runtime before calling server.main
5
+ let runtimeContext = null;
6
+
7
+ /**
8
+ * Sets the runtime context (called by the runtime before server.main).
9
+ *
10
+ * @param {import('express').Express} app - Express application instance
11
+ * @param {object} config - Configuration object with resolved paths
12
+ * @param {import('http').Server} [httpServer] - HTTP server instance for WebSocket upgrades
13
+ */
14
+ export function setContext(app, config, httpServer = null) {
15
+ runtimeContext = { app, config, httpServer };
16
+ }
17
+
18
+ /**
19
+ * Boots the backend server with Dinner (API) integration.
20
+ *
21
+ * @param {object} assets - Dinner asset mappings
22
+ * @param {object} [options] - Optional configuration
23
+ * @param {function} [options.controller_builder] - Controller builder function
24
+ * @param {function} [options.controller_args_provider] - Controller args provider function
25
+ * @returns {Promise<object>} Dinner server instance
26
+ */
27
+ export async function boot(assets, options = {}) {
28
+ if (!runtimeContext) {
29
+ throw new Error('Runtime context not set. Call setContext() before boot().');
30
+ }
31
+
32
+ const { app, config } = runtimeContext;
33
+
34
+ // Resolve dependencies from user's project context
35
+ const root = config.root || process.cwd();
36
+ const require = createRequire(path.join(root, 'package.json'));
37
+ const { Server } = require('@noego/dinner');
38
+
39
+ // Setup Dinner (API server) only
40
+ const server = await Server.createServer({
41
+ openapi_path: config.server.openapi_path,
42
+ controllers_base_path: config.server.controllers_base_path,
43
+ middleware_path: config.server.middleware_path,
44
+ server: app,
45
+ ajv_formats: true,
46
+ assets: assets,
47
+ controller_builder: options.controller_builder || (async (Controller) => {
48
+ // Default: try to instantiate with new
49
+ return new Controller();
50
+ }),
51
+ controller_args_provider: options.controller_args_provider || (async (req, res) => ({ req, res })),
52
+ });
53
+
54
+ return server;
55
+ }
56
+
57
+ /**
58
+ * Boots the frontend server with Forge (SSR) integration.
59
+ *
60
+ * @returns {Promise<void>}
61
+ */
62
+ export async function clientBoot() {
63
+ if (!runtimeContext) {
64
+ throw new Error('Runtime context not set. Call setContext() before client.boot().');
65
+ }
66
+
67
+ const { app, config } = runtimeContext;
68
+
69
+ // Resolve dependencies from user's project context
70
+ const root = config.root || process.cwd();
71
+ const require = createRequire(path.join(root, 'package.json'));
72
+ const { createServer: createForgeServer } = require('@noego/forge/server');
73
+
74
+ // Setup Forge (SSR)
75
+ // Forge expects paths relative to viteOptions.root
76
+ const forgeOptions = {
77
+ viteOptions: {
78
+ root: root,
79
+ // Configure Vite server: use Express app (middleware mode) in development
80
+ server: config.mode !== 'production'
81
+ ? {
82
+ middlewareMode: true, // Use Vite as middleware in Express, don't create separate server
83
+ hmr: {
84
+ clientPort: config.dev?.port || 3000,
85
+ // Pass HTTP server to Vite for WebSocket upgrades
86
+ server: runtimeContext.httpServer
87
+ }
88
+ }
89
+ : undefined
90
+ },
91
+ component_dir: config.client?.component_dir,
92
+ open_api_path: config.client?.openapi_path,
93
+ renderer: config.client?.shell_path,
94
+ middleware_path: config.server?.middleware_path,
95
+ development: config.mode !== 'production',
96
+ };
97
+
98
+ await createForgeServer(app, forgeOptions);
99
+ }
100
+
101
+ /**
102
+ * Browser-side client initialization.
103
+ * Reads window.__CONFIGURATION__ and initializes the Forge app.
104
+ * This function works in the browser and should be called from client.ts.
105
+ *
106
+ * @returns {Promise<void>}
107
+ */
108
+ export async function clientInit() {
109
+ // Check if we're in a browser environment
110
+ if (typeof window === 'undefined') {
111
+ throw new Error('client.init() can only be called in the browser');
112
+ }
113
+
114
+ // Get configuration from window (injected by Forge HTML renderer)
115
+ const options = window.__CONFIGURATION__;
116
+ if (!options) {
117
+ console.warn('window.__CONFIGURATION__ not found. Forge app may not initialize correctly.');
118
+ return;
119
+ }
120
+
121
+ // Find root element
122
+ const root = document.getElementById('app');
123
+ if (!root) {
124
+ console.warn('Root element with id="app" not found.');
125
+ return;
126
+ }
127
+
128
+ // Dynamically import createApp from @noego/forge/client
129
+ // This will work because Vite bundles it properly
130
+ const { createApp } = await import('@noego/forge/client');
131
+
132
+ // Initialize the app
133
+ await createApp(root, options);
134
+ }
135
+
136
+ // Export client.boot as a named export (server-side)
137
+ export const client = {
138
+ boot: clientBoot,
139
+ init: clientInit // Browser-side initialization
140
+ };
141
+