vite-plugin-smart-prefetch 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1746 @@
1
+ // src/plugin/analytics/bigquery-connector.ts
2
+ import { BigQuery } from "@google-cloud/bigquery";
3
+ import { writeFileSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+ var BigQueryAnalyticsConnector = class {
6
+ constructor(projectId, datasetId, debug = false) {
7
+ this.projectId = projectId;
8
+ this.datasetId = datasetId;
9
+ this.debug = debug;
10
+ this.bigquery = new BigQuery({
11
+ projectId
12
+ });
13
+ if (this.debug) {
14
+ console.log("\u2705 BigQuery Analytics connector initialized");
15
+ console.log(` Project ID: ${projectId}`);
16
+ console.log(` Dataset ID: ${datasetId}`);
17
+ }
18
+ }
19
+ /**
20
+ * Fetch real navigation transitions from BigQuery GA4 export
21
+ * Queries the events table for page_view events with previous_page_path parameter
22
+ */
23
+ async fetchNavigationSequences(config = {}) {
24
+ const { days = 30 } = config;
25
+ const minSessions = 1;
26
+ console.log(`\u{1F4CA} Fetching navigation data from BigQuery...`);
27
+ console.log(` Dataset: ${this.datasetId}`);
28
+ console.log(` Date range: Last ${days} days`);
29
+ try {
30
+ const query = `
31
+ WITH page_sessions AS (
32
+ SELECT
33
+ user_id,
34
+ (SELECT value.int_value FROM UNNEST(event_params) WHERE KEY = 'ga_session_id' LIMIT 1) as session_id,
35
+ (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'page_location' LIMIT 1) as page_location,
36
+ (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'page_path' LIMIT 1) as page_path,
37
+ event_timestamp,
38
+ ROW_NUMBER() OVER (
39
+ PARTITION BY user_id, (SELECT value.int_value FROM UNNEST(event_params) WHERE KEY = 'ga_session_id' LIMIT 1)
40
+ ORDER BY event_timestamp
41
+ ) as page_sequence
42
+ FROM \`${this.projectId}.${this.datasetId}.events_*\`
43
+ WHERE
44
+ event_name = 'page_view'
45
+ AND _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL ${days} DAY))
46
+ AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
47
+ AND (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'page_path' LIMIT 1) IS NOT NULL
48
+ ),
49
+ transitions AS (
50
+ SELECT
51
+ COALESCE(curr.page_path, '(direct)') as from_page,
52
+ LEAD(curr.page_path) OVER (
53
+ PARTITION BY curr.user_id, curr.session_id
54
+ ORDER BY curr.page_sequence
55
+ ) as to_page
56
+ FROM page_sessions curr
57
+ WHERE curr.page_sequence > 0
58
+ )
59
+ SELECT
60
+ from_page as previous_page_path,
61
+ to_page as page_path,
62
+ COUNT(*) as transition_count
63
+ FROM transitions
64
+ WHERE to_page IS NOT NULL
65
+ GROUP BY from_page, to_page
66
+ ORDER BY transition_count DESC
67
+ LIMIT 10000
68
+ `;
69
+ if (this.debug) {
70
+ console.log(`\u{1F4E4} BigQuery Query:`);
71
+ console.log(query);
72
+ }
73
+ const [rows] = await this.bigquery.query({
74
+ query,
75
+ location: "US"
76
+ });
77
+ if (this.debug) {
78
+ console.log(`\u2705 Query executed successfully`);
79
+ console.log(` Rows returned: ${rows.length}`);
80
+ }
81
+ const navigationData = [];
82
+ const rawData = rows.map((row) => ({
83
+ previous_page_path: row.previous_page_path,
84
+ page_path: row.page_path,
85
+ transition_count: row.transition_count
86
+ }));
87
+ rows.forEach((row) => {
88
+ const previousPage = row.previous_page_path || "(direct)";
89
+ const currentPage = row.page_path || "";
90
+ const transitionCount = parseInt(row.transition_count || "0");
91
+ const normalizedPrevious = previousPage === "(direct)" || previousPage === "(not set)" ? "(direct)" : this.normalizeRoute(previousPage);
92
+ const normalizedCurrent = this.normalizeRoute(currentPage);
93
+ if (normalizedCurrent && transitionCount >= minSessions) {
94
+ navigationData.push({
95
+ from: normalizedPrevious,
96
+ to: normalizedCurrent,
97
+ count: transitionCount
98
+ });
99
+ }
100
+ });
101
+ this.saveDataForInspection(rawData, navigationData);
102
+ if (this.debug) {
103
+ console.log(`\u2705 Processed ${navigationData.length} navigation transitions`);
104
+ if (navigationData.length > 0) {
105
+ console.log(`
106
+ Top 5 transitions:`);
107
+ navigationData.slice(0, 5).forEach((nav, i) => {
108
+ console.log(` ${i + 1}. ${nav.from} \u2192 ${nav.to} (${nav.count} transitions)`);
109
+ });
110
+ }
111
+ const uniqueRoutes = /* @__PURE__ */ new Set();
112
+ navigationData.forEach((nav) => {
113
+ if (nav.from !== "(direct)") uniqueRoutes.add(nav.from);
114
+ uniqueRoutes.add(nav.to);
115
+ });
116
+ console.log(`
117
+ \u{1F4CA} Unique routes: ${uniqueRoutes.size}`);
118
+ console.log(` Routes: ${Array.from(uniqueRoutes).sort().join(", ")}`);
119
+ console.log(`
120
+ \u{1F4C1} Raw data saved to: .bigquery-raw-data/`);
121
+ }
122
+ return navigationData;
123
+ } catch (error) {
124
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
125
+ if (errorMessage.includes("bigquery.jobs.create") || errorMessage.includes("PERMISSION_DENIED")) {
126
+ console.error("\u274C Failed to fetch BigQuery data: Permission Denied");
127
+ console.error(" Service account needs BigQuery Job User role in the project");
128
+ console.error(" Error details:", errorMessage);
129
+ } else {
130
+ console.error("\u274C Failed to fetch BigQuery data:", error);
131
+ }
132
+ throw new Error(
133
+ `BigQuery Analytics error: ${errorMessage}`
134
+ );
135
+ }
136
+ }
137
+ /**
138
+ * Map friendly page names (with spaces/capitals) back to route paths
139
+ * GA4 may log display names instead of URL paths
140
+ * Examples: "Audit Logs" → "/audit-logs", "API Documentation" → "/api-docs", "Home" → "/"
141
+ */
142
+ friendlyNameToRoute(name) {
143
+ const friendlyToRoute = {
144
+ // Exact matches (with spaces and capitals)
145
+ "API Documentation": "/api-docs",
146
+ "Audit Logs": "/audit-logs",
147
+ "/Home": "/",
148
+ "/home": "/",
149
+ "Home": "/",
150
+ "/Dashboard": "/dashboard",
151
+ "Dashboard": "/dashboard",
152
+ // PascalCase variants (all 27 routes)
153
+ "Profile": "/profile",
154
+ "Settings": "/settings",
155
+ "Preferences": "/preferences",
156
+ "Privacy": "/privacy",
157
+ "Security": "/security",
158
+ "Analytics": "/analytics",
159
+ "Reports": "/reports",
160
+ "Metrics": "/metrics",
161
+ "Projects": "/projects",
162
+ "Tasks": "/tasks",
163
+ "Teams": "/teams",
164
+ "Workspaces": "/workspaces",
165
+ "Workflows": "/workflows",
166
+ "Templates": "/templates",
167
+ "Logs": "/logs",
168
+ "AuditLogs": "/audit-logs",
169
+ "Integrations": "/integrations",
170
+ "ApiDocs": "/api-docs",
171
+ "Support": "/support",
172
+ "Help": "/help",
173
+ "Billing": "/billing",
174
+ "Plans": "/plans",
175
+ "Usage": "/usage",
176
+ "Permissions": "/permissions",
177
+ "Notifications": "/notifications"
178
+ };
179
+ if (friendlyToRoute[name]) {
180
+ return friendlyToRoute[name];
181
+ }
182
+ const lowerName = name.toLowerCase();
183
+ for (const [key, value] of Object.entries(friendlyToRoute)) {
184
+ if (key.toLowerCase() === lowerName) {
185
+ return value;
186
+ }
187
+ }
188
+ if (name.startsWith("/")) {
189
+ if (name === "/") {
190
+ return "/";
191
+ }
192
+ return name.toLowerCase();
193
+ }
194
+ return "/" + lowerName;
195
+ }
196
+ /**
197
+ * Normalize route paths
198
+ * Converts dynamic segments to parameters and friendly names to route paths
199
+ */
200
+ normalizeRoute(path2) {
201
+ const buildArtifacts = [
202
+ /\.(js|css|jpg|jpeg|png|gif|svg|webp|woff|woff2|ttf|eot|map)(\?|#|$)/i,
203
+ /\[hash\]/i,
204
+ /\/chunks\//i,
205
+ /\/vendor\//i,
206
+ /\/assets\//i,
207
+ /\.vite\//i
208
+ ];
209
+ if (buildArtifacts.some((pattern) => pattern.test(path2))) {
210
+ return "";
211
+ }
212
+ let normalized = this.friendlyNameToRoute(path2);
213
+ normalized = normalized.split("?")[0].split("#")[0];
214
+ if (normalized.length > 1 && normalized.endsWith("/")) {
215
+ normalized = normalized.slice(0, -1);
216
+ }
217
+ if (normalized !== "/") {
218
+ normalized = normalized.toLowerCase().replace(/ +/g, "-");
219
+ }
220
+ normalized = normalized.replace(/\/[0-9a-f]{24}(?=\/|$)/gi, "/:id").replace(/\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(?=\/|$)/gi, "/:id").replace(/\/\d+(?=\/|$)/g, "/:id").replace(/\/[\w.-]+@[\w.-]+\.[\w]+(?=\/|$)/gi, "/:email").replace(/\/\d{4}-\d{2}-\d{2}(?=\/|$)/g, "/:date");
221
+ return normalized;
222
+ }
223
+ /**
224
+ * Save raw and processed data for inspection
225
+ * Helps identify issues in data transformation pipeline
226
+ */
227
+ saveDataForInspection(rawData, processedData) {
228
+ try {
229
+ const outputDir = ".bigquery-raw-data";
230
+ const outputPath = join(process.cwd(), outputDir);
231
+ mkdirSync(outputPath, { recursive: true });
232
+ const rawFile = join(outputPath, "raw-bigquery-response.json");
233
+ writeFileSync(rawFile, JSON.stringify(rawData, null, 2));
234
+ const processedFile = join(outputPath, "processed-navigation-data.json");
235
+ writeFileSync(processedFile, JSON.stringify(processedData, null, 2));
236
+ const summary = {
237
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
238
+ rawRowCount: rawData.length,
239
+ processedRowCount: processedData.length,
240
+ dataLoss: rawData.length - processedData.length,
241
+ dataLossPercentage: ((rawData.length - processedData.length) / rawData.length * 100).toFixed(2) + "%",
242
+ totalTransitions: processedData.reduce((sum, d) => sum + d.count, 0),
243
+ uniqueFromRoutes: new Set(processedData.map((d) => d.from)).size,
244
+ uniqueToRoutes: new Set(processedData.map((d) => d.to)).size,
245
+ topTransitions: processedData.slice(0, 10)
246
+ };
247
+ const summaryFile = join(outputPath, "data-transformation-summary.json");
248
+ writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
249
+ if (this.debug) {
250
+ console.log(`
251
+ \u{1F4CA} Data Inspection Summary:`);
252
+ console.log(` Raw rows from BigQuery: ${summary.rawRowCount}`);
253
+ console.log(` Processed rows (after filtering): ${summary.processedRowCount}`);
254
+ console.log(` Data loss: ${summary.dataLoss} rows (${summary.dataLossPercentage})`);
255
+ console.log(` Total transitions count: ${summary.totalTransitions}`);
256
+ console.log(` Unique source routes: ${summary.uniqueFromRoutes}`);
257
+ console.log(` Unique destination routes: ${summary.uniqueToRoutes}`);
258
+ console.log(`
259
+ Files saved:`);
260
+ console.log(` \u2022 ${rawFile}`);
261
+ console.log(` \u2022 ${processedFile}`);
262
+ console.log(` \u2022 ${summaryFile}`);
263
+ }
264
+ } catch (error) {
265
+ console.warn(`\u26A0\uFE0F Could not save inspection data:`, error instanceof Error ? error.message : error);
266
+ }
267
+ }
268
+ /**
269
+ * Fetch navigation data WITH segment information
270
+ * Includes segment/role field so plugin can group by any role dynamically
271
+ * @param config - Data range configuration
272
+ * @returns Navigation data with segment field included
273
+ */
274
+ async fetchNavigationWithSegments(config = {}) {
275
+ const { days = 30 } = config;
276
+ console.log(`\u{1F4CA} Fetching navigation data with segments from BigQuery...`);
277
+ console.log(` Dataset: ${this.datasetId}`);
278
+ console.log(` Date range: Last ${days} days`);
279
+ try {
280
+ const query = `
281
+ WITH page_sessions AS (
282
+ SELECT
283
+ user_id,
284
+ (SELECT value.int_value FROM UNNEST(event_params) WHERE KEY = 'ga_session_id' LIMIT 1) as session_id,
285
+ (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'page_path' LIMIT 1) as page_path,
286
+ (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'user_role' LIMIT 1) as user_role,
287
+ (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'user_segment' LIMIT 1) as user_segment,
288
+ event_timestamp,
289
+ ROW_NUMBER() OVER (
290
+ PARTITION BY user_id, (SELECT value.int_value FROM UNNEST(event_params) WHERE KEY = 'ga_session_id' LIMIT 1)
291
+ ORDER BY event_timestamp
292
+ ) as page_sequence
293
+ FROM \`${this.projectId}.${this.datasetId}.events_*\`
294
+ WHERE
295
+ event_name = 'page_view'
296
+ AND _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL ${days} DAY))
297
+ AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
298
+ AND (SELECT value.string_value FROM UNNEST(event_params) WHERE KEY = 'page_path' LIMIT 1) IS NOT NULL
299
+ ),
300
+ transitions AS (
301
+ SELECT
302
+ COALESCE(curr.page_path, '(direct)') as from_page,
303
+ LEAD(curr.page_path) OVER (
304
+ PARTITION BY curr.user_id, curr.session_id
305
+ ORDER BY curr.page_sequence
306
+ ) as to_page,
307
+ COALESCE(curr.user_role, curr.user_segment, 'unknown') as segment
308
+ FROM page_sessions curr
309
+ WHERE curr.page_sequence > 0
310
+ )
311
+ SELECT
312
+ from_page as previous_page_path,
313
+ to_page as page_path,
314
+ segment,
315
+ COUNT(*) as transition_count
316
+ FROM transitions
317
+ WHERE to_page IS NOT NULL
318
+ GROUP BY from_page, to_page, segment
319
+ ORDER BY segment, transition_count DESC
320
+ LIMIT 50000
321
+ `;
322
+ if (this.debug) {
323
+ console.log(`\u{1F4E4} Query with segments:`);
324
+ console.log(query);
325
+ }
326
+ const [rows] = await this.bigquery.query({
327
+ query,
328
+ location: "US"
329
+ });
330
+ if (this.debug) {
331
+ console.log(`\u2705 Query executed successfully`);
332
+ console.log(` Rows returned: ${rows.length}`);
333
+ }
334
+ const navigationData = [];
335
+ rows.forEach((row) => {
336
+ const previousPage = row.previous_page_path || "(direct)";
337
+ const currentPage = row.page_path || "";
338
+ const transitionCount = parseInt(row.transition_count || "0");
339
+ const segment = row.segment || "unknown";
340
+ const normalizedPrevious = previousPage === "(direct)" || previousPage === "(not set)" ? "(direct)" : this.normalizeRoute(previousPage);
341
+ const normalizedCurrent = this.normalizeRoute(currentPage);
342
+ if (normalizedCurrent && transitionCount >= 1) {
343
+ navigationData.push({
344
+ from: normalizedPrevious,
345
+ to: normalizedCurrent,
346
+ count: transitionCount,
347
+ segment
348
+ // Include segment/role field
349
+ });
350
+ }
351
+ });
352
+ if (this.debug && navigationData.length > 0) {
353
+ console.log(`\u2705 Processed ${navigationData.length} transitions with segment data`);
354
+ const uniqueSegments = new Set(navigationData.map((d) => d.segment));
355
+ console.log(`
356
+ Detected segments: ${Array.from(uniqueSegments).sort().join(", ")}`);
357
+ const bySegment = /* @__PURE__ */ new Map();
358
+ navigationData.forEach((d) => {
359
+ if (!bySegment.has(d.segment)) {
360
+ bySegment.set(d.segment, []);
361
+ }
362
+ bySegment.get(d.segment).push(d);
363
+ });
364
+ bySegment.forEach((transitions, segment) => {
365
+ console.log(`
366
+ Top 3 transitions for ${segment}:`);
367
+ transitions.sort((a, b) => b.count - a.count).slice(0, 3).forEach((nav, i) => {
368
+ console.log(` ${i + 1}. ${nav.from} \u2192 ${nav.to} (${nav.count} transitions)`);
369
+ });
370
+ });
371
+ }
372
+ return navigationData;
373
+ } catch (error) {
374
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
375
+ console.warn(`\u26A0\uFE0F Failed to fetch navigation data with segments: ${errorMessage}`);
376
+ return [];
377
+ }
378
+ }
379
+ /**
380
+ * Test connection to BigQuery
381
+ * Note: May fail if service account lacks bigquery.jobs.create permission
382
+ * but actual queries may still work. This is expected for viewer-only accounts.
383
+ */
384
+ async testConnection() {
385
+ try {
386
+ const query = `SELECT 1 as test_value`;
387
+ await this.bigquery.query({
388
+ query,
389
+ location: "US"
390
+ });
391
+ if (this.debug) {
392
+ console.log("\u2705 BigQuery connection test successful");
393
+ }
394
+ return true;
395
+ } catch (error) {
396
+ if (this.debug) {
397
+ console.warn("\u26A0\uFE0F BigQuery connection test failed (may be expected for read-only accounts)");
398
+ }
399
+ return false;
400
+ }
401
+ }
402
+ };
403
+
404
+ // src/plugin/model/guessjs-ml-trainer.ts
405
+ var MarkovChainTrainer = class {
406
+ constructor(config, debug = false) {
407
+ this.config = config;
408
+ this.debug = debug;
409
+ }
410
+ /**
411
+ * Train Markov chain model from navigation data
412
+ * Calculates transition probabilities: P(dest | src) = transitions(src→dest) / total_from_src
413
+ */
414
+ trainMLModel(navigationData, environment) {
415
+ if (this.debug) {
416
+ console.log(`
417
+ \u{1F916} Training Markov Chain Model...`);
418
+ console.log(` Model type: ${this.config.type}`);
419
+ console.log(` Threshold: ${this.config.threshold * 100}%`);
420
+ console.log(` Max prefetch per route: ${this.config.maxPrefetch}`);
421
+ console.log(` Input transitions: ${navigationData.length}`);
422
+ }
423
+ const navigationGraph = this.buildNavigationGraph(navigationData);
424
+ const routes = this.extractRoutes(navigationData);
425
+ if (this.debug) {
426
+ console.log(`
427
+ \u{1F4CA} Analysis:`);
428
+ console.log(` Unique routes: ${routes.size}`);
429
+ console.log(` Routes: ${Array.from(routes).sort().join(", ")}`);
430
+ console.log(` Source routes: ${navigationGraph.size}`);
431
+ }
432
+ const predictions = this.calculateMarkovProbabilities(navigationGraph);
433
+ if (this.debug) {
434
+ console.log(`
435
+ Predictions generated:`);
436
+ console.log(` Routes with predictions: ${predictions.size}`);
437
+ const totalTargets = Array.from(predictions.values()).reduce(
438
+ (sum, targets) => sum + targets.length,
439
+ 0
440
+ );
441
+ console.log(` Total prefetch targets: ${totalTargets}`);
442
+ }
443
+ const model = {
444
+ version: "1.0.0",
445
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
446
+ environment,
447
+ config: this.config,
448
+ dataSource: {
449
+ provider: "markov-chain",
450
+ dateRange: this.getDateRange(30),
451
+ totalSessions: navigationData.reduce((sum, d) => sum + d.count, 0),
452
+ totalRoutes: routes.size
453
+ },
454
+ routes: {}
455
+ };
456
+ predictions.forEach((targets, source) => {
457
+ model.routes[source] = {
458
+ prefetch: targets,
459
+ metadata: this.calculateMetadata(navigationGraph, source)
460
+ };
461
+ });
462
+ if (this.debug) {
463
+ console.log(`
464
+ \u2705 Model trained successfully`);
465
+ const sampleRoutes = Array.from(predictions.entries()).slice(0, 3);
466
+ if (sampleRoutes.length > 0) {
467
+ console.log(`
468
+ Sample predictions:`);
469
+ sampleRoutes.forEach(([source, targets]) => {
470
+ console.log(` ${source}:`);
471
+ targets.forEach((target) => {
472
+ console.log(
473
+ ` \u2192 ${target.route} (${(target.probability * 100).toFixed(1)}%, ${target.priority})`
474
+ );
475
+ });
476
+ });
477
+ }
478
+ }
479
+ return model;
480
+ }
481
+ /**
482
+ * Extract all unique routes from navigation data
483
+ */
484
+ extractRoutes(data) {
485
+ const routes = /* @__PURE__ */ new Set();
486
+ data.forEach((d) => {
487
+ routes.add(d.from);
488
+ routes.add(d.to);
489
+ });
490
+ return routes;
491
+ }
492
+ /**
493
+ * Build navigation graph from user navigation patterns
494
+ * Graph represents: sourceRoute -> { targetRoute: transitionCount }
495
+ */
496
+ buildNavigationGraph(data) {
497
+ const graph = /* @__PURE__ */ new Map();
498
+ data.forEach(({ from, to, count }) => {
499
+ if (!graph.has(from)) {
500
+ graph.set(from, /* @__PURE__ */ new Map());
501
+ }
502
+ const targets = graph.get(from);
503
+ targets.set(to, (targets.get(to) || 0) + count);
504
+ });
505
+ return graph;
506
+ }
507
+ /**
508
+ * Calculate Markov probabilities with normalization
509
+ * P(dest | src) = count(src→dest) / total_transitions_from_src
510
+ * Then normalizes scores to 0-1 range
511
+ */
512
+ calculateMarkovProbabilities(navigationGraph) {
513
+ const predictions = /* @__PURE__ */ new Map();
514
+ navigationGraph.forEach((targets, source) => {
515
+ const targetArray = [];
516
+ const totalFromSource = Array.from(targets.values()).reduce(
517
+ (sum, count) => sum + count,
518
+ 0
519
+ );
520
+ if (totalFromSource === 0) return;
521
+ const probabilities = /* @__PURE__ */ new Map();
522
+ targets.forEach((count, destination) => {
523
+ probabilities.set(destination, count / totalFromSource);
524
+ });
525
+ const maxProb = Math.max(...Array.from(probabilities.values()));
526
+ if (maxProb > 0) {
527
+ probabilities.forEach((prob, dest) => {
528
+ probabilities.set(dest, prob / maxProb);
529
+ });
530
+ }
531
+ const sorted = Array.from(probabilities.entries()).sort((a, b) => b[1] - a[1]).slice(0, this.config.maxPrefetch);
532
+ sorted.forEach(([route, probability]) => {
533
+ if (probability >= this.config.threshold) {
534
+ targetArray.push({
535
+ route,
536
+ probability,
537
+ count: targets.get(route) || 0,
538
+ priority: this.getPriority(probability)
539
+ });
540
+ }
541
+ });
542
+ if (targetArray.length > 0) {
543
+ predictions.set(source, targetArray);
544
+ }
545
+ });
546
+ return predictions;
547
+ }
548
+ /**
549
+ * Determine priority based on ML confidence score
550
+ */
551
+ getPriority(score) {
552
+ if (score >= 0.7) return "high";
553
+ if (score >= 0.4) return "medium";
554
+ return "low";
555
+ }
556
+ /**
557
+ * Calculate metadata for a route
558
+ */
559
+ calculateMetadata(navigationGraph, route) {
560
+ const targets = navigationGraph.get(route);
561
+ if (!targets || targets.size === 0) {
562
+ return {
563
+ totalTransitions: 0,
564
+ topDestination: ""
565
+ };
566
+ }
567
+ const totalTransitions = Array.from(targets.values()).reduce(
568
+ (sum, count) => sum + count,
569
+ 0
570
+ );
571
+ let topDestination = "";
572
+ let maxCount = 0;
573
+ targets.forEach((count, targetRoute) => {
574
+ if (count > maxCount) {
575
+ maxCount = count;
576
+ topDestination = targetRoute;
577
+ }
578
+ });
579
+ return {
580
+ totalTransitions,
581
+ topDestination
582
+ };
583
+ }
584
+ /**
585
+ * Train segment-specific Markov models from navigation data with segment field
586
+ * Creates separate models for each user segment/role
587
+ */
588
+ trainSegmentedModels(navigationDataWithSegments, environment) {
589
+ if (this.debug) {
590
+ console.log(`
591
+ \u{1F916} Training Segment-Specific Markov Models...`);
592
+ }
593
+ const dataBySegment = /* @__PURE__ */ new Map();
594
+ navigationDataWithSegments.forEach((data) => {
595
+ const segment = data.segment || "default";
596
+ if (!dataBySegment.has(segment)) {
597
+ dataBySegment.set(segment, []);
598
+ }
599
+ dataBySegment.get(segment).push({
600
+ from: data.from,
601
+ to: data.to,
602
+ count: data.count
603
+ });
604
+ });
605
+ if (this.debug) {
606
+ console.log(`
607
+ \u{1F4CA} Detected segments:`);
608
+ dataBySegment.forEach((data, segment) => {
609
+ console.log(` \u2022 ${segment}: ${data.length} transitions`);
610
+ });
611
+ }
612
+ const segmentModels = /* @__PURE__ */ new Map();
613
+ dataBySegment.forEach((navigationData, segment) => {
614
+ if (this.debug) {
615
+ console.log(`
616
+ \u{1F504} Training model for segment: "${segment}"`);
617
+ }
618
+ const model = this.trainMLModel(navigationData, environment);
619
+ model.dataSource.provider = `markov-chain[${segment}]`;
620
+ segmentModels.set(segment, model);
621
+ if (this.debug) {
622
+ const totalTargets = Object.values(model.routes).reduce(
623
+ (sum, route) => sum + (route.prefetch?.length || 0),
624
+ 0
625
+ );
626
+ console.log(` \u2705 Model for "${segment}" trained with ${totalTargets} prefetch targets`);
627
+ }
628
+ });
629
+ if (this.debug) {
630
+ console.log(`
631
+ \u2705 All ${segmentModels.size} segment models trained successfully`);
632
+ }
633
+ return segmentModels;
634
+ }
635
+ /**
636
+ * Get date range string
637
+ */
638
+ getDateRange(days) {
639
+ const end = /* @__PURE__ */ new Date();
640
+ const start = /* @__PURE__ */ new Date();
641
+ start.setDate(start.getDate() - days);
642
+ return `${start.toISOString().split("T")[0]} to ${end.toISOString().split("T")[0]}`;
643
+ }
644
+ };
645
+
646
+ // src/plugin/config-generator.ts
647
+ var _ConfigGenerator = class _ConfigGenerator {
648
+ constructor(manifest, manualRules = {}, debug = false) {
649
+ this.manifest = manifest;
650
+ this.manualRules = manualRules;
651
+ this.debug = debug;
652
+ }
653
+ /**
654
+ * Generate final prefetch configuration with chunk mappings
655
+ * Includes common prefetch rules and segment-specific rules if available
656
+ */
657
+ generate(model) {
658
+ if (this.debug) {
659
+ console.log(`
660
+ \u{1F4E6} Generating prefetch configuration...`);
661
+ console.log(` Manifest entries: ${Object.keys(this.manifest).length}`);
662
+ console.log(` Model routes: ${Object.keys(model.routes).length}`);
663
+ console.log(` Manual rules: ${Object.keys(this.manualRules).length}`);
664
+ if (Object.values(model.routes).some((r) => r.segments)) {
665
+ console.log(` \u2139\uFE0F Segment-based rules detected`);
666
+ }
667
+ }
668
+ const mergedModel = this.mergeManualRules(model);
669
+ const config = {
670
+ version: model.version,
671
+ generatedAt: model.generatedAt,
672
+ environment: model.environment,
673
+ dataSource: model.dataSource,
674
+ model: {
675
+ type: model.config.type,
676
+ threshold: model.config.threshold,
677
+ maxPrefetch: model.config.maxPrefetch
678
+ },
679
+ routes: {},
680
+ chunks: {}
681
+ };
682
+ let mappedRoutes = 0;
683
+ let unmappedRoutes = 0;
684
+ let totalSegmentRules = 0;
685
+ Object.entries(mergedModel.routes).forEach(([sourceRoute, prediction]) => {
686
+ const prefetchTargets = [];
687
+ const segmentConfigs = {};
688
+ const sourceChunk = this.routeToChunk(sourceRoute);
689
+ if (sourceChunk && !config.chunks[sourceRoute]) {
690
+ config.chunks[sourceRoute] = sourceChunk;
691
+ }
692
+ prediction.prefetch.forEach((target) => {
693
+ const chunkFile = this.routeToChunk(target.route);
694
+ if (chunkFile) {
695
+ const manifestEntry = this.getManifestEntryByFile(chunkFile);
696
+ const importChunkIds = manifestEntry?.imports || [];
697
+ const imports = importChunkIds.map((chunkId) => {
698
+ const entry = this.manifest[chunkId];
699
+ return entry?.file;
700
+ }).filter((file) => !!file);
701
+ prefetchTargets.push({
702
+ ...target,
703
+ chunk: chunkFile,
704
+ imports
705
+ // Include dependency chunks (resolved to file paths)
706
+ });
707
+ config.chunks[target.route] = chunkFile;
708
+ mappedRoutes++;
709
+ if (this.debug) {
710
+ console.log(` \u2705 ${sourceRoute} \u2192 ${target.route}`);
711
+ console.log(` Chunk: ${chunkFile}`);
712
+ if (imports.length > 0) {
713
+ console.log(` Dependencies: ${imports.join(", ")}`);
714
+ }
715
+ }
716
+ } else {
717
+ if (this.debug) {
718
+ console.log(` \u26A0\uFE0F No chunk found for route: ${target.route}`);
719
+ console.log(` Attempted to map using routeToChunk()`);
720
+ }
721
+ unmappedRoutes++;
722
+ }
723
+ });
724
+ if (prediction.segments) {
725
+ Object.entries(prediction.segments).forEach(([segment, segmentTargets]) => {
726
+ const segmentPrefetchTargets = [];
727
+ segmentTargets.forEach((target) => {
728
+ const chunkFile = this.routeToChunk(target.route);
729
+ if (chunkFile) {
730
+ const manifestEntry = this.getManifestEntryByFile(chunkFile);
731
+ const importChunkIds = manifestEntry?.imports || [];
732
+ const imports = importChunkIds.map((chunkId) => {
733
+ const entry = this.manifest[chunkId];
734
+ return entry?.file;
735
+ }).filter((file) => !!file);
736
+ segmentPrefetchTargets.push({
737
+ ...target,
738
+ chunk: chunkFile,
739
+ imports
740
+ });
741
+ config.chunks[target.route] = chunkFile;
742
+ totalSegmentRules++;
743
+ }
744
+ });
745
+ if (segmentPrefetchTargets.length > 0) {
746
+ segmentConfigs[segment] = segmentPrefetchTargets;
747
+ }
748
+ });
749
+ if (this.debug && Object.keys(segmentConfigs).length > 0) {
750
+ console.log(` \u{1F465} Segment configs for ${sourceRoute}: ${Object.keys(segmentConfigs).join(", ")}`);
751
+ }
752
+ }
753
+ if (prefetchTargets.length > 0 || Object.keys(segmentConfigs).length > 0) {
754
+ config.routes[sourceRoute] = {
755
+ prefetch: prefetchTargets,
756
+ ...Object.keys(segmentConfigs).length > 0 && {
757
+ segments: segmentConfigs
758
+ },
759
+ metadata: prediction.metadata
760
+ };
761
+ }
762
+ });
763
+ if (this.debug) {
764
+ console.log(`\u2705 Configuration generated`);
765
+ console.log(` Routes with prefetch: ${Object.keys(config.routes).length}`);
766
+ console.log(` Mapped chunks: ${mappedRoutes}`);
767
+ console.log(` Segment-specific rules: ${totalSegmentRules}`);
768
+ console.log(` Unmapped routes: ${unmappedRoutes}`);
769
+ }
770
+ const allSegments = /* @__PURE__ */ new Set();
771
+ Object.values(config.routes).forEach((route) => {
772
+ if (route.segments) {
773
+ Object.keys(route.segments).forEach((seg) => allSegments.add(seg));
774
+ }
775
+ });
776
+ if (allSegments.size > 0) {
777
+ config.segmentInfo = {
778
+ available: Array.from(allSegments),
779
+ description: "Load segment-specific config based on user role/segment"
780
+ };
781
+ }
782
+ return config;
783
+ }
784
+ /**
785
+ * Merge manual rules into model
786
+ * Manual rules take precedence over ML predictions
787
+ */
788
+ mergeManualRules(model) {
789
+ const merged = { ...model, routes: { ...model.routes } };
790
+ Object.entries(this.manualRules).forEach(([sourceRoute, targetRoutes]) => {
791
+ if (!merged.routes[sourceRoute]) {
792
+ merged.routes[sourceRoute] = {
793
+ prefetch: []
794
+ };
795
+ }
796
+ targetRoutes.forEach((targetRoute) => {
797
+ const existing = merged.routes[sourceRoute].prefetch.find(
798
+ (t) => t.route === targetRoute
799
+ );
800
+ if (existing) {
801
+ existing.manual = true;
802
+ existing.priority = "high";
803
+ existing.probability = 1;
804
+ } else {
805
+ merged.routes[sourceRoute].prefetch.unshift({
806
+ route: targetRoute,
807
+ probability: 1,
808
+ count: 0,
809
+ // Not from analytics
810
+ priority: "high",
811
+ manual: true
812
+ });
813
+ }
814
+ });
815
+ if (this.debug) {
816
+ console.log(` \u2705 Added manual rule: ${sourceRoute} \u2192 [${targetRoutes.join(", ")}]`);
817
+ }
818
+ });
819
+ return merged;
820
+ }
821
+ /**
822
+ * NEW STRATEGY 0: Route to Chunk Name Mapping
823
+ * Maps route → component name → chunk name → manifest file
824
+ * This is the most reliable method as it uses the actual vite.config.ts chunking strategy
825
+ */
826
+ routeToChunkViaName(route) {
827
+ const normalizedRoute = route.toLowerCase();
828
+ const componentName = _ConfigGenerator.ROUTE_TO_COMPONENT_NAME[normalizedRoute];
829
+ if (!componentName) {
830
+ if (this.debug) {
831
+ console.log(` \u274C Route ${route} not in ROUTE_TO_COMPONENT_NAME mapping`);
832
+ }
833
+ return null;
834
+ }
835
+ const chunkName = _ConfigGenerator.COMPONENT_TO_CHUNK_NAME[componentName];
836
+ if (!chunkName) {
837
+ if (this.debug) {
838
+ console.log(` \u274C Component ${componentName} not in COMPONENT_TO_CHUNK_NAME mapping`);
839
+ }
840
+ return null;
841
+ }
842
+ const chunkEntry = Object.entries(this.manifest).find(
843
+ ([key, entry]) => entry.name === chunkName || key.includes(chunkName)
844
+ );
845
+ if (!chunkEntry) {
846
+ if (this.debug) {
847
+ console.log(` \u274C Chunk name ${chunkName} not found in manifest`);
848
+ }
849
+ return null;
850
+ }
851
+ const chunkFile = chunkEntry[1].file;
852
+ if (this.debug) {
853
+ console.log(
854
+ ` \u2705 Via-Name Strategy: ${route} \u2192 ${componentName} \u2192 ${chunkName} \u2192 ${chunkFile}`
855
+ );
856
+ }
857
+ return chunkFile;
858
+ }
859
+ /**
860
+ * Map route to chunk file using Vite manifest
861
+ * Tries multiple strategies to find the correct chunk
862
+ */
863
+ routeToChunk(route) {
864
+ const viaName = this.routeToChunkViaName(route);
865
+ if (viaName) {
866
+ return viaName;
867
+ }
868
+ const directMatch = this.findDirectMatch(route);
869
+ if (directMatch) {
870
+ return directMatch;
871
+ }
872
+ const patternMatch = this.findPatternMatch(route);
873
+ if (patternMatch) {
874
+ return patternMatch;
875
+ }
876
+ const fuzzyMatch = this.findFuzzyMatch(route);
877
+ if (fuzzyMatch) {
878
+ return fuzzyMatch;
879
+ }
880
+ return null;
881
+ }
882
+ /**
883
+ * Strategy 1: Direct path match
884
+ */
885
+ findDirectMatch(route) {
886
+ const normalizedRoute = route.replace(/\/[:$]\w+/g, "");
887
+ const cleanRoutePath = normalizedRoute.replace(/^\//, "");
888
+ const routeSegments = normalizedRoute.split("/").filter(Boolean);
889
+ const pascalCaseComponent = routeSegments.map((segment) => {
890
+ const words = segment.split("-");
891
+ return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
892
+ }).join("/");
893
+ let singleSegmentPascal = null;
894
+ if (routeSegments.length === 1) {
895
+ const segment = routeSegments[0];
896
+ const words = segment.split("-");
897
+ singleSegmentPascal = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
898
+ }
899
+ const patterns = [
900
+ // HIGHEST PRIORITY: Exact page component name matches
901
+ singleSegmentPascal ? `src/pages/${singleSegmentPascal}.tsx` : null,
902
+ singleSegmentPascal ? `src/pages/${singleSegmentPascal}.ts` : null,
903
+ singleSegmentPascal ? `src/pages/${singleSegmentPascal}.jsx` : null,
904
+ singleSegmentPascal ? `src/pages/${singleSegmentPascal}.js` : null,
905
+ // For multi-segment routes with hyphens
906
+ `src/pages/${pascalCaseComponent}.tsx`,
907
+ `src/pages/${pascalCaseComponent}.ts`,
908
+ `src/pages/${pascalCaseComponent}.jsx`,
909
+ `src/pages/${pascalCaseComponent}.js`,
910
+ // Features folder
911
+ `src/features${normalizedRoute}/index.ts`,
912
+ `src/features${normalizedRoute}/index.tsx`,
913
+ `src/features${normalizedRoute}/index.js`,
914
+ `src/features${normalizedRoute}/index.jsx`,
915
+ // Pages folder - try both directory and file formats
916
+ `src/pages${normalizedRoute}/index.tsx`,
917
+ `src/pages${normalizedRoute}/index.ts`,
918
+ `src/pages${normalizedRoute}/index.jsx`,
919
+ `src/pages${normalizedRoute}/index.js`,
920
+ `src/pages${normalizedRoute}.tsx`,
921
+ `src/pages${normalizedRoute}.ts`,
922
+ `src/pages${normalizedRoute}.jsx`,
923
+ `src/pages${normalizedRoute}.js`,
924
+ // Fallback to old capitalize method (single capital letter)
925
+ `src/pages/${this.capitalize(cleanRoutePath)}.tsx`,
926
+ `src/pages/${this.capitalize(cleanRoutePath)}.ts`,
927
+ // Full paths with app prefix
928
+ `apps/farmart-pro/src/features${normalizedRoute}/index.ts`,
929
+ `apps/farmart-pro/src/features${normalizedRoute}/index.tsx`,
930
+ `apps/farmart-pro/src/pages${normalizedRoute}/index.tsx`
931
+ ].filter(Boolean);
932
+ for (let i = 0; i < patterns.length; i++) {
933
+ const pattern = patterns[i];
934
+ const entry = this.manifest[pattern];
935
+ if (entry) {
936
+ return entry.file;
937
+ }
938
+ }
939
+ return null;
940
+ }
941
+ /**
942
+ * Capitalize first letter of string
943
+ */
944
+ capitalize(str) {
945
+ return str.charAt(0).toUpperCase() + str.slice(1);
946
+ }
947
+ /**
948
+ * Strategy 2: Pattern matching with wildcards
949
+ */
950
+ findPatternMatch(route) {
951
+ const routeSegments = route.split("/").filter(Boolean);
952
+ if (routeSegments.length === 0) return null;
953
+ const candidates = Object.entries(this.manifest).filter(([path2]) => {
954
+ const pathSegments = path2.split("/").filter(Boolean);
955
+ return routeSegments.every(
956
+ (segment) => pathSegments.some(
957
+ (ps) => ps.toLowerCase().includes(segment.replace(/[:$]\w+/, "").toLowerCase())
958
+ )
959
+ );
960
+ }).sort(([pathA], [pathB]) => {
961
+ if (pathA.includes("/pages/") && !pathB.includes("/pages/")) return -1;
962
+ if (!pathA.includes("/pages/") && pathB.includes("/pages/")) return 1;
963
+ const aIsEntry = pathA.includes("index.tsx") || pathA.includes("index.ts");
964
+ const bIsEntry = pathB.includes("index.tsx") || pathB.includes("index.ts");
965
+ if (aIsEntry && !bIsEntry) return -1;
966
+ if (!aIsEntry && bIsEntry) return 1;
967
+ if (pathA.includes(".tsx") && !pathB.includes(".tsx")) return -1;
968
+ if (!pathA.includes(".tsx") && pathB.includes(".tsx")) return 1;
969
+ return 0;
970
+ });
971
+ if (candidates.length > 0) {
972
+ if (this.debug) {
973
+ console.log(` \u2705 Pattern match found: ${candidates[0][0]} \u2192 ${candidates[0][1].file}`);
974
+ }
975
+ return candidates[0][1].file;
976
+ }
977
+ if (this.debug) {
978
+ console.log(` No pattern match found`);
979
+ }
980
+ return null;
981
+ }
982
+ /**
983
+ * Strategy 3: Fuzzy matching
984
+ * Converts route to camelCase/PascalCase and searches
985
+ */
986
+ findFuzzyMatch(route) {
987
+ const cleanRoute = route.replace(/\/[:$]\w+/g, "");
988
+ const routeSegments = cleanRoute.split("/").filter(Boolean);
989
+ const pascalCase = routeSegments.map((segment) => {
990
+ const words = segment.split("-");
991
+ return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
992
+ }).join("");
993
+ const camelCase = pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
994
+ if (this.debug) {
995
+ console.log(` Fuzzy match - Route: ${route}`);
996
+ console.log(` Trying PascalCase: ${pascalCase}, camelCase: ${camelCase}`);
997
+ }
998
+ const candidates = Object.entries(this.manifest).filter(([path2, entry]) => {
999
+ if (path2.startsWith("_") || path2.startsWith("node_modules")) {
1000
+ return false;
1001
+ }
1002
+ const fileName = path2.split("/").pop() || "";
1003
+ const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
1004
+ if (fileNameWithoutExt === pascalCase || fileNameWithoutExt === camelCase) {
1005
+ return true;
1006
+ }
1007
+ if (fileName.includes(pascalCase) || fileName.includes(camelCase)) {
1008
+ return true;
1009
+ }
1010
+ const pathSegments = path2.toLowerCase().split("/");
1011
+ const lowerPascal = pascalCase.toLowerCase();
1012
+ const lowerCamel = camelCase.toLowerCase();
1013
+ return pathSegments.some(
1014
+ (seg) => seg.includes(lowerPascal) || seg.includes(lowerCamel)
1015
+ );
1016
+ }).sort(([pathA, entryA], [pathB, entryB]) => {
1017
+ const aHasSrc = entryA.src ? 1 : 0;
1018
+ const bHasSrc = entryB.src ? 1 : 0;
1019
+ if (aHasSrc !== bHasSrc) return bHasSrc - aHasSrc;
1020
+ const fileA = pathA.split("/").pop()?.replace(/\.[^.]+$/, "") || "";
1021
+ const fileB = pathB.split("/").pop()?.replace(/\.[^.]+$/, "") || "";
1022
+ if (fileA === pascalCase) return -1;
1023
+ if (fileB === pascalCase) return 1;
1024
+ if (fileA === camelCase) return -1;
1025
+ if (fileB === camelCase) return 1;
1026
+ if (pathA.includes("/pages/") && !pathB.includes("/pages/")) return -1;
1027
+ if (!pathA.includes("/pages/") && pathB.includes("/pages/")) return 1;
1028
+ return 0;
1029
+ });
1030
+ if (candidates.length > 0) {
1031
+ const result = candidates[0][1].file;
1032
+ if (this.debug) {
1033
+ console.log(` \u2705 Fuzzy match found: ${candidates[0][0]} \u2192 ${result}`);
1034
+ }
1035
+ return result;
1036
+ }
1037
+ if (this.debug) {
1038
+ console.log(` No fuzzy match found`);
1039
+ }
1040
+ return null;
1041
+ }
1042
+ /**
1043
+ * Get manifest entry by file name
1044
+ */
1045
+ getManifestEntryByFile(fileName) {
1046
+ return Object.values(this.manifest).find((entry) => entry.file === fileName);
1047
+ }
1048
+ /**
1049
+ * Get all chunks referenced in config
1050
+ */
1051
+ getReferencedChunks(config) {
1052
+ return Object.values(config.chunks);
1053
+ }
1054
+ /**
1055
+ * Validate that all chunks exist in manifest
1056
+ */
1057
+ validateChunks(config) {
1058
+ const missing = [];
1059
+ const manifestFiles = new Set(Object.values(this.manifest).map((e) => e.file));
1060
+ Object.entries(config.chunks).forEach(([route, chunk]) => {
1061
+ if (!manifestFiles.has(chunk)) {
1062
+ missing.push(`${route} -> ${chunk}`);
1063
+ }
1064
+ });
1065
+ return {
1066
+ valid: missing.length === 0,
1067
+ missing
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Generate segment-specific prefetch configurations
1072
+ * Creates one config per user segment/role
1073
+ */
1074
+ generateSegmentConfigs(segmentModels) {
1075
+ const segmentConfigs = /* @__PURE__ */ new Map();
1076
+ segmentModels.forEach((model, segment) => {
1077
+ if (this.debug) {
1078
+ console.log(`
1079
+ \u{1F4E6} Generating config for segment: "${segment}"`);
1080
+ }
1081
+ const config = this.generate(model);
1082
+ config.segment = segment;
1083
+ segmentConfigs.set(segment, config);
1084
+ if (this.debug) {
1085
+ console.log(` \u2705 Config generated for "${segment}"`);
1086
+ console.log(` Routes: ${Object.keys(config.routes).length}`);
1087
+ console.log(` Chunks: ${Object.keys(config.chunks).length}`);
1088
+ }
1089
+ });
1090
+ if (this.debug) {
1091
+ console.log(`
1092
+ \u2705 Generated ${segmentConfigs.size} segment-specific configurations`);
1093
+ }
1094
+ return segmentConfigs;
1095
+ }
1096
+ };
1097
+ /**
1098
+ * Maps routes to their component names based on vite.config.ts chunk strategy
1099
+ * This is derived from the route configuration in src/routes/index.ts
1100
+ * Note: These are the core routes from vite.config.ts chunking strategy
1101
+ * Routes not listed here will fall through to fuzzy matching
1102
+ */
1103
+ _ConfigGenerator.ROUTE_TO_COMPONENT_NAME = {
1104
+ "/": "Home",
1105
+ "/home": "Home",
1106
+ "/dashboard": "Dashboard",
1107
+ "/profile": "Profile",
1108
+ "/settings": "Settings",
1109
+ "/preferences": "Preferences",
1110
+ "/privacy": "Privacy",
1111
+ "/security": "Security",
1112
+ "/analytics": "Analytics",
1113
+ "/reports": "Reports",
1114
+ "/metrics": "Metrics",
1115
+ "/projects": "Projects",
1116
+ "/tasks": "Tasks",
1117
+ "/teams": "Teams",
1118
+ "/workspaces": "Workspaces",
1119
+ "/workflows": "Workflows",
1120
+ "/templates": "Templates",
1121
+ "/logs": "Logs",
1122
+ "/audit-logs": "AuditLogs",
1123
+ "/integrations": "Integrations",
1124
+ "/api-docs": "ApiDocs",
1125
+ "/api-documentation": "ApiDocs",
1126
+ // Alias for space-separated variant
1127
+ "/support": "Support",
1128
+ "/help": "Help",
1129
+ "/billing": "Billing",
1130
+ "/plans": "Plans",
1131
+ "/usage": "Usage",
1132
+ "/permissions": "Permissions",
1133
+ "/notifications": "Notifications"
1134
+ };
1135
+ /**
1136
+ * Maps component names to chunk names based on vite.config.ts manualChunks strategy
1137
+ * Each component is assigned to a specific chunk group for code splitting
1138
+ *
1139
+ * Note: Core components (Home, Dashboard) are not in manual chunks - they're part of main bundle
1140
+ * For these, we return the main bundle file path 'js/index-*.js' which will be resolved from manifest
1141
+ */
1142
+ _ConfigGenerator.COMPONENT_TO_CHUNK_NAME = {
1143
+ // Core components - loaded with main bundle (not code-split)
1144
+ Home: "index",
1145
+ // Special marker for main entry point
1146
+ Dashboard: "index",
1147
+ // User Profile & Settings chunk
1148
+ Profile: "chunk-user-profile",
1149
+ Settings: "chunk-user-profile",
1150
+ Preferences: "chunk-user-profile",
1151
+ Privacy: "chunk-user-profile",
1152
+ Security: "chunk-user-profile",
1153
+ // Analytics & Reporting chunk
1154
+ Analytics: "chunk-analytics",
1155
+ Reports: "chunk-analytics",
1156
+ Metrics: "chunk-analytics",
1157
+ // Project Management chunk
1158
+ Projects: "chunk-projects",
1159
+ Tasks: "chunk-projects",
1160
+ Teams: "chunk-projects",
1161
+ Workspaces: "chunk-projects",
1162
+ // Workflows & Operations chunk
1163
+ Workflows: "chunk-operations",
1164
+ Templates: "chunk-operations",
1165
+ Logs: "chunk-operations",
1166
+ AuditLogs: "chunk-operations",
1167
+ // Integration chunk
1168
+ Integrations: "chunk-integrations",
1169
+ ApiDocs: "chunk-integrations",
1170
+ Support: "chunk-integrations",
1171
+ Help: "chunk-integrations",
1172
+ // Billing & Plans chunk
1173
+ Billing: "chunk-billing",
1174
+ Plans: "chunk-billing",
1175
+ Usage: "chunk-billing",
1176
+ // Admin & Notifications chunk
1177
+ Permissions: "chunk-admin",
1178
+ Notifications: "chunk-admin"
1179
+ };
1180
+ var ConfigGenerator = _ConfigGenerator;
1181
+
1182
+ // src/plugin/cache-manager.ts
1183
+ import * as fs from "fs";
1184
+ import * as path from "path";
1185
+ var CacheManager = class {
1186
+ constructor(config = {}, debug = false) {
1187
+ this.enabled = config.enabled ?? true;
1188
+ this.ttl = config.ttl ?? 24 * 60 * 60 * 1e3;
1189
+ this.cacheDir = config.path ?? path.join(process.cwd(), ".prefetch-cache");
1190
+ this.debug = debug;
1191
+ if (this.enabled && !fs.existsSync(this.cacheDir)) {
1192
+ fs.mkdirSync(this.cacheDir, { recursive: true });
1193
+ if (this.debug) {
1194
+ console.log(`\u{1F4C1} Created cache directory: ${this.cacheDir}`);
1195
+ }
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Get cached model for environment
1200
+ * Returns null if cache doesn't exist or is expired
1201
+ */
1202
+ async get(environment) {
1203
+ if (!this.enabled) {
1204
+ if (this.debug) console.log("\u23ED\uFE0F Cache disabled, skipping cache lookup");
1205
+ return null;
1206
+ }
1207
+ try {
1208
+ const cacheFile = path.join(this.cacheDir, `${environment}.json`);
1209
+ if (!fs.existsSync(cacheFile)) {
1210
+ if (this.debug) console.log(`\u{1F4E6} Cache miss for environment: ${environment}`);
1211
+ return null;
1212
+ }
1213
+ const stats = fs.statSync(cacheFile);
1214
+ const cacheAge = Date.now() - stats.mtimeMs;
1215
+ if (cacheAge > this.ttl) {
1216
+ if (this.debug) console.log(`\u23F0 Cache expired for ${environment} (${Math.round(cacheAge / 1e3 / 60)} minutes old)`);
1217
+ fs.unlinkSync(cacheFile);
1218
+ return null;
1219
+ }
1220
+ const cachedData = fs.readFileSync(cacheFile, "utf-8");
1221
+ const model = JSON.parse(cachedData);
1222
+ if (this.debug) {
1223
+ console.log(`\u2705 Cache hit for environment: ${environment} (${Math.round(cacheAge / 1e3)} seconds old)`);
1224
+ console.log(` Cached model has ${Object.keys(model.routes).length} routes`);
1225
+ }
1226
+ return model;
1227
+ } catch (error) {
1228
+ console.error(`\u274C Error reading cache for ${environment}:`, error);
1229
+ return null;
1230
+ }
1231
+ }
1232
+ /**
1233
+ * Save model to cache
1234
+ */
1235
+ async set(environment, model) {
1236
+ if (!this.enabled) {
1237
+ if (this.debug) console.log("\u23ED\uFE0F Cache disabled, skipping cache write");
1238
+ return;
1239
+ }
1240
+ try {
1241
+ const cacheFile = path.join(this.cacheDir, `${environment}.json`);
1242
+ fs.writeFileSync(cacheFile, JSON.stringify(model, null, 2), "utf-8");
1243
+ if (this.debug) {
1244
+ console.log(`\u{1F4BE} Cached model for ${environment}`);
1245
+ console.log(` Location: ${cacheFile}`);
1246
+ console.log(` Routes: ${Object.keys(model.routes).length}`);
1247
+ }
1248
+ } catch (error) {
1249
+ console.error(`\u274C Error writing cache for ${environment}:`, error);
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Invalidate cache for specific environment or all environments
1254
+ */
1255
+ async invalidate(environment) {
1256
+ if (!this.enabled) return;
1257
+ try {
1258
+ if (environment) {
1259
+ const cacheFile = path.join(this.cacheDir, `${environment}.json`);
1260
+ if (fs.existsSync(cacheFile)) {
1261
+ fs.unlinkSync(cacheFile);
1262
+ if (this.debug) {
1263
+ console.log(`\u{1F5D1}\uFE0F Invalidated cache for ${environment}`);
1264
+ }
1265
+ }
1266
+ } else {
1267
+ const files = fs.readdirSync(this.cacheDir);
1268
+ files.forEach((file) => {
1269
+ if (file.endsWith(".json")) {
1270
+ fs.unlinkSync(path.join(this.cacheDir, file));
1271
+ }
1272
+ });
1273
+ if (this.debug) {
1274
+ console.log(`\u{1F5D1}\uFE0F Cleared all cache files`);
1275
+ }
1276
+ }
1277
+ } catch (error) {
1278
+ console.error(`\u274C Error invalidating cache:`, error);
1279
+ }
1280
+ }
1281
+ /**
1282
+ * Get cache statistics
1283
+ */
1284
+ getStats() {
1285
+ const cachedEnvironments = [];
1286
+ let totalSize = 0;
1287
+ if (this.enabled && fs.existsSync(this.cacheDir)) {
1288
+ const files = fs.readdirSync(this.cacheDir);
1289
+ files.forEach((file) => {
1290
+ if (file.endsWith(".json")) {
1291
+ const filePath = path.join(this.cacheDir, file);
1292
+ const stats = fs.statSync(filePath);
1293
+ cachedEnvironments.push(file.replace(".json", ""));
1294
+ totalSize += stats.size;
1295
+ }
1296
+ });
1297
+ }
1298
+ return {
1299
+ enabled: this.enabled,
1300
+ cacheDir: this.cacheDir,
1301
+ ttl: this.ttl,
1302
+ cachedEnvironments,
1303
+ totalSize
1304
+ };
1305
+ }
1306
+ /**
1307
+ * Check if cache exists and is valid for environment
1308
+ */
1309
+ async isValid(environment) {
1310
+ if (!this.enabled) return false;
1311
+ try {
1312
+ const cacheFile = path.join(this.cacheDir, `${environment}.json`);
1313
+ if (!fs.existsSync(cacheFile)) {
1314
+ return false;
1315
+ }
1316
+ const stats = fs.statSync(cacheFile);
1317
+ const cacheAge = Date.now() - stats.mtimeMs;
1318
+ return cacheAge <= this.ttl;
1319
+ } catch {
1320
+ return false;
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Get cache age in milliseconds
1325
+ */
1326
+ async getCacheAge(environment) {
1327
+ if (!this.enabled) return null;
1328
+ try {
1329
+ const cacheFile = path.join(this.cacheDir, `${environment}.json`);
1330
+ if (!fs.existsSync(cacheFile)) {
1331
+ return null;
1332
+ }
1333
+ const stats = fs.statSync(cacheFile);
1334
+ return Date.now() - stats.mtimeMs;
1335
+ } catch {
1336
+ return null;
1337
+ }
1338
+ }
1339
+ };
1340
+
1341
+ // src/plugin/index.ts
1342
+ function smartPrefetch(options = {}) {
1343
+ const {
1344
+ framework = "react",
1345
+ strategy = "hybrid",
1346
+ analytics,
1347
+ manualRules = {},
1348
+ cache = {},
1349
+ advanced = {}
1350
+ } = options;
1351
+ const debug = advanced.debug ?? false;
1352
+ let config;
1353
+ let cacheManager;
1354
+ let prefetchModel = null;
1355
+ let segmentModels = null;
1356
+ return {
1357
+ name: "@farmart/vite-plugin-smart-prefetch",
1358
+ enforce: "post",
1359
+ async configResolved(resolvedConfig) {
1360
+ config = resolvedConfig;
1361
+ if (!config.build.manifest) {
1362
+ config.build.manifest = true;
1363
+ if (debug) {
1364
+ console.log("\u2705 Enabled Vite manifest generation");
1365
+ }
1366
+ }
1367
+ cacheManager = new CacheManager(cache, debug);
1368
+ if (debug) {
1369
+ console.log("\n\u{1F680} Smart Prefetch Plugin Initialized");
1370
+ console.log(` Framework: ${framework}`);
1371
+ console.log(` Strategy: ${strategy}`);
1372
+ console.log(` Analytics: ${analytics ? "enabled" : "disabled"}`);
1373
+ console.log(` Manual rules: ${Object.keys(manualRules).length}`);
1374
+ console.log(` Cache: ${cache.enabled !== false ? "enabled" : "disabled"}`);
1375
+ }
1376
+ if (config.command === "serve" && debug) {
1377
+ const devManifest = {};
1378
+ Object.keys(manualRules).forEach((route) => {
1379
+ const routeKey = `src/features${route}/index.tsx`;
1380
+ devManifest[routeKey] = {
1381
+ file: `features${route.replace(/\//g, "-")}.js`,
1382
+ imports: []
1383
+ };
1384
+ });
1385
+ const fs2 = await import("fs");
1386
+ const path2 = await import("path");
1387
+ const pagesDir = path2.join(config.root, "src/pages");
1388
+ if (fs2.existsSync(pagesDir)) {
1389
+ const files = fs2.readdirSync(pagesDir);
1390
+ files.forEach((file) => {
1391
+ if (file.endsWith(".tsx") || file.endsWith(".ts")) {
1392
+ const pageName = file.replace(/\.(tsx|ts)$/, "");
1393
+ const routeKey = `src/pages/${file}`;
1394
+ devManifest[routeKey] = {
1395
+ file: `chunks/${pageName}-[hash].js`,
1396
+ imports: []
1397
+ };
1398
+ }
1399
+ });
1400
+ }
1401
+ config.__smartPrefetchDevManifest = devManifest;
1402
+ }
1403
+ },
1404
+ configureServer(server) {
1405
+ return () => {
1406
+ server.middlewares.use("/prefetch-config.json", async (req, res, next) => {
1407
+ if (req.method !== "GET") {
1408
+ next();
1409
+ return;
1410
+ }
1411
+ try {
1412
+ if (!prefetchModel) {
1413
+ if (debug) {
1414
+ console.warn("\u26A0\uFE0F No prefetch model available yet");
1415
+ }
1416
+ res.statusCode = 503;
1417
+ res.end(JSON.stringify({ error: "Prefetch model not ready, still loading analytics data..." }));
1418
+ return;
1419
+ }
1420
+ const devManifest = {};
1421
+ Object.keys(prefetchModel.routes).forEach((route) => {
1422
+ const routeKey = `src/pages${route}/index.tsx`;
1423
+ const routeName = route.split("/").filter(Boolean).pop() || "index";
1424
+ const devHash = "dev-" + routeName.slice(0, 6).padEnd(8, "0");
1425
+ devManifest[routeKey] = {
1426
+ file: `chunks/${routeName}-${devHash}.js`,
1427
+ imports: []
1428
+ };
1429
+ });
1430
+ const generator = new ConfigGenerator(devManifest, manualRules, debug);
1431
+ const finalConfig = generator.generate(prefetchModel);
1432
+ res.setHeader("Content-Type", "application/json");
1433
+ res.setHeader("Access-Control-Allow-Origin", "*");
1434
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
1435
+ res.end(JSON.stringify(finalConfig, null, 2));
1436
+ if (debug) {
1437
+ console.log("\u{1F4E4} Serving fresh prefetch-config.json from development server");
1438
+ }
1439
+ return;
1440
+ } catch (error) {
1441
+ if (debug) {
1442
+ console.error("Error serving prefetch-config.json:", error);
1443
+ }
1444
+ res.statusCode = 500;
1445
+ res.end(JSON.stringify({ error: "Internal server error", details: String(error) }));
1446
+ }
1447
+ });
1448
+ };
1449
+ },
1450
+ async buildStart() {
1451
+ if (!analytics) {
1452
+ if (debug) {
1453
+ console.log("\n\u23ED\uFE0F Analytics disabled, using manual rules only");
1454
+ }
1455
+ prefetchModel = createManualModel(manualRules, config.command === "serve" ? "development" : "production");
1456
+ return;
1457
+ }
1458
+ const environment = analytics.environment || (config.command === "serve" ? "development" : "production");
1459
+ const cached = await cacheManager.get(environment);
1460
+ if (cached) {
1461
+ prefetchModel = cached;
1462
+ return;
1463
+ }
1464
+ try {
1465
+ if (debug) {
1466
+ console.log(`
1467
+ \u{1F4CA} Fetching ${analytics.provider} data and training model...`);
1468
+ }
1469
+ const bqConnector = new BigQueryAnalyticsConnector(
1470
+ analytics.credentials.projectId,
1471
+ analytics.credentials.datasetId,
1472
+ debug
1473
+ );
1474
+ console.log("\n\u{1F3AF} Using real navigation data from BigQuery GA4 export...");
1475
+ const navigationData = await bqConnector.fetchNavigationSequences(analytics.dataRange);
1476
+ if (navigationData.length === 0) {
1477
+ console.warn("\u26A0\uFE0F No navigation data found, using manual rules only");
1478
+ prefetchModel = createManualModel(manualRules, environment);
1479
+ return;
1480
+ }
1481
+ console.log(`
1482
+ \u{1F916} Training model using Markov Chain ML...`);
1483
+ const mlTrainer = new MarkovChainTrainer(analytics.model, debug);
1484
+ const prefetchModelResult = mlTrainer.trainMLModel(navigationData, environment);
1485
+ prefetchModel = prefetchModelResult;
1486
+ await cacheManager.set(environment, prefetchModel);
1487
+ try {
1488
+ const navigationWithSegments = await bqConnector.fetchNavigationWithSegments(analytics.dataRange);
1489
+ if (navigationWithSegments.length > 0) {
1490
+ console.log(`
1491
+ \u{1F465} Training segment-specific models...`);
1492
+ segmentModels = mlTrainer.trainSegmentedModels(navigationWithSegments, environment);
1493
+ if (debug && segmentModels.size > 0) {
1494
+ console.log(` \u2705 Trained ${segmentModels.size} segment-specific models`);
1495
+ }
1496
+ }
1497
+ } catch (error) {
1498
+ if (debug) {
1499
+ console.warn(`\u26A0\uFE0F Could not train segment models:`, error instanceof Error ? error.message : "Unknown error");
1500
+ }
1501
+ }
1502
+ } catch (error) {
1503
+ console.error("\u274C Failed to fetch analytics data:", error);
1504
+ console.log("\u26A0\uFE0F Falling back to manual rules only");
1505
+ prefetchModel = createManualModel(manualRules, environment);
1506
+ if (debug) {
1507
+ console.error("Error details:", error);
1508
+ }
1509
+ }
1510
+ },
1511
+ async writeBundle(outputOptions) {
1512
+ if (!prefetchModel) {
1513
+ console.warn("\u26A0\uFE0F No prefetch model available, skipping config generation");
1514
+ return;
1515
+ }
1516
+ const fs2 = await import("fs");
1517
+ const path2 = await import("path");
1518
+ const outDir = outputOptions.dir || "dist";
1519
+ const manifestPath = path2.join(outDir, ".vite", "manifest.json");
1520
+ if (!fs2.existsSync(manifestPath)) {
1521
+ console.warn("\u26A0\uFE0F Vite manifest not found at:", manifestPath);
1522
+ return;
1523
+ }
1524
+ const manifestContent = fs2.readFileSync(manifestPath, "utf-8");
1525
+ const manifest = JSON.parse(manifestContent);
1526
+ if (debug) {
1527
+ console.log(`
1528
+ \u{1F4E6} Vite manifest loaded: ${Object.keys(manifest).length} entries`);
1529
+ }
1530
+ const generator = new ConfigGenerator(manifest, manualRules, debug);
1531
+ const finalConfig = generator.generate(prefetchModel);
1532
+ const validation = generator.validateChunks(finalConfig);
1533
+ if (!validation.valid) {
1534
+ console.warn("\u26A0\uFE0F Some chunks could not be mapped:");
1535
+ validation.missing.forEach((m) => console.warn(` ${m}`));
1536
+ } else {
1537
+ console.log(`\u2705 All ${Object.keys(finalConfig.chunks).length} chunks successfully mapped with real build hashes`);
1538
+ }
1539
+ const configPath = path2.join(outDir, "prefetch-config.json");
1540
+ fs2.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
1541
+ const modelRoutesPath = path2.join(outDir, "model-routes.json");
1542
+ fs2.writeFileSync(modelRoutesPath, JSON.stringify(prefetchModel.routes, null, 2));
1543
+ if (debug) {
1544
+ console.log("\u2705 Emitted prefetch-config.json with real chunk hashes");
1545
+ console.log("\u2705 Emitted model-routes.json");
1546
+ console.log(`\u{1F4CA} Total routes configured: ${Object.keys(finalConfig.routes).length}`);
1547
+ console.log(`\u{1F4E6} Total chunks: ${Object.keys(finalConfig.chunks).length}`);
1548
+ }
1549
+ if (segmentModels && segmentModels.size > 0) {
1550
+ console.log(`
1551
+ \u{1F4C1} Generating segment-specific prefetch configurations...`);
1552
+ const segmentGenerator = new ConfigGenerator(manifest, manualRules, debug);
1553
+ const segmentConfigs = segmentGenerator.generateSegmentConfigs(segmentModels);
1554
+ const segmentDir = path2.join(outDir, "prefetch-configs");
1555
+ if (!fs2.existsSync(segmentDir)) {
1556
+ fs2.mkdirSync(segmentDir, { recursive: true });
1557
+ }
1558
+ segmentConfigs.forEach((segConfig, segment) => {
1559
+ const segmentPath = path2.join(segmentDir, `${segment}.json`);
1560
+ fs2.writeFileSync(segmentPath, JSON.stringify(segConfig, null, 2));
1561
+ if (debug) {
1562
+ console.log(` \u2705 Emitted ${segment}.json (${Object.keys(segConfig.routes).length} routes)`);
1563
+ }
1564
+ });
1565
+ const segmentIndex = {
1566
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1567
+ environment: analytics?.environment || (config.command === "serve" ? "development" : "production"),
1568
+ segments: Array.from(segmentConfigs.keys()),
1569
+ description: "Segment-specific prefetch configurations. Use based on user role/segment."
1570
+ };
1571
+ const segmentIndexPath = path2.join(segmentDir, "index.json");
1572
+ fs2.writeFileSync(segmentIndexPath, JSON.stringify(segmentIndex, null, 2));
1573
+ if (debug) {
1574
+ console.log(`
1575
+ \u2705 Emitted ${segmentConfigs.size} segment-specific configurations`);
1576
+ console.log(` Location: ${segmentDir}`);
1577
+ console.log(` Segments: ${Array.from(segmentConfigs.keys()).join(", ")}`);
1578
+ }
1579
+ }
1580
+ if (analytics?.dashboard) {
1581
+ const dashboardHtml = generateDashboard(finalConfig);
1582
+ const dashboardPath = path2.join(outDir, "prefetch-report.html");
1583
+ fs2.writeFileSync(dashboardPath, dashboardHtml);
1584
+ if (debug) {
1585
+ console.log("\u2705 Emitted prefetch-report.html");
1586
+ }
1587
+ }
1588
+ },
1589
+ transformIndexHtml() {
1590
+ return [
1591
+ {
1592
+ tag: "script",
1593
+ attrs: { type: "module" },
1594
+ children: `
1595
+ // Smart Prefetch Plugin Runtime Config
1596
+ window.__SMART_PREFETCH__ = {
1597
+ strategy: '${strategy}',
1598
+ framework: '${framework}',
1599
+ debug: ${debug},
1600
+ };
1601
+ `,
1602
+ injectTo: "head"
1603
+ }
1604
+ ];
1605
+ }
1606
+ };
1607
+ }
1608
+ function createManualModel(manualRules, environment) {
1609
+ const model = {
1610
+ version: "1.0.0",
1611
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1612
+ environment,
1613
+ routes: {},
1614
+ dataSource: {
1615
+ provider: "manual",
1616
+ dateRange: "N/A",
1617
+ totalSessions: 0,
1618
+ totalRoutes: Object.keys(manualRules).length
1619
+ },
1620
+ config: {
1621
+ type: "probability",
1622
+ threshold: 1,
1623
+ maxPrefetch: 10
1624
+ }
1625
+ };
1626
+ Object.entries(manualRules).forEach(([source, targets]) => {
1627
+ model.routes[source] = {
1628
+ prefetch: targets.map((target) => ({
1629
+ route: target,
1630
+ probability: 1,
1631
+ count: 0,
1632
+ priority: "high",
1633
+ manual: true
1634
+ }))
1635
+ };
1636
+ });
1637
+ return model;
1638
+ }
1639
+ function generateDashboard(config) {
1640
+ const totalRoutes = Object.keys(config.routes).length;
1641
+ const totalPrefetches = Object.values(config.routes).reduce(
1642
+ (sum, route) => sum + route.prefetch.length,
1643
+ 0
1644
+ );
1645
+ return `<!DOCTYPE html>
1646
+ <html lang="en">
1647
+ <head>
1648
+ <meta charset="UTF-8">
1649
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1650
+ <title>Smart Prefetch Report</title>
1651
+ <style>
1652
+ body {
1653
+ font-family: system-ui, -apple-system, sans-serif;
1654
+ max-width: 1200px;
1655
+ margin: 0 auto;
1656
+ padding: 2rem;
1657
+ background: #f5f5f5;
1658
+ }
1659
+ h1 { color: #333; }
1660
+ .stats {
1661
+ display: grid;
1662
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1663
+ gap: 1rem;
1664
+ margin: 2rem 0;
1665
+ }
1666
+ .stat-card {
1667
+ background: white;
1668
+ padding: 1.5rem;
1669
+ border-radius: 8px;
1670
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1671
+ }
1672
+ .stat-value { font-size: 2rem; font-weight: bold; color: #4CAF50; }
1673
+ .stat-label { color: #666; margin-top: 0.5rem; }
1674
+ .route-list {
1675
+ background: white;
1676
+ padding: 1.5rem;
1677
+ border-radius: 8px;
1678
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1679
+ }
1680
+ .route-item {
1681
+ padding: 1rem;
1682
+ border-bottom: 1px solid #eee;
1683
+ }
1684
+ .route-item:last-child { border-bottom: none; }
1685
+ .route-source { font-weight: bold; color: #333; }
1686
+ .prefetch-target {
1687
+ margin-left: 2rem;
1688
+ padding: 0.5rem;
1689
+ color: #666;
1690
+ }
1691
+ .priority-high { color: #4CAF50; }
1692
+ .priority-medium { color: #FF9800; }
1693
+ .priority-low { color: #9E9E9E; }
1694
+ </style>
1695
+ </head>
1696
+ <body>
1697
+ <h1>\u{1F680} Smart Prefetch Report</h1>
1698
+ <p>Generated: ${config.generatedAt}</p>
1699
+ <p>Environment: ${config.environment}</p>
1700
+
1701
+ <div class="stats">
1702
+ <div class="stat-card">
1703
+ <div class="stat-value">${totalRoutes}</div>
1704
+ <div class="stat-label">Routes with Prefetch</div>
1705
+ </div>
1706
+ <div class="stat-card">
1707
+ <div class="stat-value">${totalPrefetches}</div>
1708
+ <div class="stat-label">Total Prefetch Targets</div>
1709
+ </div>
1710
+ <div class="stat-card">
1711
+ <div class="stat-value">${config.dataSource?.totalSessions?.toLocaleString() || "N/A"}</div>
1712
+ <div class="stat-label">Sessions Analyzed</div>
1713
+ </div>
1714
+ <div class="stat-card">
1715
+ <div class="stat-value">${(config.model.threshold * 100).toFixed(0)}%</div>
1716
+ <div class="stat-label">Probability Threshold</div>
1717
+ </div>
1718
+ </div>
1719
+
1720
+ <div class="route-list">
1721
+ <h2>Prefetch Configuration</h2>
1722
+ ${Object.entries(config.routes).map(
1723
+ ([source, data]) => `
1724
+ <div class="route-item">
1725
+ <div class="route-source">${source}</div>
1726
+ ${data.prefetch.map(
1727
+ (target) => `
1728
+ <div class="prefetch-target">
1729
+ \u2192 ${target.route}
1730
+ <span class="priority-${target.priority}">
1731
+ (${(target.probability * 100).toFixed(1)}%, ${target.priority})
1732
+ </span>
1733
+ </div>
1734
+ `
1735
+ ).join("")}
1736
+ </div>
1737
+ `
1738
+ ).join("")}
1739
+ </div>
1740
+ </body>
1741
+ </html>`;
1742
+ }
1743
+ export {
1744
+ smartPrefetch
1745
+ };
1746
+ //# sourceMappingURL=index.js.map