@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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { calculateComponentPaths } from '../src/build/bootstrap.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* COMPREHENSIVE PIPELINE TEST: Config → Bootstrap → Runtime
|
|
8
|
+
*
|
|
9
|
+
* This test validates the ENTIRE path calculation pipeline:
|
|
10
|
+
* 1. Config loading calculates base paths
|
|
11
|
+
* 2. Bootstrap generates no_ego.js with ALL necessary paths
|
|
12
|
+
* 3. Runtime code (client.js) uses these paths WITHOUT recalculation
|
|
13
|
+
*
|
|
14
|
+
* Key Requirements:
|
|
15
|
+
* - ALL paths must be RELATIVE (for Docker/deployment portability)
|
|
16
|
+
* - Calculate ONCE in bootstrap, use everywhere
|
|
17
|
+
* - No mode checks in runtime code
|
|
18
|
+
*
|
|
19
|
+
* IMPORTANT: This test uses the REAL calculateComponentPaths() function
|
|
20
|
+
* from bootstrap.js to ensure we're testing actual production code.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mock runtime usage - this is what client.js SHOULD do
|
|
25
|
+
*/
|
|
26
|
+
function runtimeGetComponentDir(bootstrapConfig, mode) {
|
|
27
|
+
// Runtime should NEVER check mode - just use precalculated value
|
|
28
|
+
return bootstrapConfig.component_dir_client;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate all paths are relative (no absolute filesystem paths)
|
|
33
|
+
*/
|
|
34
|
+
function assertAllPathsRelative(config, projectName) {
|
|
35
|
+
const paths = [
|
|
36
|
+
config.component_dir_ssr,
|
|
37
|
+
config.component_dir_client,
|
|
38
|
+
config.assets_build_dir
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
paths.forEach(p => {
|
|
42
|
+
if (!p) return;
|
|
43
|
+
|
|
44
|
+
// Check it's not an absolute filesystem path
|
|
45
|
+
assert.ok(
|
|
46
|
+
!path.isAbsolute(p) || p.startsWith('/'),
|
|
47
|
+
`${projectName}: Path ${p} should be relative or HTTP absolute (starting with /), not filesystem absolute`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Check it doesn't contain system-specific paths
|
|
51
|
+
assert.ok(
|
|
52
|
+
!p.includes('/Users/') && !p.includes('/home/') && !p.includes('C:\\'),
|
|
53
|
+
`${projectName}: Path ${p} contains absolute filesystem path - not portable for Docker`
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Test 1: markdown_view (nested componentDir)
|
|
60
|
+
*
|
|
61
|
+
* Real failure we saw:
|
|
62
|
+
* - Browser requested: /chunks/props-B_GERzIc.js (404)
|
|
63
|
+
* - Should request: /assets/chunks/props-B_GERzIc.js
|
|
64
|
+
*
|
|
65
|
+
* Root cause:
|
|
66
|
+
* - component_dir was set to ".app/ssr/components" for browser
|
|
67
|
+
* - Forge client didn't detect "assets" in path
|
|
68
|
+
* - Used wrong import base
|
|
69
|
+
*/
|
|
70
|
+
test('markdown_view: nested componentDir with proper client/SSR separation', () => {
|
|
71
|
+
const projectConfig = {
|
|
72
|
+
mode: 'production',
|
|
73
|
+
rootDir: '/project/markdown_view',
|
|
74
|
+
client: {
|
|
75
|
+
main_abs: '/project/markdown_view/frontend/frontend.ts',
|
|
76
|
+
componentDir_abs: '/project/markdown_view/frontend/components'
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const bootstrap = calculateComponentPaths(projectConfig);
|
|
81
|
+
|
|
82
|
+
console.log('markdown_view bootstrap config:', bootstrap);
|
|
83
|
+
|
|
84
|
+
// Bootstrap should calculate BOTH paths
|
|
85
|
+
assert.strictEqual(bootstrap.component_dir_ssr, '.app/ssr/components',
|
|
86
|
+
'SSR should load from .app/ssr/components');
|
|
87
|
+
assert.strictEqual(bootstrap.component_dir_client, '/assets/components',
|
|
88
|
+
'Browser should load from /assets/components');
|
|
89
|
+
assert.strictEqual(bootstrap.component_base_path, '/assets',
|
|
90
|
+
'Vite base should be /assets');
|
|
91
|
+
assert.strictEqual(bootstrap.assets_build_dir, '.app/assets/components',
|
|
92
|
+
'Assets should build to .app/assets/components');
|
|
93
|
+
assert.strictEqual(bootstrap.component_suffix, 'components',
|
|
94
|
+
'Suffix should be "components" for fallback mount');
|
|
95
|
+
|
|
96
|
+
// Validate all paths are relative
|
|
97
|
+
assertAllPathsRelative(bootstrap, 'markdown_view');
|
|
98
|
+
|
|
99
|
+
// Runtime should use precalculated client path (NO mode check)
|
|
100
|
+
const runtimePath = runtimeGetComponentDir(bootstrap, 'production');
|
|
101
|
+
assert.strictEqual(runtimePath, '/assets/components',
|
|
102
|
+
'Runtime should use precalculated client path');
|
|
103
|
+
|
|
104
|
+
// Verify Forge client will detect "assets" in path
|
|
105
|
+
assert.ok(runtimePath.indexOf('assets') !== -1,
|
|
106
|
+
'Client path MUST contain "assets" for Forge to use compiled imports');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Test 2: liftlog/noblelaw (no explicit componentDir)
|
|
111
|
+
*
|
|
112
|
+
* These projects don't specify componentDir in config.
|
|
113
|
+
* Components are at UI root (same dir as main file).
|
|
114
|
+
*/
|
|
115
|
+
test('liftlog/noblelaw: no explicit componentDir (components at UI root)', () => {
|
|
116
|
+
const projectConfig = {
|
|
117
|
+
mode: 'production',
|
|
118
|
+
rootDir: '/project/liftlog',
|
|
119
|
+
client: {
|
|
120
|
+
main_abs: '/project/liftlog/ui/main.ts',
|
|
121
|
+
componentDir_abs: null
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const bootstrap = calculateComponentPaths(projectConfig);
|
|
126
|
+
|
|
127
|
+
console.log('liftlog bootstrap config:', bootstrap);
|
|
128
|
+
|
|
129
|
+
// No suffix since components at root
|
|
130
|
+
assert.strictEqual(bootstrap.component_dir_ssr, '.app/ssr');
|
|
131
|
+
assert.strictEqual(bootstrap.component_dir_client, '/assets');
|
|
132
|
+
assert.strictEqual(bootstrap.component_base_path, '/assets');
|
|
133
|
+
assert.strictEqual(bootstrap.assets_build_dir, '.app/assets');
|
|
134
|
+
assert.strictEqual(bootstrap.component_suffix, null);
|
|
135
|
+
|
|
136
|
+
assertAllPathsRelative(bootstrap, 'liftlog');
|
|
137
|
+
|
|
138
|
+
const runtimePath = runtimeGetComponentDir(bootstrap, 'production');
|
|
139
|
+
assert.strictEqual(runtimePath, '/assets');
|
|
140
|
+
assert.ok(runtimePath.indexOf('assets') !== -1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Test 3: Development mode
|
|
145
|
+
*
|
|
146
|
+
* In dev mode:
|
|
147
|
+
* - Client should use SSR path (Vite dev server serves raw .svelte files)
|
|
148
|
+
* - No asset compilation
|
|
149
|
+
*/
|
|
150
|
+
test('development mode: client uses SSR path', () => {
|
|
151
|
+
const projectConfig = {
|
|
152
|
+
mode: 'development',
|
|
153
|
+
rootDir: '/project/markdown_view',
|
|
154
|
+
client: {
|
|
155
|
+
main_abs: '/project/markdown_view/frontend/frontend.ts',
|
|
156
|
+
componentDir_abs: '/project/markdown_view/frontend/components'
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const bootstrap = calculateComponentPaths(projectConfig);
|
|
161
|
+
|
|
162
|
+
console.log('development bootstrap config:', bootstrap);
|
|
163
|
+
|
|
164
|
+
// In dev, client uses same path as SSR
|
|
165
|
+
assert.strictEqual(bootstrap.component_dir_ssr, '.app/ssr/components');
|
|
166
|
+
assert.strictEqual(bootstrap.component_dir_client, '.app/ssr/components',
|
|
167
|
+
'Dev mode: client should use SSR path');
|
|
168
|
+
assert.strictEqual(bootstrap.component_base_path, '/assets',
|
|
169
|
+
'Vite base is still /assets even in dev');
|
|
170
|
+
|
|
171
|
+
assertAllPathsRelative(bootstrap, 'dev-markdown_view');
|
|
172
|
+
|
|
173
|
+
const runtimePath = runtimeGetComponentDir(bootstrap, 'development');
|
|
174
|
+
assert.strictEqual(runtimePath, '.app/ssr/components',
|
|
175
|
+
'Dev runtime should use SSR path');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Test 4: Deeply nested componentDir
|
|
180
|
+
*
|
|
181
|
+
* Edge case: componentDir nested multiple levels deep
|
|
182
|
+
*/
|
|
183
|
+
test('deeply nested componentDir: preserves full path', () => {
|
|
184
|
+
const projectConfig = {
|
|
185
|
+
mode: 'production',
|
|
186
|
+
rootDir: '/project/test',
|
|
187
|
+
client: {
|
|
188
|
+
main_abs: '/project/test/src/ui/main.ts',
|
|
189
|
+
componentDir_abs: '/project/test/src/ui/components/shared'
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const bootstrap = calculateComponentPaths(projectConfig);
|
|
194
|
+
|
|
195
|
+
console.log('nested bootstrap config:', bootstrap);
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(bootstrap.component_dir_ssr, '.app/ssr/components/shared');
|
|
198
|
+
assert.strictEqual(bootstrap.component_dir_client, '/assets/components/shared');
|
|
199
|
+
assert.strictEqual(bootstrap.assets_build_dir, '.app/assets/components/shared');
|
|
200
|
+
assert.strictEqual(bootstrap.component_suffix, 'components/shared');
|
|
201
|
+
|
|
202
|
+
assertAllPathsRelative(bootstrap, 'nested-project');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Test 5: Path portability (Docker deployment)
|
|
207
|
+
*
|
|
208
|
+
* Simulates copying dist/ folder to different location.
|
|
209
|
+
* All paths must still work.
|
|
210
|
+
*/
|
|
211
|
+
test('portability: paths work when dist/ is moved', () => {
|
|
212
|
+
const projectConfig = {
|
|
213
|
+
mode: 'production',
|
|
214
|
+
rootDir: '/project/markdown_view',
|
|
215
|
+
client: {
|
|
216
|
+
main_abs: '/project/markdown_view/frontend/frontend.ts',
|
|
217
|
+
componentDir_abs: '/project/markdown_view/frontend/components'
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const bootstrap = calculateComponentPaths(projectConfig);
|
|
222
|
+
|
|
223
|
+
// Simulate moving dist/ to Docker container at /app/dist
|
|
224
|
+
const originalLocation = '/project/markdown_view/dist';
|
|
225
|
+
const newLocation = '/app/dist';
|
|
226
|
+
|
|
227
|
+
// These paths should work at both locations (because they're relative)
|
|
228
|
+
const relativePaths = [
|
|
229
|
+
bootstrap.component_dir_ssr,
|
|
230
|
+
bootstrap.assets_build_dir
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
relativePaths.forEach(p => {
|
|
234
|
+
if (!p) return;
|
|
235
|
+
|
|
236
|
+
// Path should work with both base directories
|
|
237
|
+
const path1 = path.join(originalLocation, p);
|
|
238
|
+
const path2 = path.join(newLocation, p);
|
|
239
|
+
|
|
240
|
+
// Both should resolve to the same relative structure
|
|
241
|
+
const rel1 = path.relative(originalLocation, path1);
|
|
242
|
+
const rel2 = path.relative(newLocation, path2);
|
|
243
|
+
|
|
244
|
+
assert.strictEqual(rel1, rel2,
|
|
245
|
+
`Path ${p} should maintain same structure when moved`);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log('✅ Paths are portable - can move dist/ folder');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Test 6: Runtime NEVER checks mode
|
|
253
|
+
*
|
|
254
|
+
* This validates that runtime code doesn't do mode-based logic.
|
|
255
|
+
* All decisions should be made in bootstrap.
|
|
256
|
+
*/
|
|
257
|
+
test('runtime code never checks mode', () => {
|
|
258
|
+
const prodConfig = {
|
|
259
|
+
mode: 'production',
|
|
260
|
+
rootDir: '/project/test',
|
|
261
|
+
client: {
|
|
262
|
+
main_abs: '/project/test/ui/main.ts',
|
|
263
|
+
componentDir_abs: '/project/test/ui/components'
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const devConfig = { ...prodConfig, mode: 'development' };
|
|
268
|
+
|
|
269
|
+
const prodBootstrap = calculateComponentPaths(prodConfig);
|
|
270
|
+
const devBootstrap = calculateComponentPaths(devConfig);
|
|
271
|
+
|
|
272
|
+
// Runtime gets different precalculated values
|
|
273
|
+
const prodRuntime = runtimeGetComponentDir(prodBootstrap, 'production');
|
|
274
|
+
const devRuntime = runtimeGetComponentDir(devBootstrap, 'development');
|
|
275
|
+
|
|
276
|
+
// But runtime function doesn't care about mode - it just returns the value
|
|
277
|
+
// Verify this by passing WRONG mode to runtime
|
|
278
|
+
const prodRuntimeWrongMode = runtimeGetComponentDir(prodBootstrap, 'development');
|
|
279
|
+
const devRuntimeWrongMode = runtimeGetComponentDir(devBootstrap, 'production');
|
|
280
|
+
|
|
281
|
+
// Should get same result regardless of mode passed to runtime
|
|
282
|
+
assert.strictEqual(prodRuntime, prodRuntimeWrongMode,
|
|
283
|
+
'Runtime should not check mode - only use bootstrap value');
|
|
284
|
+
assert.strictEqual(devRuntime, devRuntimeWrongMode,
|
|
285
|
+
'Runtime should not check mode - only use bootstrap value');
|
|
286
|
+
|
|
287
|
+
console.log('✅ Runtime never checks mode');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Test 7: Integration - Full pipeline
|
|
292
|
+
*
|
|
293
|
+
* This tests the complete flow:
|
|
294
|
+
* Config → Bootstrap → no_ego.js → Runtime → Forge
|
|
295
|
+
*/
|
|
296
|
+
test('full pipeline: config → bootstrap → runtime → forge', () => {
|
|
297
|
+
// Step 1: Project config (from hammer.config.yml)
|
|
298
|
+
const projectConfig = {
|
|
299
|
+
mode: 'production',
|
|
300
|
+
rootDir: '/project/markdown_view',
|
|
301
|
+
client: {
|
|
302
|
+
main_abs: '/project/markdown_view/frontend/frontend.ts',
|
|
303
|
+
componentDir_abs: '/project/markdown_view/frontend/components'
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Step 2: Bootstrap calculates all paths
|
|
308
|
+
const bootstrapConfig = calculateComponentPaths(projectConfig);
|
|
309
|
+
|
|
310
|
+
// Step 3: no_ego.js would contain these values
|
|
311
|
+
const noEgoJs = {
|
|
312
|
+
mode: projectConfig.mode,
|
|
313
|
+
client: {
|
|
314
|
+
component_dir_ssr: bootstrapConfig.component_dir_ssr,
|
|
315
|
+
component_dir_client: bootstrapConfig.component_dir_client,
|
|
316
|
+
component_base_path: bootstrapConfig.component_base_path,
|
|
317
|
+
assets_build_dir: bootstrapConfig.assets_build_dir,
|
|
318
|
+
component_suffix: bootstrapConfig.component_suffix
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
console.log('Generated no_ego.js:', JSON.stringify(noEgoJs, null, 2));
|
|
323
|
+
|
|
324
|
+
// Step 4: Runtime (client.js) reads from no_ego.js
|
|
325
|
+
const runtimeComponentDir = noEgoJs.client.component_dir_client;
|
|
326
|
+
|
|
327
|
+
// Step 5: Forge receives this value
|
|
328
|
+
const forgeOptions = {
|
|
329
|
+
component_dir: runtimeComponentDir,
|
|
330
|
+
development: projectConfig.mode !== 'production'
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
console.log('Forge receives:', forgeOptions);
|
|
334
|
+
|
|
335
|
+
// Validate the pipeline
|
|
336
|
+
assert.strictEqual(forgeOptions.component_dir, '/assets/components',
|
|
337
|
+
'Forge should receive /assets/components for client imports');
|
|
338
|
+
|
|
339
|
+
// Verify Forge will use compiled imports
|
|
340
|
+
const forgeWillUseCompiledImports = forgeOptions.component_dir.indexOf('assets') !== -1;
|
|
341
|
+
assert.ok(forgeWillUseCompiledImports,
|
|
342
|
+
'Forge MUST detect "assets" in path to use compiled .js imports');
|
|
343
|
+
|
|
344
|
+
// Verify browser will request correct paths
|
|
345
|
+
const browserImportPath = `${forgeOptions.component_dir}/layout/root.js`;
|
|
346
|
+
assert.strictEqual(browserImportPath, '/assets/components/layout/root.js',
|
|
347
|
+
'Browser should import from /assets/components/layout/root.js');
|
|
348
|
+
|
|
349
|
+
console.log('✅ Full pipeline working correctly');
|
|
350
|
+
console.log(` Browser will import: ${browserImportPath}`);
|
|
351
|
+
console.log(` File exists at: .app/assets/components/layout/root.js`);
|
|
352
|
+
console.log(` Express serves: .app/assets/components at /assets (with fallback)`);
|
|
353
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* These are the SAME functions used in bootstrap.js
|
|
7
|
+
* We're testing them in isolation with real project data
|
|
8
|
+
*/
|
|
9
|
+
function calculateComponentPaths(config) {
|
|
10
|
+
if (!config.client?.componentDir_abs) {
|
|
11
|
+
return {
|
|
12
|
+
component_dir: '.app/ssr',
|
|
13
|
+
componentDir_abs: '.app/ssr',
|
|
14
|
+
component_suffix: null
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const uiRootAbs = config.client.main_abs ? path.dirname(config.client.main_abs) : config.root;
|
|
19
|
+
const relativeFromUiRoot = path.relative(uiRootAbs, config.client.componentDir_abs);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
component_dir: path.join('.app/ssr', relativeFromUiRoot),
|
|
23
|
+
componentDir_abs: path.join('.app/ssr', relativeFromUiRoot),
|
|
24
|
+
component_suffix: relativeFromUiRoot || null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function calculateAssetFallback(componentSuffix) {
|
|
29
|
+
if (!componentSuffix) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return path.join('.app/assets', componentSuffix);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Test with REAL project configurations
|
|
37
|
+
*/
|
|
38
|
+
test('markdown_view: componentDir in subfolder', () => {
|
|
39
|
+
// Actual values from markdown_view/hammer.config.yml
|
|
40
|
+
const config = {
|
|
41
|
+
root: '/Users/shavauhngabay/dev/markdown_view',
|
|
42
|
+
client: {
|
|
43
|
+
main_abs: '/Users/shavauhngabay/dev/markdown_view/frontend/frontend.ts',
|
|
44
|
+
componentDir_abs: '/Users/shavauhngabay/dev/markdown_view/frontend/components'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = calculateComponentPaths(config);
|
|
49
|
+
const assetFallback = calculateAssetFallback(result.component_suffix);
|
|
50
|
+
|
|
51
|
+
console.log('markdown_view result:', result);
|
|
52
|
+
console.log('markdown_view asset fallback:', assetFallback);
|
|
53
|
+
|
|
54
|
+
// Expected: components are nested in 'components' subfolder
|
|
55
|
+
assert.strictEqual(result.component_dir, '.app/ssr/components', 'component_dir should be .app/ssr/components');
|
|
56
|
+
assert.strictEqual(result.component_suffix, 'components', 'component_suffix should be components');
|
|
57
|
+
assert.strictEqual(assetFallback, '.app/assets/components', 'asset fallback should be .app/assets/components');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('noblelaw/liftlog: no explicit componentDir', () => {
|
|
61
|
+
// When componentDir is not explicitly set, it defaults to UI root
|
|
62
|
+
const config = {
|
|
63
|
+
root: '/Users/shavauhngabay/dev/noblelaw',
|
|
64
|
+
client: {
|
|
65
|
+
main_abs: '/Users/shavauhngabay/dev/noblelaw/ui/main.ts',
|
|
66
|
+
componentDir_abs: null
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const result = calculateComponentPaths(config);
|
|
71
|
+
const assetFallback = calculateAssetFallback(result.component_suffix);
|
|
72
|
+
|
|
73
|
+
console.log('noblelaw result:', result);
|
|
74
|
+
console.log('noblelaw asset fallback:', assetFallback);
|
|
75
|
+
|
|
76
|
+
// Expected: no suffix, components at root
|
|
77
|
+
assert.strictEqual(result.component_dir, '.app/ssr', 'component_dir should be .app/ssr');
|
|
78
|
+
assert.strictEqual(result.component_suffix, null, 'component_suffix should be null');
|
|
79
|
+
assert.strictEqual(assetFallback, null, 'asset fallback should be null');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('deeply nested componentDir', () => {
|
|
83
|
+
// Edge case: deeply nested component directory
|
|
84
|
+
const config = {
|
|
85
|
+
root: '/Users/test/project',
|
|
86
|
+
client: {
|
|
87
|
+
main_abs: '/Users/test/project/src/ui/main.ts',
|
|
88
|
+
componentDir_abs: '/Users/test/project/src/ui/components/shared'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = calculateComponentPaths(config);
|
|
93
|
+
const assetFallback = calculateAssetFallback(result.component_suffix);
|
|
94
|
+
|
|
95
|
+
console.log('nested result:', result);
|
|
96
|
+
console.log('nested asset fallback:', assetFallback);
|
|
97
|
+
|
|
98
|
+
// Expected: preserves nested structure
|
|
99
|
+
assert.strictEqual(result.component_dir, '.app/ssr/components/shared', 'component_dir should preserve nested path');
|
|
100
|
+
assert.strictEqual(result.component_suffix, 'components/shared', 'component_suffix should be components/shared');
|
|
101
|
+
assert.strictEqual(assetFallback, '.app/assets/components/shared', 'asset fallback should match nested structure');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Test the ACTUAL bug: verify that when we pass component_dir to client.js,
|
|
106
|
+
* we can extract the suffix without complex string manipulation
|
|
107
|
+
*/
|
|
108
|
+
test('verify simple suffix extraction', () => {
|
|
109
|
+
const testCases = [
|
|
110
|
+
{ component_dir: '.app/ssr/components', expected: 'components' },
|
|
111
|
+
{ component_dir: '.app/ssr', expected: null },
|
|
112
|
+
{ component_dir: '.app/ssr/components/shared', expected: 'components/shared' }
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
testCases.forEach(({ component_dir, expected }) => {
|
|
116
|
+
// This is what we should store in the config
|
|
117
|
+
const suffix = component_dir.replace('.app/ssr', '').replace(/^\//, '') || null;
|
|
118
|
+
|
|
119
|
+
console.log(`component_dir: ${component_dir} → suffix: ${suffix}`);
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(
|
|
122
|
+
suffix,
|
|
123
|
+
expected,
|
|
124
|
+
`component_dir ${component_dir} should extract suffix ${expected}`
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Integration test: Full pipeline from config → bootstrap → runtime
|
|
131
|
+
*/
|
|
132
|
+
test('full pipeline: markdown_view', () => {
|
|
133
|
+
// Step 1: Config input
|
|
134
|
+
const configInput = {
|
|
135
|
+
root: '/Users/shavauhngabay/dev/markdown_view',
|
|
136
|
+
client: {
|
|
137
|
+
main_abs: '/Users/shavauhngabay/dev/markdown_view/frontend/frontend.ts',
|
|
138
|
+
componentDir_abs: '/Users/shavauhngabay/dev/markdown_view/frontend/components'
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Step 2: Bootstrap calculation
|
|
143
|
+
const bootstrapResult = calculateComponentPaths(configInput);
|
|
144
|
+
|
|
145
|
+
// Step 3: Runtime usage (what client.js needs)
|
|
146
|
+
const runtimeAssetFallback = calculateAssetFallback(bootstrapResult.component_suffix);
|
|
147
|
+
|
|
148
|
+
console.log('=== Full Pipeline Test ===');
|
|
149
|
+
console.log('Config input:', configInput.client);
|
|
150
|
+
console.log('Bootstrap result:', bootstrapResult);
|
|
151
|
+
console.log('Runtime asset fallback:', runtimeAssetFallback);
|
|
152
|
+
|
|
153
|
+
// Verify the entire flow
|
|
154
|
+
assert.strictEqual(bootstrapResult.component_dir, '.app/ssr/components');
|
|
155
|
+
assert.strictEqual(bootstrapResult.component_suffix, 'components');
|
|
156
|
+
assert.strictEqual(runtimeAssetFallback, '.app/assets/components');
|
|
157
|
+
|
|
158
|
+
// This is what we need to mount in Express
|
|
159
|
+
console.log('\n✅ Express should mount:');
|
|
160
|
+
console.log(' Primary: .app/assets at /assets');
|
|
161
|
+
console.log(` Fallback: ${runtimeAssetFallback} at /assets`);
|
|
162
|
+
console.log('\n✅ This allows browser to request: /assets/layout/root.js');
|
|
163
|
+
console.log(` And Express finds it at: ${runtimeAssetFallback}/layout/root.js`);
|
|
164
|
+
});
|