@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.
Files changed (70) hide show
  1. package/README.md +265 -0
  2. package/dist/chunk-33R4URZV.js +59 -0
  3. package/dist/chunk-33R4URZV.js.map +1 -0
  4. package/dist/chunk-64USRLVP.js +85 -0
  5. package/dist/chunk-64USRLVP.js.map +1 -0
  6. package/dist/chunk-WLOFOVCL.js +210 -0
  7. package/dist/chunk-WLOFOVCL.js.map +1 -0
  8. package/dist/chunk-WNMPTDCR.js +73 -0
  9. package/dist/chunk-WNMPTDCR.js.map +1 -0
  10. package/dist/chunk-Z2ZAL7KX.js +9 -0
  11. package/dist/chunk-Z2ZAL7KX.js.map +1 -0
  12. package/dist/chunk-ZQ4XMJH7.js +1 -0
  13. package/dist/chunk-ZQ4XMJH7.js.map +1 -0
  14. package/dist/htlkg/config.js +7 -0
  15. package/dist/htlkg/config.js.map +1 -0
  16. package/dist/htlkg/index.js +7 -0
  17. package/dist/htlkg/index.js.map +1 -0
  18. package/dist/index.js +64 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/middleware/index.js +168 -0
  21. package/dist/middleware/index.js.map +1 -0
  22. package/dist/utils/hydration.js +21 -0
  23. package/dist/utils/hydration.js.map +1 -0
  24. package/dist/utils/index.js +56 -0
  25. package/dist/utils/index.js.map +1 -0
  26. package/dist/utils/ssr.js +21 -0
  27. package/dist/utils/ssr.js.map +1 -0
  28. package/dist/utils/static.js +19 -0
  29. package/dist/utils/static.js.map +1 -0
  30. package/package.json +53 -0
  31. package/src/__mocks__/astro-middleware.ts +19 -0
  32. package/src/__mocks__/virtual-htlkg-config.ts +14 -0
  33. package/src/auth/LoginForm.vue +482 -0
  34. package/src/auth/LoginPage.astro +70 -0
  35. package/src/components/PageHeader.astro +145 -0
  36. package/src/components/Sidebar.astro +157 -0
  37. package/src/components/Topbar.astro +167 -0
  38. package/src/components/index.ts +9 -0
  39. package/src/htlkg/config.test.ts +165 -0
  40. package/src/htlkg/config.ts +242 -0
  41. package/src/htlkg/index.ts +245 -0
  42. package/src/htlkg/virtual-modules.test.ts +158 -0
  43. package/src/htlkg/virtual-modules.ts +81 -0
  44. package/src/index.ts +37 -0
  45. package/src/layouts/AdminLayout.astro +184 -0
  46. package/src/layouts/AuthLayout.astro +164 -0
  47. package/src/layouts/BrandLayout.astro +309 -0
  48. package/src/layouts/DefaultLayout.astro +25 -0
  49. package/src/layouts/PublicLayout.astro +153 -0
  50. package/src/layouts/index.ts +10 -0
  51. package/src/middleware/auth.ts +53 -0
  52. package/src/middleware/index.ts +31 -0
  53. package/src/middleware/route-guards.test.ts +182 -0
  54. package/src/middleware/route-guards.ts +218 -0
  55. package/src/patterns/admin/DetailPage.astro +195 -0
  56. package/src/patterns/admin/FormPage.astro +203 -0
  57. package/src/patterns/admin/ListPage.astro +178 -0
  58. package/src/patterns/admin/index.ts +9 -0
  59. package/src/patterns/brand/ConfigPage.astro +128 -0
  60. package/src/patterns/brand/PortalPage.astro +161 -0
  61. package/src/patterns/brand/index.ts +8 -0
  62. package/src/patterns/index.ts +8 -0
  63. package/src/utils/hydration.test.ts +154 -0
  64. package/src/utils/hydration.ts +151 -0
  65. package/src/utils/index.ts +9 -0
  66. package/src/utils/ssr.test.ts +235 -0
  67. package/src/utils/ssr.ts +139 -0
  68. package/src/utils/static.test.ts +144 -0
  69. package/src/utils/static.ts +144 -0
  70. 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
+ });