@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.
@@ -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
+ });