@hkdigital/lib-core 0.5.67 → 0.5.68

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/README.md CHANGED
@@ -289,11 +289,32 @@ node node_modules/@hkdigital/lib-core/scripts/validate-imports.mjs
289
289
  **Validation rules (enforced for `src/lib/` files only):**
290
290
 
291
291
  1. **Cross-domain imports** - Use `$lib/` instead of `../../../`
292
- 2. **Parent index.js imports** - Use `$lib/` or import specific files
293
- 3. **Non-standard extensions** - Include full extension (e.g.,
292
+ 2. **Prefer barrel exports** - Use higher-level export files when available
293
+ 3. **Parent index.js imports** - Use `$lib/` or import specific files
294
+ 4. **Non-standard extensions** - Include full extension (e.g.,
294
295
  `.svelte.js`)
295
- 4. **Directory imports** - Write explicitly (e.g., `./path/index.js`)
296
- 5. **File existence** - All import paths must resolve to existing files
296
+ 5. **Directory imports** - Write explicitly or create barrel export file
297
+ 6. **File existence** - All import paths must resolve to existing files
298
+
299
+ **Barrel export preference:**
300
+
301
+ The validator suggests using the highest-level barrel export file that
302
+ exports your target. This encourages shorter imports that can be
303
+ combined:
304
+
305
+ ```js
306
+ // Instead of deep imports:
307
+ import ProfileBlocks from '$lib/ui/components/profile-blocks/ProfileBlocks.svelte';
308
+ import Button from '$lib/ui/primitives/buttons/Button.svelte';
309
+
310
+ // Use barrel exports:
311
+ import { ProfileBlocks } from '$lib/ui/components.js';
312
+ import { Button } from '$lib/ui/primitives.js';
313
+ ```
314
+
315
+ The validator checks from highest to lowest level (`$lib/ui.js` →
316
+ `$lib/ui/components.js` → `$lib/ui/components/profile-blocks.js`) and
317
+ suggests the highest-level file that exports your target.
297
318
 
298
319
  **Routes are exempt from strict rules:**
299
320
 
@@ -305,9 +326,13 @@ files.
305
326
  **Example output:**
306
327
 
307
328
  ```
308
- src/lib/ui/components/Button.svelte:7
309
- from '$lib/ui/primitives/buttons'
310
- => from '$lib/ui/primitives/buttons/index.js'
329
+ src/lib/ui/panels/Panel.svelte:6
330
+ from '../../../components/profile-blocks/ProfileBlocks.svelte'
331
+ => from '$lib/ui/components.js' (use barrel export)
332
+
333
+ src/lib/ui/pages/Profile.svelte:8
334
+ from '$lib/ui/components/profile-blocks/ProfileBlocks.svelte'
335
+ => from '$lib/ui/components.js' (use barrel export for shorter imports)
311
336
 
312
337
  src/routes/explorer/[...path]/+page.svelte:4
313
338
  from '../components/index.js'
package/claude.md CHANGED
@@ -112,18 +112,24 @@ This is a modern SvelteKit library built with Svelte 5 and Skeleton.dev v3 compo
112
112
  - Use `$lib/domain/...` imports for cross-domain references (e.g., `$lib/media/image.js`, `$lib/network/http.js`)
113
113
  - Use relative imports (`./` or `../`) when staying within the same main folder under `$lib`
114
114
  - **Always include file extensions** (`.js`, `.svelte`) in import statements
115
+ - **Prefer barrel exports** - Use higher-level export files when available (e.g., `$lib/ui/components.js` instead of `$lib/ui/components/profile/ProfileBlocks.svelte`)
115
116
  - **For cross-domain imports, use specific export files** (e.g., `parsers.js`, `valibot.js`) rather than directory-only paths - this ensures compatibility outside the library
116
117
  - **For local imports within the same domain**, import specific files directly (e.g., `./ClassName.js`) rather than using local index files
117
118
  - Examples:
119
+ - ✅ `import { ProfileBlocks } from '$lib/ui/components.js'` (barrel export - preferred)
120
+ - ✅ `import { Button } from '$lib/ui/primitives.js'` (barrel export - preferred)
118
121
  - ✅ `import { ImageLoader } from '$lib/media/image.js'` (cross-domain import)
119
122
  - ✅ `import ImageLoader from './ImageLoader.svelte.js'` (local import within same domain)
120
123
  - ✅ `import IterableTree from './IterableTree.js'` (local import within same domain)
121
124
  - ✅ `import { v } from '$lib/valibot/valibot.js'` (cross-domain with specific export file)
122
125
  - ✅ `import { HumanUrl, Email } from '$lib/valibot/parsers.js'` (cross-domain with specific export file)
126
+ - ❌ `import ProfileBlocks from '$lib/ui/components/profile/ProfileBlocks.svelte'` (deep import when barrel exists)
123
127
  - ❌ `import { v, HumanUrl } from '$lib/valibot'` (missing specific export file)
124
128
  - ❌ `import { IterableTree } from './index.js'` (local index when specific file should be used)
125
129
  - ❌ `import something from '../../media/image.js'` (cross-domain relative import)
126
130
 
131
+ **Import validation:** Run `node scripts/validate-imports.mjs` to validate import patterns. The validator checks for barrel export files at each level and suggests the highest-level file that exports your target. These rules are enforced for `src/lib/` files only. Files in `src/routes/` can use relative imports freely. See README.md "Import Validation" section for usage in other projects.
132
+
127
133
  ## Class Export Conventions
128
134
  - **All classes should be default exports**: `export default class ClassName`
129
135
  - **Import classes without destructuring**: `import ClassName from './ClassName.js'`
@@ -1,5 +1,4 @@
1
- import MemoryResponseCache from '../cache/MemoryResponseCache.js';
2
- import IndexedDbCache from '../cache/IndexedDbCache.js';
1
+ import { MemoryResponseCache, IndexedDbCache } from '../cache.js';
3
2
 
4
3
  import { browser } from '$app/environment';
5
4
 
@@ -53,4 +53,4 @@ export default class FiniteStateMachine extends EventEmitter {
53
53
  export type TransitionData = import("./typedef.js").TransitionData;
54
54
  export type OnEnterCallback = import("./typedef.js").OnEnterCallback;
55
55
  export type OnExitCallback = import("./typedef.js").OnExitCallback;
56
- import EventEmitter from '../../../generic/events/classes/EventEmitter.js';
56
+ import { EventEmitter } from '../../../generic/events.js';
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { isTestEnv } from '../../../util/env.js';
8
- import EventEmitter from '../../../generic/events/classes/EventEmitter.js';
8
+ import { EventEmitter } from '../../../generic/events.js';
9
9
  import { ENTER, EXIT } from './constants.js';
10
10
 
11
11
  /** @typedef {import('./typedef.js').TransitionData} TransitionData */
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { onMount } from 'svelte';
9
- import { findFirst } from '../../../util/array/index.js';
9
+ import { findFirst } from '../../../util/array.js';
10
10
 
11
11
  /**
12
12
  * @type {{
@@ -20,7 +20,7 @@
20
20
  * </AppLayout>
21
21
  */
22
22
 
23
- import { useResizeObserver } from '../../../util/svelte/observe/index.js';
23
+ import { useResizeObserver } from '../../../util/svelte.js';
24
24
 
25
25
  /**
26
26
  * @typedef AppLayoutProps
@@ -1,7 +1,6 @@
1
1
  <script>
2
- import { ImageLoader } from '../../../network/loaders.js';
3
- import { ImageVariantsLoader } from '../../../network/loaders.js';
4
- import { toSingleImageMeta } from '../../../network/loaders/image/utils/index.js';
2
+ import { ImageLoader, ImageVariantsLoader } from '../../../network/loaders.js';
3
+ import { toSingleImageMeta } from '../../../network/loaders/image.js';
5
4
 
6
5
  /**
7
6
  * @type {{
@@ -1,8 +1,6 @@
1
- import { tick } from 'svelte';
1
+ import { tick, untrack } from 'svelte';
2
2
 
3
- import { findFirst } from '../../../util/array/index.js';
4
-
5
- import { untrack } from 'svelte';
3
+ import { findFirst } from '../../../util/array.js';
6
4
 
7
5
  import { HkPromise } from '../../../generic/promises.js';
8
6
 
@@ -2,7 +2,7 @@ import { tick } from 'svelte';
2
2
 
3
3
  import * as expect from '../../../util/expect.js';
4
4
 
5
- import { pushNotEmpty } from '../../../util/array/index.js';
5
+ import { pushNotEmpty } from '../../../util/array.js';
6
6
 
7
7
  import { TRANSITION_CSS, FADE_IN, FADE_OUT } from './constants.js';
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { defineStateContext } from '../../../state/context.js';
2
2
 
3
- import { useResizeObserver } from '../../../util/svelte/observe/index.js';
3
+ import { useResizeObserver } from '../../../util/svelte.js';
4
4
 
5
5
  /* ------------------------------------------------------- Define state class */
6
6
 
@@ -1,7 +1,7 @@
1
1
  <script>
2
2
  import Button from '../button/Button.svelte';
3
3
 
4
- import { SteezeIcon } from '../../icons/index.js';
4
+ import { SteezeIcon } from '../../../primitives.js';
5
5
 
6
6
  /**
7
7
  * @type {{
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Star, ExclamationTriangle, CheckCircle } from '@steeze-ui/heroicons';
6
6
 
7
- import { HkIcon } from '../../icons/index.js';
7
+ import { HkIcon } from '../../../primitives.js';
8
8
 
9
9
  import {
10
10
  PRISTINE,
@@ -1,4 +1,4 @@
1
- import { PATH_SEPARATOR } from '../object/index.js';
1
+ import { PATH_SEPARATOR } from '../object.js';
2
2
 
3
3
  /**
4
4
  * Convert a path string to an array path
@@ -1,6 +1,6 @@
1
1
  import * as expect from '../expect.js';
2
2
 
3
- import { toArrayPath } from '../array/index.js';
3
+ import { toArrayPath } from '../array.js';
4
4
 
5
5
  import { objectGet, PATH_SEPARATOR } from '../object.js';
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.5.67",
3
+ "version": "0.5.68",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"
@@ -31,6 +31,66 @@ async function findFiles(dir) {
31
31
  return files;
32
32
  }
33
33
 
34
+ /**
35
+ * Find highest-level barrel export file that exports the target
36
+ *
37
+ * For a path like $lib/ui/components/profile-blocks/ProfileBlocks.svelte:
38
+ * - Check $lib/ui.js
39
+ * - Check $lib/ui/components.js
40
+ * - Check $lib/ui/components/profile-blocks.js
41
+ *
42
+ * @param {string} importPath - Import path (e.g., $lib/ui/components/...)
43
+ * @param {string} filePath - Source file making the import
44
+ *
45
+ * @returns {Promise<string|null>} Barrel file path or null
46
+ */
47
+ async function findBarrelExportFile(importPath, filePath) {
48
+ if (!importPath.startsWith('$lib/')) return null;
49
+
50
+ // Remove $lib/ prefix and extract path segments
51
+ const pathWithoutLib = importPath.replace('$lib/', '');
52
+ const parts = pathWithoutLib.split('/');
53
+
54
+ // Extract the target filename (last part, possibly with extension)
55
+ const targetFile = parts[parts.length - 1];
56
+ const targetBase = targetFile.replace(/\.(js|svelte)$/, '');
57
+
58
+ // Check from highest level down (but skip the filename itself)
59
+ for (let i = 1; i < parts.length; i++) {
60
+ const barrelPath = '$lib/' + parts.slice(0, i).join('/') + '.js';
61
+ const fsBarrelPath = join(
62
+ PROJECT_ROOT,
63
+ barrelPath.replace('$lib/', 'src/lib/')
64
+ );
65
+
66
+ try {
67
+ const stats = await stat(fsBarrelPath);
68
+ if (stats.isFile()) {
69
+ // Check if this barrel file exports our target
70
+ const content = await readFile(fsBarrelPath, 'utf-8');
71
+
72
+ // Look for exports that match the target file
73
+ // Patterns:
74
+ // export { ProfileBlocks } from './path/ProfileBlocks.svelte';
75
+ // export * from './path/ProfileBlocks.svelte';
76
+ // More flexible pattern to catch various export styles
77
+ const exportPattern = new RegExp(
78
+ `export\\s+(?:\\*|\\{[^}]*\\})\\s+from\\s+['"](?:.*/)?(${targetBase}(?:\\.(?:js|svelte))?)['"]`,
79
+ 'gm'
80
+ );
81
+
82
+ if (exportPattern.test(content)) {
83
+ return barrelPath;
84
+ }
85
+ }
86
+ } catch {
87
+ // File doesn't exist or can't be read, continue
88
+ }
89
+ }
90
+
91
+ return null;
92
+ }
93
+
34
94
  /**
35
95
  * Check if import path resolves to directory with index.js
36
96
  *
@@ -129,11 +189,29 @@ async function validateFile(filePath) {
129
189
  // Check 1: Cross-domain relative imports (3+ levels up)
130
190
  // Only enforce for lib files
131
191
  if (isInLib && importPath.match(/^\.\.\/\.\.\/\.\.\//)) {
132
- errors.push(
133
- `${relativePath}:${lineNum}\n` +
134
- ` from '${importPath}'\n` +
135
- ` => Use $lib/ for cross-domain imports`
192
+ // Convert relative path to $lib/ path
193
+ const fsPath = resolve(dirname(filePath), importPath);
194
+ const libPath = fsPath.replace(
195
+ join(PROJECT_ROOT, 'src/lib/'),
196
+ '$lib/'
136
197
  );
198
+
199
+ // Check for barrel export files
200
+ const barrelFile = await findBarrelExportFile(libPath, filePath);
201
+
202
+ if (barrelFile) {
203
+ errors.push(
204
+ `${relativePath}:${lineNum}\n` +
205
+ ` from '${importPath}'\n` +
206
+ ` => from '${barrelFile}' (use barrel export)`
207
+ );
208
+ } else {
209
+ errors.push(
210
+ `${relativePath}:${lineNum}\n` +
211
+ ` from '${importPath}'\n` +
212
+ ` => from '${libPath}'`
213
+ );
214
+ }
137
215
  continue;
138
216
  }
139
217
 
@@ -323,7 +401,52 @@ async function validateFile(filePath) {
323
401
  }
324
402
  }
325
403
 
326
- // Check 5: File existence (after all other checks)
404
+ // Check 5: Suggest barrel exports for deep $lib/ imports
405
+ // Only for specific cases, not for named export files
406
+ if (isInLib && importPath.startsWith('$lib/')) {
407
+ const pathWithoutLib = importPath.replace('$lib/', '');
408
+ const segments = pathWithoutLib.split('/');
409
+
410
+ // Only suggest barrel exports for:
411
+ // 1. Directory imports (no extension, resolves to index.js)
412
+ // 2. Explicit index.js imports
413
+ // 3. Deep component/class files (.svelte or capitalized .js files)
414
+ let shouldCheckBarrel = false;
415
+
416
+ if (segments.length >= 3) {
417
+ const lastSegment = segments[segments.length - 1];
418
+
419
+ // Case 1: Explicit index.js import
420
+ if (lastSegment === 'index.js') {
421
+ shouldCheckBarrel = true;
422
+ }
423
+ // Case 2: Directory import (will be caught by Check 4)
424
+ // Case 3: Component or class file (.svelte or capitalized)
425
+ else if (lastSegment.endsWith('.svelte')) {
426
+ shouldCheckBarrel = true;
427
+ } else if (lastSegment.match(/^[A-Z][^/]*\.js$/)) {
428
+ // Capitalized .js file (likely a class)
429
+ shouldCheckBarrel = true;
430
+ }
431
+ // Skip named export files like methods.js, http.js, etc.
432
+ // These are lowercase and ARE the public API
433
+ }
434
+
435
+ if (shouldCheckBarrel) {
436
+ const barrelFile = await findBarrelExportFile(importPath, filePath);
437
+
438
+ if (barrelFile) {
439
+ errors.push(
440
+ `${relativePath}:${lineNum}\n` +
441
+ ` from '${importPath}'\n` +
442
+ ` => from '${barrelFile}' (use barrel export for shorter imports)`
443
+ );
444
+ continue;
445
+ }
446
+ }
447
+ }
448
+
449
+ // Check 6: File existence (after all other checks)
327
450
  // Verify that the import path resolves to an existing file
328
451
  // Follow Node.js/Vite resolution order
329
452
  let fileExists = false;