@hkdigital/lib-core 0.5.68 → 0.5.70
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 +44 -3
- package/package.json +1 -1
- package/scripts/validate-imports.mjs +207 -4
package/README.md
CHANGED
|
@@ -280,9 +280,26 @@ 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
|
|
|
@@ -295,6 +312,8 @@ node node_modules/@hkdigital/lib-core/scripts/validate-imports.mjs
|
|
|
295
312
|
`.svelte.js`)
|
|
296
313
|
5. **Directory imports** - Write explicitly or create barrel export file
|
|
297
314
|
6. **File existence** - All import paths must resolve to existing files
|
|
315
|
+
7. **External package optimization** - Suggests barrel exports for
|
|
316
|
+
`@hkdigital/*` packages
|
|
298
317
|
|
|
299
318
|
**Barrel export preference:**
|
|
300
319
|
|
|
@@ -303,18 +322,26 @@ exports your target. This encourages shorter imports that can be
|
|
|
303
322
|
combined:
|
|
304
323
|
|
|
305
324
|
```js
|
|
306
|
-
//
|
|
325
|
+
// Internal imports - instead of deep imports:
|
|
307
326
|
import ProfileBlocks from '$lib/ui/components/profile-blocks/ProfileBlocks.svelte';
|
|
308
327
|
import Button from '$lib/ui/primitives/buttons/Button.svelte';
|
|
309
328
|
|
|
310
329
|
// Use barrel exports:
|
|
311
330
|
import { ProfileBlocks } from '$lib/ui/components.js';
|
|
312
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';
|
|
313
339
|
```
|
|
314
340
|
|
|
315
341
|
The validator checks from highest to lowest level (`$lib/ui.js` →
|
|
316
342
|
`$lib/ui/components.js` → `$lib/ui/components/profile-blocks.js`) and
|
|
317
|
-
suggests the highest-level file that exports your target.
|
|
343
|
+
suggests the highest-level file that exports your target. The same
|
|
344
|
+
logic applies to external `@hkdigital/*` packages.
|
|
318
345
|
|
|
319
346
|
**Routes are exempt from strict rules:**
|
|
320
347
|
|
|
@@ -334,11 +361,25 @@ src/lib/ui/pages/Profile.svelte:8
|
|
|
334
361
|
from '$lib/ui/components/profile-blocks/ProfileBlocks.svelte'
|
|
335
362
|
=> from '$lib/ui/components.js' (use barrel export for shorter imports)
|
|
336
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)
|
|
367
|
+
|
|
337
368
|
src/routes/explorer/[...path]/+page.svelte:4
|
|
338
369
|
from '../components/index.js'
|
|
339
370
|
✅ Allowed in routes
|
|
340
371
|
```
|
|
341
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
|
+
|
|
342
383
|
### Import Patterns and Export Structure
|
|
343
384
|
|
|
344
385
|
**Public exports use domain-specific files matching folder names:**
|
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
|
*
|
|
@@ -141,6 +147,173 @@ async function isDirectoryWithIndex(fsPath) {
|
|
|
141
147
|
return false;
|
|
142
148
|
}
|
|
143
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
|
+
// Read package.json to check for exports mapping
|
|
244
|
+
let exportsMapping = null;
|
|
245
|
+
try {
|
|
246
|
+
const pkgJsonPath = join(nodeModulesPath, 'package.json');
|
|
247
|
+
const pkgJsonContent = await readFile(pkgJsonPath, 'utf-8');
|
|
248
|
+
const pkgJson = JSON.parse(pkgJsonContent);
|
|
249
|
+
|
|
250
|
+
// Check if there's a "./*" export mapping
|
|
251
|
+
if (pkgJson.exports && pkgJson.exports['./*']) {
|
|
252
|
+
const mapping = pkgJson.exports['./*'];
|
|
253
|
+
const mappingStr = typeof mapping === 'string' ?
|
|
254
|
+
mapping : mapping.default;
|
|
255
|
+
|
|
256
|
+
// Extract prefix from mapping like "./dist/*" -> "dist/"
|
|
257
|
+
if (mappingStr && mappingStr.includes('*')) {
|
|
258
|
+
exportsMapping = mappingStr.replace(/\/?\*$/, '');
|
|
259
|
+
if (exportsMapping.startsWith('./')) {
|
|
260
|
+
exportsMapping = exportsMapping.slice(2);
|
|
261
|
+
}
|
|
262
|
+
if (exportsMapping && !exportsMapping.endsWith('/')) {
|
|
263
|
+
exportsMapping += '/';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// Could not read package.json, continue without mapping
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Try progressively higher-level barrel files
|
|
272
|
+
for (let i = 1; i < pathInPackage.length; i++) {
|
|
273
|
+
const barrelPath = pathInPackage.slice(0, i).join('/') + '.js';
|
|
274
|
+
|
|
275
|
+
// Try both with and without exports mapping
|
|
276
|
+
const pathsToTry = [
|
|
277
|
+
join(nodeModulesPath, barrelPath),
|
|
278
|
+
exportsMapping ?
|
|
279
|
+
join(nodeModulesPath, exportsMapping + barrelPath) : null
|
|
280
|
+
].filter(Boolean);
|
|
281
|
+
|
|
282
|
+
for (const fsBarrelPath of pathsToTry) {
|
|
283
|
+
try {
|
|
284
|
+
const stats = await stat(fsBarrelPath);
|
|
285
|
+
if (stats.isFile()) {
|
|
286
|
+
const content = await readFile(fsBarrelPath, 'utf-8');
|
|
287
|
+
|
|
288
|
+
// Check if this barrel exports our target
|
|
289
|
+
// Patterns to match:
|
|
290
|
+
// export { TextButton } from './path';
|
|
291
|
+
// export * from './path';
|
|
292
|
+
const exportPatterns = [
|
|
293
|
+
// Named export with exact name
|
|
294
|
+
new RegExp(
|
|
295
|
+
`export\\s+\\{[^}]*\\b${targetName}\\b[^}]*\\}`,
|
|
296
|
+
'm'
|
|
297
|
+
),
|
|
298
|
+
// Re-export all
|
|
299
|
+
/export\s+\*\s+from/,
|
|
300
|
+
// Default export
|
|
301
|
+
new RegExp(`export\\s+default\\s+${targetName}\\b`, 'm')
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
if (exportPatterns.some(pattern => pattern.test(content))) {
|
|
305
|
+
return `${pkgName}/${barrelPath}`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// File doesn't exist, continue
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
144
317
|
/**
|
|
145
318
|
* Validate import paths in a file
|
|
146
319
|
*
|
|
@@ -179,10 +352,40 @@ async function validateFile(filePath) {
|
|
|
179
352
|
// Strip query parameters (Vite asset imports like ?preset=render)
|
|
180
353
|
const importPath = importPathRaw.split('?')[0];
|
|
181
354
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
355
|
+
// Check external packages from configured scopes
|
|
356
|
+
const isExternalPackage = !importPath.startsWith('./') &&
|
|
357
|
+
!importPath.startsWith('../') &&
|
|
358
|
+
!importPath.startsWith('$lib/');
|
|
359
|
+
|
|
360
|
+
if (isExternalPackage) {
|
|
361
|
+
// Extract package name/scope
|
|
362
|
+
const parts = importPath.split('/');
|
|
363
|
+
const scope = importPath.startsWith('@') ? parts[0] : null;
|
|
364
|
+
|
|
365
|
+
// Check if this scope should be validated
|
|
366
|
+
if (scope && EXTERNAL_SCOPES_TO_VALIDATE.includes(scope)) {
|
|
367
|
+
// Extract imported names from the import statement
|
|
368
|
+
const importedNames = extractImportNames(line);
|
|
369
|
+
|
|
370
|
+
// Check each imported name for barrel exports
|
|
371
|
+
for (const importedName of importedNames) {
|
|
372
|
+
const barrelPath = await findExternalBarrelExport(
|
|
373
|
+
importPath,
|
|
374
|
+
importedName
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
if (barrelPath) {
|
|
378
|
+
errors.push(
|
|
379
|
+
`${relativePath}:${lineNum}\n` +
|
|
380
|
+
` from '${importPath}'\n` +
|
|
381
|
+
` => from '${barrelPath}' (use barrel export)`
|
|
382
|
+
);
|
|
383
|
+
break; // Only report once per line
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Skip further validation for external packages
|
|
186
389
|
continue;
|
|
187
390
|
}
|
|
188
391
|
|