@noego/app 0.0.7 → 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.
@@ -0,0 +1,211 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * TEST: Express Asset Mounting
7
+ *
8
+ * Validates that client.js correctly mounts assets based on bootstrap config.
9
+ * This test ensures:
10
+ * 1. We use assets_build_dir from bootstrap (not reconstruct paths)
11
+ * 2. Fallback mount is set up correctly
12
+ * 3. Express receives the correct directory paths
13
+ */
14
+
15
+ /**
16
+ * Mock the assets() function from client.js
17
+ * This simulates what Forge's assets() helper does
18
+ */
19
+ function mockExpressAssets(mountConfig) {
20
+ // Returns what Express would mount
21
+ const mounts = [];
22
+
23
+ for (const [route, dirs] of Object.entries(mountConfig)) {
24
+ if (Array.isArray(dirs)) {
25
+ dirs.forEach(dir => mounts.push({ route, dir }));
26
+ } else {
27
+ mounts.push({ route, dir: dirs });
28
+ }
29
+ }
30
+
31
+ return mounts;
32
+ }
33
+
34
+ /**
35
+ * Mock client.js asset mounting logic
36
+ */
37
+ function calculateAssetMounts(bootstrapConfig, mode) {
38
+ const mountConfig = mode === 'production' && bootstrapConfig.component_suffix
39
+ ? {
40
+ '/assets': [path.join('.app/assets', bootstrapConfig.component_suffix)]
41
+ }
42
+ : {};
43
+
44
+ return mockExpressAssets(mountConfig);
45
+ }
46
+
47
+ /**
48
+ * BETTER: Use assets_build_dir directly from bootstrap
49
+ */
50
+ function calculateAssetMountsBetter(bootstrapConfig, mode) {
51
+ const mountConfig = mode === 'production' && bootstrapConfig.assets_build_dir !== '.app/assets'
52
+ ? {
53
+ '/assets': [bootstrapConfig.assets_build_dir]
54
+ }
55
+ : {};
56
+
57
+ return mockExpressAssets(mountConfig);
58
+ }
59
+
60
+ /**
61
+ * Dual-mount strategy used by the runtime: mount at '/' and '/assets'
62
+ */
63
+ function calculateAssetMountsDual(bootstrapConfig, mode) {
64
+ if (mode !== 'production') return [];
65
+ const dir = bootstrapConfig.assets_build_dir || '.app/assets';
66
+ const mountConfig = {
67
+ '/': [dir],
68
+ '/assets': [dir]
69
+ };
70
+ return mockExpressAssets(mountConfig);
71
+ }
72
+
73
+ test('markdown_view: Express mounts fallback for nested components', () => {
74
+ const bootstrap = {
75
+ component_dir_ssr: '.app/ssr/components',
76
+ component_dir_client: '/assets/components',
77
+ component_base_path: '/assets',
78
+ assets_build_dir: '.app/assets/components',
79
+ component_suffix: 'components'
80
+ };
81
+
82
+ const mounts = calculateAssetMounts(bootstrap, 'production');
83
+
84
+ console.log('Express mounts:', mounts);
85
+
86
+ // Should mount fallback
87
+ assert.strictEqual(mounts.length, 1, 'Should have 1 fallback mount');
88
+ assert.strictEqual(mounts[0].route, '/assets');
89
+ assert.strictEqual(mounts[0].dir, '.app/assets/components');
90
+
91
+ console.log('✅ Express will serve .app/assets/components at /assets');
92
+ console.log(' Browser request: /assets/layout/root.js');
93
+ console.log(' Express serves from: .app/assets/components/layout/root.js');
94
+ });
95
+
96
+ test('liftlog: No fallback mount needed (components at root)', () => {
97
+ const bootstrap = {
98
+ component_dir_ssr: '.app/ssr',
99
+ component_dir_client: '/assets',
100
+ component_base_path: '/assets',
101
+ assets_build_dir: '.app/assets',
102
+ component_suffix: null
103
+ };
104
+
105
+ const mounts = calculateAssetMounts(bootstrap, 'production');
106
+
107
+ console.log('Express mounts:', mounts);
108
+
109
+ // No fallback needed since components at root
110
+ assert.strictEqual(mounts.length, 0, 'Should have no fallback mount');
111
+
112
+ console.log('✅ No fallback needed - components at asset root');
113
+ console.log(' Forge default mount handles everything');
114
+ });
115
+
116
+ test('Better approach: Use assets_build_dir directly', () => {
117
+ const bootstrap = {
118
+ component_dir_ssr: '.app/ssr/components',
119
+ component_dir_client: '/assets/components',
120
+ component_base_path: '/assets',
121
+ assets_build_dir: '.app/assets/components',
122
+ component_suffix: 'components'
123
+ };
124
+
125
+ const mounts = calculateAssetMountsBetter(bootstrap, 'production');
126
+
127
+ console.log('Better Express mounts:', mounts);
128
+
129
+ assert.strictEqual(mounts.length, 1);
130
+ assert.strictEqual(mounts[0].dir, '.app/assets/components',
131
+ 'Should use assets_build_dir directly, not reconstruct from component_suffix');
132
+
133
+ console.log('✅ Using assets_build_dir avoids path reconstruction');
134
+ });
135
+
136
+ test('Dual mount: serve assets at root and /assets', () => {
137
+ const bootstrap = {
138
+ assets_build_dir: '.app/assets/components'
139
+ };
140
+
141
+ const mounts = calculateAssetMountsDual(bootstrap, 'production');
142
+
143
+ // Expect two mounts
144
+ assert.strictEqual(mounts.length, 2);
145
+ const routes = mounts.map(m => m.route).sort();
146
+ assert.deepStrictEqual(routes, ['/', '/assets']);
147
+ mounts.forEach(m => assert.strictEqual(m.dir, '.app/assets/components'));
148
+
149
+ console.log('✅ Dual mount configured: "/" and "/assets" -> .app/assets/components');
150
+ });
151
+
152
+ test('Development mode: No fallback mounts', () => {
153
+ const bootstrap = {
154
+ component_dir_ssr: '.app/ssr/components',
155
+ component_dir_client: '.app/ssr/components',
156
+ component_base_path: '/assets',
157
+ assets_build_dir: '.app/assets/components',
158
+ component_suffix: 'components'
159
+ };
160
+
161
+ const mounts = calculateAssetMounts(bootstrap, 'development');
162
+
163
+ console.log('Dev mode mounts:', mounts);
164
+
165
+ assert.strictEqual(mounts.length, 0, 'Dev mode: no fallback mounts needed');
166
+
167
+ console.log('✅ Dev mode uses Vite dev server - no static mounts');
168
+ });
169
+
170
+ test('Verify client.js uses correct path construction', () => {
171
+ const bootstrap = {
172
+ component_suffix: 'components',
173
+ assets_build_dir: '.app/assets/components'
174
+ };
175
+
176
+ // Current approach (reconstructs path)
177
+ const currentPath = path.join('.app/assets', bootstrap.component_suffix);
178
+
179
+ // Better approach (uses precalculated)
180
+ const betterPath = bootstrap.assets_build_dir;
181
+
182
+ console.log('Current approach:', currentPath);
183
+ console.log('Better approach:', betterPath);
184
+
185
+ // They should produce same result
186
+ assert.strictEqual(currentPath, betterPath,
187
+ 'Both approaches should produce same path');
188
+
189
+ console.log('✅ Current implementation is correct (but could use assets_build_dir directly)');
190
+ console.log(' Recommendation: Change client.js to use config.client.assets_build_dir');
191
+ });
192
+
193
+ test('Edge case: Deeply nested components', () => {
194
+ const bootstrap = {
195
+ component_suffix: 'components/shared',
196
+ assets_build_dir: '.app/assets/components/shared'
197
+ };
198
+
199
+ const currentPath = path.join('.app/assets', bootstrap.component_suffix);
200
+ const betterPath = bootstrap.assets_build_dir;
201
+
202
+ assert.strictEqual(currentPath, betterPath);
203
+
204
+ const mounts = [{
205
+ route: '/assets',
206
+ dir: betterPath
207
+ }];
208
+
209
+ console.log('Deeply nested mount:', mounts[0]);
210
+ console.log('✅ Works correctly for nested paths too');
211
+ });
@@ -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
+ });