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