@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.
- package/.claude/settings.local.json +11 -4
- package/docs/asset-serving-fix.md +381 -0
- package/package.json +3 -2
- package/src/args.js +3 -1
- package/src/build/bootstrap.js +107 -5
- package/src/build/html.js +4 -2
- package/src/build/runtime-manifest.js +1 -1
- package/src/build/server.js +14 -4
- package/src/build/ssr.js +130 -13
- package/src/build/ui-common.js +19 -2
- package/src/client.js +14 -2
- package/src/config.js +10 -0
- package/src/runtime/runtime.js +49 -6
- package/test/asset-mounting.test.js +211 -0
- package/test/config-pipeline.test.js +353 -0
- package/test/path-resolution.test.js +164 -0
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
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.
|
|
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
|
|
package/src/build/bootstrap.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
?
|
|
30
|
+
? `${config.client?.component_base_path || '/assets'}/${toPosix(artifacts.client.entryGraph.entry.file)}`
|
|
31
31
|
: null
|
|
32
32
|
} : null,
|
|
33
33
|
ssr: artifacts.ssr ? {
|
package/src/build/server.js
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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)
|