@htlkg/core 0.0.2 → 0.0.3
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 +51 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -8
- package/src/amplify-astro-adapter/amplify-astro-adapter.md +167 -0
- package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.test.ts +296 -0
- package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.ts +97 -0
- package/src/amplify-astro-adapter/createRunWithAmplifyServerContext.ts +84 -0
- package/src/amplify-astro-adapter/errors.test.ts +115 -0
- package/src/amplify-astro-adapter/errors.ts +105 -0
- package/src/amplify-astro-adapter/globalSettings.test.ts +78 -0
- package/src/amplify-astro-adapter/globalSettings.ts +16 -0
- package/src/amplify-astro-adapter/index.ts +14 -0
- package/src/amplify-astro-adapter/types.ts +55 -0
- package/src/auth/auth.md +178 -0
- package/src/auth/index.test.ts +180 -0
- package/src/auth/index.ts +294 -0
- package/src/constants/constants.md +132 -0
- package/src/constants/index.test.ts +116 -0
- package/src/constants/index.ts +98 -0
- package/src/core-exports.property.test.ts +186 -0
- package/src/errors/errors.md +177 -0
- package/src/errors/index.test.ts +153 -0
- package/src/errors/index.ts +134 -0
- package/src/index.ts +65 -0
- package/src/routes/index.ts +225 -0
- package/src/routes/routes.md +189 -0
- package/src/types/index.ts +94 -0
- package/src/types/types.md +144 -0
- package/src/utils/index.test.ts +257 -0
- package/src/utils/index.ts +112 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/utils.md +199 -0
- package/src/workspace.property.test.ts +235 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @htlkg/core/utils
|
|
3
|
+
* Core utility functions for Hotelinking applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Logger utility
|
|
7
|
+
export { logger, createLogger } from './logger';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format a date to a localized string
|
|
11
|
+
* @param date - Date to format
|
|
12
|
+
* @param locale - Locale string (default: 'en-US')
|
|
13
|
+
* @param options - Intl.DateTimeFormatOptions
|
|
14
|
+
* @returns Formatted date string
|
|
15
|
+
*/
|
|
16
|
+
export function formatDate(
|
|
17
|
+
date: Date | string | number,
|
|
18
|
+
locale = "en-US",
|
|
19
|
+
options: Intl.DateTimeFormatOptions = {
|
|
20
|
+
year: "numeric",
|
|
21
|
+
month: "short",
|
|
22
|
+
day: "numeric",
|
|
23
|
+
},
|
|
24
|
+
): string {
|
|
25
|
+
const dateObj = typeof date === "string" || typeof date === "number" ? new Date(date) : date;
|
|
26
|
+
return new Intl.DateTimeFormat(locale, options).format(dateObj);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Truncate text to a maximum length with ellipsis
|
|
31
|
+
* @param text - Text to truncate
|
|
32
|
+
* @param maxLength - Maximum length before truncation
|
|
33
|
+
* @param suffix - Suffix to append (default: '...')
|
|
34
|
+
* @returns Truncated text
|
|
35
|
+
*/
|
|
36
|
+
export function truncateText(
|
|
37
|
+
text: string,
|
|
38
|
+
maxLength: number,
|
|
39
|
+
suffix = "...",
|
|
40
|
+
): string {
|
|
41
|
+
if (text.length <= maxLength) return text;
|
|
42
|
+
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Group an array of items by a key
|
|
47
|
+
* @param items - Array of items to group
|
|
48
|
+
* @param keyFn - Function to extract the grouping key
|
|
49
|
+
* @returns Object with keys as group names and values as arrays of items
|
|
50
|
+
*/
|
|
51
|
+
export function groupBy<T>(
|
|
52
|
+
items: T[],
|
|
53
|
+
keyFn: (item: T) => string | number,
|
|
54
|
+
): Record<string | number, T[]> {
|
|
55
|
+
return items.reduce(
|
|
56
|
+
(groups, item) => {
|
|
57
|
+
const key = keyFn(item);
|
|
58
|
+
if (!groups[key]) {
|
|
59
|
+
groups[key] = [];
|
|
60
|
+
}
|
|
61
|
+
groups[key].push(item);
|
|
62
|
+
return groups;
|
|
63
|
+
},
|
|
64
|
+
{} as Record<string | number, T[]>,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Debounce a function call
|
|
70
|
+
* @param fn - Function to debounce
|
|
71
|
+
* @param delay - Delay in milliseconds
|
|
72
|
+
* @returns Debounced function
|
|
73
|
+
*/
|
|
74
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
75
|
+
fn: T,
|
|
76
|
+
delay: number,
|
|
77
|
+
): (...args: Parameters<T>) => void {
|
|
78
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
79
|
+
|
|
80
|
+
return function debounced(...args: Parameters<T>) {
|
|
81
|
+
if (timeoutId !== null) {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
}
|
|
84
|
+
timeoutId = setTimeout(() => {
|
|
85
|
+
fn(...args);
|
|
86
|
+
timeoutId = null;
|
|
87
|
+
}, delay);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Throttle a function call
|
|
93
|
+
* @param fn - Function to throttle
|
|
94
|
+
* @param limit - Minimum time between calls in milliseconds
|
|
95
|
+
* @returns Throttled function
|
|
96
|
+
*/
|
|
97
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
98
|
+
fn: T,
|
|
99
|
+
limit: number,
|
|
100
|
+
): (...args: Parameters<T>) => void {
|
|
101
|
+
let inThrottle = false;
|
|
102
|
+
|
|
103
|
+
return function throttled(...args: Parameters<T>) {
|
|
104
|
+
if (!inThrottle) {
|
|
105
|
+
fn(...args);
|
|
106
|
+
inThrottle = true;
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
inThrottle = false;
|
|
109
|
+
}, limit);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple debug logger utility
|
|
3
|
+
*
|
|
4
|
+
* Debug logs are only shown when DEBUG=true or HTLKG_DEBUG=true is set in environment.
|
|
5
|
+
* Info logs are always shown.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { logger } from '@htlkg/core/utils/logger';
|
|
10
|
+
*
|
|
11
|
+
* logger.debug('server-client', 'Detailed debug info', { data });
|
|
12
|
+
* logger.info('server-client', 'Important info message');
|
|
13
|
+
* logger.warn('server-client', 'Warning message');
|
|
14
|
+
* logger.error('server-client', 'Error occurred', error);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
19
|
+
|
|
20
|
+
const isDebugEnabled = (): boolean => {
|
|
21
|
+
// Check Node.js environment variables
|
|
22
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
23
|
+
return process.env.DEBUG === 'true' ||
|
|
24
|
+
process.env.HTLKG_DEBUG === 'true' ||
|
|
25
|
+
process.env.DEBUG === '*';
|
|
26
|
+
}
|
|
27
|
+
// Browser/Vite environment - check for DEV mode
|
|
28
|
+
try {
|
|
29
|
+
// @ts-ignore - import.meta.env is Vite-specific
|
|
30
|
+
if (typeof import.meta !== 'undefined' && import.meta.env) {
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
return import.meta.env.DEBUG === 'true' ||
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
import.meta.env.HTLKG_DEBUG === 'true' ||
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
import.meta.env.DEV === true;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Ignore errors accessing import.meta
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const formatMessage = (namespace: string, message: string): string => {
|
|
45
|
+
return `[${namespace}] ${message}`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const logger = {
|
|
49
|
+
/**
|
|
50
|
+
* Debug log - only shown when DEBUG=true
|
|
51
|
+
*/
|
|
52
|
+
debug(namespace: string, message: string, ...args: unknown[]): void {
|
|
53
|
+
if (isDebugEnabled()) {
|
|
54
|
+
console.log(formatMessage(namespace, message), ...args);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Info log - always shown
|
|
60
|
+
*/
|
|
61
|
+
info(namespace: string, message: string, ...args: unknown[]): void {
|
|
62
|
+
console.info(formatMessage(namespace, message), ...args);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Warning log - always shown
|
|
67
|
+
*/
|
|
68
|
+
warn(namespace: string, message: string, ...args: unknown[]): void {
|
|
69
|
+
console.warn(formatMessage(namespace, message), ...args);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Error log - always shown
|
|
74
|
+
*/
|
|
75
|
+
error(namespace: string, message: string, ...args: unknown[]): void {
|
|
76
|
+
console.error(formatMessage(namespace, message), ...args);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a namespaced logger for a specific module
|
|
82
|
+
*/
|
|
83
|
+
export const createLogger = (namespace: string) => ({
|
|
84
|
+
debug: (message: string, ...args: unknown[]) => logger.debug(namespace, message, ...args),
|
|
85
|
+
info: (message: string, ...args: unknown[]) => logger.info(namespace, message, ...args),
|
|
86
|
+
warn: (message: string, ...args: unknown[]) => logger.warn(namespace, message, ...args),
|
|
87
|
+
error: (message: string, ...args: unknown[]) => logger.error(namespace, message, ...args),
|
|
88
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Utils Module
|
|
2
|
+
|
|
3
|
+
Common utility functions for formatting, data manipulation, and function control.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
logger,
|
|
10
|
+
createLogger,
|
|
11
|
+
formatDate,
|
|
12
|
+
truncateText,
|
|
13
|
+
groupBy,
|
|
14
|
+
debounce,
|
|
15
|
+
throttle,
|
|
16
|
+
} from '@htlkg/core/utils';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Logger
|
|
20
|
+
|
|
21
|
+
Debug-aware logging utility.
|
|
22
|
+
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { logger } from '@htlkg/core/utils';
|
|
27
|
+
|
|
28
|
+
logger.debug('namespace', 'Debug message', { data }); // Only when DEBUG=true
|
|
29
|
+
logger.info('namespace', 'Info message'); // Always shown
|
|
30
|
+
logger.warn('namespace', 'Warning message'); // Always shown
|
|
31
|
+
logger.error('namespace', 'Error message', error); // Always shown
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Namespaced Logger
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { createLogger } from '@htlkg/core/utils';
|
|
38
|
+
|
|
39
|
+
const log = createLogger('my-module');
|
|
40
|
+
|
|
41
|
+
log.debug('Processing started');
|
|
42
|
+
log.info('Operation complete');
|
|
43
|
+
log.warn('Deprecated feature used');
|
|
44
|
+
log.error('Operation failed', error);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Enabling Debug Logs
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Node.js
|
|
51
|
+
DEBUG=true node app.js
|
|
52
|
+
HTLKG_DEBUG=true node app.js
|
|
53
|
+
|
|
54
|
+
# Vite (in .env)
|
|
55
|
+
DEBUG=true
|
|
56
|
+
HTLKG_DEBUG=true
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## formatDate
|
|
60
|
+
|
|
61
|
+
Format dates with locale support.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { formatDate } from '@htlkg/core/utils';
|
|
65
|
+
|
|
66
|
+
formatDate(new Date());
|
|
67
|
+
// "Dec 30, 2024"
|
|
68
|
+
|
|
69
|
+
formatDate('2024-01-15', 'es-ES');
|
|
70
|
+
// "15 ene 2024"
|
|
71
|
+
|
|
72
|
+
formatDate(1704067200000, 'en-US', {
|
|
73
|
+
year: 'numeric',
|
|
74
|
+
month: 'long',
|
|
75
|
+
day: 'numeric',
|
|
76
|
+
hour: '2-digit',
|
|
77
|
+
minute: '2-digit',
|
|
78
|
+
});
|
|
79
|
+
// "January 1, 2024, 12:00 AM"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Parameters
|
|
83
|
+
|
|
84
|
+
| Param | Type | Default | Description |
|
|
85
|
+
|-------|------|---------|-------------|
|
|
86
|
+
| `date` | `Date \| string \| number` | - | Date to format |
|
|
87
|
+
| `locale` | `string` | `'en-US'` | Locale string |
|
|
88
|
+
| `options` | `Intl.DateTimeFormatOptions` | `{ year, month, day }` | Format options |
|
|
89
|
+
|
|
90
|
+
## truncateText
|
|
91
|
+
|
|
92
|
+
Truncate text with ellipsis.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { truncateText } from '@htlkg/core/utils';
|
|
96
|
+
|
|
97
|
+
truncateText('Hello World', 8);
|
|
98
|
+
// "Hello..."
|
|
99
|
+
|
|
100
|
+
truncateText('Short', 10);
|
|
101
|
+
// "Short"
|
|
102
|
+
|
|
103
|
+
truncateText('Long text here', 10, ' →');
|
|
104
|
+
// "Long text →"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Parameters
|
|
108
|
+
|
|
109
|
+
| Param | Type | Default | Description |
|
|
110
|
+
|-------|------|---------|-------------|
|
|
111
|
+
| `text` | `string` | - | Text to truncate |
|
|
112
|
+
| `maxLength` | `number` | - | Max length including suffix |
|
|
113
|
+
| `suffix` | `string` | `'...'` | Suffix to append |
|
|
114
|
+
|
|
115
|
+
## groupBy
|
|
116
|
+
|
|
117
|
+
Group array items by a key.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { groupBy } from '@htlkg/core/utils';
|
|
121
|
+
|
|
122
|
+
const users = [
|
|
123
|
+
{ name: 'Alice', role: 'admin' },
|
|
124
|
+
{ name: 'Bob', role: 'user' },
|
|
125
|
+
{ name: 'Carol', role: 'admin' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
groupBy(users, user => user.role);
|
|
129
|
+
// {
|
|
130
|
+
// admin: [{ name: 'Alice', ... }, { name: 'Carol', ... }],
|
|
131
|
+
// user: [{ name: 'Bob', ... }]
|
|
132
|
+
// }
|
|
133
|
+
|
|
134
|
+
// Group by numeric key
|
|
135
|
+
const items = [
|
|
136
|
+
{ id: 1, category: 10 },
|
|
137
|
+
{ id: 2, category: 20 },
|
|
138
|
+
{ id: 3, category: 10 },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
groupBy(items, item => item.category);
|
|
142
|
+
// { 10: [...], 20: [...] }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## debounce
|
|
146
|
+
|
|
147
|
+
Delay function execution until after a pause in calls.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { debounce } from '@htlkg/core/utils';
|
|
151
|
+
|
|
152
|
+
const search = debounce((query: string) => {
|
|
153
|
+
console.log('Searching:', query);
|
|
154
|
+
}, 300);
|
|
155
|
+
|
|
156
|
+
// Rapid calls
|
|
157
|
+
search('h');
|
|
158
|
+
search('he');
|
|
159
|
+
search('hel');
|
|
160
|
+
search('hello');
|
|
161
|
+
// Only logs "Searching: hello" after 300ms pause
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Use Cases
|
|
165
|
+
|
|
166
|
+
- Search input handlers
|
|
167
|
+
- Window resize handlers
|
|
168
|
+
- Form auto-save
|
|
169
|
+
|
|
170
|
+
## throttle
|
|
171
|
+
|
|
172
|
+
Limit function execution to once per time period.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { throttle } from '@htlkg/core/utils';
|
|
176
|
+
|
|
177
|
+
const handleScroll = throttle(() => {
|
|
178
|
+
console.log('Scroll position:', window.scrollY);
|
|
179
|
+
}, 100);
|
|
180
|
+
|
|
181
|
+
window.addEventListener('scroll', handleScroll);
|
|
182
|
+
// Logs at most once every 100ms during scrolling
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Use Cases
|
|
186
|
+
|
|
187
|
+
- Scroll event handlers
|
|
188
|
+
- Mouse move handlers
|
|
189
|
+
- API rate limiting
|
|
190
|
+
|
|
191
|
+
## Comparison: debounce vs throttle
|
|
192
|
+
|
|
193
|
+
| Scenario | debounce | throttle |
|
|
194
|
+
|----------|----------|----------|
|
|
195
|
+
| Search input | ✓ Wait for typing pause | ✗ |
|
|
196
|
+
| Scroll tracking | ✗ | ✓ Regular updates |
|
|
197
|
+
| Window resize | ✓ Final size only | ✗ |
|
|
198
|
+
| Button spam prevention | ✗ | ✓ First click works |
|
|
199
|
+
| Auto-save | ✓ After editing stops | ✗ |
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature: htlkg-modular-architecture, Property 2: Workspace dependencies resolve correctly
|
|
3
|
+
*
|
|
4
|
+
* This test validates that workspace dependencies resolve correctly across all modules.
|
|
5
|
+
* For any module that depends on another workspace module, importing from the dependency
|
|
6
|
+
* should resolve to the local workspace package, not an external npm package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import fc from "fast-check";
|
|
11
|
+
import { readFileSync, existsSync } from "fs";
|
|
12
|
+
import { join, dirname } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// Get the workspace root (shared/)
|
|
19
|
+
const workspaceRoot = join(__dirname, "../../..");
|
|
20
|
+
|
|
21
|
+
interface PackageJson {
|
|
22
|
+
name: string;
|
|
23
|
+
dependencies?: Record<string, string>;
|
|
24
|
+
devDependencies?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper to read package.json
|
|
28
|
+
function readPackageJson(packagePath: string): PackageJson {
|
|
29
|
+
const pkgPath = join(workspaceRoot, packagePath, "package.json");
|
|
30
|
+
if (!existsSync(pkgPath)) {
|
|
31
|
+
throw new Error(`Package.json not found at ${pkgPath}`);
|
|
32
|
+
}
|
|
33
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to check if a dependency uses workspace protocol
|
|
37
|
+
function usesWorkspaceProtocol(version: string): boolean {
|
|
38
|
+
return version.startsWith("workspace:");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get all workspace packages
|
|
42
|
+
const workspacePackages = [
|
|
43
|
+
"packages/core",
|
|
44
|
+
"packages/data",
|
|
45
|
+
"packages/components",
|
|
46
|
+
"packages/astro",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
describe("Workspace Structure Property Tests", () => {
|
|
50
|
+
/**
|
|
51
|
+
* Property 2: Workspace dependencies resolve correctly
|
|
52
|
+
* Validates: Requirements 1.4, 7.1, 7.2, 7.3, 7.4
|
|
53
|
+
*
|
|
54
|
+
* Note: Packages use version numbers (e.g., ^0.0.1) instead of workspace protocol
|
|
55
|
+
* because they are published to npm. This is valid for published packages.
|
|
56
|
+
*/
|
|
57
|
+
it("should have valid internal dependencies", () => {
|
|
58
|
+
// For each package in the workspace
|
|
59
|
+
for (const packagePath of workspacePackages) {
|
|
60
|
+
const pkg = readPackageJson(packagePath);
|
|
61
|
+
|
|
62
|
+
// Check all dependencies
|
|
63
|
+
const allDeps = {
|
|
64
|
+
...pkg.dependencies,
|
|
65
|
+
...pkg.devDependencies,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// For each dependency
|
|
69
|
+
for (const [depName, depVersion] of Object.entries(allDeps)) {
|
|
70
|
+
// If it's an @htlkg package (internal workspace package)
|
|
71
|
+
if (depName.startsWith("@htlkg/")) {
|
|
72
|
+
// It should have a valid version (either workspace protocol or semver)
|
|
73
|
+
const isValid = usesWorkspaceProtocol(depVersion) || /^\^?\d+\.\d+\.\d+/.test(depVersion);
|
|
74
|
+
expect(
|
|
75
|
+
isValid,
|
|
76
|
+
`Package ${pkg.name} should have valid version for ${depName}, but got ${depVersion}`,
|
|
77
|
+
).toBe(true);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should have valid package structure for all workspace packages", () => {
|
|
84
|
+
for (const packagePath of workspacePackages) {
|
|
85
|
+
const pkg = readPackageJson(packagePath);
|
|
86
|
+
|
|
87
|
+
// Each package should have a name
|
|
88
|
+
expect(pkg.name).toBeDefined();
|
|
89
|
+
expect(pkg.name).toMatch(/^@htlkg\//);
|
|
90
|
+
|
|
91
|
+
// Each package should have a src directory
|
|
92
|
+
const srcPath = join(workspaceRoot, packagePath, "src");
|
|
93
|
+
expect(
|
|
94
|
+
existsSync(srcPath),
|
|
95
|
+
`Package ${pkg.name} should have a src directory`,
|
|
96
|
+
).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Each package should have an index.ts
|
|
99
|
+
const indexPath = join(srcPath, "index.ts");
|
|
100
|
+
expect(
|
|
101
|
+
existsSync(indexPath),
|
|
102
|
+
`Package ${pkg.name} should have an index.ts file`,
|
|
103
|
+
).toBe(true);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should have correct TypeScript configuration for all packages", () => {
|
|
108
|
+
for (const packagePath of workspacePackages) {
|
|
109
|
+
const tsconfigPath = join(workspaceRoot, packagePath, "tsconfig.json");
|
|
110
|
+
|
|
111
|
+
expect(
|
|
112
|
+
existsSync(tsconfigPath),
|
|
113
|
+
`Package at ${packagePath} should have a tsconfig.json`,
|
|
114
|
+
).toBe(true);
|
|
115
|
+
|
|
116
|
+
const tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
|
|
117
|
+
|
|
118
|
+
// Should have compiler options
|
|
119
|
+
expect(tsconfig.compilerOptions).toBeDefined();
|
|
120
|
+
|
|
121
|
+
// Should have proper output directory (if it has one)
|
|
122
|
+
if (tsconfig.compilerOptions.outDir) {
|
|
123
|
+
expect(tsconfig.compilerOptions.outDir).toMatch(/\.\/dist|\.\/build/);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should have correct build configuration for all packages", () => {
|
|
129
|
+
for (const packagePath of workspacePackages) {
|
|
130
|
+
const pkg = readPackageJson(packagePath);
|
|
131
|
+
|
|
132
|
+
// Should have build script
|
|
133
|
+
expect(pkg.scripts?.build, `Package ${pkg.name} should have a build script`).toBeDefined();
|
|
134
|
+
|
|
135
|
+
// Should have dev script
|
|
136
|
+
expect(pkg.scripts?.dev, `Package ${pkg.name} should have a dev script`).toBeDefined();
|
|
137
|
+
|
|
138
|
+
// Should have test scripts
|
|
139
|
+
expect(pkg.scripts?.test, `Package ${pkg.name} should have a test script`).toBeDefined();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Property-based test: Dependency graph should be acyclic
|
|
145
|
+
* This ensures no circular dependencies exist
|
|
146
|
+
*/
|
|
147
|
+
it("should not have circular dependencies", () => {
|
|
148
|
+
// Build dependency graph
|
|
149
|
+
const graph = new Map<string, Set<string>>();
|
|
150
|
+
|
|
151
|
+
for (const packagePath of workspacePackages) {
|
|
152
|
+
const pkg = readPackageJson(packagePath);
|
|
153
|
+
const deps = new Set<string>();
|
|
154
|
+
|
|
155
|
+
// Collect all @htlkg dependencies
|
|
156
|
+
const allDeps = {
|
|
157
|
+
...pkg.dependencies,
|
|
158
|
+
...pkg.devDependencies,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const depName of Object.keys(allDeps)) {
|
|
162
|
+
if (depName.startsWith("@htlkg/")) {
|
|
163
|
+
deps.add(depName);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
graph.set(pkg.name, deps);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for cycles using DFS
|
|
171
|
+
function hasCycle(
|
|
172
|
+
node: string,
|
|
173
|
+
visited: Set<string>,
|
|
174
|
+
recStack: Set<string>,
|
|
175
|
+
): boolean {
|
|
176
|
+
visited.add(node);
|
|
177
|
+
recStack.add(node);
|
|
178
|
+
|
|
179
|
+
const neighbors = graph.get(node) || new Set();
|
|
180
|
+
for (const neighbor of neighbors) {
|
|
181
|
+
if (!visited.has(neighbor)) {
|
|
182
|
+
if (hasCycle(neighbor, visited, recStack)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
} else if (recStack.has(neighbor)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
recStack.delete(node);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const visited = new Set<string>();
|
|
195
|
+
for (const node of graph.keys()) {
|
|
196
|
+
if (!visited.has(node)) {
|
|
197
|
+
const recStack = new Set<string>();
|
|
198
|
+
expect(
|
|
199
|
+
hasCycle(node, visited, recStack),
|
|
200
|
+
`Circular dependency detected starting from ${node}`,
|
|
201
|
+
).toBe(false);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Property-based test: All exports should be defined
|
|
208
|
+
*/
|
|
209
|
+
it("should have all declared exports as files", () => {
|
|
210
|
+
for (const packagePath of workspacePackages) {
|
|
211
|
+
const pkg = readPackageJson(packagePath);
|
|
212
|
+
|
|
213
|
+
if (!pkg.exports) continue;
|
|
214
|
+
|
|
215
|
+
// Check each export path
|
|
216
|
+
for (const [exportKey, exportValue] of Object.entries(pkg.exports)) {
|
|
217
|
+
if (typeof exportValue === "string") {
|
|
218
|
+
// Skip wildcard exports as they represent patterns, not specific files
|
|
219
|
+
if (exportValue.includes("*")) continue;
|
|
220
|
+
|
|
221
|
+
// Simple export
|
|
222
|
+
const exportPath = join(
|
|
223
|
+
workspaceRoot,
|
|
224
|
+
packagePath,
|
|
225
|
+
exportValue.replace("./dist/", "src/").replace(".js", ".ts"),
|
|
226
|
+
);
|
|
227
|
+
expect(
|
|
228
|
+
existsSync(exportPath),
|
|
229
|
+
`Export ${exportKey} in ${pkg.name} should have corresponding source file at ${exportPath}`,
|
|
230
|
+
).toBe(true);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|