Skip to content

Commit abb072a

Browse files
committed
fix(cpp.js): make config list output readable
`config list -t project` passed the fully-resolved, deeply-nested config object straight to console.table, which created one column per nested sub-key — an unreadable 60+ column table. Flatten it into a two-column dotted-key view instead: paths shown relative to base, long values middle-truncated, dependency arrays shown as names, and large per-target maps (allDependencyPaths) collapsed to their keys.
1 parent 2ae3d7c commit abb072a

3 files changed

Lines changed: 175 additions & 2 deletions

File tree

cppjs-core/cpp.js/src/bin.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getBuildTargets, getFilteredBuildTargets, getFilteredTargetSpec } from
1515

1616
import downloadAndExtractFile from './utils/downloadAndExtractFile.js';
1717
import writeJson from './utils/writeJson.js';
18+
import flattenConfigForTable from './utils/flattenConfigForTable.js';
1819
import systemKeys from './utils/systemKeys.js';
1920
import logger from './utils/logger.js';
2021
import { getDockerImage } from './utils/pullDockerImage.js';
@@ -197,8 +198,8 @@ function listSystemConfig(type) {
197198
}
198199

199200
if (type === 'all' || type === 'project') {
200-
console.log('Project Configuration');
201-
console.table(projectConfig);
201+
console.log(`Project Configuration (paths relative to ${projectConfig.paths.base})`);
202+
console.table(flattenConfigForTable(projectConfig, { base: projectConfig.paths.base }));
202203
}
203204
}
204205

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
2+
3+
// Object maps with at least this many object-valued entries (e.g. allDependencyPaths,
4+
// keyed per target) collapse to their key list instead of exploding into rows.
5+
const LARGE_MAP_MIN_KEYS = 8;
6+
7+
function relativizePath(value, base) {
8+
if (base && typeof value === 'string' && value.startsWith(`${base}/`)) {
9+
return value.slice(base.length + 1);
10+
}
11+
return value;
12+
}
13+
14+
function truncateMiddle(value, max) {
15+
if (typeof value !== 'string' || value.length <= max) return value;
16+
const head = Math.ceil((max - 1) / 2);
17+
const tail = Math.floor((max - 1) / 2);
18+
return `${value.slice(0, head)}${value.slice(value.length - tail)}`;
19+
}
20+
21+
function formatScalar(value, base, maxLen) {
22+
if (typeof value === 'function') return '[Function]';
23+
if (typeof value === 'string') return truncateMiddle(relativizePath(value, base), maxLen);
24+
return value;
25+
}
26+
27+
// Flattens a resolved cpp.js config into a { 'dotted.key': displayValue } map so
28+
// console.table renders a readable two-column view instead of one column per
29+
// nested sub-key. Paths are shown relative to base and long values truncated in
30+
// the middle; dependency arrays collapse to names, large per-target maps collapse
31+
// to their keys, and other structures recurse with dotted/indexed keys.
32+
export default function flattenConfigForTable(config, { base = null, maxDepth = 5, maxValueLength = 80 } = {}) {
33+
const out = {};
34+
35+
const walk = (value, prefix, depth) => {
36+
if (Array.isArray(value)) {
37+
if (value.length === 0) {
38+
out[prefix] = '[]';
39+
} else if (value.every((v) => v === null || typeof v !== 'object')) {
40+
out[prefix] = truncateMiddle(value.map((v) => relativizePath(v, base)).join(', '), maxValueLength);
41+
} else {
42+
const names = value.map((v) => v?.general?.name ?? v?.package?.name).filter(Boolean);
43+
if (names.length === value.length) {
44+
out[prefix] = names.join(', ');
45+
} else if (depth <= 0) {
46+
out[prefix] = `<${value.length} items>`;
47+
} else {
48+
value.forEach((v, i) => walk(v, `${prefix}.${i}`, depth - 1));
49+
}
50+
}
51+
return;
52+
}
53+
54+
if (!isPlainObject(value)) {
55+
out[prefix] = formatScalar(value, base, maxValueLength);
56+
return;
57+
}
58+
59+
const keys = Object.keys(value);
60+
const isLargeObjectMap = prefix !== '' && keys.length >= LARGE_MAP_MIN_KEYS
61+
&& keys.every((k) => isPlainObject(value[k]));
62+
if (keys.length === 0) {
63+
out[prefix] = '{}';
64+
} else if (isLargeObjectMap) {
65+
out[prefix] = truncateMiddle(keys.join(', '), maxValueLength);
66+
} else if (depth <= 0) {
67+
out[prefix] = '{…}';
68+
} else {
69+
keys.forEach((k) => walk(value[k], prefix ? `${prefix}.${k}` : k, depth - 1));
70+
}
71+
};
72+
73+
walk(config, '', maxDepth);
74+
return out;
75+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { describe, test, expect } from 'vitest';
2+
import flattenConfigForTable from '../src/utils/flattenConfigForTable.js';
3+
4+
describe('flattenConfigForTable', () => {
5+
test('flattens nested objects into dotted keys', () => {
6+
const out = flattenConfigForTable({ general: { name: 'app', alias: { package: 'gdal3.js' } } });
7+
expect(out['general.name']).toBe('app');
8+
expect(out['general.alias.package']).toBe('gdal3.js');
9+
});
10+
11+
test('joins arrays of primitives inline', () => {
12+
const out = flattenConfigForTable({ ext: { header: ['h', 'hpp', 'hxx'] } });
13+
expect(out['ext.header']).toBe('h, hpp, hxx');
14+
});
15+
16+
test('collapses arrays of dependency objects to their names', () => {
17+
const out = flattenConfigForTable({
18+
allDependencies: [{ general: { name: 'gdal' } }, { package: { name: '@cpp.js/package-proj' } }],
19+
});
20+
expect(out.allDependencies).toBe('gdal, @cpp.js/package-proj');
21+
});
22+
23+
test('recurses arrays of nameless objects into indexed keys', () => {
24+
const out = flattenConfigForTable({ targetSpecs: [{ specs: { env: { FOO: '1' } } }] });
25+
expect(out['targetSpecs.0.specs.env.FOO']).toBe('1');
26+
});
27+
28+
test('summarizes nameless object arrays once depth is exhausted', () => {
29+
const out = flattenConfigForTable({ targetSpecs: [{ specs: {} }, { specs: {} }] }, { maxDepth: 1 });
30+
expect(out.targetSpecs).toBe('<2 items>');
31+
});
32+
33+
test('renders functions as [Function]', () => {
34+
const out = flattenConfigForTable({ functions: { isEnabled: () => true } });
35+
expect(out['functions.isEnabled']).toBe('[Function]');
36+
});
37+
38+
test('shows keys for large maps of objects instead of expanding them', () => {
39+
const targets = {};
40+
for (let i = 0; i < 10; i += 1) targets[`t${i}`] = { gdal: { root: '/x' } };
41+
const out = flattenConfigForTable({ allDependencyPaths: targets });
42+
expect(out.allDependencyPaths).toContain('t0');
43+
expect(out['allDependencyPaths.t0']).toBeUndefined();
44+
expect(out['allDependencyPaths.t0.gdal']).toBeUndefined();
45+
});
46+
47+
test('recurses small maps of objects to show their contents', () => {
48+
const out = flattenConfigForTable({ specs: { env: { FOO: '1', BAR: '2' } } });
49+
expect(out['specs.env.FOO']).toBe('1');
50+
expect(out['specs.env.BAR']).toBe('2');
51+
});
52+
53+
test('caps nesting depth to keep output terse', () => {
54+
const out = flattenConfigForTable({ a: { b: { c: { d: 1 } } } }, { maxDepth: 2 });
55+
expect(out['a.b']).toBe('{…}');
56+
expect(out['a.b.c']).toBeUndefined();
57+
});
58+
59+
test('marks empty objects and arrays', () => {
60+
const out = flattenConfigForTable({ build: {}, binHeaders: [] });
61+
expect(out.build).toBe('{}');
62+
expect(out.binHeaders).toBe('[]');
63+
});
64+
65+
test('relativizes absolute paths under base, leaving outside paths absolute', () => {
66+
const out = flattenConfigForTable(
67+
{ paths: { native: '/repo/packages/core/native', systemConfig: '/home/u/.cppjs.json' } },
68+
{ base: '/repo' },
69+
);
70+
expect(out['paths.native']).toBe('packages/core/native');
71+
expect(out['paths.systemConfig']).toBe('/home/u/.cppjs.json');
72+
});
73+
74+
test('relativizes each entry of an absolute-path array (globs)', () => {
75+
const out = flattenConfigForTable(
76+
{ dependencyParameters: { nativeGlob: ['/repo/src/*.c', '/repo/src/*.cpp'] } },
77+
{ base: '/repo' },
78+
);
79+
expect(out['dependencyParameters.nativeGlob']).toBe('src/*.c, src/*.cpp');
80+
});
81+
82+
test('truncates long values in the middle, preserving head and tail', () => {
83+
const long = `/repo/${'x'.repeat(100)}/CMakeLists.txt`;
84+
const out = flattenConfigForTable({ paths: { cmake: long } }, { base: '/repo', maxValueLength: 40 });
85+
const value = out['paths.cmake'];
86+
expect(value.length).toBe(40);
87+
expect(value).toContain('…');
88+
expect(value.endsWith('CMakeLists.txt')).toBe(true);
89+
});
90+
91+
test('never truncates the dependency name list', () => {
92+
const deps = Array.from({ length: 20 }, (_, i) => ({ general: { name: `dep${i}` } }));
93+
const out = flattenConfigForTable({ allDependencies: deps }, { maxValueLength: 20 });
94+
expect(out.allDependencies).toContain('dep19');
95+
expect(out.allDependencies).not.toContain('…');
96+
});
97+
});

0 commit comments

Comments
 (0)