@hkdigital/lib-core 0.5.67 → 0.5.69
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 +74 -8
- package/claude.md +6 -0
- package/dist/network/http/caching.js +1 -2
- package/dist/state/machines/finite-state-machine/FiniteStateMachine.svelte.d.ts +1 -1
- package/dist/state/machines/finite-state-machine/FiniteStateMachine.svelte.js +1 -1
- package/dist/ui/components/button-group/ButtonGroup.svelte +1 -1
- package/dist/ui/components/hk-app-layout/HkAppLayout.svelte +1 -1
- package/dist/ui/components/image-box/ImageBox.svelte +2 -3
- package/dist/ui/components/presenter/Presenter.state.svelte.js +2 -4
- package/dist/ui/components/presenter/util.js +1 -1
- package/dist/ui/components/tab-bar/HkTabBarSelector.state.svelte.js +1 -1
- package/dist/ui/primitives/buttons/button-icon-steeze/SteezeIconButton.svelte +1 -1
- package/dist/ui/primitives/inputs/text-input/TextInput.svelte +1 -1
- package/dist/util/string/array-path.js +1 -1
- package/dist/util/string/interpolate.js +1 -1
- package/package.json +1 -1
- package/scripts/validate-imports.mjs +299 -9
package/README.md
CHANGED
|
@@ -280,20 +280,68 @@ pnpm run publish:npm # Version bump and publish to npm
|
|
|
280
280
|
### Import Validation
|
|
281
281
|
|
|
282
282
|
The library includes a validation script to enforce consistent import
|
|
283
|
-
patterns
|
|
283
|
+
patterns across both your project files and external `@hkdigital/*`
|
|
284
|
+
package imports.
|
|
285
|
+
|
|
286
|
+
**Add to your project's package.json:**
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"scripts": {
|
|
291
|
+
"lint:imports": "node node_modules/@hkdigital/lib-core/scripts/validate-imports.mjs"
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Run validation:**
|
|
284
297
|
|
|
285
298
|
```bash
|
|
299
|
+
# Using npm script (recommended)
|
|
300
|
+
pnpm run lint:imports
|
|
301
|
+
|
|
302
|
+
# Or directly
|
|
286
303
|
node node_modules/@hkdigital/lib-core/scripts/validate-imports.mjs
|
|
287
304
|
```
|
|
288
305
|
|
|
289
306
|
**Validation rules (enforced for `src/lib/` files only):**
|
|
290
307
|
|
|
291
308
|
1. **Cross-domain imports** - Use `$lib/` instead of `../../../`
|
|
292
|
-
2. **
|
|
293
|
-
3. **
|
|
309
|
+
2. **Prefer barrel exports** - Use higher-level export files when available
|
|
310
|
+
3. **Parent index.js imports** - Use `$lib/` or import specific files
|
|
311
|
+
4. **Non-standard extensions** - Include full extension (e.g.,
|
|
294
312
|
`.svelte.js`)
|
|
295
|
-
|
|
296
|
-
|
|
313
|
+
5. **Directory imports** - Write explicitly or create barrel export file
|
|
314
|
+
6. **File existence** - All import paths must resolve to existing files
|
|
315
|
+
7. **External package optimization** - Suggests barrel exports for
|
|
316
|
+
`@hkdigital/*` packages
|
|
317
|
+
|
|
318
|
+
**Barrel export preference:**
|
|
319
|
+
|
|
320
|
+
The validator suggests using the highest-level barrel export file that
|
|
321
|
+
exports your target. This encourages shorter imports that can be
|
|
322
|
+
combined:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
// Internal imports - instead of deep imports:
|
|
326
|
+
import ProfileBlocks from '$lib/ui/components/profile-blocks/ProfileBlocks.svelte';
|
|
327
|
+
import Button from '$lib/ui/primitives/buttons/Button.svelte';
|
|
328
|
+
|
|
329
|
+
// Use barrel exports:
|
|
330
|
+
import { ProfileBlocks } from '$lib/ui/components.js';
|
|
331
|
+
import { Button } from '$lib/ui/primitives.js';
|
|
332
|
+
|
|
333
|
+
// External imports - instead of deep imports:
|
|
334
|
+
import { TextButton } from '@hkdigital/lib-core/ui/primitives/buttons/index.js';
|
|
335
|
+
import { TextInput } from '@hkdigital/lib-core/ui/primitives/inputs/index.js';
|
|
336
|
+
|
|
337
|
+
// Use barrel exports:
|
|
338
|
+
import { TextButton, TextInput } from '@hkdigital/lib-core/ui/primitives.js';
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The validator checks from highest to lowest level (`$lib/ui.js` →
|
|
342
|
+
`$lib/ui/components.js` → `$lib/ui/components/profile-blocks.js`) and
|
|
343
|
+
suggests the highest-level file that exports your target. The same
|
|
344
|
+
logic applies to external `@hkdigital/*` packages.
|
|
297
345
|
|
|
298
346
|
**Routes are exempt from strict rules:**
|
|
299
347
|
|
|
@@ -305,15 +353,33 @@ files.
|
|
|
305
353
|
**Example output:**
|
|
306
354
|
|
|
307
355
|
```
|
|
308
|
-
src/lib/ui/
|
|
309
|
-
from '
|
|
310
|
-
=> from '$lib/ui/
|
|
356
|
+
src/lib/ui/panels/Panel.svelte:6
|
|
357
|
+
from '../../../components/profile-blocks/ProfileBlocks.svelte'
|
|
358
|
+
=> from '$lib/ui/components.js' (use barrel export)
|
|
359
|
+
|
|
360
|
+
src/lib/ui/pages/Profile.svelte:8
|
|
361
|
+
from '$lib/ui/components/profile-blocks/ProfileBlocks.svelte'
|
|
362
|
+
=> from '$lib/ui/components.js' (use barrel export for shorter imports)
|
|
363
|
+
|
|
364
|
+
src/lib/forms/LoginForm.svelte:4
|
|
365
|
+
from '@hkdigital/lib-core/ui/primitives/buttons/index.js'
|
|
366
|
+
=> from '@hkdigital/lib-core/ui/primitives.js' (use barrel export)
|
|
311
367
|
|
|
312
368
|
src/routes/explorer/[...path]/+page.svelte:4
|
|
313
369
|
from '../components/index.js'
|
|
314
370
|
✅ Allowed in routes
|
|
315
371
|
```
|
|
316
372
|
|
|
373
|
+
**What gets checked for external packages:**
|
|
374
|
+
|
|
375
|
+
The validator only suggests barrel exports for:
|
|
376
|
+
- Explicit `index.js` imports
|
|
377
|
+
- Component files (`.svelte`)
|
|
378
|
+
- Class files (capitalized `.js` files)
|
|
379
|
+
|
|
380
|
+
Intentional imports like `helpers.js`, `config.js`, or other lowercase
|
|
381
|
+
utility files are assumed to be the public API and won't be flagged.
|
|
382
|
+
|
|
317
383
|
### Import Patterns and Export Structure
|
|
318
384
|
|
|
319
385
|
**Public exports use domain-specific files matching folder names:**
|
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'`
|
|
@@ -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
|
|
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
|
|
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 */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { ImageLoader } from '../../../network/loaders.js';
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
3
|
+
import { useResizeObserver } from '../../../util/svelte.js';
|
|
4
4
|
|
|
5
5
|
/* ------------------------------------------------------- Define state class */
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,12 @@ import { join, relative, resolve, dirname } from 'node:path';
|
|
|
6
6
|
const PROJECT_ROOT = process.cwd();
|
|
7
7
|
const SRC_DIR = join(PROJECT_ROOT, 'src');
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Scopes to validate for barrel exports
|
|
11
|
+
* Any package under these scopes will be checked
|
|
12
|
+
*/
|
|
13
|
+
const EXTERNAL_SCOPES_TO_VALIDATE = ['@hkdigital'];
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Find all JS and Svelte files recursively
|
|
11
17
|
*
|
|
@@ -31,6 +37,66 @@ async function findFiles(dir) {
|
|
|
31
37
|
return files;
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Find highest-level barrel export file that exports the target
|
|
42
|
+
*
|
|
43
|
+
* For a path like $lib/ui/components/profile-blocks/ProfileBlocks.svelte:
|
|
44
|
+
* - Check $lib/ui.js
|
|
45
|
+
* - Check $lib/ui/components.js
|
|
46
|
+
* - Check $lib/ui/components/profile-blocks.js
|
|
47
|
+
*
|
|
48
|
+
* @param {string} importPath - Import path (e.g., $lib/ui/components/...)
|
|
49
|
+
* @param {string} filePath - Source file making the import
|
|
50
|
+
*
|
|
51
|
+
* @returns {Promise<string|null>} Barrel file path or null
|
|
52
|
+
*/
|
|
53
|
+
async function findBarrelExportFile(importPath, filePath) {
|
|
54
|
+
if (!importPath.startsWith('$lib/')) return null;
|
|
55
|
+
|
|
56
|
+
// Remove $lib/ prefix and extract path segments
|
|
57
|
+
const pathWithoutLib = importPath.replace('$lib/', '');
|
|
58
|
+
const parts = pathWithoutLib.split('/');
|
|
59
|
+
|
|
60
|
+
// Extract the target filename (last part, possibly with extension)
|
|
61
|
+
const targetFile = parts[parts.length - 1];
|
|
62
|
+
const targetBase = targetFile.replace(/\.(js|svelte)$/, '');
|
|
63
|
+
|
|
64
|
+
// Check from highest level down (but skip the filename itself)
|
|
65
|
+
for (let i = 1; i < parts.length; i++) {
|
|
66
|
+
const barrelPath = '$lib/' + parts.slice(0, i).join('/') + '.js';
|
|
67
|
+
const fsBarrelPath = join(
|
|
68
|
+
PROJECT_ROOT,
|
|
69
|
+
barrelPath.replace('$lib/', 'src/lib/')
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const stats = await stat(fsBarrelPath);
|
|
74
|
+
if (stats.isFile()) {
|
|
75
|
+
// Check if this barrel file exports our target
|
|
76
|
+
const content = await readFile(fsBarrelPath, 'utf-8');
|
|
77
|
+
|
|
78
|
+
// Look for exports that match the target file
|
|
79
|
+
// Patterns:
|
|
80
|
+
// export { ProfileBlocks } from './path/ProfileBlocks.svelte';
|
|
81
|
+
// export * from './path/ProfileBlocks.svelte';
|
|
82
|
+
// More flexible pattern to catch various export styles
|
|
83
|
+
const exportPattern = new RegExp(
|
|
84
|
+
`export\\s+(?:\\*|\\{[^}]*\\})\\s+from\\s+['"](?:.*/)?(${targetBase}(?:\\.(?:js|svelte))?)['"]`,
|
|
85
|
+
'gm'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (exportPattern.test(content)) {
|
|
89
|
+
return barrelPath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// File doesn't exist or can't be read, continue
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
34
100
|
/**
|
|
35
101
|
* Check if import path resolves to directory with index.js
|
|
36
102
|
*
|
|
@@ -81,6 +147,137 @@ async function isDirectoryWithIndex(fsPath) {
|
|
|
81
147
|
return false;
|
|
82
148
|
}
|
|
83
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Extract imported names from import statement
|
|
152
|
+
*
|
|
153
|
+
* @param {string} line - Import statement line
|
|
154
|
+
*
|
|
155
|
+
* @returns {string[]} Array of imported names
|
|
156
|
+
*/
|
|
157
|
+
function extractImportNames(line) {
|
|
158
|
+
const names = [];
|
|
159
|
+
|
|
160
|
+
// Default import: import Foo from '...'
|
|
161
|
+
const defaultMatch = line.match(/import\s+(\w+)\s+from/);
|
|
162
|
+
if (defaultMatch) {
|
|
163
|
+
names.push(defaultMatch[1]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Named imports: import { Foo, Bar } from '...'
|
|
167
|
+
const namedMatch = line.match(/import\s+\{([^}]+)\}\s+from/);
|
|
168
|
+
if (namedMatch) {
|
|
169
|
+
const namedImports = namedMatch[1]
|
|
170
|
+
.split(',')
|
|
171
|
+
.map(name => {
|
|
172
|
+
// Handle 'as' aliases: { Foo as Bar } → extract 'Foo'
|
|
173
|
+
const parts = name.trim().split(/\s+as\s+/);
|
|
174
|
+
return parts[0].trim();
|
|
175
|
+
})
|
|
176
|
+
.filter(name => name && name !== '*');
|
|
177
|
+
names.push(...namedImports);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return names;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find highest-level barrel export in external package
|
|
185
|
+
*
|
|
186
|
+
* For @hkdigital/lib-core/ui/primitives/buttons/index.js:
|
|
187
|
+
* - Check @hkdigital/lib-core/ui/primitives.js
|
|
188
|
+
* - Check @hkdigital/lib-core/ui.js
|
|
189
|
+
*
|
|
190
|
+
* @param {string} importPath - External import path
|
|
191
|
+
* @param {string} targetName - Name of export to find
|
|
192
|
+
*
|
|
193
|
+
* @returns {Promise<string|null>} Suggested barrel path or null
|
|
194
|
+
*/
|
|
195
|
+
async function findExternalBarrelExport(importPath, targetName) {
|
|
196
|
+
// Extract package name (handle scoped packages)
|
|
197
|
+
const parts = importPath.split('/');
|
|
198
|
+
let pkgName;
|
|
199
|
+
let pathInPackage;
|
|
200
|
+
|
|
201
|
+
if (importPath.startsWith('@')) {
|
|
202
|
+
// Scoped package: @scope/package/path/to/file
|
|
203
|
+
pkgName = `${parts[0]}/${parts[1]}`;
|
|
204
|
+
pathInPackage = parts.slice(2);
|
|
205
|
+
} else {
|
|
206
|
+
// Regular package: package/path/to/file
|
|
207
|
+
pkgName = parts[0];
|
|
208
|
+
pathInPackage = parts.slice(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if this scope should be validated
|
|
212
|
+
const scope = pkgName.startsWith('@') ?
|
|
213
|
+
pkgName.split('/')[0] : null;
|
|
214
|
+
if (scope && !EXTERNAL_SCOPES_TO_VALIDATE.includes(scope)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// If no path in package, nothing to suggest
|
|
219
|
+
if (pathInPackage.length === 0) return null;
|
|
220
|
+
|
|
221
|
+
const nodeModulesPath = join(PROJECT_ROOT, 'node_modules', pkgName);
|
|
222
|
+
|
|
223
|
+
// Extract target to find (last part without extension)
|
|
224
|
+
const lastPart = pathInPackage[pathInPackage.length - 1];
|
|
225
|
+
const targetBase = lastPart.replace(/\.(js|svelte)$/, '');
|
|
226
|
+
|
|
227
|
+
// Only check for specific import types (matches internal logic)
|
|
228
|
+
// 1. Explicit index.js imports
|
|
229
|
+
// 2. Component files (.svelte)
|
|
230
|
+
// 3. Class files (capitalized .js)
|
|
231
|
+
let shouldCheck = false;
|
|
232
|
+
|
|
233
|
+
if (lastPart === 'index.js') {
|
|
234
|
+
shouldCheck = true;
|
|
235
|
+
} else if (lastPart.endsWith('.svelte')) {
|
|
236
|
+
shouldCheck = true;
|
|
237
|
+
} else if (lastPart.match(/^[A-Z][^/]*\.js$/)) {
|
|
238
|
+
shouldCheck = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!shouldCheck) return null;
|
|
242
|
+
|
|
243
|
+
// Try progressively higher-level barrel files
|
|
244
|
+
for (let i = 1; i < pathInPackage.length; i++) {
|
|
245
|
+
const barrelPath = pathInPackage.slice(0, i).join('/') + '.js';
|
|
246
|
+
const fsBarrelPath = join(nodeModulesPath, barrelPath);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const stats = await stat(fsBarrelPath);
|
|
250
|
+
if (stats.isFile()) {
|
|
251
|
+
const content = await readFile(fsBarrelPath, 'utf-8');
|
|
252
|
+
|
|
253
|
+
// Check if this barrel exports our target
|
|
254
|
+
// Patterns to match:
|
|
255
|
+
// export { TextButton } from './path';
|
|
256
|
+
// export * from './path';
|
|
257
|
+
const exportPatterns = [
|
|
258
|
+
// Named export with exact name
|
|
259
|
+
new RegExp(
|
|
260
|
+
`export\\s+\\{[^}]*\\b${targetName}\\b[^}]*\\}`,
|
|
261
|
+
'm'
|
|
262
|
+
),
|
|
263
|
+
// Re-export all
|
|
264
|
+
/export\s+\*\s+from/,
|
|
265
|
+
// Default export
|
|
266
|
+
new RegExp(`export\\s+default\\s+${targetName}\\b`, 'm')
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
if (exportPatterns.some(pattern => pattern.test(content))) {
|
|
270
|
+
return `${pkgName}/${barrelPath}`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// File doesn't exist, continue
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
84
281
|
/**
|
|
85
282
|
* Validate import paths in a file
|
|
86
283
|
*
|
|
@@ -119,21 +316,69 @@ async function validateFile(filePath) {
|
|
|
119
316
|
// Strip query parameters (Vite asset imports like ?preset=render)
|
|
120
317
|
const importPath = importPathRaw.split('?')[0];
|
|
121
318
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
319
|
+
// Check external packages from configured scopes
|
|
320
|
+
const isExternalPackage = !importPath.startsWith('./') &&
|
|
321
|
+
!importPath.startsWith('../') &&
|
|
322
|
+
!importPath.startsWith('$lib/');
|
|
323
|
+
|
|
324
|
+
if (isExternalPackage) {
|
|
325
|
+
// Extract package name/scope
|
|
326
|
+
const parts = importPath.split('/');
|
|
327
|
+
const scope = importPath.startsWith('@') ? parts[0] : null;
|
|
328
|
+
|
|
329
|
+
// Check if this scope should be validated
|
|
330
|
+
if (scope && EXTERNAL_SCOPES_TO_VALIDATE.includes(scope)) {
|
|
331
|
+
// Extract imported names from the import statement
|
|
332
|
+
const importedNames = extractImportNames(line);
|
|
333
|
+
|
|
334
|
+
// Check each imported name for barrel exports
|
|
335
|
+
for (const importedName of importedNames) {
|
|
336
|
+
const barrelPath = await findExternalBarrelExport(
|
|
337
|
+
importPath,
|
|
338
|
+
importedName
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (barrelPath) {
|
|
342
|
+
errors.push(
|
|
343
|
+
`${relativePath}:${lineNum}\n` +
|
|
344
|
+
` from '${importPath}'\n` +
|
|
345
|
+
` => from '${barrelPath}' (use barrel export)`
|
|
346
|
+
);
|
|
347
|
+
break; // Only report once per line
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Skip further validation for external packages
|
|
126
353
|
continue;
|
|
127
354
|
}
|
|
128
355
|
|
|
129
356
|
// Check 1: Cross-domain relative imports (3+ levels up)
|
|
130
357
|
// Only enforce for lib files
|
|
131
358
|
if (isInLib && importPath.match(/^\.\.\/\.\.\/\.\.\//)) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
359
|
+
// Convert relative path to $lib/ path
|
|
360
|
+
const fsPath = resolve(dirname(filePath), importPath);
|
|
361
|
+
const libPath = fsPath.replace(
|
|
362
|
+
join(PROJECT_ROOT, 'src/lib/'),
|
|
363
|
+
'$lib/'
|
|
136
364
|
);
|
|
365
|
+
|
|
366
|
+
// Check for barrel export files
|
|
367
|
+
const barrelFile = await findBarrelExportFile(libPath, filePath);
|
|
368
|
+
|
|
369
|
+
if (barrelFile) {
|
|
370
|
+
errors.push(
|
|
371
|
+
`${relativePath}:${lineNum}\n` +
|
|
372
|
+
` from '${importPath}'\n` +
|
|
373
|
+
` => from '${barrelFile}' (use barrel export)`
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
errors.push(
|
|
377
|
+
`${relativePath}:${lineNum}\n` +
|
|
378
|
+
` from '${importPath}'\n` +
|
|
379
|
+
` => from '${libPath}'`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
137
382
|
continue;
|
|
138
383
|
}
|
|
139
384
|
|
|
@@ -323,7 +568,52 @@ async function validateFile(filePath) {
|
|
|
323
568
|
}
|
|
324
569
|
}
|
|
325
570
|
|
|
326
|
-
// Check 5:
|
|
571
|
+
// Check 5: Suggest barrel exports for deep $lib/ imports
|
|
572
|
+
// Only for specific cases, not for named export files
|
|
573
|
+
if (isInLib && importPath.startsWith('$lib/')) {
|
|
574
|
+
const pathWithoutLib = importPath.replace('$lib/', '');
|
|
575
|
+
const segments = pathWithoutLib.split('/');
|
|
576
|
+
|
|
577
|
+
// Only suggest barrel exports for:
|
|
578
|
+
// 1. Directory imports (no extension, resolves to index.js)
|
|
579
|
+
// 2. Explicit index.js imports
|
|
580
|
+
// 3. Deep component/class files (.svelte or capitalized .js files)
|
|
581
|
+
let shouldCheckBarrel = false;
|
|
582
|
+
|
|
583
|
+
if (segments.length >= 3) {
|
|
584
|
+
const lastSegment = segments[segments.length - 1];
|
|
585
|
+
|
|
586
|
+
// Case 1: Explicit index.js import
|
|
587
|
+
if (lastSegment === 'index.js') {
|
|
588
|
+
shouldCheckBarrel = true;
|
|
589
|
+
}
|
|
590
|
+
// Case 2: Directory import (will be caught by Check 4)
|
|
591
|
+
// Case 3: Component or class file (.svelte or capitalized)
|
|
592
|
+
else if (lastSegment.endsWith('.svelte')) {
|
|
593
|
+
shouldCheckBarrel = true;
|
|
594
|
+
} else if (lastSegment.match(/^[A-Z][^/]*\.js$/)) {
|
|
595
|
+
// Capitalized .js file (likely a class)
|
|
596
|
+
shouldCheckBarrel = true;
|
|
597
|
+
}
|
|
598
|
+
// Skip named export files like methods.js, http.js, etc.
|
|
599
|
+
// These are lowercase and ARE the public API
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (shouldCheckBarrel) {
|
|
603
|
+
const barrelFile = await findBarrelExportFile(importPath, filePath);
|
|
604
|
+
|
|
605
|
+
if (barrelFile) {
|
|
606
|
+
errors.push(
|
|
607
|
+
`${relativePath}:${lineNum}\n` +
|
|
608
|
+
` from '${importPath}'\n` +
|
|
609
|
+
` => from '${barrelFile}' (use barrel export for shorter imports)`
|
|
610
|
+
);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Check 6: File existence (after all other checks)
|
|
327
617
|
// Verify that the import path resolves to an existing file
|
|
328
618
|
// Follow Node.js/Vite resolution order
|
|
329
619
|
let fileExists = false;
|