@hkdigital/lib-core 0.4.28 → 0.4.30

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/CLAUDE.md ADDED
@@ -0,0 +1,350 @@
1
+ # SvelteKit Library
2
+
3
+ ## Project Overview
4
+ This is a modern SvelteKit library built with Svelte 5 and Skeleton.dev v3 components. The project emphasizes accessibility, clean code standards, and modern JavaScript patterns.
5
+
6
+ ## Architecture & Structure
7
+ - **Framework**: SvelteKit with Svelte 5 runes
8
+ - **UI Components**: Skeleton.dev version 3
9
+ - **Package Manager**: PNPM
10
+ - **Language**: Modern JavaScript (ES6+), no TypeScript
11
+ - **Module System**: ES Modules only (import/export)
12
+
13
+ ## Key Technical Decisions
14
+ - Uses Svelte 5 runes (`$state()`, `$derived()`, `$effect()`, `$props()`) for state management
15
+ - Snippet syntax with `{@render children()}` instead of deprecated `<slot />`
16
+ - WCAG 2.1 Level AA accessibility compliance
17
+ - 80-character line limit with rare exceptions
18
+ - Two-space indentation
19
+ - Descriptive camelCase naming conventions
20
+
21
+ ## Component Patterns
22
+ - All components use `let { ... } = $props()` with JSDoc type annotations
23
+ - Props always include `classes` and `...attrs` for flexibility
24
+ - Event handlers use Svelte 5 syntax (`onclick`, `onchange`)
25
+ - Snippet props typed as `import('svelte').Snippet`
26
+ - Private methods use hash prefix (`#methodName`)
27
+
28
+ ## JavaScript Class Patterns
29
+ - Use modern ES private methods with hash prefix: `#methodName()` instead of `_methodName()`
30
+ - Private fields also use hash prefix: `#privateField`
31
+ - Apply this to all JavaScript classes, not just Svelte components
32
+ - Never use `@private` in JSDoc for methods that start with `#` - the hash already indicates privacy
33
+
34
+ ## Development Standards
35
+ - Readable code over concise code
36
+ - Explicit error handling with try/catch for async operations
37
+ - JSDoc comments for all functions and variables
38
+ - English for all documentation and comments
39
+ - No dollar signs in variable names (reserved for Svelte)
40
+
41
+ ### ESLint Rule Suppression
42
+ - Use specific rule suppression instead of blanket disables
43
+ - For unused variables in method signatures (e.g., base class methods to be overridden):
44
+ ```javascript
45
+ // eslint-disable-next-line no-unused-vars
46
+ async _configure(newConfig, oldConfig = null) {
47
+ // Override in subclass
48
+ }
49
+ ```
50
+
51
+ ## Documentation & Comment Style
52
+ - **80-character line limit** - strictly enforce for ALL code and documentation
53
+ - Applies to code lines, JSDoc comments, parameter descriptions, and all text
54
+ - Wrap long JSDoc descriptions across multiple lines
55
+ - Break long parameter lists and descriptions at 80 characters
56
+ - **JSDoc formatting conventions:**
57
+ - Blank line between description and first `@param`
58
+ - Blank line between last `@param` and `@returns`
59
+ - No blank lines between consecutive `@param` tags with short descriptions
60
+ - Add extra newlines between `@param` entries ONLY when they have multi-line descriptions for better readability
61
+ - For long parameter types or descriptions, place description on next line:
62
+ ```javascript
63
+ // Multi-line @param descriptions (use extra newlines between)
64
+ /**
65
+ * Convert a path string to an array path
66
+ *
67
+ * @param {string|string[]} path
68
+ * String or array path (e.g. "some.path.to")
69
+ *
70
+ * @param {string} [pathSeparator=PATH_SEPARATOR]
71
+ * A custom path separator to use instead of the default "."
72
+ *
73
+ * @returns {string[]} array path (e.g. ["some", "path", "to"])
74
+ */
75
+
76
+ // Short descriptions (no extra newlines needed)
77
+ /**
78
+ * Log an info message
79
+ *
80
+ * @param {string} message - Log message
81
+ * @param {*} [details] - Additional details
82
+ *
83
+ * @returns {boolean} True if the log was emitted
84
+ */
85
+ ```
86
+ - **Concise descriptions** - avoid obvious/redundant explanations
87
+ - Keep only essential information that adds value for developers
88
+ - Remove descriptions that simply restate parameter names or types
89
+ - Examples:
90
+ - ✅ `@property {number} [exp] - Expiration time (seconds since epoch)`
91
+ - ❌ `@property {number} [exp] - Expiration time - timestamp when the JWT expires`
92
+ - ✅ `@property {boolean} [ignoreExpiration] - Skip expiration validation`
93
+ - ❌ `@property {boolean} [ignoreExpiration] - If true, do not validate the expiration of the token`
94
+
95
+ ## Writing Style
96
+ - Use normal English capitalization rules - avoid unnecessary capitals
97
+ - Don't capitalize common nouns like "server", "client", "button", "error", "validation", etc.
98
+ - Only capitalize proper nouns, beginnings of sentences, and official names
99
+ - Examples:
100
+ - ✅ "server-side validation" not "Server-side Validation"
101
+ - ✅ "expect function" not "Expect Function"
102
+ - ✅ "client error" not "Client Error"
103
+ - ✅ "JavaScript" (proper noun) but "validation error" (common noun)
104
+
105
+ ## Import Path Conventions
106
+ - Use `$lib/domain/...` imports for cross-domain references (e.g., `$lib/media/image.js`, `$lib/network/http.js`)
107
+ - Use relative imports (`./` or `../`) when staying within the same main folder under `$lib`
108
+ - **Always include file extensions** (`.js`, `.svelte`) in import statements
109
+ - **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
110
+ - **For local imports within the same domain**, import specific files directly (e.g., `./ClassName.js`) rather than using local index files
111
+ - Examples:
112
+ - ✅ `import { ImageLoader } from '$lib/media/image.js'` (cross-domain import)
113
+ - ✅ `import ImageLoader from './ImageLoader.svelte.js'` (local import within same domain)
114
+ - ✅ `import IterableTree from './IterableTree.js'` (local import within same domain)
115
+ - ✅ `import { v } from '$lib/valibot/valibot.js'` (cross-domain with specific export file)
116
+ - ✅ `import { HumanUrl, Email } from '$lib/valibot/parsers.js'` (cross-domain with specific export file)
117
+ - ❌ `import { v, HumanUrl } from '$lib/valibot'` (missing specific export file)
118
+ - ❌ `import { IterableTree } from './index.js'` (local index when specific file should be used)
119
+ - ❌ `import something from '../../media/image.js'` (cross-domain relative import)
120
+
121
+ ## Class Export Conventions
122
+ - **All classes should be default exports**: `export default class ClassName`
123
+ - **Import classes without destructuring**: `import ClassName from './ClassName.js'`
124
+ - Examples:
125
+ - ✅ `export default class HkPromise extends Promise {}`
126
+ - ✅ `import HkPromise from './HkPromise.js'`
127
+ - ❌ `export class HkPromise extends Promise {}` (named export)
128
+ - ❌ `import { HkPromise } from './HkPromise.js'` (destructuring import)
129
+
130
+ ## Accessibility Requirements
131
+ - Proper ARIA roles, states, and properties
132
+ - Descriptive aria-labels for interactive elements
133
+ - Keyboard navigation support
134
+ - Logical tab order and focus management
135
+ - Proper heading hierarchy
136
+ - Screen reader compatibility (VoiceOver, NVDA)
137
+
138
+ ## Common Patterns to Follow
139
+ - Server/client code separation in SvelteKit
140
+ - Clear component hierarchy
141
+ - Minimal, targeted changes to working code
142
+ - Ask before making assumptions about existing structure
143
+ - Focus on specific requests rather than general improvements
144
+
145
+ ## Design System Usage in Examples
146
+
147
+ When creating examples in `routes/examples/`, always use the HKdigital Design System:
148
+
149
+ ### Typography
150
+ - Use complete typography classes: `type-heading-h1`, `type-base-md`, `type-ui-sm`
151
+ - Include dark mode variants when relevant: `type-heading-h1-dark`
152
+ - Never use raw Tailwind text classes like `text-lg` - use `text-base-lg` instead
153
+
154
+ ### Spacing
155
+ - Use UI points for element spacing: `p-20up`, `m-10up`, `gap-16up`
156
+ - Use text-based spacing for typography-related spacing: `mb-16bt`, `mt-12ut`
157
+ - Never use raw Tailwind spacing like `p-4` - use `p-4up` instead
158
+
159
+ ### Colors
160
+ - Use themed colors: `bg-primary-500`, `bg-surface-100`, `text-error-500`
161
+ - Always use contrast colors for accessibility: `text-primary-contrast-500`
162
+ - Include both light and dark mode considerations
163
+
164
+ ### Layout & Components
165
+ - Use design system utilities: `rounded-md`, `border-width-normal`
166
+ - Follow the responsive scaling system built around 1024×768 design reference
167
+ - Use component data attributes: `data-component="button"` `data-role="primary"`
168
+
169
+ ### Custom Styling for Examples
170
+ When examples require custom CSS beyond the design system:
171
+
172
+ - Always add a `data-page` attribute to the outer page element
173
+ - Create a `style.css` file alongside the `+page.svelte` file
174
+ - Use `<style src="./style.css"></style>` to include the styles
175
+ - Scope all CSS rules under `[data-page]` to prevent global conflicts
176
+ - Use CSS nesting syntax for better organization
177
+
178
+ **Example structure:**
179
+ ```svelte
180
+ <!-- +page.svelte -->
181
+ <div data-page>
182
+ <div data-section="content">
183
+ <!-- example content -->
184
+ </div>
185
+ </div>
186
+
187
+ <style src="./style.css"></style>
188
+ ```
189
+
190
+ ```css
191
+ /* style.css */
192
+ [data-page] {
193
+ & [data-section="content"] {
194
+ /* scoped styles here */
195
+ position: relative;
196
+ padding: 20px;
197
+ }
198
+
199
+ & .custom-element {
200
+ /* more scoped styles */
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Structure
206
+ - Always include proper heading hierarchy (`type-heading-h1`, `type-heading-h2`, etc.)
207
+ - Use semantic HTML with appropriate ARIA attributes
208
+ - Follow WCAG 2.1 Level AA accessibility guidelines
209
+
210
+ ### UI Components
211
+ - Always use components from `$lib/ui/primitives/` when available
212
+ - Prefer the `Button` component over raw `<button>` elements
213
+ - Import from the index: `import { Button } from '$lib/ui/primitives.js'`
214
+ - Use snippet syntax: `<Button>{content}</Button>` instead of slot syntax
215
+
216
+ ### Example Template
217
+ ```svelte
218
+ <script>
219
+ import { Button } from '$lib/ui/primitives.js';
220
+ </script>
221
+
222
+ <div class="container mx-auto p-20up" data-page>
223
+ <h1 class="type-heading-h1 mb-20up">Example Title</h1>
224
+
225
+ <div class="card p-20up mb-20up">
226
+ <p class="type-base-md mb-12bt">Description text</p>
227
+ <Button>
228
+ Action
229
+ </Button>
230
+ </div>
231
+ </div>
232
+
233
+ <!-- Only include if custom CSS is needed -->
234
+ <style src="./style.css"></style>
235
+ ```
236
+
237
+ ## Styling System & Tailwind CSS
238
+
239
+ ### Critical: Design System Spacing Values
240
+
241
+ **IMPORTANT**: Only use spacing values that exist in the design system configuration. Invalid spacing utilities will cause build failures.
242
+
243
+ #### Available Viewport-Based Spacing (`up` suffix)
244
+ Valid values: `1up`, `2up`, `4up`, `5up`, `6up`, `10up`, `20up`, `30up`, `40up`, `50up`, `60up`, `70up`, `80up`, `90up`, `100up`, `120up`, `140up`, `160up`, `180up`, `200up`
245
+
246
+ #### Available Text-Based Spacing (`ut`, `bt`, `ht` suffixes)
247
+ Valid values: `1ut/bt/ht`, `2ut/bt/ht`, `4ut/bt/ht`, `6ut/bt/ht`, `8ut/bt/ht`, `10ut/bt/ht`, `11ut/bt/ht`, `12ut/bt/ht`, `16ut/bt/ht`, `20ut/bt/ht`, `24ut/bt/ht`, `28ut/bt/ht`, `32ut/bt/ht`, `36ut/bt/ht`, `50ut/bt/ht`
248
+
249
+ #### ❌ Common Invalid Values (Will Cause Build Failures)
250
+ - `p-16up` (use `p-20up` instead)
251
+ - `px-16up py-12up` (use `px-20up py-10up` instead)
252
+ - `mb-16up` (use `mb-20up` instead)
253
+ - Any spacing value not listed above
254
+
255
+ ### External CSS Files (`<style src="./style.css">`)
256
+
257
+ When using external CSS files with `@apply` directives, you **MUST** include the `@reference` directive:
258
+
259
+ #### ✅ Correct External CSS
260
+ ```css
261
+ /* style.css */
262
+ @reference '../../app.css';
263
+
264
+ [data-page] {
265
+ & .my-component {
266
+ @apply p-20up bg-surface-300 border border-primary-500;
267
+ }
268
+ }
269
+ ```
270
+
271
+ #### ❌ Broken External CSS
272
+ ```css
273
+ /* style.css - MISSING @reference */
274
+ [data-page] {
275
+ & .my-component {
276
+ @apply p-20up bg-surface-300; /* ERROR: Cannot apply unknown utility class */
277
+ }
278
+ }
279
+ ```
280
+
281
+ ### Path Resolution for @reference
282
+
283
+ The `@reference` path must be relative to your CSS file's location:
284
+ - From `/src/routes/examples/style.css` → `@reference '../../app.css'`
285
+ - From `/src/routes/examples/ui/style.css` → `@reference '../../../app.css'`
286
+ - From `/src/lib/components/style.css` → `@reference '../../app.css'`
287
+
288
+ ### Inline vs External CSS
289
+
290
+ #### Inline Styles (No @reference needed)
291
+ ```svelte
292
+ <style>
293
+ [data-page] {
294
+ & .my-component {
295
+ @apply p-20up bg-surface-300 border border-primary-500;
296
+ }
297
+ }
298
+ </style>
299
+ ```
300
+
301
+ #### External CSS (Requires @reference)
302
+ ```svelte
303
+ <!-- Component.svelte -->
304
+ <div class="my-component">Content</div>
305
+ <style src="./style.css"></style>
306
+ ```
307
+
308
+ ### Troubleshooting Build Errors
309
+
310
+ #### "Cannot apply unknown utility class 'p-XYZup'"
311
+ 1. Check if the spacing value exists in `VIEWPORT_POINT_SIZES` or `TEXT_POINT_SIZES`
312
+ 2. Replace with the nearest valid value (e.g., `16up` → `20up`)
313
+ 3. If using external CSS, verify `@reference` directive is present and path is correct
314
+
315
+ #### "Are you using CSS modules or similar and missing @reference?"
316
+ 1. Add `@reference '../../app.css'` at the top of your external CSS file
317
+ 2. Verify the relative path from CSS file to `src/app.css` is correct
318
+ 3. Consider switching to inline styles if path resolution is problematic
319
+
320
+ ### Design System Integration
321
+
322
+ #### Always Use Design System Classes
323
+ - ✅ `type-heading-h1`, `type-base-md`, `type-ui-sm`
324
+ - ✅ `p-20up`, `m-10up`, `gap-16up` (viewport-based)
325
+ - ✅ `mb-16bt`, `mt-12ut` (text-based)
326
+ - ✅ `bg-surface-300`, `text-primary-500`, `border-error-500`
327
+ - ❌ Raw Tailwind: `text-lg`, `p-4`, `bg-gray-300`
328
+
329
+ #### Color System
330
+ All design system colors are available with contrast variants:
331
+ - Base colors: `bg-primary-500`, `bg-surface-100`, `bg-error-500`
332
+ - Contrast colors: `text-primary-contrast-500`, `text-surface-contrast-100`
333
+ - Shades: `50`, `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`, `950`
334
+
335
+ ### CSS Architecture
336
+ - Use `[data-page]` scoping for page-specific styles
337
+ - Use `[data-component]` attributes for component identification
338
+ - Prefer CSS nesting with `&` selector
339
+ - Layer styles: `@layer theme, base, utilities, components`
340
+
341
+ ## What Not to Do
342
+ - Don't use deprecated Svelte 4 syntax
343
+ - Don't mix slot and snippet syntax
344
+ - Don't use TypeScript syntax
345
+ - Don't use CommonJS (require/module.exports)
346
+ - Don't use underscore prefixes for private methods
347
+ - Don't use invalid spacing values (e.g., `p-16up`, `mb-14up`) - check design system configuration
348
+ - Don't use external CSS with `@apply` without the `@reference` directive
349
+ - Don't modify unrelated code unless necessary
350
+ - **NEVER run `npm run dev` or `pnpm run dev`** - it interferes with the user's running development server
package/README.md CHANGED
@@ -187,10 +187,16 @@ The library includes a comprehensive logging system that provides:
187
187
  - **Client-side**: Enhanced console logging with structured data display in browser inspector
188
188
  - **Consistent API**: Same logging interface for both server and client environments
189
189
 
190
- For detailed setup guides see:
191
- - **Design system**: [src/lib/design/README.md](./src/lib/design/README.md)
192
- - **Vite configuration**: [src/lib/config/README.md](./src/lib/config/README.md)
193
- - **Logging system**: [src/lib/logging/README.md](./src/lib/logging/README.md)
190
+ ## Documentation
191
+
192
+ For detailed setup guides and configuration:
193
+ - **Project setup**: [docs/setup/new-project.md](./docs/setup/new-project.md) - SvelteKit project setup
194
+ - **Library setup**: [docs/setup/new-lib.md](./docs/setup/new-lib.md) - SvelteKit library setup
195
+ - **Services & logging**: [docs/setup/services-logging.md](./docs/setup/services-logging.md) - Service management architecture
196
+ - **Configuration files**: [docs/config/root-config-files.md](./docs/config/root-config-files.md) - Config file reference
197
+ - **Design system**: [src/lib/design/README.md](./src/lib/design/README.md) - Design tokens and theming
198
+ - **Vite configuration**: [src/lib/config/README.md](./src/lib/config/README.md) - Build configuration
199
+ - **Logging system**: [src/lib/logging/README.md](./src/lib/logging/README.md) - Server and client logging
194
200
 
195
201
  ### Update
196
202
 
@@ -1,5 +1,5 @@
1
1
  export { createServerLogger } from "./internal/factories/server.js";
2
2
  export { createClientLogger } from "./internal/factories/client.js";
3
3
  export { Logger } from "./internal/logger/index.js";
4
- export * from "./constants.js";
4
+ export * from "./levels.js";
5
5
  export * from "./typedef.js";
@@ -5,6 +5,6 @@ export { createClientLogger } from './internal/factories/client.js';
5
5
  // Logger (for advanced usage)
6
6
  export { Logger } from './internal/logger/index.js';
7
7
 
8
- // Constants and typedefs
9
- export * from './constants.js';
8
+ export * from './levels.js';
9
+
10
10
  export * from './typedef.js';
@@ -1,5 +1,5 @@
1
1
  import { dev } from '$app/environment';
2
- import { LEVELS } from '../../constants.js';
2
+ import { LEVELS } from '../../levels.js';
3
3
  import {
4
4
  findRelevantFrameIndex,
5
5
  detectErrorMeta,
@@ -1,6 +1,6 @@
1
1
  import { Logger } from '../logger/index.js';
2
2
  import { ConsoleAdapter } from '../adapters/console.js';
3
- import { INFO } from '../../constants.js';
3
+ import { INFO } from '../../levels.js';
4
4
 
5
5
  /**
6
6
  * Create a client-side logger with console adapter
@@ -1,6 +1,6 @@
1
1
  import { Logger } from '../logger/index.js';
2
2
  import { PinoAdapter } from '../adapters/pino.js';
3
- import { INFO } from '../../constants.js';
3
+ import { INFO } from '../../levels.js';
4
4
  // import { expectNoSSRContext } from '../../../util/ssr/index.js';
5
5
 
6
6
  /**
@@ -43,7 +43,7 @@ import {
43
43
  ERROR,
44
44
  LEVELS,
45
45
  LOG
46
- } from '../../constants.js';
46
+ } from '../../levels.js';
47
47
 
48
48
  import { DetailedError } from '../../../generic/errors.js';
49
49
  // import { LoggerError } from '../../errors.js';
@@ -237,6 +237,37 @@ const systemHealth = await manager.checkHealth();
237
237
 
238
238
  ### Error Handling and Recovery
239
239
 
240
+ ### Logging Configuration
241
+
242
+ ServiceManager provides centralized logging control for all services:
243
+
244
+ ```javascript
245
+ const manager = new ServiceManager({
246
+ debug: true, // Sets defaultLogLevel to DEBUG
247
+ defaultLogLevel: 'INFO', // Default level for all services
248
+ managerLogLevel: 'DEBUG', // Level for ServiceManager itself
249
+ serviceLogLevels: { // Per-service levels
250
+ database: 'ERROR',
251
+ auth: 'DEBUG'
252
+ }
253
+ });
254
+
255
+ // Change manager log level at runtime
256
+ manager.setManagerLogLevel('ERROR');
257
+
258
+ // Change service log levels at runtime
259
+ manager.setServiceLogLevel('database', 'INFO');
260
+
261
+ // Set multiple service levels at once
262
+ manager.setServiceLogLevel({
263
+ database: 'INFO',
264
+ auth: 'DEBUG'
265
+ });
266
+
267
+ // Parse string format
268
+ manager.setServiceLogLevel('database:info,auth:debug');
269
+ ```
270
+
240
271
  ### ServiceManager events
241
272
 
242
273
  ServiceManager emits these events (constants from `$lib/services/service-manager/constants.js`):
@@ -264,14 +295,14 @@ await manager.recoverService('database');
264
295
  Forward all service log events to a centralised logger:
265
296
 
266
297
  ```javascript
267
- import { ServiceManager, SERVICE_LOG } from '$lib/services/index.js';
298
+ import { ServiceManager } from '$lib/services/index.js';
268
299
  import { createServerLogger } from '$lib/logging/index.js';
269
300
 
270
301
  const manager = new ServiceManager();
271
302
  const logger = createServerLogger('SystemLogger');
272
303
 
273
304
  // Listen to all log events and forward them to the logger
274
- manager.on(SERVICE_LOG, (logEvent) => {
305
+ const unsubscribe = manager.onServiceLogEvent((logEvent) => {
275
306
  logger.logFromEvent('manager:service:log', logEvent);
276
307
  });
277
308
 
@@ -280,6 +311,9 @@ manager.register('database', DatabaseService, { ... });
280
311
  manager.register('auth', AuthService, { ... });
281
312
 
282
313
  await manager.startAll();
314
+
315
+ // Cleanup when done
316
+ unsubscribe();
283
317
  ```
284
318
 
285
319
  ## Plugins
@@ -129,12 +129,32 @@ export class ServiceManager extends EventEmitter {
129
129
  */
130
130
  isRunning(name: string): Promise<boolean>;
131
131
  /**
132
- * Set log level for a service or globally
132
+ * Listen to log messages emitted by individual services
133
133
  *
134
- * @param {string} name - Service name or '*' for global
135
- * @param {string} level - Log level to set
134
+ * @param {Function} listener - Log event handler
135
+ *
136
+ * @returns {Function} Unsubscribe function
137
+ */
138
+ onServiceLogEvent(listener: Function): Function;
139
+ /**
140
+ * Set log level for the ServiceManager itself
141
+ *
142
+ * @param {string} level - Log level to set for the ServiceManager
143
+ */
144
+ setManagerLogLevel(level: string): void;
145
+ /**
146
+ * Set log level for individual services
147
+ *
148
+ * @param {string|Object<string,string>} nameOrConfig
149
+ * Service configuration:
150
+ * - String with service name: 'auth' (requires level parameter)
151
+ * - String with config: 'auth:debug,database:info'
152
+ * - Object: { auth: 'debug', database: 'info' }
153
+ * @param {string} [level] - Log level (required when nameOrConfig is service name)
136
154
  */
137
- setLogLevel(name: string, level: string): void;
155
+ setServiceLogLevel(nameOrConfig: string | {
156
+ [x: string]: string;
157
+ }, level?: string): void;
138
158
  /**
139
159
  * Get all services with a specific tag
140
160
  *
@@ -51,11 +51,11 @@
51
51
  *
52
52
  * @example
53
53
  * // Logging control
54
- * // Set global log level
55
- * manager.setLogLevel('*', 'DEBUG');
54
+ * // Set manager log level (affects all services)
55
+ * manager.setManagerLogLevel('DEBUG');
56
56
  *
57
57
  * // Set specific service log level
58
- * manager.setLogLevel('database', 'ERROR');
58
+ * manager.setServiceLogLevel('database', 'ERROR');
59
59
  *
60
60
  * // Listen to all service logs
61
61
  * manager.on('service:log', (logEvent) => {
@@ -64,7 +64,10 @@
64
64
  */
65
65
 
66
66
  import { EventEmitter } from '../../generic/events.js';
67
- import { Logger, DEBUG, INFO, WARN } from '../../logging/index.js';
67
+ import { Logger, DEBUG, INFO } from '../../logging/index.js';
68
+
69
+ import { SERVICE_LOG } from './constants.js';
70
+ import { parseServiceLogLevels } from './util.js';
68
71
 
69
72
  import {
70
73
  STATE_NOT_CREATED,
@@ -103,18 +106,29 @@ export class ServiceManager extends EventEmitter {
103
106
  /** @type {Map<string, ServiceEntry>} */
104
107
  this.services = new Map();
105
108
 
109
+ const defaultLogLevel =
110
+ config.defaultLogLevel || (config.debug ? DEBUG : INFO);
111
+ const managerLogLevel = config.managerLogLevel || defaultLogLevel;
112
+ const serviceLogLevels = config.serviceLogLevels;
113
+
106
114
  /** @type {Logger} */
107
- this.logger = new Logger('ServiceManager', config.logLevel || INFO);
115
+ this.logger = new Logger('ServiceManager', managerLogLevel);
108
116
 
109
117
  /** @type {ServiceManagerConfig} */
110
118
  this.config = {
111
119
  debug: config.debug ?? false,
112
120
  autoStart: config.autoStart ?? false,
113
121
  stopTimeout: config.stopTimeout || 10000,
114
- logConfig: config.logConfig || {}
122
+ defaultLogLevel
123
+ // managerLogLevel will be set bysetManagerLogLevel()
124
+ // serviceLogLevels will be set by setServiceLogLevel()
115
125
  };
116
126
 
117
- this.#setupLogging();
127
+ this.setManagerLogLevel(managerLogLevel);
128
+
129
+ if (serviceLogLevels) {
130
+ this.setServiceLogLevel(serviceLogLevels);
131
+ }
118
132
  }
119
133
 
120
134
  /**
@@ -474,34 +488,70 @@ export class ServiceManager extends EventEmitter {
474
488
  }
475
489
 
476
490
  /**
477
- * Set log level for a service or globally
491
+ * Listen to log messages emitted by individual services
478
492
  *
479
- * @param {string} name - Service name or '*' for global
480
- * @param {string} level - Log level to set
493
+ * @param {Function} listener - Log event handler
494
+ *
495
+ * @returns {Function} Unsubscribe function
481
496
  */
482
- setLogLevel(name, level) {
483
- if (name === '*') {
484
- // Global level
485
- this.config.logConfig.globalLevel = level;
486
-
487
- // Apply to all existing services
488
- // eslint-disable-next-line no-unused-vars
489
- for (const [_, entry] of this.services) {
490
- if (entry.instance) {
491
- entry.instance.setLogLevel(level);
497
+ onServiceLogEvent(listener) {
498
+ return this.on(SERVICE_LOG, listener);
499
+ }
500
+
501
+ /**
502
+ * Set log level for the ServiceManager itself
503
+ *
504
+ * @param {string} level - Log level to set for the ServiceManager
505
+ */
506
+ setManagerLogLevel(level) {
507
+ this.config.managerLogLevel = level;
508
+ this.logger.setLevel(level);
509
+ }
510
+
511
+ /**
512
+ * Set log level for individual services
513
+ *
514
+ * @param {string|Object<string,string>} nameOrConfig
515
+ * Service configuration:
516
+ * - String with service name: 'auth' (requires level parameter)
517
+ * - String with config: 'auth:debug,database:info'
518
+ * - Object: { auth: 'debug', database: 'info' }
519
+ * @param {string} [level] - Log level (required when nameOrConfig is service name)
520
+ */
521
+ setServiceLogLevel(nameOrConfig, level) {
522
+ /** @type {{[name:string]: string}} */
523
+ let serviceLevels = {};
524
+
525
+ if (typeof nameOrConfig === 'string') {
526
+ if (nameOrConfig.includes(':')) {
527
+ // Parse string config: 'auth:debug,database:info'
528
+ serviceLevels = parseServiceLogLevels(nameOrConfig);
529
+ } else {
530
+ // Single service name
531
+ if (!level) {
532
+ throw new Error(
533
+ `Level parameter required for service '${nameOrConfig}'`
534
+ );
492
535
  }
536
+ serviceLevels[nameOrConfig] = level;
493
537
  }
494
538
  } else {
495
- // Service-specific level
496
- if (!this.config.logConfig.serviceLevels) {
497
- this.config.logConfig.serviceLevels = {};
498
- }
499
- this.config.logConfig.serviceLevels[name] = level;
539
+ // Object config: { auth: 'debug', database: 'info' }
540
+ serviceLevels = nameOrConfig;
541
+ }
542
+
543
+ if (!this.config.serviceLogLevels) {
544
+ this.config.serviceLogLevels = {};
545
+ }
546
+
547
+ // Apply service-specific log levels
548
+ for (const [name, logLevel] of Object.entries(serviceLevels)) {
549
+ this.config.serviceLogLevels[name] = logLevel;
500
550
 
501
551
  // Apply to existing instance
502
552
  const instance = this.get(name);
503
553
  if (instance) {
504
- instance.setLogLevel(level);
554
+ instance.setLogLevel(logLevel);
505
555
  }
506
556
  }
507
557
  }
@@ -586,23 +636,6 @@ export class ServiceManager extends EventEmitter {
586
636
  }
587
637
  }
588
638
 
589
- /**
590
- * Setup logging configuration based on config.dev
591
- */
592
- #setupLogging() {
593
- // Set default log levels based on config.debug flag
594
- if (this.config.debug) {
595
- this.config.logConfig.defaultLevel = DEBUG;
596
- } else {
597
- this.config.logConfig.defaultLevel = WARN;
598
- }
599
-
600
- // Apply config
601
- if (this.config.logConfig.globalLevel) {
602
- this.logger.setLevel(this.config.logConfig.globalLevel);
603
- }
604
- }
605
-
606
639
  /**
607
640
  * Get the appropriate log level for a service
608
641
  *
@@ -611,21 +644,20 @@ export class ServiceManager extends EventEmitter {
611
644
  * @returns {string|undefined} Log level or undefined
612
645
  */
613
646
  #getServiceLogLevel(name) {
614
- const config = this.config.logConfig;
647
+ const config = this.config;
615
648
 
616
649
  // Check in order of precedence:
617
- // 1. Global level (overrides everything)
618
- if (config.globalLevel) {
619
- return config.globalLevel;
650
+ // 1. Service-specific level
651
+ if (config.serviceLogLevels?.[name]) {
652
+ return config.serviceLogLevels[name];
620
653
  }
621
654
 
622
- // 2. Service-specific level
623
- if (config.serviceLevels?.[name]) {
624
- return config.serviceLevels[name];
655
+ // 2. Default level fallback
656
+ if (config.defaultLogLevel) {
657
+ return config.defaultLogLevel;
625
658
  }
626
659
 
627
- // 3. Don't use defaultLevel as it might be too restrictive
628
- // Return undefined to let the service use its own default
660
+ // 3. No fallback - let service use its own default
629
661
  return undefined;
630
662
  }
631
663
 
@@ -2,3 +2,5 @@ export const SERVICE_STATE_CHANGED: "service:state-changed";
2
2
  export const SERVICE_HEALTH_CHANGED: "service:health-changed";
3
3
  export const SERVICE_ERROR: "service:error";
4
4
  export const SERVICE_LOG: "service:log";
5
+ export const ANY_LOG_LEVEL: "*";
6
+ export const ANY_SERVICE_NAME: "*";
@@ -3,3 +3,6 @@ export const SERVICE_STATE_CHANGED = 'service:state-changed';
3
3
  export const SERVICE_HEALTH_CHANGED = 'service:health-changed';
4
4
  export const SERVICE_ERROR = 'service:error';
5
5
  export const SERVICE_LOG = 'service:log';
6
+
7
+ export const ANY_LOG_LEVEL = '*';
8
+ export const ANY_SERVICE_NAME = '*';
@@ -38,30 +38,19 @@ export type ServiceManagerConfig = {
38
38
  */
39
39
  stopTimeout?: number;
40
40
  /**
41
- * - Initial log level for ServiceManager
41
+ * - Default log level for new services
42
42
  */
43
- logLevel?: string;
43
+ defaultLogLevel?: string;
44
44
  /**
45
- * - Logging configuration
46
- */
47
- logConfig?: LogConfig;
48
- };
49
- /**
50
- * Logging configuration
51
- */
52
- export type LogConfig = {
53
- /**
54
- * - Default log level for services
55
- */
56
- defaultLevel?: string;
57
- /**
58
- * - Override level for all services
45
+ * - Initial log level for ServiceManager
59
46
  */
60
- globalLevel?: string;
47
+ managerLogLevel?: string;
61
48
  /**
62
- * - Per-service log levels
49
+ * Per-service log levels:
50
+ * - String: "auth:debug,database:info"
51
+ * - Object: { auth: "debug", database: "info" }
63
52
  */
64
- serviceLevels?: {
53
+ serviceLogLevels?: string | {
65
54
  [x: string]: string;
66
55
  };
67
56
  };
@@ -52,17 +52,12 @@
52
52
  * @property {boolean} [debug=false] - Debug mode switch
53
53
  * @property {boolean} [autoStart=false] - Auto-start services on registration
54
54
  * @property {number} [stopTimeout=10000] - Default timeout for stopping services
55
- * @property {string} [logLevel] - Initial log level for ServiceManager
56
- * @property {LogConfig} [logConfig={}] - Logging configuration
57
- */
58
-
59
- /**
60
- * Logging configuration
61
- *
62
- * @typedef {Object} LogConfig
63
- * @property {string} [defaultLevel] - Default log level for services
64
- * @property {string} [globalLevel] - Override level for all services
65
- * @property {Object<string, string>} [serviceLevels] - Per-service log levels
55
+ * @property {string} [defaultLogLevel] - Default log level for new services
56
+ * @property {string} [managerLogLevel] - Initial log level for ServiceManager
57
+ * @property {string|Object<string,string>} [serviceLogLevels]
58
+ * Per-service log levels:
59
+ * - String: "auth:debug,database:info"
60
+ * - Object: { auth: "debug", database: "info" }
66
61
  */
67
62
 
68
63
  /**
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Parse comma-separated service:level configuration string
3
+ *
4
+ * @param {string} configString
5
+ * Comma-separated string like "auth:debug,database:info,cache:warn"
6
+ *
7
+ * @returns {Object<string, string>} Service name to log level mapping
8
+ *
9
+ * @example
10
+ * const config = parseServiceLogLevels("auth:debug,database:info");
11
+ * // Returns: { auth: "debug", database: "info" }
12
+ */
13
+ export function parseServiceLogLevels(configString: string): {
14
+ [x: string]: string;
15
+ };
16
+ /**
17
+ * Expand log levels to include higher severity levels
18
+ *
19
+ * @param {{[name:string]: string}} serviceLevels
20
+ * Service name to log level mapping
21
+ *
22
+ * @returns {Object<string, string[]>} Service name to array of log levels
23
+ *
24
+ * @example
25
+ * const levels = expandLogLevels({ auth: "debug", cache: "warn" });
26
+ * // Returns: {
27
+ * // auth: ["debug", "info", "warn", "error"],
28
+ * // cache: ["warn", "error"]
29
+ * // }
30
+ */
31
+ export function expandLogLevels(serviceLevels: {
32
+ [name: string]: string;
33
+ }): {
34
+ [x: string]: string[];
35
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @fileoverview Service Manager utility functions
3
+ *
4
+ * Provides utility functions for parsing service configurations, handling
5
+ * log level hierarchies, and managing service-specific operations.
6
+ */
7
+
8
+ import { DEBUG, INFO, WARN, ERROR } from '../../logging/index.js';
9
+
10
+ /**
11
+ * Parse comma-separated service:level configuration string
12
+ *
13
+ * @param {string} configString
14
+ * Comma-separated string like "auth:debug,database:info,cache:warn"
15
+ *
16
+ * @returns {Object<string, string>} Service name to log level mapping
17
+ *
18
+ * @example
19
+ * const config = parseServiceLogLevels("auth:debug,database:info");
20
+ * // Returns: { auth: "debug", database: "info" }
21
+ */
22
+ export function parseServiceLogLevels(configString) {
23
+ if (!configString || typeof configString !== 'string') {
24
+ /** @type {Object<string, string>} */
25
+ return {};
26
+ }
27
+
28
+ /** @type {Object<string, string>} */
29
+ const result = {};
30
+
31
+ const services = configString.split(',');
32
+
33
+ for (const serviceExpression of services) {
34
+ const trimmed = serviceExpression.trim();
35
+ if (!trimmed) continue;
36
+
37
+ const parts = trimmed.split(':');
38
+ if (parts.length === 2) {
39
+ const [serviceName, logLevel] = parts;
40
+ result[serviceName.trim()] = logLevel.trim();
41
+ }
42
+ }
43
+
44
+ return result;
45
+ }
46
+
47
+ /**
48
+ * Expand log levels to include higher severity levels
49
+ *
50
+ * @param {{[name:string]: string}} serviceLevels
51
+ * Service name to log level mapping
52
+ *
53
+ * @returns {Object<string, string[]>} Service name to array of log levels
54
+ *
55
+ * @example
56
+ * const levels = expandLogLevels({ auth: "debug", cache: "warn" });
57
+ * // Returns: {
58
+ * // auth: ["debug", "info", "warn", "error"],
59
+ * // cache: ["warn", "error"]
60
+ * // }
61
+ */
62
+ export function expandLogLevels(serviceLevels) {
63
+ /** @type {Object<string, string[]>} */
64
+ const result = {};
65
+
66
+ for (const [serviceName, level] of Object.entries(serviceLevels)) {
67
+ const levels = [];
68
+
69
+ switch (level.toLowerCase()) {
70
+ case DEBUG:
71
+ levels.push(DEBUG, INFO, WARN, ERROR);
72
+ break;
73
+ case INFO:
74
+ levels.push(INFO, WARN, ERROR);
75
+ break;
76
+ case WARN:
77
+ levels.push(WARN, ERROR);
78
+ break;
79
+ case ERROR:
80
+ levels.push(ERROR);
81
+ break;
82
+ default:
83
+ levels.push(level);
84
+ }
85
+
86
+ result[serviceName] = levels;
87
+ }
88
+
89
+ return result;
90
+ }
@@ -6,10 +6,10 @@
6
6
  * Event emitted when entering a state
7
7
  * @type {string}
8
8
  */
9
- export const ENTER = 'enter';
9
+ export const ENTER = '_enter';
10
10
 
11
11
  /**
12
12
  * Event emitted when exiting a state
13
13
  * @type {string}
14
14
  */
15
- export const EXIT = 'exit';
15
+ export const EXIT = '_exit';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"
@@ -52,6 +52,7 @@
52
52
  },
53
53
  "files": [
54
54
  "dist",
55
+ "CLAUDE.md",
55
56
  "!dist/**/*.test.*",
56
57
  "!dist/**/*.spec.*",
57
58
  "!dist/**/testdata.*",
@@ -76,13 +77,17 @@
76
77
  "@skeletonlabs/skeleton": "^3.1.7",
77
78
  "@steeze-ui/heroicons": "^2.4.2",
78
79
  "@sveltejs/kit": "^2.28.0",
80
+ "@tailwindcss/postcss": "^4.1.11",
81
+ "autoprefixer": "^10.4.21",
79
82
  "eslint-plugin-import": "^2.32.0",
80
83
  "jsonwebtoken": "^9.0.0",
81
84
  "pino": "^9.8.0",
82
85
  "pino-pretty": "^13.1.1",
86
+ "postcss": "^8.5.6",
83
87
  "runed": "^0.31.1",
84
88
  "svelte": "^5.38.1",
85
89
  "svelte-preprocess": "^6.0.3",
90
+ "tailwindcss": "^4.1.11",
86
91
  "valibot": "^1.1.0",
87
92
  "vite-imagetools": "^8.0.0"
88
93
  },
@@ -98,6 +103,7 @@
98
103
  "@tailwindcss/typography": "^0.5.16",
99
104
  "@testing-library/svelte": "^5.2.8",
100
105
  "@testing-library/user-event": "^14.6.1",
106
+ "@tailwindcss/postcss": "^4.1.11",
101
107
  "@types/eslint": "^9.6.1",
102
108
  "@types/node": "^24.2.1",
103
109
  "autoprefixer": "^10.4.21",
@@ -128,8 +134,5 @@
128
134
  "vite": "^7.1.2",
129
135
  "vite-imagetools": "^8.0.0",
130
136
  "vitest": "^3.2.4"
131
- },
132
- "dependencies": {
133
- "@tailwindcss/postcss": "^4.1.11"
134
137
  }
135
138
  }
File without changes
File without changes