@htlkg/astro 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.
@@ -0,0 +1,290 @@
1
+ /**
2
+ * List Page Factory
3
+ *
4
+ * Factory function for creating standardized list pages with automatic
5
+ * data fetching, filtering, sorting, and pagination.
6
+ */
7
+
8
+ import type { AstroGlobal } from "astro";
9
+ import type { WritableAtom } from "nanostores";
10
+
11
+ import {
12
+ parseListParams,
13
+ type ListParams,
14
+ type ListParamConfig,
15
+ type FilterFieldConfig,
16
+ } from "../utils/params";
17
+ import {
18
+ buildGraphQLFilter,
19
+ applyClientFilters,
20
+ sortItems,
21
+ paginateItems,
22
+ } from "../utils/filters";
23
+
24
+ /**
25
+ * Breadcrumb item
26
+ */
27
+ export interface BreadcrumbItem {
28
+ label: string;
29
+ href?: string;
30
+ }
31
+
32
+ /**
33
+ * Layout props returned by the factory
34
+ */
35
+ export interface LayoutProps {
36
+ title: string;
37
+ description?: string;
38
+ currentPage: string;
39
+ breadcrumbs: BreadcrumbItem[];
40
+ }
41
+
42
+ /**
43
+ * Table initial state
44
+ */
45
+ export interface TableInitialState {
46
+ currentPage: number;
47
+ pageSize: number;
48
+ sortKey: string;
49
+ sortOrder: "asc" | "desc";
50
+ totalItems: number;
51
+ totalPages: number;
52
+ filters: Record<string, any>;
53
+ search?: string;
54
+ }
55
+
56
+ /**
57
+ * Related store configuration
58
+ */
59
+ export interface RelatedStoreConfig<T = any> {
60
+ store: WritableAtom<T[]>;
61
+ fetch: () => Promise<T[]>;
62
+ }
63
+
64
+ /**
65
+ * Configuration for createListPage
66
+ */
67
+ export interface ListPageConfig<T> {
68
+ // Page metadata
69
+ /** Page title */
70
+ title: string;
71
+ /** Page description */
72
+ description?: string;
73
+ /** Page identifier (for sidebar active state) */
74
+ pageId: string;
75
+ /** Custom breadcrumbs (auto-generated if not provided) */
76
+ breadcrumbs?: BreadcrumbItem[];
77
+
78
+ // Data fetching
79
+ /** Fetch function that retrieves data */
80
+ fetchFn: (params: {
81
+ filter: any;
82
+ limit?: number;
83
+ nextToken?: string;
84
+ }) => Promise<{ data: T[]; nextToken?: string | null }>;
85
+
86
+ // Data handling
87
+ /** Transform function to apply to fetched data */
88
+ transform?: (item: any) => T;
89
+ /** Main store to set with data */
90
+ store: WritableAtom<T[]>;
91
+ /** Related stores to populate */
92
+ relatedStores?: Record<string, RelatedStoreConfig>;
93
+
94
+ // Filtering/sorting config
95
+ /** Searchable field keys */
96
+ searchableFields?: string[];
97
+ /** Filterable field configurations */
98
+ filterableFields?: FilterFieldConfig[];
99
+ /** Sortable field keys (all by default) */
100
+ sortableFields?: string[];
101
+ /** Default sort configuration */
102
+ defaultSort?: { key: string; order: "asc" | "desc" };
103
+ /** Default page size */
104
+ defaultPageSize?: number;
105
+ /** Maximum items to fetch from API */
106
+ fetchLimit?: number;
107
+ }
108
+
109
+ /**
110
+ * Result from createListPage
111
+ */
112
+ export interface ListPageResult<T> {
113
+ /** Props to spread to Layout component */
114
+ layoutProps: LayoutProps;
115
+ /** Initial state for table component */
116
+ initialState: TableInitialState;
117
+ /** Processed items (paginated) */
118
+ items: T[];
119
+ /** All items (pre-pagination, for client-side operations) */
120
+ allItems: T[];
121
+ /** Parsed URL parameters */
122
+ params: ListParams;
123
+ }
124
+
125
+ /**
126
+ * Default breadcrumb generator
127
+ */
128
+ function generateBreadcrumbs(pageId: string, title: string): BreadcrumbItem[] {
129
+ return [
130
+ { label: "Admin", href: "/admin" },
131
+ { label: title },
132
+ ];
133
+ }
134
+
135
+ /**
136
+ * Create a list page with automatic data handling
137
+ *
138
+ * This factory handles:
139
+ * - URL parameter parsing (page, pageSize, sortKey, sortOrder, filters)
140
+ * - Data fetching with GraphQL filter building
141
+ * - Data transformation
142
+ * - Client-side filtering (for computed fields)
143
+ * - Sorting
144
+ * - Pagination
145
+ * - Store population
146
+ *
147
+ * @example
148
+ * ```astro
149
+ * ---
150
+ * import { createListPage } from '@htlkg/astro/factories';
151
+ * import { $accounts } from '@/stores/accounts';
152
+ *
153
+ * const { layoutProps, initialState, items } = await createListPage(Astro, {
154
+ * title: 'Accounts',
155
+ * pageId: 'accounts',
156
+ * store: $accounts,
157
+ *
158
+ * fetchFn: async ({ filter }) => {
159
+ * const client = getServerClient(Astro);
160
+ * return await client.models.Account.list({ filter });
161
+ * },
162
+ *
163
+ * transform: (account) => ({
164
+ * id: account.id,
165
+ * name: account.name,
166
+ * brandCount: account.brands?.length ?? 0,
167
+ * }),
168
+ *
169
+ * filterableFields: [
170
+ * { key: 'name', type: 'text', graphql: true },
171
+ * { key: 'status', type: 'select', graphql: true, options: ['active', 'inactive'] },
172
+ * ],
173
+ *
174
+ * defaultSort: { key: 'name', order: 'asc' },
175
+ * });
176
+ * ---
177
+ *
178
+ * <Layout {...layoutProps}>
179
+ * <AccountsTable client:load initialState={initialState} />
180
+ * </Layout>
181
+ * ```
182
+ */
183
+ export async function createListPage<T extends Record<string, any>>(
184
+ astro: AstroGlobal,
185
+ config: ListPageConfig<T>
186
+ ): Promise<ListPageResult<T>> {
187
+ const {
188
+ title,
189
+ description,
190
+ pageId,
191
+ breadcrumbs,
192
+ fetchFn,
193
+ transform,
194
+ store,
195
+ relatedStores,
196
+ searchableFields,
197
+ filterableFields = [],
198
+ defaultSort,
199
+ defaultPageSize = 25,
200
+ fetchLimit = 1000,
201
+ } = config;
202
+
203
+ // 1. Parse URL parameters
204
+ const paramConfig: ListParamConfig = {
205
+ defaultPageSize,
206
+ defaultSort,
207
+ searchableFields,
208
+ filterableFields,
209
+ };
210
+ const params = parseListParams(astro.url, paramConfig);
211
+
212
+ // 2. Build GraphQL filter from URL params
213
+ const graphqlFilter = buildGraphQLFilter(params.filters, filterableFields, {
214
+ search: params.search,
215
+ searchFields: searchableFields,
216
+ });
217
+
218
+ // 3. Fetch data with pagination
219
+ let allItems: T[] = [];
220
+ let nextToken: string | null | undefined = undefined;
221
+
222
+ do {
223
+ const result = await fetchFn({
224
+ filter: graphqlFilter,
225
+ limit: fetchLimit,
226
+ nextToken: nextToken ?? undefined,
227
+ });
228
+
229
+ const data = result.data ?? [];
230
+ allItems = allItems.concat(data);
231
+ nextToken = result.nextToken;
232
+ } while (nextToken);
233
+
234
+ // 4. Transform data if needed
235
+ if (transform) {
236
+ allItems = allItems.map(transform);
237
+ }
238
+
239
+ // 5. Apply client-side filtering (for computed fields not in GraphQL)
240
+ allItems = applyClientFilters(allItems, params.filters, filterableFields, {
241
+ search: params.search,
242
+ searchFields: searchableFields,
243
+ });
244
+
245
+ // 6. Sort
246
+ allItems = sortItems(allItems, params.sortKey, params.sortOrder);
247
+
248
+ // 7. Paginate
249
+ const { paginatedItems, totalItems, totalPages, currentPage, pageSize } = paginateItems(
250
+ allItems,
251
+ params.page,
252
+ params.pageSize
253
+ );
254
+
255
+ // 8. Set main store
256
+ store.set(paginatedItems);
257
+
258
+ // 9. Fetch and set related stores
259
+ if (relatedStores) {
260
+ await Promise.all(
261
+ Object.entries(relatedStores).map(async ([, config]) => {
262
+ const data = await config.fetch();
263
+ config.store.set(data);
264
+ })
265
+ );
266
+ }
267
+
268
+ // 10. Build result
269
+ return {
270
+ layoutProps: {
271
+ title,
272
+ description,
273
+ currentPage: pageId,
274
+ breadcrumbs: breadcrumbs ?? generateBreadcrumbs(pageId, title),
275
+ },
276
+ initialState: {
277
+ currentPage,
278
+ pageSize,
279
+ sortKey: params.sortKey,
280
+ sortOrder: params.sortOrder,
281
+ totalItems,
282
+ totalPages,
283
+ filters: params.filters,
284
+ search: params.search,
285
+ },
286
+ items: paginatedItems,
287
+ allItems,
288
+ params,
289
+ };
290
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Page Factories for @htlkg/astro
3
+ *
4
+ * Factory functions for creating standardized Astro pages with automatic
5
+ * data handling, filtering, sorting, and pagination.
6
+ */
7
+
8
+ export {
9
+ createListPage,
10
+ type ListPageConfig,
11
+ type ListPageResult,
12
+ type LayoutProps,
13
+ type TableInitialState,
14
+ type BreadcrumbItem,
15
+ type RelatedStoreConfig,
16
+ } from "./createListPage";
@@ -209,6 +209,16 @@ export interface HtlkgIntegrationOptions {
209
209
  * @default undefined (Tailwind enabled with default config)
210
210
  */
211
211
  tailwind?: Record<string, unknown> | false;
212
+
213
+ /**
214
+ * Vue app setup mode.
215
+ * - 'full': Use the Amplify-configured Vue app setup (requires SSR)
216
+ * - 'basic': Use basic Vue integration without Amplify app setup (works with static)
217
+ * - 'auto': Automatically detect based on output mode (default)
218
+ *
219
+ * @default 'auto'
220
+ */
221
+ vueAppSetup?: 'full' | 'basic' | 'auto';
212
222
  }
213
223
 
214
224
  /**
@@ -0,0 +1,63 @@
1
+ # htlkg Integration
2
+
3
+ Zero-config Astro integration for Hotelinking applications.
4
+
5
+ ## Installation
6
+
7
+ ```javascript
8
+ // astro.config.mjs
9
+ import { defineConfig } from 'astro/config';
10
+ import { htlkg } from '@htlkg/astro';
11
+
12
+ export default defineConfig({
13
+ integrations: [
14
+ htlkg({
15
+ auth: {
16
+ enabled: true,
17
+ loginPage: '/login',
18
+ publicRoutes: ['/login', '/public'],
19
+ },
20
+ brandRoutes: {
21
+ enabled: true,
22
+ pattern: '/[brandId]',
23
+ },
24
+ }),
25
+ ],
26
+ });
27
+ ```
28
+
29
+ ## Features
30
+
31
+ - Automatic middleware setup
32
+ - Route guard configuration
33
+ - Login page injection
34
+ - Brand route handling
35
+ - Virtual module configuration
36
+
37
+ ## Options
38
+
39
+ ```typescript
40
+ interface HtlkgIntegrationOptions {
41
+ auth?: {
42
+ enabled: boolean;
43
+ loginPage?: string;
44
+ publicRoutes?: RoutePattern[];
45
+ protectedRoutes?: RoutePattern[];
46
+ };
47
+ brandRoutes?: {
48
+ enabled: boolean;
49
+ pattern?: string;
50
+ };
51
+ }
52
+ ```
53
+
54
+ ## Virtual Modules
55
+
56
+ The integration provides virtual modules for configuration:
57
+
58
+ ```typescript
59
+ // Access config in your code
60
+ import config from 'virtual:htlkg-config';
61
+
62
+ console.log(config.auth.loginPage);
63
+ ```
@@ -1,12 +1,7 @@
1
1
  /**
2
2
  * htlkg Astro Integration
3
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
4
+ * Supports static, hybrid, and full SSR output modes.
10
5
  */
11
6
 
12
7
  import tailwind from '@astrojs/tailwind';
@@ -16,46 +11,11 @@ import type { HtlkgIntegrationOptions } from './config.js';
16
11
  import { createVirtualModulePlugin, virtualModuleTypes } from './virtual-modules.js';
17
12
  import vueDevTools from 'vite-plugin-vue-devtools';
18
13
 
19
- /**
20
- * Default environment variables required for AWS Amplify authentication
21
- */
22
14
  const DEFAULT_ENV_VARS = [
23
15
  'PUBLIC_COGNITO_USER_POOL_ID',
24
16
  'PUBLIC_COGNITO_USER_POOL_CLIENT_ID'
25
17
  ];
26
18
 
27
- /**
28
- * htlkg Astro integration that provides zero-config authentication setup.
29
- *
30
- * This integration automatically:
31
- * - Includes Tailwind CSS integration (can be disabled)
32
- * - Includes Vue 3 integration with Amplify and Nanostores setup
33
- * - Injects authentication middleware for AWS Amplify
34
- * - Configures route guards based on declarative configuration
35
- * - Validates required environment variables (optional)
36
- * - Injects TypeScript types for Astro.locals.user
37
- * - Provides a default login page (optional)
38
- *
39
- * @param options - Configuration options for the integration
40
- * @returns Astro integration object or array of integrations
41
- *
42
- * @example
43
- * // astro.config.mjs
44
- * import { htlkg } from '@htlkg/astro';
45
- *
46
- * export default defineConfig({
47
- * integrations: [
48
- * htlkg({
49
- * tailwind: { configFile: './tailwind.config.mjs' },
50
- * auth: {
51
- * publicRoutes: ['/login', '/'],
52
- * adminRoutes: [/^\/admin/],
53
- * loginUrl: '/login'
54
- * }
55
- * })
56
- * ]
57
- * });
58
- */
59
19
  export function htlkg(
60
20
  options: HtlkgIntegrationOptions = {},
61
21
  ): AstroIntegration | AstroIntegration[] {
@@ -66,6 +26,7 @@ export function htlkg(
66
26
  requiredEnvVars = DEFAULT_ENV_VARS,
67
27
  tailwind: tailwindOptions,
68
28
  amplify,
29
+ vueAppSetup = 'auto',
69
30
  } = options;
70
31
 
71
32
  const integrations: AstroIntegration[] = [];
@@ -79,154 +40,77 @@ export function htlkg(
79
40
  );
80
41
  }
81
42
 
82
- // Add Vue integration with Amplify setup entrypoint
83
- // Use package import specifier that Vite can resolve
84
- integrations.push(
85
- vue({
86
- appEntrypoint: '@htlkg/astro/vue-app-setup',
87
- }),
88
- );
43
+ // Determine Vue setup mode:
44
+ // - 'full': Use Amplify app entrypoint (requires SSR)
45
+ // - 'basic': Basic Vue without app entrypoint (works with static)
46
+ // - 'auto': Default to 'basic' for compatibility with static builds
47
+ const useFullVueSetup = vueAppSetup === 'full';
48
+
49
+ // Add Vue integration
50
+ if (useFullVueSetup) {
51
+ integrations.push(vue({ appEntrypoint: '@htlkg/astro/vue-app-setup' }));
52
+ } else {
53
+ integrations.push(vue());
54
+ }
89
55
 
90
56
  // Add the main htlkg integration
91
57
  integrations.push({
92
58
  name: '@htlkg/astro',
93
59
  hooks: {
94
- 'astro:config:setup': ({
95
- config,
96
- updateConfig,
97
- addMiddleware,
98
- injectRoute,
99
- logger,
100
- }) => {
60
+ 'astro:config:setup': ({ updateConfig, addMiddleware, injectRoute, logger }) => {
101
61
  try {
102
- // 1. Verify Vue integration is present
103
- const hasVue = config.integrations.some(
104
- (i) => i.name === '@astrojs/vue',
105
- );
106
- if (hasVue) {
107
- logger.info('Vue integration configured with Amplify app setup');
108
- }
62
+ logger.info(useFullVueSetup
63
+ ? 'Vue configured with Amplify app setup'
64
+ : 'Vue configured (basic mode)');
109
65
 
110
- // 2. Amplify will be configured by the middleware on first request
111
66
  if (amplify) {
112
- logger.info('Amplify configuration provided - will be configured on first request');
113
- } else {
114
- logger.info('No Amplify configuration provided - will use environment variables');
67
+ logger.info('Amplify configuration provided');
115
68
  }
116
69
 
117
- // 3. Validate environment variables (only if not using amplify_outputs.json)
118
- if (validateEnv && !amplify) {
119
- const missing = requiredEnvVars.filter(
120
- (varName) => !process.env[varName],
121
- );
122
-
70
+ // Validate env vars (only for full setup)
71
+ if (validateEnv && !amplify && useFullVueSetup) {
72
+ const missing = requiredEnvVars.filter(v => !process.env[v]);
123
73
  if (missing.length > 0) {
124
- logger.warn(
125
- `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')}`,
126
- );
127
- } else {
128
- logger.info('All required environment variables are present');
74
+ logger.warn(`Missing env vars: ${missing.join(', ')}`);
129
75
  }
130
76
  }
131
77
 
132
- // 4. Create Vite virtual module plugin to pass configuration to middleware and pages
133
- try {
134
- const virtualModulePlugin = createVirtualModulePlugin(
135
- auth,
136
- loginPage,
137
- amplify || null
138
- );
139
-
140
- const vitePlugins: any[] = [virtualModulePlugin];
141
-
142
- // Add Vue DevTools plugin in development
143
- if (import.meta.env?.DEV !== false) {
144
- vitePlugins.push(vueDevTools());
145
- logger.info('Vue DevTools plugin enabled for development');
146
- }
147
-
148
- updateConfig({
149
- vite: {
150
- plugins: vitePlugins,
151
- },
152
- });
153
- } catch (error) {
154
- const errorMsg =
155
- error instanceof Error ? error.message : 'Unknown error';
156
- logger.error(
157
- `Failed to create virtual module for route configuration: ${errorMsg}`,
158
- );
159
- throw error;
160
- }
161
-
162
- // 5. Inject middleware
163
- try {
164
- addMiddleware({
165
- entrypoint: '@htlkg/astro/middleware',
166
- order: 'pre',
167
- });
168
- logger.info('Authentication middleware injected successfully');
169
- } catch (error) {
170
- const errorMsg =
171
- error instanceof Error ? error.message : 'Unknown error';
172
- logger.error(`Failed to inject middleware: ${errorMsg}`);
173
- throw error;
78
+ // Create Vite virtual module plugin
79
+ const virtualModulePlugin = createVirtualModulePlugin(auth, loginPage, amplify || null);
80
+ const vitePlugins: any[] = [virtualModulePlugin];
81
+
82
+ if (import.meta.env?.DEV !== false) {
83
+ vitePlugins.push(vueDevTools());
84
+ logger.info('Vue DevTools enabled');
174
85
  }
175
86
 
176
- // 6. Verify Vue app entrypoint is configured
177
- const vueIntegrationIndex = config.integrations.findIndex(
178
- (i) => i.name === '@astrojs/vue',
179
- );
180
-
181
- if (vueIntegrationIndex === -1) {
182
- logger.warn(
183
- '@astrojs/vue integration not found.\n' +
184
- 'The htlkg integration should have added it automatically.\n' +
185
- 'If you see this warning, there may be an integration ordering issue.',
186
- );
187
- } else {
188
- logger.info('Vue app setup with Amplify configuration enabled');
189
- }
190
-
191
- // 7. Verify Tailwind integration
192
- const hasTailwind = config.integrations.some(
193
- (i) =>
194
- i.name === '@astrojs/tailwind' || i.name === 'astro:tailwind',
195
- );
87
+ updateConfig({ vite: { plugins: vitePlugins } });
196
88
 
197
- if (hasTailwind) {
198
- logger.info('Tailwind CSS integration configured');
89
+ // Inject middleware (only for full setup)
90
+ if (useFullVueSetup) {
91
+ addMiddleware({ entrypoint: '@htlkg/astro/middleware', order: 'pre' });
92
+ logger.info('Authentication middleware injected');
199
93
  }
200
94
 
201
- // 8. Inject login page route if enabled
202
- if (loginPage !== false) {
203
- try {
204
- const loginPath = loginPage.path || '/login';
205
- injectRoute({
206
- pattern: loginPath,
207
- entrypoint: '@htlkg/astro/auth/LoginPage.astro',
208
- prerender: false,
209
- });
210
- logger.info(`Injected default login page at ${loginPath}`);
211
- } catch (error) {
212
- const errorMsg =
213
- error instanceof Error ? error.message : 'Unknown error';
214
- logger.warn(`Failed to inject login page: ${errorMsg}`);
215
- }
95
+ // Inject login page (only for full setup)
96
+ if (loginPage !== false && useFullVueSetup) {
97
+ const loginPath = loginPage.path || '/login';
98
+ injectRoute({
99
+ pattern: loginPath,
100
+ entrypoint: '@htlkg/astro/auth/LoginPage.astro',
101
+ prerender: false,
102
+ });
103
+ logger.info(`Login page injected at ${loginPath}`);
216
104
  }
217
105
 
218
- logger.info('htlkg integration configured successfully');
106
+ logger.info('htlkg integration configured');
219
107
  } catch (error) {
220
- const errorMsg =
221
- error instanceof Error ? error.message : 'Unknown error';
222
- logger.error(
223
- `Failed to configure htlkg integration: ${errorMsg}`,
224
- );
108
+ const msg = error instanceof Error ? error.message : 'Unknown error';
109
+ logger.error(`htlkg configuration failed: ${msg}`);
225
110
  throw error;
226
111
  }
227
112
  },
228
113
  'astro:config:done': ({ injectTypes }) => {
229
- // Inject TypeScript types for Astro.locals and virtual module
230
114
  injectTypes({
231
115
  filename: 'htlkg.d.ts',
232
116
  content: `
@@ -249,6 +133,5 @@ export {};
249
133
  },
250
134
  });
251
135
 
252
- // Return single integration or array based on what was added
253
- return integrations.length === 1 ? integrations[0] : integrations;
136
+ return integrations;
254
137
  }
package/src/index.ts CHANGED
@@ -32,6 +32,9 @@ export { isAuthenticatedUser } from './htlkg/config.js';
32
32
  // Utils
33
33
  export * from './utils';
34
34
 
35
+ // Page Factories
36
+ export * from './factories';
37
+
35
38
  // Middleware is not exported from main index to avoid bundling astro:middleware
36
39
  // It's loaded by Astro at runtime via the entrypoint specified in addMiddleware
37
40
  // Import from '@htlkg/astro/middleware' if needed