vite-plugin-smart-prefetch 0.3.7 → 0.3.9

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.d.cts CHANGED
@@ -39,6 +39,8 @@ interface BigQueryCredentials {
39
39
  datasetId: string;
40
40
  /** BigQuery dataset region (default: asia-south1 for Mumbai, also supports: US, EU, etc.) */
41
41
  region?: string;
42
+ /** Path to service account JSON key file (uses this instead of Application Default Credentials) */
43
+ keyFilePath?: string;
42
44
  }
43
45
  interface DataRangeConfig {
44
46
  /** Number of days to analyze (default: 30) */
@@ -229,7 +231,7 @@ declare class BigQueryAnalyticsConnector {
229
231
  private datasetId;
230
232
  private location;
231
233
  private debug;
232
- constructor(projectId: string, datasetId: string, location?: string, debug?: boolean);
234
+ constructor(projectId: string, datasetId: string, location?: string, debug?: boolean, keyFilePath?: string);
233
235
  /**
234
236
  * Fetch real navigation transitions from BigQuery GA4 export
235
237
  * Queries the events table for page_view events with previous_page_path parameter
package/dist/index.d.ts CHANGED
@@ -39,6 +39,8 @@ interface BigQueryCredentials {
39
39
  datasetId: string;
40
40
  /** BigQuery dataset region (default: asia-south1 for Mumbai, also supports: US, EU, etc.) */
41
41
  region?: string;
42
+ /** Path to service account JSON key file (uses this instead of Application Default Credentials) */
43
+ keyFilePath?: string;
42
44
  }
43
45
  interface DataRangeConfig {
44
46
  /** Number of days to analyze (default: 30) */
@@ -229,7 +231,7 @@ declare class BigQueryAnalyticsConnector {
229
231
  private datasetId;
230
232
  private location;
231
233
  private debug;
232
- constructor(projectId: string, datasetId: string, location?: string, debug?: boolean);
234
+ constructor(projectId: string, datasetId: string, location?: string, debug?: boolean, keyFilePath?: string);
233
235
  /**
234
236
  * Fetch real navigation transitions from BigQuery GA4 export
235
237
  * Queries the events table for page_view events with previous_page_path parameter
package/dist/index.js CHANGED
@@ -3,19 +3,24 @@ import { BigQuery } from "@google-cloud/bigquery";
3
3
  import { writeFileSync, mkdirSync } from "fs";
4
4
  import { join } from "path";
5
5
  var BigQueryAnalyticsConnector = class {
6
- constructor(projectId, datasetId, location = "asia-south1", debug = false) {
6
+ constructor(projectId, datasetId, location = "asia-south1", debug = false, keyFilePath) {
7
7
  this.projectId = projectId;
8
8
  this.datasetId = datasetId;
9
9
  this.location = location && location !== "asia" ? location : "asia-south1";
10
10
  this.debug = debug;
11
- this.bigquery = new BigQuery({
11
+ const bigqueryConfig = {
12
12
  projectId
13
- });
13
+ };
14
+ if (keyFilePath) {
15
+ bigqueryConfig.keyFilename = keyFilePath;
16
+ }
17
+ this.bigquery = new BigQuery(bigqueryConfig);
14
18
  if (this.debug) {
15
19
  console.log("\u2705 BigQuery Analytics connector initialized");
16
20
  console.log(` Project ID: ${projectId}`);
17
21
  console.log(` Dataset ID: ${datasetId}`);
18
22
  console.log(` Location: ${this.location}`);
23
+ console.log(` Authentication: ${keyFilePath ? `Service Account (${keyFilePath})` : "Application Default Credentials"}`);
19
24
  console.log(` Querying daily events tables: ${projectId}.${datasetId}.events_*`);
20
25
  }
21
26
  }
@@ -775,7 +780,15 @@ var MarkovChainTrainer = class {
775
780
  };
776
781
 
777
782
  // src/plugin/config-generator.ts
778
- var _ConfigGenerator = class _ConfigGenerator {
783
+ var ConfigGenerator = class {
784
+ /**
785
+ * NOTE: Hardcoded route/component mappings have been REMOVED to make the plugin generic.
786
+ * The plugin now uses dynamic strategies (direct match, pattern match, fuzzy match)
787
+ * to discover routes from the Vite manifest and actual file structure.
788
+ *
789
+ * This allows the plugin to work with ANY project structure without project-specific
790
+ * configuration hardcoded in the plugin code.
791
+ */
779
792
  constructor(manifest, manualRules = {}, debug = false, vite) {
780
793
  this.vite = null;
781
794
  this.isDev = false;
@@ -964,224 +977,69 @@ var _ConfigGenerator = class _ConfigGenerator {
964
977
  return merged;
965
978
  }
966
979
  /**
967
- * NEW STRATEGY 0: Route to Chunk Name Mapping
968
- * Maps route → component name → chunk name → manifest file
969
- * This is the most reliable method as it uses the actual vite.config.ts chunking strategy
970
- */
971
- routeToChunkViaName(route) {
972
- const normalizedRoute = route.toLowerCase();
973
- const componentName = _ConfigGenerator.ROUTE_TO_COMPONENT_NAME[normalizedRoute];
974
- if (!componentName) {
975
- if (this.debug) {
976
- console.log(` \u274C Route ${route} not in ROUTE_TO_COMPONENT_NAME mapping`);
977
- }
978
- return null;
979
- }
980
- const chunkName = _ConfigGenerator.COMPONENT_TO_CHUNK_NAME[componentName];
981
- if (!chunkName) {
982
- if (this.debug) {
983
- console.log(` \u274C Component ${componentName} not in COMPONENT_TO_CHUNK_NAME mapping`);
984
- }
985
- return null;
986
- }
987
- const chunkEntry = Object.entries(this.manifest).find(
988
- ([key, entry]) => entry.name === chunkName || key.includes(chunkName)
989
- );
990
- if (!chunkEntry) {
991
- if (this.debug) {
992
- console.log(` \u274C Chunk name ${chunkName} not found in manifest`);
993
- }
994
- return null;
995
- }
996
- const chunkFile = chunkEntry[1].file;
997
- if (this.debug) {
998
- console.log(
999
- ` \u2705 Via-Name Strategy: ${route} \u2192 ${componentName} \u2192 ${chunkName} \u2192 ${chunkFile}`
1000
- );
1001
- }
1002
- return chunkFile;
1003
- }
1004
- /**
1005
- * Map route to chunk file using Vite manifest
1006
- * Tries multiple strategies to find the correct chunk
980
+ * Map BigQuery route to Vite manifest chunk file
981
+ *
982
+ * The routes come from BigQuery navigation data (e.g., /purchase-order, /dispatch-order/:id)
983
+ * We match them directly to manifest entries without trying to discover routes.
984
+ *
985
+ * Strategy: Match route segments against manifest file paths, ignoring hash suffixes
986
+ * Example:
987
+ * Route: /purchase-order/:id
988
+ * Normalized: /purchase-order
989
+ * Manifest: src/pages/purchase-order/index.tsx {file: assets/purchase-order-abc123.js}
990
+ * Match: YES → return assets/purchase-order-abc123.js
1007
991
  */
1008
992
  routeToChunk(route) {
1009
- const viaName = this.routeToChunkViaName(route);
1010
- if (viaName) {
1011
- return viaName;
1012
- }
1013
- const directMatch = this.findDirectMatch(route);
1014
- if (directMatch) {
1015
- return directMatch;
1016
- }
1017
- const patternMatch = this.findPatternMatch(route);
1018
- if (patternMatch) {
1019
- return patternMatch;
1020
- }
1021
- const fuzzyMatch = this.findFuzzyMatch(route);
1022
- if (fuzzyMatch) {
1023
- return fuzzyMatch;
1024
- }
1025
- return null;
1026
- }
1027
- /**
1028
- * Strategy 1: Direct path match
1029
- */
1030
- findDirectMatch(route) {
1031
993
  const normalizedRoute = route.replace(/\/[:$]\w+/g, "");
1032
- const cleanRoutePath = normalizedRoute.replace(/^\//, "");
1033
994
  const routeSegments = normalizedRoute.split("/").filter(Boolean);
1034
- const pascalCaseComponent = routeSegments.map((segment) => {
1035
- const words = segment.split("-");
1036
- return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1037
- }).join("/");
1038
- let singleSegmentPascal = null;
1039
- if (routeSegments.length === 1) {
1040
- const segment = routeSegments[0];
1041
- const words = segment.split("-");
1042
- singleSegmentPascal = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
1043
- }
1044
- const patterns = [
1045
- // HIGHEST PRIORITY: Exact page component name matches
1046
- singleSegmentPascal ? `src/pages/${singleSegmentPascal}.tsx` : null,
1047
- singleSegmentPascal ? `src/pages/${singleSegmentPascal}.ts` : null,
1048
- singleSegmentPascal ? `src/pages/${singleSegmentPascal}.jsx` : null,
1049
- singleSegmentPascal ? `src/pages/${singleSegmentPascal}.js` : null,
1050
- // For multi-segment routes with hyphens
1051
- `src/pages/${pascalCaseComponent}.tsx`,
1052
- `src/pages/${pascalCaseComponent}.ts`,
1053
- `src/pages/${pascalCaseComponent}.jsx`,
1054
- `src/pages/${pascalCaseComponent}.js`,
1055
- // Features folder
1056
- `src/features${normalizedRoute}/index.ts`,
1057
- `src/features${normalizedRoute}/index.tsx`,
1058
- `src/features${normalizedRoute}/index.js`,
1059
- `src/features${normalizedRoute}/index.jsx`,
1060
- // Pages folder - try both directory and file formats
1061
- `src/pages${normalizedRoute}/index.tsx`,
1062
- `src/pages${normalizedRoute}/index.ts`,
1063
- `src/pages${normalizedRoute}/index.jsx`,
1064
- `src/pages${normalizedRoute}/index.js`,
1065
- `src/pages${normalizedRoute}.tsx`,
1066
- `src/pages${normalizedRoute}.ts`,
1067
- `src/pages${normalizedRoute}.jsx`,
1068
- `src/pages${normalizedRoute}.js`,
1069
- // Fallback to old capitalize method (single capital letter)
1070
- `src/pages/${this.capitalize(cleanRoutePath)}.tsx`,
1071
- `src/pages/${this.capitalize(cleanRoutePath)}.ts`,
1072
- // Full paths with app prefix
1073
- `apps/farmart-pro/src/features${normalizedRoute}/index.ts`,
1074
- `apps/farmart-pro/src/features${normalizedRoute}/index.tsx`,
1075
- `apps/farmart-pro/src/pages${normalizedRoute}/index.tsx`
1076
- ].filter(Boolean);
1077
- for (let i = 0; i < patterns.length; i++) {
1078
- const pattern = patterns[i];
1079
- if (!pattern) continue;
1080
- const entry = this.manifest[pattern];
1081
- if (entry) {
1082
- return entry.file;
1083
- }
1084
- }
1085
- return null;
1086
- }
1087
- /**
1088
- * Capitalize first letter of string
1089
- */
1090
- capitalize(str) {
1091
- return str.charAt(0).toUpperCase() + str.slice(1);
1092
- }
1093
- /**
1094
- * Strategy 2: Pattern matching with wildcards
1095
- */
1096
- findPatternMatch(route) {
1097
- const routeSegments = route.split("/").filter(Boolean);
1098
- if (routeSegments.length === 0) return null;
1099
- const candidates = Object.entries(this.manifest).filter(([path2]) => {
1100
- const pathSegments = path2.split("/").filter(Boolean);
1101
- return routeSegments.every(
1102
- (segment) => pathSegments.some(
1103
- (ps) => ps.toLowerCase().includes(segment.replace(/[:$]\w+/, "").toLowerCase())
1104
- )
995
+ if (routeSegments.length === 0) {
996
+ const candidates2 = Object.entries(this.manifest).filter(
997
+ ([path2]) => path2 === "src/pages/index.tsx" || path2 === "src/app.tsx" || path2 === "src/App.tsx" || path2 === "src/main.tsx"
1105
998
  );
1106
- }).sort(([pathA], [pathB]) => {
1107
- if (pathA.includes("/pages/") && !pathB.includes("/pages/")) return -1;
1108
- if (!pathA.includes("/pages/") && pathB.includes("/pages/")) return 1;
1109
- const aIsEntry = pathA.includes("index.tsx") || pathA.includes("index.ts");
1110
- const bIsEntry = pathB.includes("index.tsx") || pathB.includes("index.ts");
1111
- if (aIsEntry && !bIsEntry) return -1;
1112
- if (!aIsEntry && bIsEntry) return 1;
1113
- if (pathA.includes(".tsx") && !pathB.includes(".tsx")) return -1;
1114
- if (!pathA.includes(".tsx") && pathB.includes(".tsx")) return 1;
1115
- return 0;
1116
- });
1117
- if (candidates.length > 0) {
1118
- if (this.debug) {
1119
- console.log(` \u2705 Pattern match found: ${candidates[0][0]} \u2192 ${candidates[0][1].file}`);
1120
- }
1121
- return candidates[0][1].file;
1122
- }
1123
- if (this.debug) {
1124
- console.log(` No pattern match found`);
1125
- }
1126
- return null;
1127
- }
1128
- /**
1129
- * Strategy 3: Fuzzy matching
1130
- * Converts route to camelCase/PascalCase and searches
1131
- */
1132
- findFuzzyMatch(route) {
1133
- const cleanRoute = route.replace(/\/[:$]\w+/g, "");
1134
- const routeSegments = cleanRoute.split("/").filter(Boolean);
1135
- const pascalCase = routeSegments.map((segment) => {
1136
- const words = segment.split("-");
1137
- return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
1138
- }).join("");
1139
- const camelCase = pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
1140
- if (this.debug) {
1141
- console.log(` Fuzzy match - Route: ${route}`);
1142
- console.log(` Trying PascalCase: ${pascalCase}, camelCase: ${camelCase}`);
999
+ if (candidates2.length > 0) return candidates2[0][1].file;
1000
+ return null;
1143
1001
  }
1144
- const candidates = Object.entries(this.manifest).filter(([path2, entry]) => {
1145
- if (path2.startsWith("_") || path2.startsWith("node_modules")) {
1002
+ const candidates = Object.entries(this.manifest).filter(([path2]) => {
1003
+ if (path2.startsWith("node_modules") || path2.startsWith("_")) {
1146
1004
  return false;
1147
1005
  }
1148
- const fileName = path2.split("/").pop() || "";
1149
- const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
1150
- if (fileNameWithoutExt === pascalCase || fileNameWithoutExt === camelCase) {
1151
- return true;
1152
- }
1153
- if (fileName.includes(pascalCase) || fileName.includes(camelCase)) {
1154
- return true;
1155
- }
1156
- const pathSegments = path2.toLowerCase().split("/");
1157
- const lowerPascal = pascalCase.toLowerCase();
1158
- const lowerCamel = camelCase.toLowerCase();
1159
- return pathSegments.some(
1160
- (seg) => seg.includes(lowerPascal) || seg.includes(lowerCamel)
1006
+ const pathLower = path2.toLowerCase();
1007
+ return routeSegments.every(
1008
+ (segment) => pathLower.includes(segment.toLowerCase())
1161
1009
  );
1162
- }).sort(([pathA, entryA], [pathB, entryB]) => {
1163
- const aHasSrc = entryA.src ? 1 : 0;
1164
- const bHasSrc = entryB.src ? 1 : 0;
1165
- if (aHasSrc !== bHasSrc) return bHasSrc - aHasSrc;
1166
- const fileA = pathA.split("/").pop()?.replace(/\.[^.]+$/, "") || "";
1167
- const fileB = pathB.split("/").pop()?.replace(/\.[^.]+$/, "") || "";
1168
- if (fileA === pascalCase) return -1;
1169
- if (fileB === pascalCase) return 1;
1170
- if (fileA === camelCase) return -1;
1171
- if (fileB === camelCase) return 1;
1172
- if (pathA.includes("/pages/") && !pathB.includes("/pages/")) return -1;
1173
- if (!pathA.includes("/pages/") && pathB.includes("/pages/")) return 1;
1174
- return 0;
1010
+ }).sort(([pathA], [pathB]) => {
1011
+ let scoreA = 0;
1012
+ let scoreB = 0;
1013
+ if (pathA.includes("/pages/")) scoreA += 10;
1014
+ if (pathB.includes("/pages/")) scoreB += 10;
1015
+ if (pathA.includes("index.tsx")) scoreA += 8;
1016
+ if (pathB.includes("index.tsx")) scoreB += 8;
1017
+ if (pathA.includes("index.ts")) scoreA += 7;
1018
+ if (pathB.includes("index.ts")) scoreB += 7;
1019
+ const pathASegments = pathA.split("/").map((s) => s.toLowerCase()).filter((s) => s && !s.startsWith("."));
1020
+ const pathBSegments = pathB.split("/").map((s) => s.toLowerCase()).filter((s) => s && !s.startsWith("."));
1021
+ const matchCountA = routeSegments.filter(
1022
+ (seg) => pathASegments.some((ps) => ps.includes(seg.toLowerCase()))
1023
+ ).length;
1024
+ const matchCountB = routeSegments.filter(
1025
+ (seg) => pathBSegments.some((ps) => ps.includes(seg.toLowerCase()))
1026
+ ).length;
1027
+ scoreA += matchCountA * 5;
1028
+ scoreB += matchCountB * 5;
1029
+ return scoreB - scoreA;
1175
1030
  });
1176
1031
  if (candidates.length > 0) {
1177
- const result = candidates[0][1].file;
1032
+ const manifestEntry = candidates[0][1];
1178
1033
  if (this.debug) {
1179
- console.log(` \u2705 Fuzzy match found: ${candidates[0][0]} \u2192 ${result}`);
1034
+ console.log(` \u2705 Found chunk for route ${route}`);
1035
+ console.log(` Manifest: ${candidates[0][0]}`);
1036
+ console.log(` Chunk file: ${manifestEntry.file}`);
1180
1037
  }
1181
- return result;
1038
+ return manifestEntry.file;
1182
1039
  }
1183
1040
  if (this.debug) {
1184
- console.log(` No fuzzy match found`);
1041
+ console.log(` \u26A0\uFE0F No chunk found for route: ${route}`);
1042
+ console.log(` Searched for segments: ${routeSegments.join(", ")}`);
1185
1043
  }
1186
1044
  return null;
1187
1045
  }
@@ -1299,90 +1157,6 @@ var _ConfigGenerator = class _ConfigGenerator {
1299
1157
  return segmentConfigs;
1300
1158
  }
1301
1159
  };
1302
- /**
1303
- * Maps routes to their component names based on vite.config.ts chunk strategy
1304
- * This is derived from the route configuration in src/routes/index.ts
1305
- * Note: These are the core routes from vite.config.ts chunking strategy
1306
- * Routes not listed here will fall through to fuzzy matching
1307
- */
1308
- _ConfigGenerator.ROUTE_TO_COMPONENT_NAME = {
1309
- "/": "Home",
1310
- "/home": "Home",
1311
- "/dashboard": "Dashboard",
1312
- "/profile": "Profile",
1313
- "/settings": "Settings",
1314
- "/preferences": "Preferences",
1315
- "/privacy": "Privacy",
1316
- "/security": "Security",
1317
- "/analytics": "Analytics",
1318
- "/reports": "Reports",
1319
- "/metrics": "Metrics",
1320
- "/projects": "Projects",
1321
- "/tasks": "Tasks",
1322
- "/teams": "Teams",
1323
- "/workspaces": "Workspaces",
1324
- "/workflows": "Workflows",
1325
- "/templates": "Templates",
1326
- "/logs": "Logs",
1327
- "/audit-logs": "AuditLogs",
1328
- "/integrations": "Integrations",
1329
- "/api-docs": "ApiDocs",
1330
- "/api-documentation": "ApiDocs",
1331
- // Alias for space-separated variant
1332
- "/support": "Support",
1333
- "/help": "Help",
1334
- "/billing": "Billing",
1335
- "/plans": "Plans",
1336
- "/usage": "Usage",
1337
- "/permissions": "Permissions",
1338
- "/notifications": "Notifications"
1339
- };
1340
- /**
1341
- * Maps component names to chunk names based on vite.config.ts manualChunks strategy
1342
- * Each component is assigned to a specific chunk group for code splitting
1343
- *
1344
- * Note: Core components (Home, Dashboard) are not in manual chunks - they're part of main bundle
1345
- * For these, we return the main bundle file path 'js/index-*.js' which will be resolved from manifest
1346
- */
1347
- _ConfigGenerator.COMPONENT_TO_CHUNK_NAME = {
1348
- // Core components - loaded with main bundle (not code-split)
1349
- Home: "index",
1350
- // Special marker for main entry point
1351
- Dashboard: "index",
1352
- // User Profile & Settings chunk
1353
- Profile: "chunk-user-profile",
1354
- Settings: "chunk-user-profile",
1355
- Preferences: "chunk-user-profile",
1356
- Privacy: "chunk-user-profile",
1357
- Security: "chunk-user-profile",
1358
- // Analytics & Reporting chunk
1359
- Analytics: "chunk-analytics",
1360
- Reports: "chunk-analytics",
1361
- Metrics: "chunk-analytics",
1362
- // Project Management chunk
1363
- Projects: "chunk-projects",
1364
- Tasks: "chunk-projects",
1365
- Teams: "chunk-projects",
1366
- Workspaces: "chunk-projects",
1367
- // Workflows & Operations chunk
1368
- Workflows: "chunk-operations",
1369
- Templates: "chunk-operations",
1370
- Logs: "chunk-operations",
1371
- AuditLogs: "chunk-operations",
1372
- // Integration chunk
1373
- Integrations: "chunk-integrations",
1374
- ApiDocs: "chunk-integrations",
1375
- Support: "chunk-integrations",
1376
- Help: "chunk-integrations",
1377
- // Billing & Plans chunk
1378
- Billing: "chunk-billing",
1379
- Plans: "chunk-billing",
1380
- Usage: "chunk-billing",
1381
- // Admin & Notifications chunk
1382
- Permissions: "chunk-admin",
1383
- Notifications: "chunk-admin"
1384
- };
1385
- var ConfigGenerator = _ConfigGenerator;
1386
1160
 
1387
1161
  // src/plugin/cache-manager.ts
1388
1162
  import * as fs from "fs";
@@ -1675,7 +1449,8 @@ function smartPrefetch(options = {}) {
1675
1449
  analytics.credentials.projectId,
1676
1450
  analytics.credentials.datasetId,
1677
1451
  analytics.credentials.region || "asia-south1",
1678
- debug
1452
+ debug,
1453
+ analytics.credentials.keyFilePath
1679
1454
  );
1680
1455
  console.log("\n\u{1F3AF} Using real navigation data from BigQuery GA4 export...");
1681
1456
  const navigationData = await bqConnector.fetchNavigationSequences(analytics.dataRange);
@@ -1804,6 +1579,37 @@ function smartPrefetch(options = {}) {
1804
1579
  framework: '${framework}',
1805
1580
  debug: ${debug},
1806
1581
  };
1582
+
1583
+ // Initialize prefetch manager after page load
1584
+ // This allows the app to be built first, then PrefetchManager can load the config
1585
+ if (document.readyState === 'loading') {
1586
+ document.addEventListener('DOMContentLoaded', initPrefetch);
1587
+ } else {
1588
+ initPrefetch();
1589
+ }
1590
+
1591
+ async function initPrefetch() {
1592
+ try {
1593
+ // Dynamically import PrefetchManager from the bundled app
1594
+ // The app should have @farmart/vite-plugin-smart-prefetch/runtime in its dependencies
1595
+ const { PrefetchManager } = await import('@farmart/vite-plugin-smart-prefetch/runtime');
1596
+ const manager = new PrefetchManager('${strategy}', ${debug});
1597
+ await manager.init();
1598
+
1599
+ // Expose manager globally for debugging
1600
+ window.__PREFETCH_MANAGER__ = manager;
1601
+ if (${debug}) {
1602
+ console.log('\u2705 Smart Prefetch Manager initialized');
1603
+ }
1604
+ } catch (error) {
1605
+ if (${debug}) {
1606
+ console.warn('\u26A0\uFE0F Could not initialize Smart Prefetch Manager:', error);
1607
+ console.log('This is expected if:');
1608
+ console.log('1. @farmart/vite-plugin-smart-prefetch/runtime is not installed');
1609
+ console.log('2. prefetch-config.json was not generated (BigQuery data fetch failed)');
1610
+ }
1611
+ }
1612
+ }
1807
1613
  `,
1808
1614
  injectTo: "head"
1809
1615
  }