@zpress/core 0.2.1 → 0.4.0

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/dist/index.d.ts CHANGED
@@ -74,7 +74,7 @@ export declare interface CardConfig {
74
74
  */
75
75
  export declare interface ConfigError {
76
76
  readonly _tag: 'ConfigError';
77
- readonly type: 'empty_sections' | 'missing_field' | 'duplicate_prefix' | 'invalid_icon' | 'invalid_entry' | 'missing_nav_icon';
77
+ readonly type: 'empty_sections' | 'missing_field' | 'duplicate_prefix' | 'invalid_icon' | 'invalid_entry';
78
78
  readonly message: string;
79
79
  }
80
80
 
@@ -98,21 +98,15 @@ export declare type ConfigResult<T> = readonly [ConfigError, null] | readonly [n
98
98
  export declare function createPaths(dir: string): Paths;
99
99
 
100
100
  /**
101
- * Type-safe config helper with validation.
101
+ * Type-safe config helper for user config files.
102
102
  *
103
- * Validates the config structure and exits with a clear error message
104
- * if any issues are found. This is the primary entry point for user-
105
- * provided config validation happens at the boundary.
106
- *
107
- * `defineConfig` is called in user config files (e.g. `zpress.config.ts`)
108
- * where the return value is consumed by the c12 config loader, which
109
- * expects a plain `ZpressConfig` object — not a `Result` tuple.
110
- * Because this is the outermost user-facing boundary (not a library
111
- * function), `process.exit(1)` is acceptable here: the user must fix
112
- * their config before any downstream code can run.
103
+ * This is a passthrough that provides type safety and editor
104
+ * autocompletion in `zpress.config.ts`. Validation is deferred to
105
+ * `loadConfig` at CLI runtime, so errors surface with structured
106
+ * feedback rather than a raw `process.exit`.
113
107
  *
114
108
  * @param config - Raw zpress config object
115
- * @returns The validated config (unchanged)
109
+ * @returns The config (unchanged)
116
110
  */
117
111
  export declare function defineConfig(config: ZpressConfig): ZpressConfig;
118
112
 
@@ -252,9 +246,8 @@ export declare interface Entry {
252
246
  */
253
247
  readonly indexFile?: string;
254
248
  /**
255
- * Iconify icon identifier for sidebar icon rail display.
256
- * Format: `"prefix:name"` (e.g., `"pixelarticons:folder"`, `"devicon:typescript"`).
257
- * Only meaningful on top-level sections — ignored on leaf pages.
249
+ * Iconify icon identifier (e.g. `"pixelarticons:speed-fast"`).
250
+ * Used on home page feature cards when auto-generated from sections.
258
251
  */
259
252
  readonly icon?: string;
260
253
  /**
@@ -401,10 +394,14 @@ declare type GlobPattern = string;
401
394
  export declare function hasGlobChars(s: string): boolean;
402
395
 
403
396
  /**
404
- * Load zpress config at runtime via c12.
397
+ * Load and validate zpress config at runtime via c12.
405
398
  *
406
- * Returns a `ConfigResult` tuple instead of calling `process.exit` the
407
- * CLI boundary is responsible for surfacing the error and exiting.
399
+ * Returns a `ConfigResult` tuple the CLI boundary is responsible for
400
+ * surfacing any error and exiting. Validation runs here (not in
401
+ * `defineConfig`) so every consumer gets structured error feedback.
402
+ *
403
+ * @param dir - Repository root directory to search for `zpress.config.*`
404
+ * @returns A `ConfigResult` tuple — `[null, config]` on success or `[ConfigError, null]` on failure
408
405
  */
409
406
  export declare function loadConfig(dir: string): Promise<ConfigResult<ZpressConfig>>;
410
407
 
@@ -461,12 +458,6 @@ export declare interface NavItem {
461
458
  readonly link?: UrlPath;
462
459
  readonly items?: readonly NavItem[];
463
460
  readonly activeMatch?: string;
464
- /**
465
- * Iconify icon identifier for nav bar display.
466
- * Format: `"prefix:name"` (e.g., `"pixelarticons:folder"`, `"devicon:typescript"`).
467
- * Required on top-level nav items when nav is explicit (validated at config boundary).
468
- */
469
- readonly icon?: string;
470
461
  }
471
462
 
472
463
  /**
@@ -662,10 +653,6 @@ export declare interface SidebarItem {
662
653
  */
663
654
  readonly collapsed?: boolean;
664
655
  readonly items?: readonly SidebarItem[];
665
- /**
666
- * Iconify identifier for sidebar icon rail. Only present on top-level sections.
667
- */
668
- readonly icon?: string;
669
656
  }
670
657
 
671
658
  /**
@@ -768,6 +755,14 @@ export declare interface SyncResult {
768
755
  */
769
756
  declare type UrlPath = string;
770
757
 
758
+ /**
759
+ * Validate the entire config, returning the first error found.
760
+ *
761
+ * @param config - Raw zpress config object to validate
762
+ * @returns A `ConfigResult` tuple — `[null, config]` on success or `[ConfigError, null]` on failure
763
+ */
764
+ export declare function validateConfig(config: ZpressConfig): ConfigResult<ZpressConfig>;
765
+
771
766
  /**
772
767
  * A named group of workspace items for custom workspace categories.
773
768
  *
package/dist/index.mjs CHANGED
@@ -48,11 +48,6 @@ function collectResults(results) {
48
48
  ]);
49
49
  }
50
50
  function defineConfig(config) {
51
- const [err] = validateConfig(config);
52
- if (err) {
53
- process.stderr.write(`[zpress] ${err.message}\n`);
54
- process.exit(1);
55
- }
56
51
  return config;
57
52
  }
58
53
  function validateConfig(config) {
@@ -90,11 +85,6 @@ function validateConfig(config) {
90
85
  featErr,
91
86
  null
92
87
  ];
93
- const [navErr] = validateNav(config.nav);
94
- if (navErr) return [
95
- navErr,
96
- null
97
- ];
98
88
  return [
99
89
  null,
100
90
  config
@@ -227,29 +217,6 @@ function validateFeature(feature) {
227
217
  if (feature.icon && !feature.icon.includes(':')) return configError('invalid_icon', `Feature "${feature.text}": icon must be an Iconify identifier (e.g. "pixelarticons:speed-fast")`);
228
218
  return null;
229
219
  }
230
- function validateNav(nav) {
231
- if ('auto' === nav || void 0 === nav) return [
232
- null,
233
- true
234
- ];
235
- const navError = nav.reduce((acc, item)=>{
236
- if (acc) return acc;
237
- return validateNavItem(item);
238
- }, null);
239
- if (navError) return [
240
- navError,
241
- null
242
- ];
243
- return [
244
- null,
245
- true
246
- ];
247
- }
248
- function validateNavItem(item) {
249
- if (!item.icon) return configError('missing_nav_icon', `NavItem "${item.text}": top-level nav items require an "icon" (Iconify identifier)`);
250
- if (!item.icon.includes(':')) return configError('invalid_icon', `NavItem "${item.text}": icon must be an Iconify identifier (e.g. "pixelarticons:folder")`);
251
- return null;
252
- }
253
220
  async function config_loadConfig(dir) {
254
221
  const { config } = await loadConfig({
255
222
  cwd: dir,
@@ -263,10 +230,7 @@ async function config_loadConfig(dir) {
263
230
  configError('empty_sections', 'Failed to load zpress.config — no sections found'),
264
231
  null
265
232
  ];
266
- return [
267
- null,
268
- config
269
- ];
233
+ return validateConfig(config);
270
234
  }
271
235
  const FIGLET_CHARS = Object.freeze({
272
236
  A: [
@@ -1457,7 +1421,7 @@ function buildFeatures(sections, repoRoot) {
1457
1421
  return Promise.all(sections.slice(0, 3).map(async (section, index)=>{
1458
1422
  const link = section.link ?? findFirstChildLink(section);
1459
1423
  const details = await extractSectionDescription(section, repoRoot);
1460
- const iconId = match(section.icon).with(P.nonNullable, (id)=>id).otherwise(()=>null);
1424
+ const iconId = section.icon ?? null;
1461
1425
  const iconColor = ICON_COLORS[index % ICON_COLORS.length];
1462
1426
  return {
1463
1427
  title: section.text,
@@ -2168,13 +2132,12 @@ function deduplicateByLink(entries) {
2168
2132
  });
2169
2133
  return result;
2170
2134
  }
2171
- function buildSidebarEntry(entry, icon) {
2135
+ function buildSidebarEntry(entry) {
2172
2136
  if (entry.items && entry.items.length > 0) return {
2173
2137
  text: entry.text,
2174
2138
  items: generateSidebar(entry.items),
2175
2139
  ...maybeCollapsed(entry.collapsible),
2176
- ...maybeLink(entry.link),
2177
- ...maybeIcon(icon)
2140
+ ...maybeLink(entry.link)
2178
2141
  };
2179
2142
  if (null === entry.link || void 0 === entry.link) {
2180
2143
  log.error(`[zpress] Leaf entry "${entry.text}" has no link — skipping`);
@@ -2184,33 +2147,28 @@ function buildSidebarEntry(entry, icon) {
2184
2147
  }
2185
2148
  return {
2186
2149
  text: entry.text,
2187
- link: entry.link,
2188
- ...maybeIcon(icon)
2150
+ link: entry.link
2189
2151
  };
2190
2152
  }
2191
- function generateSidebar(entries, icons) {
2153
+ function generateSidebar(entries) {
2192
2154
  const visible = entries.filter((e)=>!e.hidden);
2193
2155
  const pages = visible.filter((e)=>!e.items || 0 === e.items.length);
2194
2156
  const sections = visible.filter((e)=>e.items && e.items.length > 0);
2195
2157
  return [
2196
2158
  ...pages,
2197
2159
  ...sections
2198
- ].map((entry)=>{
2199
- const icon = resolveIcon(icons, entry.text);
2200
- return buildSidebarEntry(entry, icon);
2201
- });
2160
+ ].map(buildSidebarEntry);
2202
2161
  }
2203
- function buildNavEntry(entry, icon) {
2162
+ function buildNavEntry(entry) {
2204
2163
  const link = sidebar_resolveLink(entry);
2205
2164
  const children = resolveChildren(entry);
2206
2165
  return {
2207
2166
  text: entry.text,
2208
2167
  link,
2209
- ...maybeIcon(icon),
2210
2168
  ...maybeChildren(children)
2211
2169
  };
2212
2170
  }
2213
- function generateNav(config, resolved, icons) {
2171
+ function generateNav(config, resolved) {
2214
2172
  if ('auto' !== config.nav && void 0 !== config.nav) return [
2215
2173
  ...config.nav
2216
2174
  ];
@@ -2220,7 +2178,7 @@ function generateNav(config, resolved, icons) {
2220
2178
  return [
2221
2179
  ...nonIsolated,
2222
2180
  ...isolated
2223
- ].map((entry)=>buildNavEntry(entry, icons.get(entry.text))).filter((item)=>void 0 !== item.link);
2181
+ ].map(buildNavEntry).filter((item)=>void 0 !== item.link);
2224
2182
  }
2225
2183
  function sidebar_findFirstLink(entry) {
2226
2184
  if (entry.link) return entry.link;
@@ -2241,15 +2199,6 @@ function maybeLink(link) {
2241
2199
  };
2242
2200
  return {};
2243
2201
  }
2244
- function maybeIcon(icon) {
2245
- if (icon) return {
2246
- icon
2247
- };
2248
- return {};
2249
- }
2250
- function resolveIcon(icons, text) {
2251
- if (icons) return icons.get(text);
2252
- }
2253
2202
  function sidebar_resolveLink(entry) {
2254
2203
  if (entry.link) return entry.link;
2255
2204
  return sidebar_findFirstLink(entry);
@@ -2379,10 +2328,10 @@ function resolveTags(tags) {
2379
2328
  ...tags
2380
2329
  ];
2381
2330
  }
2382
- function buildMultiSidebar(resolved, openapiSidebar, icons) {
2331
+ function buildMultiSidebar(resolved, openapiSidebar) {
2383
2332
  const rootEntries = resolved.filter((e)=>!e.isolated);
2384
2333
  const isolatedEntries = resolved.filter((e)=>e.isolated && e.link);
2385
- const docsSidebar = generateSidebar(rootEntries, icons);
2334
+ const docsSidebar = generateSidebar(rootEntries);
2386
2335
  const childrenByLink = new Map(isolatedEntries.map((entry)=>{
2387
2336
  const link = entry.link;
2388
2337
  const items = resolveEntryItems(entry.items);
@@ -2394,23 +2343,19 @@ function buildMultiSidebar(resolved, openapiSidebar, icons) {
2394
2343
  const isolatedSidebar = Object.fromEntries(isolatedEntries.flatMap((entry)=>{
2395
2344
  const entryLink = entry.link;
2396
2345
  const children = resolveChildrenByLink(childrenByLink, entryLink);
2397
- const icon = icons.get(entry.text);
2398
2346
  const parentLink = resolveParentLink(entryLink);
2399
2347
  const parentEntry = match(parentLink).with(P.nonNullable, (pl)=>isolatedEntries.find((e)=>e.link === pl)).otherwise(()=>{});
2400
2348
  const isChild = null != parentEntry && parentEntry !== entry;
2401
2349
  const landing = {
2402
2350
  text: entry.text,
2403
- link: entryLink,
2404
- ...multi_maybeIcon(icon)
2351
+ link: entryLink
2405
2352
  };
2406
2353
  const sidebarItems = match(isChild).with(true, ()=>{
2407
2354
  const pe = parentEntry;
2408
2355
  const peLink = pe.link;
2409
- const parentIcon = icons.get(pe.text);
2410
2356
  const parentLanding = {
2411
2357
  text: pe.text,
2412
- link: peLink,
2413
- ...multi_maybeIcon(parentIcon)
2358
+ link: peLink
2414
2359
  };
2415
2360
  const siblings = isolatedEntries.filter((sib)=>{
2416
2361
  const sibLink = sib.link;
@@ -2480,12 +2425,6 @@ function resolveParentLink(entryLink) {
2480
2425
  if (segments) return segments;
2481
2426
  return null;
2482
2427
  }
2483
- function multi_maybeIcon(icon) {
2484
- if (icon) return {
2485
- icon
2486
- };
2487
- return {};
2488
- }
2489
2428
  function buildSidebarGroup(text, link, children, collapsed) {
2490
2429
  if (children.length > 0) return {
2491
2430
  text,
@@ -2522,7 +2461,6 @@ function synthesizeWorkspaceSections(config) {
2522
2461
  text: 'Apps',
2523
2462
  link: '/apps',
2524
2463
  isolated: true,
2525
- icon: 'pixelarticons:device-laptop',
2526
2464
  frontmatter: {
2527
2465
  description: 'Deployable applications that make up the platform.'
2528
2466
  },
@@ -2532,7 +2470,6 @@ function synthesizeWorkspaceSections(config) {
2532
2470
  text: 'Packages',
2533
2471
  link: '/packages',
2534
2472
  isolated: true,
2535
- icon: 'pixelarticons:archive',
2536
2473
  frontmatter: {
2537
2474
  description: 'Shared libraries and utilities consumed across apps and services.'
2538
2475
  },
@@ -2545,7 +2482,6 @@ function synthesizeWorkspaceSections(config) {
2545
2482
  text: group.name,
2546
2483
  link,
2547
2484
  isolated: true,
2548
- icon: group.icon,
2549
2485
  frontmatter: {
2550
2486
  description: group.description
2551
2487
  },
@@ -2766,9 +2702,8 @@ async function sync(config, options) {
2766
2702
  skipped: 0
2767
2703
  }));
2768
2704
  const removed = await match(previousManifest).with(P.nonNullable, async (m)=>await cleanStaleFiles(outDir, m, ctx.manifest)).otherwise(()=>Promise.resolve(0));
2769
- const icons = buildIconMap(allSections);
2770
- const sortedSidebar = buildMultiSidebar(resolved, openapiSidebar, icons);
2771
- const nav = generateNav(config, resolved, icons);
2705
+ const sortedSidebar = buildMultiSidebar(resolved, openapiSidebar);
2706
+ const nav = generateNav(config, resolved);
2772
2707
  await promises.writeFile(node_path.resolve(outDir, '.generated/sidebar.json'), JSON.stringify(sortedSidebar, null, 2), 'utf8');
2773
2708
  await promises.writeFile(node_path.resolve(outDir, '.generated/nav.json'), JSON.stringify(nav, null, 2), 'utf8');
2774
2709
  await saveManifest(outDir, ctx.manifest);
@@ -2864,17 +2799,6 @@ async function copyAll(src, dest) {
2864
2799
  else await promises.copyFile(srcPath, destPath);
2865
2800
  }, Promise.resolve());
2866
2801
  }
2867
- function buildIconMap(sections) {
2868
- return new Map(sections.flatMap((section)=>{
2869
- if (section.icon) return [
2870
- [
2871
- section.text,
2872
- section.icon
2873
- ]
2874
- ];
2875
- return [];
2876
- }));
2877
- }
2878
2802
  function resolveQuiet(quiet) {
2879
2803
  if (null != quiet) return quiet;
2880
2804
  return false;
@@ -2907,4 +2831,4 @@ function createPaths(dir) {
2907
2831
  cacheDir: node_path.resolve(outputRoot, 'cache')
2908
2832
  };
2909
2833
  }
2910
- export { configError, config_loadConfig as loadConfig, createPaths, defineConfig, generateAssets, generateBannerSvg, generateIconSvg, generateLogoSvg, hasGlobChars, loadManifest, resolveEntries, sync, syncError };
2834
+ export { configError, config_loadConfig as loadConfig, createPaths, defineConfig, generateAssets, generateBannerSvg, generateIconSvg, generateLogoSvg, hasGlobChars, loadManifest, resolveEntries, sync, syncError, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zpress/core",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Config loading, sync engine, and asset utilities for zpress",
5
5
  "keywords": [
6
6
  "config",