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/README.md +605 -0
- package/dist/index.cjs +1783 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +211 -0
- package/dist/index.d.ts +211 -0
- package/dist/index.js +1746 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +798 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +175 -0
- package/dist/react/index.d.ts +175 -0
- package/dist/react/index.js +758 -0
- package/dist/react/index.js.map +1 -0
- package/dist/runtime/index.cjs +678 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.cts +223 -0
- package/dist/runtime/index.d.ts +223 -0
- package/dist/runtime/index.js +645 -0
- package/dist/runtime/index.js.map +1 -0
- package/package.json +67 -0
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
|