@superblocksteam/sdk 1.14.2 → 2.0.3-next.46

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.
Files changed (152) hide show
  1. package/.mocharc.json +7 -0
  2. package/.prettierrc +18 -0
  3. package/dist/cli-replacement/dev.d.mts +19 -0
  4. package/dist/cli-replacement/dev.d.mts.map +1 -0
  5. package/dist/cli-replacement/dev.mjs +122 -0
  6. package/dist/cli-replacement/dev.mjs.map +1 -0
  7. package/dist/cli-replacement/init.d.ts +14 -0
  8. package/dist/cli-replacement/init.d.ts.map +1 -0
  9. package/dist/cli-replacement/init.js +26 -0
  10. package/dist/cli-replacement/init.js.map +1 -0
  11. package/dist/client.d.ts +31 -17
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +137 -155
  14. package/dist/client.js.map +1 -0
  15. package/dist/dbfs/client.d.ts +6 -0
  16. package/dist/dbfs/client.d.ts.map +1 -0
  17. package/dist/dbfs/client.js +117 -0
  18. package/dist/dbfs/client.js.map +1 -0
  19. package/dist/dbfs/local.d.ts +15 -0
  20. package/dist/dbfs/local.d.ts.map +1 -0
  21. package/dist/dbfs/local.js +126 -0
  22. package/dist/dbfs/local.js.map +1 -0
  23. package/dist/dev-utils/custom-build.d.mts +4 -0
  24. package/dist/dev-utils/custom-build.d.mts.map +1 -0
  25. package/dist/dev-utils/custom-build.mjs +99 -0
  26. package/dist/dev-utils/custom-build.mjs.map +1 -0
  27. package/dist/dev-utils/custom-config.d.mts +2 -0
  28. package/dist/dev-utils/custom-config.d.mts.map +1 -0
  29. package/dist/dev-utils/custom-config.mjs +57 -0
  30. package/dist/dev-utils/custom-config.mjs.map +1 -0
  31. package/dist/dev-utils/dev-logger.d.mts +8 -0
  32. package/dist/dev-utils/dev-logger.d.mts.map +1 -0
  33. package/dist/dev-utils/dev-logger.mjs +25 -0
  34. package/dist/dev-utils/dev-logger.mjs.map +1 -0
  35. package/dist/dev-utils/dev-server.d.mts +18 -0
  36. package/dist/dev-utils/dev-server.d.mts.map +1 -0
  37. package/dist/dev-utils/dev-server.mjs +265 -0
  38. package/dist/dev-utils/dev-server.mjs.map +1 -0
  39. package/dist/dev-utils/dev-tracer.d.ts +3 -0
  40. package/dist/dev-utils/dev-tracer.d.ts.map +1 -0
  41. package/dist/dev-utils/dev-tracer.js +28 -0
  42. package/dist/dev-utils/dev-tracer.js.map +1 -0
  43. package/dist/dev-utils/vite-plugin-dd-rum.d.mts +10 -0
  44. package/dist/dev-utils/vite-plugin-dd-rum.d.mts.map +1 -0
  45. package/dist/dev-utils/vite-plugin-dd-rum.mjs +34 -0
  46. package/dist/dev-utils/vite-plugin-dd-rum.mjs.map +1 -0
  47. package/dist/dev-utils/vite-plugin-react-transform.d.mts +7 -0
  48. package/dist/dev-utils/vite-plugin-react-transform.d.mts.map +1 -0
  49. package/dist/dev-utils/vite-plugin-react-transform.mjs +110 -0
  50. package/dist/dev-utils/vite-plugin-react-transform.mjs.map +1 -0
  51. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts +34 -0
  52. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts.map +1 -0
  53. package/dist/dev-utils/vite-plugin-sb-cdn.mjs +720 -0
  54. package/dist/dev-utils/vite-plugin-sb-cdn.mjs.map +1 -0
  55. package/dist/errors.d.ts +1 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +4 -9
  58. package/dist/errors.js.map +1 -0
  59. package/dist/flag.d.ts +2 -2
  60. package/dist/flag.d.ts.map +1 -0
  61. package/dist/flag.js +5 -9
  62. package/dist/flag.js.map +1 -0
  63. package/dist/index.d.ts +10 -4
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +10 -20
  66. package/dist/index.js.map +1 -0
  67. package/dist/sdk.d.ts +42 -18
  68. package/dist/sdk.d.ts.map +1 -0
  69. package/dist/sdk.js +47 -43
  70. package/dist/sdk.js.map +1 -0
  71. package/dist/socket/handlers.d.ts +3 -128
  72. package/dist/socket/handlers.d.ts.map +1 -0
  73. package/dist/socket/handlers.js +7 -9
  74. package/dist/socket/handlers.js.map +1 -0
  75. package/dist/socket/index.d.ts +4 -3
  76. package/dist/socket/index.d.ts.map +1 -0
  77. package/dist/socket/index.js +12 -21
  78. package/dist/socket/index.js.map +1 -0
  79. package/dist/socket/signing.d.ts +3 -1
  80. package/dist/socket/signing.d.ts.map +1 -0
  81. package/dist/socket/signing.js +8 -17
  82. package/dist/socket/signing.js.map +1 -0
  83. package/dist/socket/socket.d.ts +3 -2
  84. package/dist/socket/socket.d.ts.map +1 -0
  85. package/dist/socket/socket.js +9 -19
  86. package/dist/socket/socket.js.map +1 -0
  87. package/dist/types/common.d.ts +2 -103
  88. package/dist/types/common.d.ts.map +1 -0
  89. package/dist/types/common.js +8 -24
  90. package/dist/types/common.js.map +1 -0
  91. package/dist/types/index.d.ts +5 -4
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/index.js +5 -20
  94. package/dist/types/index.js.map +1 -0
  95. package/dist/types/plugin.d.ts +1 -0
  96. package/dist/types/plugin.d.ts.map +1 -0
  97. package/dist/types/plugin.js +3 -5
  98. package/dist/types/plugin.js.map +1 -0
  99. package/dist/types/signing.d.ts +2 -1
  100. package/dist/types/signing.d.ts.map +1 -0
  101. package/dist/types/signing.js +2 -2
  102. package/dist/types/signing.js.map +1 -0
  103. package/dist/types/socket.d.ts +1 -0
  104. package/dist/types/socket.d.ts.map +1 -0
  105. package/dist/types/socket.js +2 -5
  106. package/dist/types/socket.js.map +1 -0
  107. package/dist/utils.d.ts +3 -1
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/utils.js +14 -23
  110. package/dist/utils.js.map +1 -0
  111. package/dist/version-control.d.mts +59 -0
  112. package/dist/version-control.d.mts.map +1 -0
  113. package/dist/version-control.mjs +899 -0
  114. package/dist/version-control.mjs.map +1 -0
  115. package/eslint.config.js +85 -0
  116. package/package.json +72 -32
  117. package/src/cli-replacement/dev.mts +182 -0
  118. package/src/cli-replacement/init.ts +47 -0
  119. package/src/client.ts +114 -38
  120. package/src/dbfs/client.ts +162 -0
  121. package/src/dbfs/local.ts +163 -0
  122. package/src/dev-utils/custom-build.mts +113 -0
  123. package/src/dev-utils/custom-config.mts +66 -0
  124. package/src/dev-utils/dev-logger.mts +39 -0
  125. package/src/dev-utils/dev-server.mts +342 -0
  126. package/src/dev-utils/dev-tracer.ts +31 -0
  127. package/src/dev-utils/vite-plugin-dd-rum.mts +47 -0
  128. package/src/dev-utils/vite-plugin-react-transform.mts +130 -0
  129. package/src/dev-utils/vite-plugin-sb-cdn.mts +988 -0
  130. package/src/flag.ts +2 -3
  131. package/src/index.ts +119 -4
  132. package/src/sdk.ts +91 -17
  133. package/src/socket/handlers.ts +9 -147
  134. package/src/socket/index.ts +6 -9
  135. package/src/socket/signing.ts +7 -8
  136. package/src/socket/socket.ts +8 -9
  137. package/src/types/common.ts +2 -119
  138. package/src/types/index.ts +4 -4
  139. package/src/types/signing.ts +1 -1
  140. package/src/types/socket.ts +1 -1
  141. package/src/utils.ts +5 -6
  142. package/src/version-control.mts +1351 -0
  143. package/test/dev-utils/fixture/index.html +12 -0
  144. package/test/dev-utils/fixture/main.jsx +22 -0
  145. package/test/dev-utils/fixture/package-lock.json +25 -0
  146. package/test/dev-utils/fixture/package.json +9 -0
  147. package/test/dev-utils/vite-plugin-sb-cdn.test.mts +74 -0
  148. package/test/tsconfig.json +9 -0
  149. package/test/version-control.test.mts +1412 -0
  150. package/tsconfig.json +15 -4
  151. package/tsconfig.tsbuildinfo +1 -1
  152. package/.eslintrc.json +0 -55
@@ -0,0 +1,720 @@
1
+ import { commaLists, stripIndent } from "common-tags";
2
+ import baseDebug from "debug";
3
+ import { parse, init } from "es-module-lexer";
4
+ import prettier from "prettier";
5
+ import { createIdResolver, } from "vite";
6
+ import { reactTransformPlugin } from "./vite-plugin-react-transform.mjs";
7
+ // Create and extend the debug logger
8
+ const debug = baseDebug("sb:sdk:vite-plugin-import-map");
9
+ /**
10
+ * Injects content into HTML after the charset meta tag, or falls back to before </head>
11
+ *
12
+ * @param html - The original HTML content
13
+ * @param contentToInject - The content to inject
14
+ * @param debugMessage - Optional debug message to log
15
+ * @returns The modified HTML with content injected
16
+ */
17
+ function injectAfterCharset(html, contentToInject, debugMessage) {
18
+ if (!contentToInject)
19
+ return html;
20
+ if (debugMessage) {
21
+ debug(debugMessage);
22
+ }
23
+ // Try to find the charset meta tag
24
+ const charsetRegex = /<meta[^>]*charset[^>]*>/i;
25
+ const charsetMatch = html.match(charsetRegex);
26
+ if (charsetMatch && charsetMatch.index !== undefined) {
27
+ // Calculate position right after the charset tag
28
+ const insertPosition = charsetMatch.index + charsetMatch[0].length;
29
+ // Insert content after the charset tag
30
+ const result = html.substring(0, insertPosition) +
31
+ contentToInject +
32
+ html.substring(insertPosition);
33
+ debug("Inserted content after charset meta tag");
34
+ return result;
35
+ }
36
+ else {
37
+ // Fallback: insert before closing head tag if no charset tag is found
38
+ debug("No charset meta tag found, inserting content before </head>");
39
+ return html.replace("</head>", `${contentToInject}</head>`);
40
+ }
41
+ }
42
+ /**
43
+ * Calculates a SHA-384 integrity hash for a resource
44
+ *
45
+ * @param content - The content to hash (string, ArrayBuffer, or Uint8Array)
46
+ * @returns The base64-encoded SHA-384 hash prefixed with 'sha384-'
47
+ */
48
+ async function calculateIntegrity(content) {
49
+ // Safely handle different content types
50
+ let buffer;
51
+ if (content instanceof ArrayBuffer) {
52
+ buffer = content;
53
+ }
54
+ else if (content instanceof Uint8Array) {
55
+ buffer = content.buffer;
56
+ }
57
+ else {
58
+ // Handle string or any other type by converting to string first
59
+ buffer = new TextEncoder().encode(String(content));
60
+ }
61
+ // Calculate SHA-384 hash
62
+ const hashBuffer = await crypto.subtle.digest("SHA-384", buffer);
63
+ // Convert to base64
64
+ const hashBase64 = btoa(Array.from(new Uint8Array(hashBuffer))
65
+ .map((byte) => String.fromCharCode(byte))
66
+ .join(""));
67
+ return `sha384-${hashBase64}`;
68
+ }
69
+ /**
70
+ * Extracts import specifiers from JavaScript/TypeScript code, separating absolute and relative imports
71
+ *
72
+ * @param code - The source code to analyze
73
+ * @returns Object containing absolute and relative imports found in the code
74
+ */
75
+ async function extractImportsFromCode(code) {
76
+ try {
77
+ // Initialize the lexer if it hasn't been initialized yet
78
+ await init;
79
+ const [imports] = parse(code);
80
+ const allImports = [];
81
+ for (const imp of imports) {
82
+ // d === -1 means it's a static import (not dynamic, not import.meta)
83
+ if (imp.d === -1) {
84
+ // For static imports, use the name field when available
85
+ // or extract from the source string
86
+ const importSpecifier = imp.n || code.substring(imp.s, imp.e);
87
+ // Clean up the import string if needed (remove quotes)
88
+ const cleanedImport = typeof importSpecifier === "string"
89
+ ? importSpecifier.replace(/['"\`]/g, "")
90
+ : "";
91
+ // Only include if it's not an absolute URL
92
+ if (cleanedImport && !cleanedImport.startsWith("http")) {
93
+ allImports.push(cleanedImport);
94
+ }
95
+ }
96
+ }
97
+ debug("Processed imports:", allImports);
98
+ // Separate into absolute and relative imports
99
+ const absolute = [];
100
+ const relative = [];
101
+ for (const imp of allImports) {
102
+ if (imp.startsWith("/") ||
103
+ imp.startsWith("./") ||
104
+ imp.startsWith("../")) {
105
+ relative.push(imp);
106
+ }
107
+ else if (!imp.startsWith("/")) {
108
+ // Skip absolute file paths
109
+ absolute.push(imp);
110
+ }
111
+ }
112
+ return { absolute, relative };
113
+ }
114
+ catch (err) {
115
+ console.error("Failed to parse imports:", err);
116
+ return { absolute: [], relative: [] };
117
+ }
118
+ }
119
+ /**
120
+ * Fetches a module and recursively analyzes all its imports
121
+ *
122
+ * @param baseUrl - The base URL for resolving relative imports
123
+ * @param url - The URL of the module to analyze
124
+ * @param debug - Whether to output debug information
125
+ * @param visited - Set of already visited URLs to prevent cycles
126
+ * @param visitedModules - Map to track all visited modules with their content
127
+ * @returns Object containing all dependencies, content, binary data, and visited modules
128
+ */
129
+ async function analyzeModuleGraph(baseUrl, url, visited = new Set(), visitedModules = new Map()) {
130
+ // Avoid circular dependencies
131
+ if (visited.has(url)) {
132
+ return {
133
+ dependencies: new Set(),
134
+ content: "",
135
+ binary: new ArrayBuffer(0),
136
+ visitedModules,
137
+ };
138
+ }
139
+ visited.add(url);
140
+ try {
141
+ debug(`Fetching module: ${url}`);
142
+ // First fetch as ArrayBuffer for efficiency
143
+ const response = await fetch(url);
144
+ if (!response.ok) {
145
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
146
+ }
147
+ // Get binary data once
148
+ const binary = await response.arrayBuffer();
149
+ // Convert to text for import analysis
150
+ const decoder = new TextDecoder();
151
+ const code = decoder.decode(binary);
152
+ // Store this module in the visitedModules map
153
+ visitedModules.set(url, { content: code, binary });
154
+ // Extract both absolute and relative imports
155
+ const { absolute, relative } = await extractImportsFromCode(code);
156
+ debug(`Found in ${url}:\n Absolute: ${absolute.join(", ")}\n Relative: ${relative.join(", ")}`);
157
+ // Initialize with the direct absolute dependencies
158
+ const dependencies = new Set(absolute);
159
+ // Recursively process relative imports
160
+ for (const relativeImport of relative) {
161
+ // Resolve relative path against the current URL
162
+ const resolvedUrl = new URL(relativeImport, url).href;
163
+ debug(`Resolving relative import ${relativeImport} from ${url} to ${resolvedUrl}`);
164
+ // Recursively analyze this dependency
165
+ const { dependencies: nestedDeps } = await analyzeModuleGraph(baseUrl, resolvedUrl, visited, visitedModules);
166
+ // Add all nested dependencies to our set
167
+ for (const dep of nestedDeps) {
168
+ dependencies.add(dep);
169
+ }
170
+ }
171
+ return { dependencies, content: code, binary, visitedModules };
172
+ }
173
+ catch (err) {
174
+ console.error(`Error analyzing ${url}:`, err);
175
+ return {
176
+ dependencies: new Set(),
177
+ content: "",
178
+ binary: new ArrayBuffer(0),
179
+ visitedModules,
180
+ };
181
+ }
182
+ }
183
+ /**
184
+ * Fetches a remote module and extracts all its imports, including following relative imports
185
+ *
186
+ * @param url - The URL of the module to analyze
187
+ * @param debug - Whether to output debug information
188
+ * @returns Object containing all found dependencies, content, binary data, and all visited modules
189
+ */
190
+ async function analyzeRemoteModule(url) {
191
+ try {
192
+ debug(`Analyzing remote module: ${url}`);
193
+ const baseUrl = new URL(url).href;
194
+ const { dependencies, content, binary, visitedModules } = await analyzeModuleGraph(baseUrl, url);
195
+ // First analyze the main module's needed exports
196
+ const mainModuleNeededExports = await extractNeededExports(url, content);
197
+ // Start with the main module's exports and merge in exports from dependencies
198
+ const allNeededExports = new Map(mainModuleNeededExports);
199
+ // Recursively analyze all related modules to find their imports
200
+ // This is critical for handling transitive dependencies
201
+ let analyzedModules = 0;
202
+ for (const [moduleUrl, moduleData] of visitedModules.entries()) {
203
+ if (moduleUrl === url)
204
+ continue; // Skip the main module we already analyzed
205
+ analyzedModules++;
206
+ const moduleNeededExports = await extractNeededExports(moduleUrl, moduleData.content);
207
+ // Merge all needed exports into our map
208
+ for (const [importedModule, exportNames,] of moduleNeededExports.entries()) {
209
+ if (!allNeededExports.has(importedModule)) {
210
+ allNeededExports.set(importedModule, new Set());
211
+ }
212
+ // Add all export names from this module
213
+ for (const name of exportNames) {
214
+ allNeededExports.get(importedModule)?.add(name);
215
+ }
216
+ }
217
+ }
218
+ debug(`Analyzed ${analyzedModules} related modules for ${url}\n` +
219
+ `Found ${dependencies.size} dependencies: ${Array.from(dependencies).join(", ")}`);
220
+ // Log all needed exports for debugging
221
+ if (allNeededExports.size > 0) {
222
+ debug(`Needed exports from dependencies:`);
223
+ for (const [module, exports] of allNeededExports.entries()) {
224
+ debug(` ${module}: ${[...exports].join(", ")}`);
225
+ }
226
+ }
227
+ else {
228
+ debug(`No dependency exports needed by ${url}`);
229
+ }
230
+ return {
231
+ dependencies: Array.from(dependencies),
232
+ content,
233
+ binary,
234
+ allModules: visitedModules,
235
+ neededExports: allNeededExports,
236
+ };
237
+ }
238
+ catch (err) {
239
+ console.error(`Error analyzing remote module ${url}:`, err);
240
+ return {
241
+ dependencies: [],
242
+ content: "",
243
+ binary: new ArrayBuffer(0),
244
+ allModules: new Map(),
245
+ neededExports: new Map(),
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * Extracts export specifiers used by a module and its dependencies
251
+ * @param content - The source code to analyze
252
+ * @returns Map of module names to their needed exports
253
+ */
254
+ async function extractNeededExports(moduleName, content) {
255
+ try {
256
+ // Initialize the lexer if it hasn't been initialized yet
257
+ await init;
258
+ const result = new Map();
259
+ const [imports] = parse(content);
260
+ for (const imp of imports) {
261
+ // Static imports
262
+ if (imp.d === -1 && imp.n) {
263
+ // Get the imported module name
264
+ const importedModule = imp.n;
265
+ // Check for namespace imports: import * as X from 'module'
266
+ if (imp.ss === -1) {
267
+ // This is a namespace import, meaning we need all exports
268
+ if (!result.has(importedModule)) {
269
+ result.set(importedModule, new Set(["*"]));
270
+ }
271
+ continue;
272
+ }
273
+ // Extract the specific imported names for named imports
274
+ const importCode = content.substring(imp.ss, imp.se);
275
+ const namedImportMatch = importCode.match(/\{([^}]*)\}/);
276
+ if (namedImportMatch) {
277
+ const namedImports = namedImportMatch[1]
278
+ .split(",")
279
+ .map((name) => name.trim().split(" as ")[0].trim())
280
+ .filter(Boolean);
281
+ if (!result.has(importedModule)) {
282
+ result.set(importedModule, new Set());
283
+ }
284
+ for (const name of namedImports) {
285
+ result.get(importedModule)?.add(name);
286
+ }
287
+ }
288
+ else {
289
+ // Default import: import X from 'module'
290
+ if (!result.has(importedModule)) {
291
+ result.set(importedModule, new Set(["default"]));
292
+ }
293
+ else {
294
+ result.get(importedModule)?.add("default");
295
+ }
296
+ }
297
+ }
298
+ }
299
+ debug(`Extracted needed exports for ${moduleName}: %O`, Object.fromEntries(Array.from(result.entries()).map(([key, value]) => [
300
+ key,
301
+ Array.from(value),
302
+ ])));
303
+ return result;
304
+ }
305
+ catch (err) {
306
+ console.error(`Error extracting needed exports for ${moduleName}:`, err);
307
+ return new Map();
308
+ }
309
+ }
310
+ /**
311
+ * Creates a Vite plugin that injects an import map into the HTML.
312
+ * It analyzes remote modules to find their imports and maps them to local Vite modules.
313
+ *
314
+ * Features:
315
+ * - Automatically discovers and maps imports in remote modules
316
+ * - Efficiently fetches binary data once and reuses it for multiple purposes
317
+ * - In production mode, adds integrity hashes and modulepreload directives
318
+ * - In development mode, only adds the import map for faster iteration
319
+ * - Ensures proper ordering of elements in head for correct module resolution
320
+ *
321
+ * @param options - The import map configuration options
322
+ * @returns A Vite plugin that injects the import map into HTML
323
+ */
324
+ export async function superblocksCdnPlugin(options) {
325
+ const { imports: initialImportMap = {}, scopes: additionalScopes = {}, cssImports = {}, } = options;
326
+ let mode;
327
+ let config;
328
+ // Get the keys from the imports object to use as the ignored imports
329
+ const imports = Object.keys(initialImportMap);
330
+ // tracks imports of modules so we can map them to chunks later
331
+ const moduleToImportsMap = new Map();
332
+ // maps imports to chunks so we can resolve them in the import map
333
+ const importToChunkMap = new Map();
334
+ // tracks the dependencies of modules in the import map
335
+ const importMapModulesDependencies = new Map();
336
+ // tracks remote modules to preload with their content and integrity
337
+ const modulesToPreload = new Map();
338
+ // a map of imports required by CDN bundles, grouped by their import specifier
339
+ const cdnDependencies = new Map();
340
+ // a map tracking the chunks that CDN dependencies end up in, created in buildStart()
341
+ const cdnDependencyChunks = new Map();
342
+ // For each import, download the file and analyze its imports
343
+ for (const [, importUrl] of Object.entries(initialImportMap)) {
344
+ // Skip non-HTTP imports
345
+ if (!importUrl.startsWith("http"))
346
+ continue;
347
+ // Add the main module to preload
348
+ modulesToPreload.set(importUrl, { url: importUrl });
349
+ // Extract the base URL (e.g., http://localhost:4173/)
350
+ const urlObj = new URL(importUrl);
351
+ const baseUrl = `${urlObj.protocol}//${urlObj.host}/`;
352
+ debug(`Extracted base URL from %s: %s`, importUrl, baseUrl);
353
+ // Analyze the remote module to find all dependencies (including recursive ones)
354
+ const { dependencies, content, binary, allModules, neededExports } = await analyzeRemoteModule(importUrl);
355
+ // Store the list of dependencies for this import
356
+ importMapModulesDependencies.set(baseUrl, [
357
+ ...(importMapModulesDependencies.get(baseUrl) ?? []),
358
+ ...dependencies,
359
+ ]);
360
+ // Store the needed exports for each dependency
361
+ for (const [dependency, exportNames] of neededExports) {
362
+ if (dependency.startsWith("/") ||
363
+ dependency.startsWith("./") ||
364
+ dependency.startsWith("../")) {
365
+ continue;
366
+ }
367
+ if (!cdnDependencies.has(dependency)) {
368
+ cdnDependencies.set(dependency, new Set());
369
+ }
370
+ // Add all needed exports from this CDN module
371
+ for (const exportName of exportNames) {
372
+ cdnDependencies.get(dependency)?.add(exportName);
373
+ }
374
+ }
375
+ // Add module info to preload map for main module
376
+ const modulePreloadData = modulesToPreload.get(importUrl);
377
+ if (modulePreloadData) {
378
+ modulePreloadData.content = content;
379
+ modulePreloadData.integrity = await calculateIntegrity(binary);
380
+ }
381
+ // Process all discovered modules and add them to the preload list
382
+ for (const [moduleUrl, moduleData] of allModules) {
383
+ // Don't add the main module again
384
+ if (moduleUrl === importUrl)
385
+ continue;
386
+ // Only add modules from the same origin (relative modules)
387
+ if (moduleUrl.startsWith(baseUrl)) {
388
+ // Add the module to preload if not already added
389
+ if (!modulesToPreload.has(moduleUrl)) {
390
+ // Calculate the integrity hash
391
+ const integrity = await calculateIntegrity(moduleData.binary);
392
+ modulesToPreload.set(moduleUrl, {
393
+ url: moduleUrl,
394
+ content: moduleData.content,
395
+ integrity,
396
+ });
397
+ debug(`Added relative module for preload: %s with integrity %s`, moduleUrl, integrity);
398
+ }
399
+ }
400
+ }
401
+ }
402
+ return [
403
+ reactTransformPlugin(),
404
+ /**
405
+ * When a CDN module imports symbols from a local package (e.g., h, Fragment
406
+ * from preact), Vite/Rollup may tree-shake these exports if they're not used locally.
407
+ * This causes runtime errors when the CDN module tries to use these missing exports.
408
+ *
409
+ * This plugin creates virtual entry chunks that import and re-export exactly
410
+ * the symbols needed by CDN modules. By using emitFile with preserveSignature: "strict",
411
+ * we ensure these specific exports are preserved without modifying the original chunks.
412
+ */
413
+ {
414
+ name: "preserve-cdn-dependency-imports",
415
+ // When a module is parsed that matches one of the dependencies of our CDN bundle,
416
+ // create a virtual chunk for it to ensure its exports remain available
417
+ moduleParsed(moduleInfo) {
418
+ const match = [...cdnDependencyChunks.entries()].find(([, id]) => id === moduleInfo.id);
419
+ if (!match)
420
+ return;
421
+ const [specifier] = match;
422
+ const exportList = cdnDependencies.get(specifier);
423
+ debug(`Emitting virtual chunk for ${specifier} with exports: ${exportList ? [...exportList].join(", ") : "none"}`);
424
+ // create a virtual chunk that Rollup treats as an entry point,
425
+ // forcing exports to be preserved
426
+ this.emitFile({
427
+ type: "chunk",
428
+ id: `virtual:preserve-exports/${specifier}`,
429
+ // this option ensures that the modules' import/export signature remains unmodified
430
+ preserveSignature: "strict",
431
+ });
432
+ },
433
+ resolveId(id) {
434
+ if (id.startsWith("virtual:preserve-exports")) {
435
+ debug(`Resolving virtual export preservation module: ${id}`);
436
+ // return the id unchanged to handle it in the load hook
437
+ return id;
438
+ }
439
+ return null;
440
+ },
441
+ // Generate the content for our virtual export preservation modules
442
+ load(id) {
443
+ if (id.startsWith("virtual:preserve-exports/")) {
444
+ const parts = id.split("/");
445
+ const dependency = parts.slice(1).join("/");
446
+ const exports = cdnDependencies.get(dependency);
447
+ if (!exports || exports.size === 0) {
448
+ debug(`No exports to preserve for ${dependency}`);
449
+ return "";
450
+ }
451
+ const exportsList = Array.from(exports);
452
+ debug(`Generating preservation module for ${dependency} with exports: ${exportsList.join(", ")}`);
453
+ return stripIndent `
454
+ import { ${commaLists `${exportsList}`} } from "${dependency}";
455
+ export { ${commaLists `${exportsList}`} };
456
+ `;
457
+ }
458
+ return null;
459
+ },
460
+ },
461
+ {
462
+ name: "vite-plugin-superblocks-cdn",
463
+ // run before Vite's module resolution
464
+ enforce: "pre",
465
+ /**
466
+ * Configure Vite to properly handle CDN modules
467
+ * - Exclude CDN modules from optimization
468
+ * - Configure build for proper chunking and export preservation
469
+ */
470
+ config(_config, { mode: configuredMode }) {
471
+ mode = configuredMode;
472
+ debug(`Plugin initializing in ${mode} mode`);
473
+ // Exclude CDN imports from pre-bundling
474
+ // This prevents Vite from trying to resolve and optimize modules
475
+ // that will actually be loaded from CDN at runtime
476
+ const optimizeDepsExclude = [
477
+ ...(_config.optimizeDeps?.exclude || []),
478
+ ...Object.keys(initialImportMap),
479
+ ];
480
+ debug(`Excluding CDN modules from optimization: ${optimizeDepsExclude.join(", ")}`);
481
+ return {
482
+ ..._config,
483
+ optimizeDeps: {
484
+ ..._config.optimizeDeps,
485
+ exclude: optimizeDepsExclude,
486
+ },
487
+ build: {
488
+ ..._config.build,
489
+ rollupOptions: {
490
+ ..._config.build?.rollupOptions,
491
+ output: {
492
+ ..._config.build?.rollupOptions?.output,
493
+ // Group all node_modules into a vendor chunk
494
+ manualChunks: (source) => source.includes("node_modules") ? "vendor" : null,
495
+ },
496
+ },
497
+ },
498
+ };
499
+ },
500
+ configResolved(_config) {
501
+ config = _config;
502
+ },
503
+ async buildStart() {
504
+ // create an internal resolver to avoid calling this.resolve(),
505
+ // which would trigger most plugin hooks.
506
+ const resolver = createIdResolver(config);
507
+ // resolve all CDN dependencies to their local modules
508
+ for (const [dep] of cdnDependencies) {
509
+ if (cdnDependencyChunks.has(dep))
510
+ continue;
511
+ const resolved = await resolver(this.environment, dep);
512
+ if (!resolved)
513
+ continue;
514
+ cdnDependencyChunks.set(dep, resolved);
515
+ }
516
+ },
517
+ resolveId(id, importer) {
518
+ // Build a graph of module relationships for later chunk resolution
519
+ // This is needed for mapping imports in the scopes section of the import map
520
+ if (importer) {
521
+ if (!moduleToImportsMap.has(importer)) {
522
+ moduleToImportsMap.set(importer, new Set());
523
+ }
524
+ moduleToImportsMap.get(importer)?.add(id);
525
+ }
526
+ // Externalize modules specified in the import map
527
+ // The cdn: prefix ensures they're properly resolved to the CDN URL
528
+ if (imports.includes(id)) {
529
+ debug(`Externalizing module for CDN resolution: ${id}`);
530
+ return { id: `cdn:${id}`, external: true };
531
+ }
532
+ return null;
533
+ },
534
+ /**
535
+ * Handle loading of externalized CDN modules
536
+ * These modules will be loaded from CDN at runtime, so we return empty content
537
+ */
538
+ async load(id) {
539
+ if (id.startsWith("cdn:")) {
540
+ debug(`Providing empty module for CDN import: ${id}`);
541
+ return "";
542
+ }
543
+ // returning null here will cause Vite to continue with default behavior
544
+ return null;
545
+ },
546
+ // track which modules are bundled in which runtime chunk
547
+ generateBundle(_, bundle) {
548
+ const moduleToChunkMap = new Map();
549
+ for (const [fileName, chunk] of Object.entries(bundle)) {
550
+ if (chunk.type === "chunk" && chunk.modules) {
551
+ for (const moduleId in chunk.modules) {
552
+ moduleToChunkMap.set(moduleId, fileName);
553
+ }
554
+ }
555
+ }
556
+ // Map imports to chunks using our tracking data
557
+ for (const [moduleId, importIds] of moduleToImportsMap) {
558
+ const chunkName = moduleToChunkMap.get(moduleId);
559
+ if (chunkName) {
560
+ for (const importId of importIds) {
561
+ importToChunkMap.set(importId, chunkName);
562
+ }
563
+ }
564
+ }
565
+ },
566
+ async transformIndexHtml(html, { server }) {
567
+ const isDevMode = !!server;
568
+ // Create the base import map with explicit ordering for proper resolution
569
+ // Important: Import maps require an "imports" object at the top level
570
+ const importMap = {
571
+ imports: {
572
+ ...Object.fromEntries(Object.entries(initialImportMap).map(([module, url]) => [
573
+ `${isDevMode ? "/@id/" : ""}cdn:${module}`,
574
+ url,
575
+ ])),
576
+ },
577
+ // Scopes apply to specific URL prefixes for more granular control
578
+ scopes: { ...additionalScopes },
579
+ };
580
+ for (const [baseUrl, dependencies] of importMapModulesDependencies) {
581
+ if (dependencies.length === 0)
582
+ continue;
583
+ // Create a scope for this remote URL if we found dependencies
584
+ importMap.scopes = importMap.scopes || {};
585
+ importMap.scopes[baseUrl] = importMap.scopes[baseUrl] || {};
586
+ debug(`Creating scope for %s with %s dependencies`, baseUrl, dependencies.length);
587
+ const base = config.base ?? "/";
588
+ for (const moduleName of dependencies) {
589
+ let dependencyChunkUrl = "";
590
+ try {
591
+ if (isDevMode) {
592
+ // this.resolve is not available in this plugin hook
593
+ const mod = await server.moduleGraph.ensureEntryFromUrl(moduleName);
594
+ if (mod) {
595
+ // Need to get a properly resolved path for the browser
596
+ // For import maps to work correctly, the URL must be resolvable by the browser
597
+ // First try to get the resolved URL from Vite
598
+ let resolvedChunkUrl = "";
599
+ // Use the module's resolved URL or its resolved ID
600
+ if (mod.id) {
601
+ // mod.id contains the full absolute path to the file, change to relative
602
+ resolvedChunkUrl = mod.id.replace(server.config.root, "");
603
+ }
604
+ // Get the asset URL as it would be served to the browser
605
+ // For Vite to correctly resolve this module in the browser
606
+ if (resolvedChunkUrl.startsWith("/")) {
607
+ // For paths starting with /, they're already root-relative
608
+ // Make sure we don't duplicate the base path
609
+ dependencyChunkUrl = resolvedChunkUrl.startsWith(base)
610
+ ? resolvedChunkUrl
611
+ : `${base}${resolvedChunkUrl.slice(1)}`;
612
+ }
613
+ else if (resolvedChunkUrl.startsWith("http")) {
614
+ // For absolute URLs, use them directly
615
+ dependencyChunkUrl = resolvedChunkUrl;
616
+ }
617
+ else {
618
+ // For other paths, prepend the base path
619
+ dependencyChunkUrl = `${base}${resolvedChunkUrl}`;
620
+ }
621
+ }
622
+ }
623
+ else {
624
+ // Production mode approach
625
+ const chunkPath = importToChunkMap.get(moduleName);
626
+ if (chunkPath) {
627
+ dependencyChunkUrl = `${base}${chunkPath}`;
628
+ }
629
+ }
630
+ if (dependencyChunkUrl === "") {
631
+ throw new Error("No module found for " + moduleName);
632
+ }
633
+ }
634
+ catch (error) {
635
+ console.warn(`Failed to resolve module ${moduleName}: %O`, error);
636
+ }
637
+ finally {
638
+ if (dependencyChunkUrl) {
639
+ debug(`Resolved %s to %s`, moduleName, dependencyChunkUrl);
640
+ // Store in the import map scope
641
+ importMap.scopes[baseUrl][moduleName] = dependencyChunkUrl;
642
+ }
643
+ }
644
+ }
645
+ }
646
+ const importMapJson = JSON.stringify(importMap, null, 2);
647
+ debug(`Final generated import map:\n${importMapJson}`);
648
+ const script = `<script type="importmap">${importMapJson}</script>`;
649
+ // Generate preload links with integrity when available
650
+ const preloadLinks = Array.from(modulesToPreload.values())
651
+ .map((module) => {
652
+ const integrityAttr = module.integrity
653
+ ? ` integrity="${module.integrity}" crossorigin="anonymous"`
654
+ : "";
655
+ return `<link rel="modulepreload" href="${module.url}"${integrityAttr}>`;
656
+ })
657
+ .join("\n");
658
+ // Import map must come before preload links for proper module resolution
659
+ const combinedContent = preloadLinks
660
+ ? `${script}\n${preloadLinks}`
661
+ : script;
662
+ // Use the utility function to inject content after charset meta tag
663
+ const modifiedHtml = injectAfterCharset(html, combinedContent, "Injecting import map and preload links after charset meta tag");
664
+ // Format the HTML and return
665
+ return await prettier.format(modifiedHtml, {
666
+ parser: "html",
667
+ embeddedLanguageFormatting: "auto",
668
+ });
669
+ },
670
+ },
671
+ /**
672
+ * Injects CDN stylesheets into the HTML. CSS files are not supported in import maps,
673
+ * which is why we need a separate solution for it.
674
+ */
675
+ {
676
+ name: "inject-cdn-stylesheets",
677
+ transform(code) {
678
+ // Create a regex pattern for each CSS import to remove
679
+ // This allows for multiple CSS files to be imported
680
+ const cssImportPatterns = Object.keys(cssImports).map((key) => {
681
+ // Create a regex that matches imports for this key
682
+ // Escaping slashes and dots in the import path
683
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
684
+ return new RegExp(`import ["'](\.\.\/)*(${escapedKey})["'];?`, "g");
685
+ });
686
+ // Apply all patterns to the code
687
+ let transformedCode = code;
688
+ for (const pattern of cssImportPatterns) {
689
+ transformedCode = transformedCode.replace(pattern, "");
690
+ }
691
+ if (transformedCode !== code) {
692
+ debug("Removed CSS imports from code");
693
+ return transformedCode;
694
+ }
695
+ return code;
696
+ },
697
+ async transformIndexHtml(html) {
698
+ // Get cssImports from options, with a default empty object
699
+ const cssImportsMap = options.cssImports || {};
700
+ // Generate link tags for each CSS import
701
+ const cssLinks = Object.values(cssImportsMap)
702
+ .map((url) => `<link rel="stylesheet" href="${url}">\n`)
703
+ .join("");
704
+ if (!cssLinks) {
705
+ return await prettier.format(html, {
706
+ parser: "html",
707
+ embeddedLanguageFormatting: "auto",
708
+ });
709
+ }
710
+ // Use the shared utility function to inject CSS links
711
+ const modifiedHtml = injectAfterCharset(html, cssLinks, `Injecting CSS links: ${cssLinks}`);
712
+ return await prettier.format(modifiedHtml, {
713
+ parser: "html",
714
+ embeddedLanguageFormatting: "auto",
715
+ });
716
+ },
717
+ },
718
+ ];
719
+ }
720
+ //# sourceMappingURL=vite-plugin-sb-cdn.mjs.map