@htlkg/astro 0.0.1
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 +265 -0
- package/dist/chunk-33R4URZV.js +59 -0
- package/dist/chunk-33R4URZV.js.map +1 -0
- package/dist/chunk-64USRLVP.js +85 -0
- package/dist/chunk-64USRLVP.js.map +1 -0
- package/dist/chunk-WLOFOVCL.js +210 -0
- package/dist/chunk-WLOFOVCL.js.map +1 -0
- package/dist/chunk-WNMPTDCR.js +73 -0
- package/dist/chunk-WNMPTDCR.js.map +1 -0
- package/dist/chunk-Z2ZAL7KX.js +9 -0
- package/dist/chunk-Z2ZAL7KX.js.map +1 -0
- package/dist/chunk-ZQ4XMJH7.js +1 -0
- package/dist/chunk-ZQ4XMJH7.js.map +1 -0
- package/dist/htlkg/config.js +7 -0
- package/dist/htlkg/config.js.map +1 -0
- package/dist/htlkg/index.js +7 -0
- package/dist/htlkg/index.js.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.js +168 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/utils/hydration.js +21 -0
- package/dist/utils/hydration.js.map +1 -0
- package/dist/utils/index.js +56 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/ssr.js +21 -0
- package/dist/utils/ssr.js.map +1 -0
- package/dist/utils/static.js +19 -0
- package/dist/utils/static.js.map +1 -0
- package/package.json +53 -0
- package/src/__mocks__/astro-middleware.ts +19 -0
- package/src/__mocks__/virtual-htlkg-config.ts +14 -0
- package/src/auth/LoginForm.vue +482 -0
- package/src/auth/LoginPage.astro +70 -0
- package/src/components/PageHeader.astro +145 -0
- package/src/components/Sidebar.astro +157 -0
- package/src/components/Topbar.astro +167 -0
- package/src/components/index.ts +9 -0
- package/src/htlkg/config.test.ts +165 -0
- package/src/htlkg/config.ts +242 -0
- package/src/htlkg/index.ts +245 -0
- package/src/htlkg/virtual-modules.test.ts +158 -0
- package/src/htlkg/virtual-modules.ts +81 -0
- package/src/index.ts +37 -0
- package/src/layouts/AdminLayout.astro +184 -0
- package/src/layouts/AuthLayout.astro +164 -0
- package/src/layouts/BrandLayout.astro +309 -0
- package/src/layouts/DefaultLayout.astro +25 -0
- package/src/layouts/PublicLayout.astro +153 -0
- package/src/layouts/index.ts +10 -0
- package/src/middleware/auth.ts +53 -0
- package/src/middleware/index.ts +31 -0
- package/src/middleware/route-guards.test.ts +182 -0
- package/src/middleware/route-guards.ts +218 -0
- package/src/patterns/admin/DetailPage.astro +195 -0
- package/src/patterns/admin/FormPage.astro +203 -0
- package/src/patterns/admin/ListPage.astro +178 -0
- package/src/patterns/admin/index.ts +9 -0
- package/src/patterns/brand/ConfigPage.astro +128 -0
- package/src/patterns/brand/PortalPage.astro +161 -0
- package/src/patterns/brand/index.ts +8 -0
- package/src/patterns/index.ts +8 -0
- package/src/utils/hydration.test.ts +154 -0
- package/src/utils/hydration.ts +151 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/ssr.test.ts +235 -0
- package/src/utils/ssr.ts +139 -0
- package/src/utils/static.test.ts +144 -0
- package/src/utils/static.ts +144 -0
- package/src/vue-app-setup.ts +88 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration types for the htlkg Astro integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AuthUser } from '@htlkg/core/types';
|
|
6
|
+
|
|
7
|
+
// Re-export AuthUser for convenience
|
|
8
|
+
export type { AuthUser };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Route pattern that can be either a RegExp or a string.
|
|
12
|
+
* - String patterns match exact paths or paths that start with the pattern
|
|
13
|
+
* - RegExp patterns allow for more complex matching logic
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // String pattern - matches /login and /login/*
|
|
17
|
+
* '/login'
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // RegExp pattern - matches /api/v1/* and /api/v2/*
|
|
21
|
+
* /^\/api\/v[12]\//
|
|
22
|
+
*/
|
|
23
|
+
export type RoutePattern = RegExp | string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration for brand-specific routes that require brand ID extraction.
|
|
27
|
+
* The pattern should include a capture group for the brand ID.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* {
|
|
31
|
+
* pattern: /^\/brands\/(\d+)/,
|
|
32
|
+
* brandIdParam: 1 // First capture group
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
export interface BrandRouteConfig {
|
|
36
|
+
/**
|
|
37
|
+
* Regular expression pattern with a capture group for the brand ID
|
|
38
|
+
*/
|
|
39
|
+
pattern: RegExp;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Index of the capture group containing the brand ID (1-based)
|
|
43
|
+
*/
|
|
44
|
+
brandIdParam: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for the default login page injected by the integration.
|
|
49
|
+
*/
|
|
50
|
+
export interface LoginPageConfig {
|
|
51
|
+
/**
|
|
52
|
+
* URL path where the login page should be accessible.
|
|
53
|
+
* @default '/login'
|
|
54
|
+
*/
|
|
55
|
+
path?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Logo URL to display on the login page.
|
|
59
|
+
* @default undefined
|
|
60
|
+
*/
|
|
61
|
+
logo?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Title text to display on the login page.
|
|
65
|
+
* @default 'Sign In'
|
|
66
|
+
*/
|
|
67
|
+
title?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* URL to redirect to after successful login.
|
|
71
|
+
* @default '/admin'
|
|
72
|
+
*/
|
|
73
|
+
redirectUrl?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Configuration for route guards that protect routes based on authentication
|
|
78
|
+
* status and user permissions.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* {
|
|
82
|
+
* publicRoutes: ['/login', '/register', /^\/public\//],
|
|
83
|
+
* authenticatedRoutes: ['/dashboard', /^\/profile/],
|
|
84
|
+
* adminRoutes: ['/admin', /^\/admin\//],
|
|
85
|
+
* brandRoutes: [
|
|
86
|
+
* { pattern: /^\/brands\/(\d+)/, brandIdParam: 1 }
|
|
87
|
+
* ],
|
|
88
|
+
* loginUrl: '/login'
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
export interface RouteGuardConfig {
|
|
92
|
+
/**
|
|
93
|
+
* Routes that are accessible without authentication.
|
|
94
|
+
* These routes bypass all authentication checks.
|
|
95
|
+
*
|
|
96
|
+
* @default []
|
|
97
|
+
*/
|
|
98
|
+
publicRoutes?: RoutePattern[];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Routes that require any authenticated user.
|
|
102
|
+
* Users must be logged in but no specific permissions are required.
|
|
103
|
+
*
|
|
104
|
+
* @default []
|
|
105
|
+
*/
|
|
106
|
+
authenticatedRoutes?: RoutePattern[];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Routes that require admin privileges.
|
|
110
|
+
* Only users with isAdmin=true can access these routes.
|
|
111
|
+
*
|
|
112
|
+
* @default []
|
|
113
|
+
*/
|
|
114
|
+
adminRoutes?: RoutePattern[];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Routes that require brand-specific access.
|
|
118
|
+
* Users must have access to the specific brand ID extracted from the route,
|
|
119
|
+
* or be an admin.
|
|
120
|
+
*
|
|
121
|
+
* @default []
|
|
122
|
+
*/
|
|
123
|
+
brandRoutes?: BrandRouteConfig[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* URL to redirect to when authentication is required.
|
|
127
|
+
* Query parameters will be added for return URL and error messages.
|
|
128
|
+
*
|
|
129
|
+
* @default '/login'
|
|
130
|
+
*/
|
|
131
|
+
loginUrl?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Options for configuring the htlkg Astro integration.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* htlkg({
|
|
139
|
+
* auth: {
|
|
140
|
+
* publicRoutes: ['/login', '/'],
|
|
141
|
+
* adminRoutes: [/^\/admin/],
|
|
142
|
+
* loginUrl: '/login'
|
|
143
|
+
* },
|
|
144
|
+
* loginPage: {
|
|
145
|
+
* logo: 'https://example.com/logo.png',
|
|
146
|
+
* title: 'Admin Portal',
|
|
147
|
+
* redirectUrl: '/admin/dashboard'
|
|
148
|
+
* },
|
|
149
|
+
* validateEnv: true,
|
|
150
|
+
* tailwind: { configFile: './tailwind.config.mjs' }
|
|
151
|
+
* })
|
|
152
|
+
*/
|
|
153
|
+
export interface HtlkgIntegrationOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Route guard configuration for authentication and authorization.
|
|
156
|
+
* Defines which routes are public, require authentication, or require
|
|
157
|
+
* specific permissions.
|
|
158
|
+
*
|
|
159
|
+
* @default {}
|
|
160
|
+
*/
|
|
161
|
+
auth?: RouteGuardConfig;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Configuration for the default login page.
|
|
165
|
+
* Set to false to disable automatic login page injection.
|
|
166
|
+
*
|
|
167
|
+
* @default { path: '/login', title: 'Sign In', redirectUrl: '/admin' }
|
|
168
|
+
*/
|
|
169
|
+
loginPage?: LoginPageConfig | false;
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate that required environment variables are present.
|
|
175
|
+
* When enabled, the integration will check for required Amplify
|
|
176
|
+
* environment variables and log warnings if any are missing.
|
|
177
|
+
*
|
|
178
|
+
* @default true
|
|
179
|
+
*/
|
|
180
|
+
validateEnv?: boolean;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* List of required environment variable names to validate.
|
|
184
|
+
* Only used when validateEnv is true.
|
|
185
|
+
*
|
|
186
|
+
* @default ['PUBLIC_COGNITO_USER_POOL_ID', 'PUBLIC_COGNITO_USER_POOL_CLIENT_ID']
|
|
187
|
+
*/
|
|
188
|
+
requiredEnvVars?: string[];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* AWS Amplify configuration.
|
|
192
|
+
* Pass the amplify_outputs.json content directly for automatic configuration.
|
|
193
|
+
* This is the recommended approach as it ensures consistency with Amplify CLI.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* import amplifyOutputs from './amplify_outputs.json';
|
|
197
|
+
*
|
|
198
|
+
* htlkg({
|
|
199
|
+
* amplify: amplifyOutputs
|
|
200
|
+
* })
|
|
201
|
+
*/
|
|
202
|
+
amplify?: Record<string, unknown>;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Tailwind CSS integration configuration.
|
|
206
|
+
* Set to false to disable automatic Tailwind integration.
|
|
207
|
+
* Pass an object to configure Tailwind options (e.g., configFile).
|
|
208
|
+
*
|
|
209
|
+
* @default undefined (Tailwind enabled with default config)
|
|
210
|
+
*/
|
|
211
|
+
tailwind?: Record<string, unknown> | false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Type guard to check if a value is an authenticated user.
|
|
216
|
+
* Useful for narrowing types in TypeScript when checking user authentication.
|
|
217
|
+
*
|
|
218
|
+
* @param user - Value to check
|
|
219
|
+
* @returns True if the value is a valid AuthUser object
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* if (isAuthenticatedUser(locals.user)) {
|
|
223
|
+
* // TypeScript knows locals.user is AuthUser here
|
|
224
|
+
* console.log(locals.user.email);
|
|
225
|
+
* }
|
|
226
|
+
*/
|
|
227
|
+
export function isAuthenticatedUser(user: unknown): user is AuthUser {
|
|
228
|
+
return (
|
|
229
|
+
user !== null &&
|
|
230
|
+
typeof user === 'object' &&
|
|
231
|
+
'username' in user &&
|
|
232
|
+
'email' in user &&
|
|
233
|
+
'brandIds' in user &&
|
|
234
|
+
'accountIds' in user &&
|
|
235
|
+
'isAdmin' in user &&
|
|
236
|
+
typeof (user as AuthUser).username === 'string' &&
|
|
237
|
+
typeof (user as AuthUser).email === 'string' &&
|
|
238
|
+
Array.isArray((user as AuthUser).brandIds) &&
|
|
239
|
+
Array.isArray((user as AuthUser).accountIds) &&
|
|
240
|
+
typeof (user as AuthUser).isAdmin === 'boolean'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* htlkg Astro Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides zero-config setup for Hotelinking applications with:
|
|
5
|
+
* - Tailwind CSS integration
|
|
6
|
+
* - Vue 3 integration with Amplify setup
|
|
7
|
+
* - Authentication middleware
|
|
8
|
+
* - Route guards
|
|
9
|
+
* - Login page generation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import tailwind from '@astrojs/tailwind';
|
|
13
|
+
import vue from '@astrojs/vue';
|
|
14
|
+
import type { AstroIntegration } from 'astro';
|
|
15
|
+
import type { HtlkgIntegrationOptions } from './config.js';
|
|
16
|
+
import { createVirtualModulePlugin, virtualModuleTypes } from './virtual-modules.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default environment variables required for AWS Amplify authentication
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_ENV_VARS = [
|
|
22
|
+
'PUBLIC_COGNITO_USER_POOL_ID',
|
|
23
|
+
'PUBLIC_COGNITO_USER_POOL_CLIENT_ID'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* htlkg Astro integration that provides zero-config authentication setup.
|
|
28
|
+
*
|
|
29
|
+
* This integration automatically:
|
|
30
|
+
* - Includes Tailwind CSS integration (can be disabled)
|
|
31
|
+
* - Includes Vue 3 integration with Amplify and Nanostores setup
|
|
32
|
+
* - Injects authentication middleware for AWS Amplify
|
|
33
|
+
* - Configures route guards based on declarative configuration
|
|
34
|
+
* - Validates required environment variables (optional)
|
|
35
|
+
* - Injects TypeScript types for Astro.locals.user
|
|
36
|
+
* - Provides a default login page (optional)
|
|
37
|
+
*
|
|
38
|
+
* @param options - Configuration options for the integration
|
|
39
|
+
* @returns Astro integration object or array of integrations
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // astro.config.mjs
|
|
43
|
+
* import { htlkg } from '@htlkg/astro';
|
|
44
|
+
*
|
|
45
|
+
* export default defineConfig({
|
|
46
|
+
* integrations: [
|
|
47
|
+
* htlkg({
|
|
48
|
+
* tailwind: { configFile: './tailwind.config.mjs' },
|
|
49
|
+
* auth: {
|
|
50
|
+
* publicRoutes: ['/login', '/'],
|
|
51
|
+
* adminRoutes: [/^\/admin/],
|
|
52
|
+
* loginUrl: '/login'
|
|
53
|
+
* }
|
|
54
|
+
* })
|
|
55
|
+
* ]
|
|
56
|
+
* });
|
|
57
|
+
*/
|
|
58
|
+
export function htlkg(
|
|
59
|
+
options: HtlkgIntegrationOptions = {},
|
|
60
|
+
): AstroIntegration | AstroIntegration[] {
|
|
61
|
+
const {
|
|
62
|
+
auth = {},
|
|
63
|
+
loginPage = { path: '/login', title: 'Sign In', redirectUrl: '/admin' },
|
|
64
|
+
validateEnv = true,
|
|
65
|
+
requiredEnvVars = DEFAULT_ENV_VARS,
|
|
66
|
+
tailwind: tailwindOptions,
|
|
67
|
+
amplify,
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
const integrations: AstroIntegration[] = [];
|
|
71
|
+
|
|
72
|
+
// Add Tailwind integration (enabled by default)
|
|
73
|
+
if (tailwindOptions !== false) {
|
|
74
|
+
const tailwindConfig =
|
|
75
|
+
typeof tailwindOptions === 'object' ? tailwindOptions : undefined;
|
|
76
|
+
integrations.push(
|
|
77
|
+
tailwind(tailwindConfig as Parameters<typeof tailwind>[0]),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add Vue integration with Amplify setup entrypoint
|
|
82
|
+
// Use package import specifier that Vite can resolve
|
|
83
|
+
integrations.push(
|
|
84
|
+
vue({
|
|
85
|
+
appEntrypoint: '@htlkg/astro/vue-app-setup',
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Add the main htlkg integration
|
|
90
|
+
integrations.push({
|
|
91
|
+
name: '@htlkg/astro',
|
|
92
|
+
hooks: {
|
|
93
|
+
'astro:config:setup': ({
|
|
94
|
+
config,
|
|
95
|
+
updateConfig,
|
|
96
|
+
addMiddleware,
|
|
97
|
+
injectRoute,
|
|
98
|
+
logger,
|
|
99
|
+
}) => {
|
|
100
|
+
try {
|
|
101
|
+
// 1. Verify Vue integration is present
|
|
102
|
+
const hasVue = config.integrations.some(
|
|
103
|
+
(i) => i.name === '@astrojs/vue',
|
|
104
|
+
);
|
|
105
|
+
if (hasVue) {
|
|
106
|
+
logger.info('Vue integration configured with Amplify app setup');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Amplify will be configured by the middleware on first request
|
|
110
|
+
if (amplify) {
|
|
111
|
+
logger.info('Amplify configuration provided - will be configured on first request');
|
|
112
|
+
} else {
|
|
113
|
+
logger.info('No Amplify configuration provided - will use environment variables');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. Validate environment variables (only if not using amplify_outputs.json)
|
|
117
|
+
if (validateEnv && !amplify) {
|
|
118
|
+
const missing = requiredEnvVars.filter(
|
|
119
|
+
(varName) => !process.env[varName],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (missing.length > 0) {
|
|
123
|
+
logger.warn(
|
|
124
|
+
`Missing required environment variables: ${missing.join(', ')}\nAuthentication may not work correctly. Please set these in your .env file:\n${missing.map((v) => ` - ${v}`).join('\n')}`,
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
logger.info('All required environment variables are present');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 4. Create Vite virtual module plugin to pass configuration to middleware and pages
|
|
132
|
+
try {
|
|
133
|
+
const virtualModulePlugin = createVirtualModulePlugin(
|
|
134
|
+
auth,
|
|
135
|
+
loginPage,
|
|
136
|
+
amplify || null
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
updateConfig({
|
|
140
|
+
vite: {
|
|
141
|
+
plugins: [virtualModulePlugin],
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const errorMsg =
|
|
146
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
147
|
+
logger.error(
|
|
148
|
+
`Failed to create virtual module for route configuration: ${errorMsg}`,
|
|
149
|
+
);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. Inject middleware
|
|
154
|
+
try {
|
|
155
|
+
addMiddleware({
|
|
156
|
+
entrypoint: '@htlkg/astro/middleware',
|
|
157
|
+
order: 'pre',
|
|
158
|
+
});
|
|
159
|
+
logger.info('Authentication middleware injected successfully');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const errorMsg =
|
|
162
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
163
|
+
logger.error(`Failed to inject middleware: ${errorMsg}`);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 6. Verify Vue app entrypoint is configured
|
|
168
|
+
const vueIntegrationIndex = config.integrations.findIndex(
|
|
169
|
+
(i) => i.name === '@astrojs/vue',
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (vueIntegrationIndex === -1) {
|
|
173
|
+
logger.warn(
|
|
174
|
+
'@astrojs/vue integration not found.\n' +
|
|
175
|
+
'The htlkg integration should have added it automatically.\n' +
|
|
176
|
+
'If you see this warning, there may be an integration ordering issue.',
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
logger.info('Vue app setup with Amplify configuration enabled');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 7. Verify Tailwind integration
|
|
183
|
+
const hasTailwind = config.integrations.some(
|
|
184
|
+
(i) =>
|
|
185
|
+
i.name === '@astrojs/tailwind' || i.name === 'astro:tailwind',
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (hasTailwind) {
|
|
189
|
+
logger.info('Tailwind CSS integration configured');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 8. Inject login page route if enabled
|
|
193
|
+
if (loginPage !== false) {
|
|
194
|
+
try {
|
|
195
|
+
const loginPath = loginPage.path || '/login';
|
|
196
|
+
injectRoute({
|
|
197
|
+
pattern: loginPath,
|
|
198
|
+
entrypoint: '@htlkg/astro/auth/LoginPage.astro',
|
|
199
|
+
prerender: false,
|
|
200
|
+
});
|
|
201
|
+
logger.info(`Injected default login page at ${loginPath}`);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const errorMsg =
|
|
204
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
205
|
+
logger.warn(`Failed to inject login page: ${errorMsg}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
logger.info('htlkg integration configured successfully');
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const errorMsg =
|
|
212
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
213
|
+
logger.error(
|
|
214
|
+
`Failed to configure htlkg integration: ${errorMsg}`,
|
|
215
|
+
);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
'astro:config:done': ({ injectTypes }) => {
|
|
220
|
+
// Inject TypeScript types for Astro.locals and virtual module
|
|
221
|
+
injectTypes({
|
|
222
|
+
filename: 'htlkg.d.ts',
|
|
223
|
+
content: `
|
|
224
|
+
import type { AuthUser } from '@htlkg/core/types';
|
|
225
|
+
|
|
226
|
+
declare global {
|
|
227
|
+
namespace App {
|
|
228
|
+
interface Locals {
|
|
229
|
+
user: AuthUser | null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
${virtualModuleTypes}
|
|
235
|
+
|
|
236
|
+
export {};
|
|
237
|
+
`,
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Return single integration or array based on what was added
|
|
244
|
+
return integrations.length === 1 ? integrations[0] : integrations;
|
|
245
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for virtual module setup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { createVirtualModulePlugin } from './virtual-modules.js';
|
|
7
|
+
|
|
8
|
+
describe('Virtual Module Plugin', () => {
|
|
9
|
+
it('should create a valid Vite plugin', () => {
|
|
10
|
+
const authConfig = {
|
|
11
|
+
publicRoutes: ['/login', '/'],
|
|
12
|
+
authenticatedRoutes: ['/dashboard'],
|
|
13
|
+
adminRoutes: [/^\/admin/],
|
|
14
|
+
brandRoutes: [],
|
|
15
|
+
loginUrl: '/login',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const plugin = createVirtualModulePlugin(authConfig, null, null);
|
|
19
|
+
|
|
20
|
+
expect(plugin).toBeDefined();
|
|
21
|
+
expect(plugin.name).toBe('htlkg-config');
|
|
22
|
+
expect(typeof plugin.resolveId).toBe('function');
|
|
23
|
+
expect(typeof plugin.load).toBe('function');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should resolve virtual module ID', () => {
|
|
27
|
+
const plugin = createVirtualModulePlugin({}, null, null);
|
|
28
|
+
|
|
29
|
+
const resolved = plugin.resolveId('virtual:htlkg-config');
|
|
30
|
+
expect(resolved).toBe('\0virtual:htlkg-config');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should not resolve non-virtual module IDs', () => {
|
|
34
|
+
const plugin = createVirtualModulePlugin({}, null, null);
|
|
35
|
+
|
|
36
|
+
const resolved = plugin.resolveId('some-other-module');
|
|
37
|
+
expect(resolved).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should generate module code with string patterns', () => {
|
|
41
|
+
const authConfig = {
|
|
42
|
+
publicRoutes: ['/login', '/register'],
|
|
43
|
+
authenticatedRoutes: ['/dashboard'],
|
|
44
|
+
adminRoutes: [],
|
|
45
|
+
brandRoutes: [],
|
|
46
|
+
loginUrl: '/login',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const plugin = createVirtualModulePlugin(authConfig, null, null);
|
|
50
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
51
|
+
|
|
52
|
+
expect(code).toContain('publicRoutes: ["/login", "/register"]');
|
|
53
|
+
expect(code).toContain('authenticatedRoutes: ["/dashboard"]');
|
|
54
|
+
expect(code).toContain('loginUrl: "/login"');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should generate module code with RegExp patterns', () => {
|
|
58
|
+
const authConfig = {
|
|
59
|
+
publicRoutes: [],
|
|
60
|
+
authenticatedRoutes: [],
|
|
61
|
+
adminRoutes: [/^\/admin/],
|
|
62
|
+
brandRoutes: [],
|
|
63
|
+
loginUrl: '/login',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const plugin = createVirtualModulePlugin(authConfig, null, null);
|
|
67
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
68
|
+
|
|
69
|
+
expect(code).toContain('new RegExp');
|
|
70
|
+
// The pattern is serialized as a string, so we check for the pattern source
|
|
71
|
+
expect(code).toContain('adminRoutes: [new RegExp(');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should generate module code with mixed patterns', () => {
|
|
75
|
+
const authConfig = {
|
|
76
|
+
publicRoutes: ['/login', /^\/public/],
|
|
77
|
+
authenticatedRoutes: ['/dashboard', /^\/app/],
|
|
78
|
+
adminRoutes: [],
|
|
79
|
+
brandRoutes: [],
|
|
80
|
+
loginUrl: '/login',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const plugin = createVirtualModulePlugin(authConfig, null, null);
|
|
84
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
85
|
+
|
|
86
|
+
expect(code).toContain('"/login"');
|
|
87
|
+
expect(code).toContain('new RegExp');
|
|
88
|
+
expect(code).toContain('"/dashboard"');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle empty route arrays', () => {
|
|
92
|
+
const authConfig = {
|
|
93
|
+
publicRoutes: [],
|
|
94
|
+
authenticatedRoutes: [],
|
|
95
|
+
adminRoutes: [],
|
|
96
|
+
brandRoutes: [],
|
|
97
|
+
loginUrl: '/login',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const plugin = createVirtualModulePlugin(authConfig, null, null);
|
|
101
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
102
|
+
|
|
103
|
+
expect(code).toContain('publicRoutes: []');
|
|
104
|
+
expect(code).toContain('authenticatedRoutes: []');
|
|
105
|
+
expect(code).toContain('adminRoutes: []');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should serialize amplify config when provided', () => {
|
|
109
|
+
const amplifyConfig = {
|
|
110
|
+
userPoolId: 'us-east-1_abc123',
|
|
111
|
+
userPoolClientId: 'abc123def456',
|
|
112
|
+
region: 'us-east-1',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const plugin = createVirtualModulePlugin({}, null, amplifyConfig);
|
|
116
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
117
|
+
|
|
118
|
+
expect(code).toContain('amplifyConfig');
|
|
119
|
+
expect(code).toContain('userPoolId');
|
|
120
|
+
expect(code).toContain('us-east-1_abc123');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should set amplify config to null when not provided', () => {
|
|
124
|
+
const plugin = createVirtualModulePlugin({}, null, null);
|
|
125
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
126
|
+
|
|
127
|
+
expect(code).toContain('amplifyConfig = null');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should serialize login page config when provided', () => {
|
|
131
|
+
const loginPageConfig = {
|
|
132
|
+
path: '/custom-login',
|
|
133
|
+
title: 'Custom Login',
|
|
134
|
+
redirectUrl: '/dashboard',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const plugin = createVirtualModulePlugin({}, loginPageConfig, null);
|
|
138
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
139
|
+
|
|
140
|
+
expect(code).toContain('loginPageConfig');
|
|
141
|
+
expect(code).toContain('custom-login');
|
|
142
|
+
expect(code).toContain('Custom Login');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should set login page config to null when disabled', () => {
|
|
146
|
+
const plugin = createVirtualModulePlugin({}, false, null);
|
|
147
|
+
const code = plugin.load('\0virtual:htlkg-config');
|
|
148
|
+
|
|
149
|
+
expect(code).toContain('loginPageConfig = null');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should not load non-virtual module IDs', () => {
|
|
153
|
+
const plugin = createVirtualModulePlugin({}, null, null);
|
|
154
|
+
const code = plugin.load('some-other-module');
|
|
155
|
+
|
|
156
|
+
expect(code).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
});
|