@noego/app 0.0.9 → 0.0.10

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,10 +1,17 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(cat:*)",
5
- "Bash(curl:*)",
6
- "Bash(noego dev)",
7
- "WebSearch"
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"
8
15
  ],
9
16
  "deny": [],
10
17
  "ask": []
@@ -0,0 +1,381 @@
1
+ # Asset Serving Architecture Investigation
2
+
3
+ **Status:** Under Investigation
4
+ **Date:** 2025-01-11 (Updated)
5
+ **Author:** Engineering Team
6
+ **Context:** Deep dive after initial runtime.js fix revealed deeper Forge integration issue
7
+
8
+ ---
9
+
10
+ ## Investigation Summary
11
+
12
+ After fixing the initial asset serving issue in `runtime.js` (mounting `.app` at root `/`), we discovered a **deeper architectural problem**: The browser is loading SSR components instead of client-bundled components, causing `effect_orphan` errors.
13
+
14
+ ---
15
+
16
+ ## What We Know FOR SURE (Confirmed via Code Review & Logs)
17
+
18
+ ### 1. Build Output Structure
19
+ ```
20
+ dist/
21
+ ├── .app/
22
+ │ ├── assets/ ← Client-bundled components (Vite output)
23
+ │ │ ├── chunks/ ← Shared code chunks
24
+ │ │ └── components/ ← Browser-ready .js files
25
+ │ │ ├── layout/
26
+ │ │ │ └── root.js ← VITE-BUNDLED, imports from ../../chunks/
27
+ │ │ └── views/
28
+ │ │ └── home.js
29
+ │ └── ssr/ ← SSR components (server-side)
30
+ │ ├── chunks/
31
+ │ └── components/
32
+ │ ├── layout/
33
+ │ │ └── root.js ← SSR VERSION, bare imports fail in browser
34
+ │ └── views/
35
+ │ └── home.js
36
+ ├── no_ego.js ← Bootstrap config
37
+ └── [compiled app files]
38
+ ```
39
+
40
+ ### 2. Generated Config Values (from `no_ego.js`)
41
+ ```javascript
42
+ {
43
+ "component_dir": ".app/ssr/components",
44
+ "component_dir_ssr": ".app/ssr/components",
45
+ "component_dir_client": "/assets/components",
46
+ "component_base_path": "/assets",
47
+ "assets_build_dir": ".app/assets/components",
48
+ "component_suffix": "components"
49
+ }
50
+ ```
51
+
52
+ ### 3. Express Static Mounts (4 total)
53
+
54
+ **Mount 1** (`runtime.js` line 701-702):
55
+ ```javascript
56
+ app.use('/client', express.static('/dist/.app/assets'))
57
+ ```
58
+
59
+ **Mount 2** (`runtime.js` line 703-704):
60
+ ```javascript
61
+ app.use('/chunks', express.static('/dist/.app/ssr/chunks'))
62
+ ```
63
+
64
+ **Mount 3** (Forge `server.ts` via `client.js` assets):
65
+ ```javascript
66
+ app.use('/', express.static('/dist/.app/assets'))
67
+ app.use('/assets', express.static('/dist/.app/assets'))
68
+ ```
69
+
70
+ **Mount 4** (Forge `server.ts` line 135-137):
71
+ ```javascript
72
+ app.use('/.app/ssr/components', express.static('/dist/.app/ssr/components'))
73
+ ```
74
+
75
+ ### 4. Browser Behavior (Confirmed via Network Tab)
76
+ ```
77
+ Browser requests: http://localhost:3050/.app/ssr/components/layout/root.js
78
+ Server responds: 200 OK (serves SSR component file)
79
+ Browser executes: SSR JavaScript with bare imports
80
+ Result: Module not found errors + effect_orphan error
81
+ ```
82
+
83
+ ### 5. Forge's Component Directory Injection (Line 264)
84
+ ```typescript
85
+ // express_server_adapter.ts:264
86
+ const clientComponentDir = this.isProd ? this.componentDir : '/assets';
87
+
88
+ // Line 281:
89
+ window.__COMPONENT_DIR__ = ${JSON.stringify(clientComponentDir)}
90
+ ```
91
+
92
+ **In production mode:**
93
+ - `this.componentDir` = `".app/ssr/components"` (passed from hammer/client.js)
94
+ - Therefore: `window.__COMPONENT_DIR__ = ".app/ssr/components"`
95
+ - Browser constructs URLs: `/.app/ssr/components/layout/root.js`
96
+
97
+ ### 6. Forge's Single component_dir Design (Confirmed)
98
+ **File:** `/Users/shavauhngabay/dev/forge/src/options/ServerOptions.ts`
99
+ ```typescript
100
+ export interface ServerOptions {
101
+ component_dir?: string; // SINGLE option for BOTH SSR and client
102
+ // NO component_dir_ssr
103
+ // NO component_dir_client
104
+ }
105
+ ```
106
+
107
+ **Usage in `server.ts` line 67:**
108
+ ```typescript
109
+ const COMPONENT_DIR = !full_options.component_dir ? root : path.join(root, full_options.component_dir);
110
+ ```
111
+
112
+ **Used for:**
113
+ - SSR: `ProdComponentLoader(COMPONENT_DIR)` - loads components server-side
114
+ - Client: Passed to `ExpressServerAdapter` → becomes `window.__COMPONENT_DIR__`
115
+ - Static serving: `app.use(\`/\${component_dir}\`, express.static(COMPONENT_DIR))`
116
+
117
+ ---
118
+
119
+ ## The Core Problem
120
+
121
+ ### What's Happening
122
+ 1. **hammer/client.js** passes: `component_dir: '.app/ssr/components'`
123
+ 2. **Forge** uses this for EVERYTHING:
124
+ - SSR component loading ✅ (correct - Node.js can load SSR files)
125
+ - Client directory injection ❌ (wrong - browser gets SSR path)
126
+ - Static file serving ✅ (Mount 4 serves SSR files at `/.app/ssr/components`)
127
+ 3. **Browser** receives: `window.__COMPONENT_DIR__ = ".app/ssr/components"`
128
+ 4. **Browser** requests: `/.app/ssr/components/layout/root.js`
129
+ 5. **Express** serves SSR component (via Mount 4) ✅
130
+ 6. **Browser** executes SSR JavaScript:
131
+ - Contains bare imports: `import { onMount } from 'svelte'`
132
+ - Contains chunk imports: `await import("../../chunks/liveWebsocket-BL1FcyHq.js")`
133
+ - Resolves to: `.app/ssr/chunks/liveWebsocket-BL1FcyHq.js` ❌ (doesn't exist)
134
+ 7. **Result:** Module not found → `effect_orphan` error
135
+
136
+ ### Why SSR Components Fail in Browser
137
+ SSR components are compiled for Node.js:
138
+ - Use bare module imports (`'svelte'`, `'svelte/server'`)
139
+ - Expect Node.js module resolution
140
+ - Cannot run in browser environment
141
+
142
+ Client components are Vite-bundled for browsers:
143
+ - All imports are resolved and bundled
144
+ - Chunks are properly linked
145
+ - Can execute in browser
146
+
147
+ ---
148
+
149
+ ## What We're GUESSING / Hypothesizing
150
+
151
+ ### Hypothesis 1: We're Passing the Wrong Path
152
+ **Theory:** We should pass `component_dir_client` to Forge instead of `component_dir_ssr`.
153
+
154
+ **Problem:** If we pass `/assets/components`:
155
+ - Browser would load client components ✅ (correct)
156
+ - But SSR would try to load from `/dist/assets/components/` ❌
157
+ - SSR files are actually at `/dist/.app/ssr/components/`
158
+ - SSR would break
159
+
160
+ **Conclusion:** Forge's single-path design doesn't support separate SSR/client directories.
161
+
162
+ ### Hypothesis 2: Build Output Should Match Forge's Expectations
163
+ **Theory:** Maybe we should output components to a single directory that works for both SSR and client.
164
+
165
+ **Problem:** SSR and client components are fundamentally different:
166
+ - SSR: ES modules with bare imports (for Node.js)
167
+ - Client: Vite-bundled with resolved imports (for browser)
168
+ - They MUST be separate files
169
+
170
+ **Conclusion:** We need two directories, not one.
171
+
172
+ ### Hypothesis 3: Forge Needs to Support Separate Paths
173
+ **Theory:** Modify Forge to accept `component_dir_ssr` and `component_dir_client`.
174
+
175
+ **Changes needed:**
176
+ 1. `ServerOptions.ts`: Add new optional fields
177
+ 2. `server.ts`: Calculate both paths
178
+ 3. `express_server_adapter.ts`: Use client path for `window.__COMPONENT_DIR__`
179
+ 4. `ProdComponentLoader`: Use SSR path for server-side loading
180
+
181
+ **Status:** This seems like the correct solution, but requires Forge modifications.
182
+
183
+ ### Hypothesis 4: Pass Relative Path "components" to Forge
184
+ **Theory:** The original hammer.config.yml specifies `componentDir: frontend/components`. Relative to `frontend/frontend.ts`, this is just `components`. Maybe we should pass just `"components"` to Forge?
185
+
186
+ **Analysis:**
187
+ - Forge would calculate: `COMPONENT_DIR = /dist/components`
188
+ - Browser would request: `/components/layout/root.js`
189
+ - But files don't exist at `/dist/components/` ❌
190
+ - Files are at `/dist/.app/assets/components/` and `/dist/.app/ssr/components/`
191
+
192
+ **Conclusion:** This doesn't match our current build structure.
193
+
194
+ ### Hypothesis 5: Change Build Structure
195
+ **Theory:** Output components to `/dist/components/` (single location) instead of separate SSR/client dirs.
196
+
197
+ **Problems:**
198
+ - How would we differentiate SSR vs client builds?
199
+ - Vite outputs to one location, SSR to another
200
+ - Would require major build pipeline changes
201
+ - Unclear if this is even possible with current tools
202
+
203
+ **Status:** Needs more investigation into Vite's capabilities.
204
+
205
+ ---
206
+
207
+ ## Original Config Intent
208
+
209
+ **From `hammer.config.yml`:**
210
+ ```yaml
211
+ client:
212
+ main: frontend/frontend.ts # UI root: /project/frontend/
213
+ componentDir: frontend/components # Components: /project/frontend/components/
214
+ ```
215
+
216
+ **Relative calculation:**
217
+ - UI root: `frontend/`
218
+ - Component dir: `frontend/components/`
219
+ - Relative path: `components/` (just the subdirectory)
220
+
221
+ **Bootstrap calculates:**
222
+ - `component_dir_ssr`: `.app/ssr/components`
223
+ - `component_dir_client`: `/assets/components`
224
+
225
+ **The question:** Should Forge receive:
226
+ - Option A: `.app/ssr/components` (full SSR path, current behavior)
227
+ - Option B: `/assets/components` (full client path, breaks SSR)
228
+ - Option C: `components` (relative path, no files at `/dist/components/`)
229
+ - Option D: Both paths via new Forge API
230
+
231
+ ---
232
+
233
+ ## Potential Solutions
234
+
235
+ ### Solution 1: Modify Forge (Most Correct)
236
+ **Pros:**
237
+ - Clean separation of concerns
238
+ - SSR and client use correct directories
239
+ - No breaking changes to build pipeline
240
+
241
+ **Cons:**
242
+ - Requires Forge modifications
243
+ - Need to update Forge across all projects
244
+
245
+ **Files to change:**
246
+ 1. `forge/src/options/ServerOptions.ts` - Add `component_dir_ssr` and `component_dir_client`
247
+ 2. `forge/src/server/server.ts` - Calculate both paths
248
+ 3. `forge/src/routing/server_adapter/express_server_adapter.ts` - Use client path for injection
249
+ 4. `hammer/src/client.js` - Pass both paths
250
+
251
+ ### Solution 2: Change Hammer Build to Output Single Component Directory
252
+ **Approach:** Build both SSR and client to same location, differentiate by filename suffix.
253
+
254
+ **Example:**
255
+ ```
256
+ dist/components/
257
+ ├── layout/
258
+ │ ├── root.ssr.js # For server-side rendering
259
+ │ └── root.js # For browser
260
+ ```
261
+
262
+ **Pros:**
263
+ - Works with Forge's current API
264
+ - No Forge modifications needed
265
+
266
+ **Cons:**
267
+ - Major build pipeline changes
268
+ - Unclear if Vite supports this pattern
269
+ - May break component loader expectations
270
+ - High risk of regressions
271
+
272
+ ### Solution 3: Pass Client Path to Forge, Handle SSR Separately
273
+ **Approach:** Pass `/assets/components` to Forge, manually load SSR components elsewhere.
274
+
275
+ **Problems:**
276
+ - How would SSR loading work?
277
+ - Would need to bypass Forge's component loader
278
+ - Unclear if this is even possible
279
+ - High complexity, fragile
280
+
281
+ ### Solution 4: Mount Client Components at SSR Path
282
+ **Approach:** Add another Express mount that serves client components at `/.app/ssr/components`.
283
+
284
+ **Example:**
285
+ ```javascript
286
+ app.use('/.app/ssr/components', express.static('/dist/.app/assets/components'))
287
+ ```
288
+
289
+ **Pros:**
290
+ - No code changes to Forge
291
+ - Quick fix
292
+
293
+ **Cons:**
294
+ - Conceptually wrong (SSR path serves client files)
295
+ - Confusing for developers
296
+ - SSR would still try to load from wrong location
297
+ - Doesn't fix the root problem
298
+
299
+ ---
300
+
301
+ ## Open Questions
302
+
303
+ 1. **Can Vite output to a single component directory?**
304
+ - Can we build SSR and client to same location with different naming?
305
+ - Does this conflict with Vite's chunk optimization?
306
+
307
+ 2. **Does Forge's dev mode work correctly?**
308
+ - Dev mode uses Vite dev server
309
+ - Does it have the same SSR/client split issue?
310
+ - Line 264: `clientComponentDir = this.isProd ? this.componentDir : '/assets'`
311
+ - In dev: Always uses `/assets` - why does this work?
312
+
313
+ 3. **Is the `.app/` directory structure mandatory?**
314
+ - Could we output to `/dist/ssr/` and `/dist/assets/` instead?
315
+ - Would this simplify path calculations?
316
+
317
+ 4. **What did the original Forge design expect?**
318
+ - Was Forge designed for projects with separate SSR/client builds?
319
+ - Or was it designed for unified component directories?
320
+
321
+ 5. **Are there other projects using this stack successfully?**
322
+ - How do they structure their builds?
323
+ - Can we learn from existing patterns?
324
+
325
+ ---
326
+
327
+ ## Next Steps
328
+
329
+ 1. **Decide on solution approach:**
330
+ - Option 1: Modify Forge (cleanest, most correct)
331
+ - Option 2: Restructure build output (risky, high effort)
332
+ - Option 3: Quick mount hack (temporary workaround)
333
+
334
+ 2. **If Option 1 (Modify Forge):**
335
+ - Design the new API surface
336
+ - Write tests for dual-path support
337
+ - Implement changes in Forge
338
+ - Update hammer to use new API
339
+ - Test with markdown_view, liftlog, noblelaw
340
+
341
+ 3. **If Option 2 (Restructure Build):**
342
+ - Research Vite multi-output capabilities
343
+ - Design new directory structure
344
+ - Update build pipeline
345
+ - Test thoroughly across all projects
346
+
347
+ 4. **Validate the fix:**
348
+ - Run all 18 unit tests
349
+ - Test markdown_view in production mode
350
+ - Verify no `effect_orphan` errors
351
+ - Confirm browser loads correct (client) components
352
+ - Verify SSR still works
353
+
354
+ ---
355
+
356
+ ## Timeline Estimate
357
+
358
+ - **Option 1 (Modify Forge):** 2-3 hours implementation + 1 hour testing
359
+ - **Option 2 (Restructure Build):** 1-2 days research + 2-3 days implementation + 2 hours testing
360
+ - **Option 3 (Quick Hack):** 30 minutes, but doesn't solve root cause
361
+
362
+ ---
363
+
364
+ ## Files Reference
365
+
366
+ ### Confirmed File Locations
367
+ - `/Users/shavauhngabay/dev/hammer/src/client.js` - Passes component_dir to Forge
368
+ - `/Users/shavauhngabay/dev/hammer/src/runtime/runtime.js` - Express static mounts
369
+ - `/Users/shavauhngabay/dev/hammer/src/build/bootstrap.js` - Calculates component paths
370
+ - `/Users/shavauhngabay/dev/forge/src/server/server.ts` - Forge server initialization
371
+ - `/Users/shavauhngabay/dev/forge/src/options/ServerOptions.ts` - Forge options interface
372
+ - `/Users/shavauhngabay/dev/forge/src/routing/server_adapter/express_server_adapter.ts` - Injects window.__COMPONENT_DIR__
373
+ - `/Users/shavauhngabay/dev/forge/src/routing/component_loader/component_loader.ts` - ProdComponentLoader
374
+ - `/Users/shavauhngabay/dev/markdown_view/hammer.config.yml` - Project configuration
375
+ - `/Users/shavauhngabay/dev/markdown_view/dist/no_ego.js` - Generated runtime config
376
+
377
+ ---
378
+
379
+ **END OF DOCUMENT**
380
+
381
+ For questions or to proceed with implementation, contact the engineering team.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/app",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Production build tool for Dinner/Forge apps.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,8 +29,9 @@
29
29
  "license": "MIT",
30
30
  "author": "App Build CLI",
31
31
  "dependencies": {
32
- "glob-parent": "^6.0.2",
33
32
  "deepmerge": "^4.3.1",
33
+ "glob-parent": "^6.0.2",
34
+ "http-proxy": "^1.18.1",
34
35
  "picomatch": "^2.3.1",
35
36
  "yaml": "^2.6.0"
36
37
  },
package/src/args.js CHANGED
@@ -25,12 +25,13 @@ const FLAG_MAP = new Map([
25
25
  ['split-serve', 'splitServe'],
26
26
  ['frontend-cmd', 'frontendCmd'],
27
27
  ['mode', 'mode'],
28
+ ['verbose', 'verbose'],
28
29
  ['help', 'help'],
29
30
  ['version', 'version']
30
31
  ]);
31
32
 
32
33
  const MULTI_VALUE_FLAGS = new Set(['sqlGlob', 'assets', 'clientExclude', 'watchPath']);
33
- const BOOLEAN_FLAGS = new Set(['watch', 'splitServe']);
34
+ const BOOLEAN_FLAGS = new Set(['watch', 'splitServe', 'verbose']);
34
35
 
35
36
  export function parseCliArgs(argv) {
36
37
  const result = {
@@ -158,6 +159,7 @@ Options (shared):
158
159
  --split-serve Run frontend dev server in a separate process (watch mode only)
159
160
  --frontend-cmd <name> Frontend command: currently supports 'vite' (default when split-serve)
160
161
  --mode <value> Build mode forwarded to Vite (default: production)
162
+ --verbose Enable verbose debug logging
161
163
  --help Show this message
162
164
  --version Show App version
163
165
 
@@ -1,6 +1,104 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
+ /**
5
+ * Calculate all component paths once based on project config.
6
+ * This is the SINGLE SOURCE OF TRUTH for path calculations.
7
+ *
8
+ * @param {object} config - Project config with mode, client.main_abs, client.componentDir_abs
9
+ * @returns {object} All calculated paths
10
+ */
11
+ export async function calculateComponentPaths(config) {
12
+ const uiRootAbs = config.client?.main_abs ? path.dirname(config.client.main_abs) : config.rootDir || config.root;
13
+ const componentDirAbs = config.client?.componentDir_abs || uiRootAbs;
14
+ const relativeFromUiRoot = toPosix(path.relative(uiRootAbs, componentDirAbs) || '');
15
+ const uiRelFromRoot = toPosix(path.relative(config.rootDir || config.root, uiRootAbs) || '');
16
+ const distAbs = config.outDir_abs || config.outDir || path.resolve(config.rootDir || config.root, 'dist');
17
+
18
+ // Compute client filesystem base candidates
19
+ const clientCandidates = [
20
+ path.join(distAbs, '.app', 'assets'),
21
+ path.join(distAbs, 'client'),
22
+ uiRelFromRoot ? path.join(distAbs, uiRelFromRoot) : null
23
+ ].filter(Boolean);
24
+
25
+ const clientFsBase = await firstExistingDir(clientCandidates) || path.join(distAbs, '.app', 'assets');
26
+
27
+ // Determine client public base
28
+ let client_base_path = '/assets';
29
+ if (endsWithDir(clientFsBase, path.join(distAbs, uiRelFromRoot))) {
30
+ client_base_path = `/${uiRelFromRoot}`;
31
+ }
32
+ client_base_path = toPosix(client_base_path);
33
+
34
+ // Compute client component public base (append component suffix if present)
35
+ const component_suffix = relativeFromUiRoot || null;
36
+ const component_dir_client = component_suffix
37
+ ? toPosix(path.posix.join(client_base_path, component_suffix))
38
+ : client_base_path;
39
+
40
+ // Compute SSR filesystem base candidates
41
+ const ssrCandidates = [
42
+ path.join(distAbs, '.app', 'ssr', relativeFromUiRoot),
43
+ path.join(distAbs, '.app', 'ssr'),
44
+ path.join(distAbs, 'ssr', relativeFromUiRoot),
45
+ path.join(distAbs, 'ssr'),
46
+ uiRelFromRoot ? path.join(distAbs, uiRelFromRoot, relativeFromUiRoot) : null,
47
+ uiRelFromRoot ? path.join(distAbs, 'server', uiRelFromRoot, relativeFromUiRoot) : null
48
+ ].filter(Boolean);
49
+
50
+ const ssrFsBase = await firstExistingDir(ssrCandidates) || path.join(distAbs, '.app', 'ssr', relativeFromUiRoot || '');
51
+
52
+ // Resolve values relative to project root for serialization; generate relative-to-dist for Forge
53
+ const distRel = (abs) => toPosix(path.relative(distAbs, abs));
54
+ const rootRel = (abs) => toPosix(path.relative(config.rootDir || config.root, abs));
55
+
56
+ // Directories for runtime mounting and Forge consumption
57
+ const assets_build_root_rel = distRel(clientFsBase) || '.app/assets';
58
+ const component_dir_ssr = distRel(ssrFsBase) || '.app/ssr';
59
+
60
+ return {
61
+ // Forge SSR base directory (relative to dist root)
62
+ component_dir_ssr,
63
+ // Browser public base for component imports
64
+ component_dir_client,
65
+ // Browser public base for all assets (CSR root)
66
+ component_base_path: client_base_path,
67
+ // Filesystem directory (relative to dist root) that holds client bundle
68
+ assets_build_dir: assets_build_root_rel,
69
+ // Extra: absolute paths (resolved at runtime by bootstrap template)
70
+ assets_build_abs: rootRel(path.join(distAbs, assets_build_root_rel)),
71
+ ssr_build_abs: rootRel(path.join(distAbs, component_dir_ssr)),
72
+ component_suffix,
73
+ // Legacy aliases for backwards compatibility
74
+ component_dir: component_dir_ssr,
75
+ componentDir_abs: component_dir_ssr
76
+ };
77
+ }
78
+
79
+ async function firstExistingDir(candidates) {
80
+ for (const p of candidates) {
81
+ try {
82
+ const stat = await fs.stat(p);
83
+ if (stat.isDirectory()) return p;
84
+ } catch {}
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function endsWithDir(child, parent) {
90
+ try {
91
+ const rel = path.relative(parent, child);
92
+ return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function toPosix(value) {
99
+ return String(value || '').split(path.sep).join('/');
100
+ }
101
+
4
102
  /**
5
103
  * Generates a bootstrap wrapper (no_ego.js) that sets up the runtime environment
6
104
  * before importing the compiled application entry.
@@ -47,15 +145,19 @@ export async function generateBootstrap(context) {
47
145
  openapi: config.client?.openapi,
48
146
  openapi_abs: config.client?.openapi_abs ? './' + path.relative(config.rootDir, config.client.openapi_abs) : null,
49
147
  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 || []
148
+ // Calculate ALL component paths ONCE - used by SSR, client, and Vite
149
+ // Philosophy: Calculate once in bootstrap, use everywhere without recalculation
150
+ ...(await calculateComponentPaths(config)),
151
+ assetDirs: config.client?.assetDirs ? config.client.assetDirs.map(assetDir => ({
152
+ mountPath: assetDir.mountPath,
153
+ // Convert absolute path to relative path from project root
154
+ absolutePath: './' + path.relative(config.rootDir, assetDir.absolutePath)
155
+ })) : []
53
156
  } : undefined,
54
157
  dev: {
55
158
  port: config.dev?.port || 3000,
56
159
  backendPort: config.dev?.backendPort || 3001
57
- },
58
- assets: []
160
+ }
59
161
  };
60
162
 
61
163
  // Compute relative path from outDir to compiled entry
package/src/build/html.js CHANGED
@@ -19,8 +19,8 @@ export async function rewriteHtmlTemplate(context, clientArtifacts) {
19
19
  const pattern = new RegExp(`\\s*${escaped}\\s*`, 'g');
20
20
  html = html.replace(pattern, '');
21
21
  }
22
- // Client bundle public base served at '/assets'
23
- const clientBase = '/assets';
22
+ // Client bundle public base served at configured base
23
+ const clientBase = config.client?.component_base_path || '/assets';
24
24
  html = html.replace(/(href|src)=(['"])\/assets\//g, `$1=$2${clientBase}/`);
25
25
  // Remove inline dev loader blocks ( import('<whatever>/client.ts') ) and related comments
26
26
  html = html.replace(/\s*<!--[^>]*Development[^>]*-->\s*/gi, '');
@@ -56,6 +56,8 @@ export async function rewriteHtmlTemplate(context, clientArtifacts) {
56
56
  const scriptTag = `<script type="module" src="${makeClientPublicPath(entryFile)}"></script>`;
57
57
  html = injectBeforeClose(html, '</body>', scriptTag);
58
58
 
59
+ // Do not inject window.__COMPONENT_DIR__ here; Forge injects it at response time.
60
+
59
61
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
60
62
  await fs.writeFile(targetPath, html, 'utf8');
61
63
 
@@ -27,7 +27,7 @@ export async function writeRuntimeManifest(context, artifacts) {
27
27
  client: artifacts.client ? {
28
28
  manifest: artifacts.client.manifestPath ? relativeToOut(config, artifacts.client.manifestPath) : null,
29
29
  entry: artifacts.client.entryGraph?.entry?.file
30
- ? `/assets/${toPosix(artifacts.client.entryGraph.entry.file)}`
30
+ ? `${config.client?.component_base_path || '/assets'}/${toPosix(artifacts.client.entryGraph.entry.file)}`
31
31
  : null
32
32
  } : null,
33
33
  ssr: artifacts.ssr ? {
@@ -81,10 +81,12 @@ async function reorganizeServerOutputs(context) {
81
81
 
82
82
  // Mirror middleware directory into dist root for Dinner runtime lookup
83
83
  await fs.rm(middlewareOutDir, { recursive: true, force: true });
84
- const middlewareRel = path.relative(config.rootDir, config.server.middlewareDir);
85
- if (!middlewareRel.startsWith('..')) {
86
- const compiledMiddleware = path.join(serverOutDir, middlewareRel);
87
- await copyTree(compiledMiddleware, middlewareOutDir);
84
+ if (config.server.middlewareDir) {
85
+ const middlewareRel = path.relative(config.rootDir, config.server.middlewareDir);
86
+ if (!middlewareRel.startsWith('..')) {
87
+ const compiledMiddleware = path.join(serverOutDir, middlewareRel);
88
+ await copyTree(compiledMiddleware, middlewareOutDir);
89
+ }
88
90
  }
89
91
 
90
92
  logger.info('Server TypeScript output staged');
@@ -162,6 +164,14 @@ async function mirrorUiModules(context) {
162
164
  await fs.rm(destinationDir, { recursive: true, force: true }).catch(() => {});
163
165
 
164
166
  // Overlay compiled UI outputs from tsc emit so .ts becomes .js under mirrored UI root
167
+ if (config.verbose) {
168
+ console.log('[server] overlayUi called:', {
169
+ 'config.ui': config.ui,
170
+ 'config.ui.rootDir': config.ui?.rootDir,
171
+ 'config.rootDir': config.rootDir
172
+ });
173
+ }
174
+
165
175
  const compiledUiDir = path.join(
166
176
  config.layout.serverOutDir,
167
177
  path.relative(config.rootDir, config.ui.rootDir)