@superblocksteam/sdk 2.0.34 → 2.0.35-next.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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/dist/application-build.d.mts +1 -2
  3. package/dist/application-build.d.mts.map +1 -1
  4. package/dist/application-build.mjs +3 -29
  5. package/dist/application-build.mjs.map +1 -1
  6. package/dist/cli-replacement/dev.d.mts +1 -2
  7. package/dist/cli-replacement/dev.d.mts.map +1 -1
  8. package/dist/cli-replacement/dev.mjs +2 -3
  9. package/dist/cli-replacement/dev.mjs.map +1 -1
  10. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  11. package/dist/dev-utils/dev-server.mjs +18 -28
  12. package/dist/dev-utils/dev-server.mjs.map +1 -1
  13. package/dist/flag.d.ts +1 -0
  14. package/dist/flag.d.ts.map +1 -1
  15. package/dist/flag.js +3 -0
  16. package/dist/flag.js.map +1 -1
  17. package/dist/types/common.d.ts +1 -0
  18. package/dist/types/common.d.ts.map +1 -1
  19. package/dist/types/common.js.map +1 -1
  20. package/package.json +6 -6
  21. package/src/application-build.mts +1 -32
  22. package/src/cli-replacement/dev.mts +9 -16
  23. package/src/dev-utils/dev-server.mts +20 -35
  24. package/src/flag.ts +4 -0
  25. package/src/types/common.ts +1 -0
  26. package/tsconfig.tsbuildinfo +1 -1
  27. package/turbo.json +1 -0
  28. package/dist/dev-utils/vite-plugin-react-transform.d.mts +0 -7
  29. package/dist/dev-utils/vite-plugin-react-transform.d.mts.map +0 -1
  30. package/dist/dev-utils/vite-plugin-react-transform.mjs +0 -111
  31. package/dist/dev-utils/vite-plugin-react-transform.mjs.map +0 -1
  32. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts +0 -34
  33. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts.map +0 -1
  34. package/dist/dev-utils/vite-plugin-sb-cdn.mjs +0 -755
  35. package/dist/dev-utils/vite-plugin-sb-cdn.mjs.map +0 -1
  36. package/src/dev-utils/vite-plugin-react-transform.mts +0 -131
  37. package/src/dev-utils/vite-plugin-sb-cdn.mts +0 -1033
@@ -1,1033 +0,0 @@
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 {
6
- type Plugin,
7
- type ConfigEnv,
8
- type UserConfig,
9
- createIdResolver,
10
- } from "vite";
11
- import { reactTransformPlugin } from "./vite-plugin-react-transform.mjs";
12
-
13
- import type { Environment, ResolvedConfig } from "vite";
14
-
15
- // Create and extend the debug logger
16
- const debug = baseDebug("sb:sdk:vite-plugin-import-map");
17
-
18
- /**
19
- * Options for configuring the import map plugin
20
- */
21
- export interface ImportMapOptions {
22
- /**
23
- * Base imports that apply globally
24
- */
25
- imports?: Record<string, string>;
26
- /**
27
- * Additional scoped imports to add manually
28
- */
29
- scopes?: Record<string, Record<string, string>>;
30
- /**
31
- * CSS imports that need to be loaded from CDN
32
- */
33
- cssImports?: Record<string, string>;
34
- }
35
-
36
- /**
37
- * Represents a module to be preloaded with optional integrity information
38
- */
39
- interface PreloadModule {
40
- /** The URL of the module to preload */
41
- url: string;
42
- /** The content of the module (if available) */
43
- content?: string;
44
- /** The integrity hash (if calculated) */
45
- integrity?: string;
46
- }
47
-
48
- /**
49
- * Injects content into HTML after the charset meta tag, or falls back to before </head>
50
- *
51
- * @param html - The original HTML content
52
- * @param contentToInject - The content to inject
53
- * @param debugMessage - Optional debug message to log
54
- * @returns The modified HTML with content injected
55
- */
56
- function injectAfterCharset(
57
- html: string,
58
- contentToInject: string,
59
- debugMessage?: string,
60
- ): string {
61
- if (!contentToInject) return html;
62
-
63
- if (debugMessage) {
64
- debug(debugMessage);
65
- }
66
-
67
- // Try to find the charset meta tag
68
- const charsetRegex = /<meta[^>]*charset[^>]*>/i;
69
- const charsetMatch = html.match(charsetRegex);
70
-
71
- if (charsetMatch && charsetMatch.index !== undefined) {
72
- // Calculate position right after the charset tag
73
- const insertPosition = charsetMatch.index + charsetMatch[0].length;
74
-
75
- // Insert content after the charset tag
76
- const result =
77
- html.substring(0, insertPosition) +
78
- contentToInject +
79
- html.substring(insertPosition);
80
- debug("Inserted content after charset meta tag");
81
- return result;
82
- } else {
83
- // Fallback: insert before closing head tag if no charset tag is found
84
- debug("No charset meta tag found, inserting content before </head>");
85
- return html.replace("</head>", `${contentToInject}</head>`);
86
- }
87
- }
88
-
89
- /**
90
- * Calculates a SHA-384 integrity hash for a resource
91
- *
92
- * @param content - The content to hash (string, ArrayBuffer, or Uint8Array)
93
- * @returns The base64-encoded SHA-384 hash prefixed with 'sha384-'
94
- */
95
- async function calculateIntegrity(
96
- content: string | ArrayBuffer | Uint8Array | unknown,
97
- ): Promise<string> {
98
- // Safely handle different content types
99
- let buffer: ArrayBuffer;
100
- if (content instanceof ArrayBuffer) {
101
- buffer = content;
102
- } else if (content instanceof Uint8Array) {
103
- buffer = content.buffer;
104
- } else {
105
- // Handle string or any other type by converting to string first
106
- buffer = new TextEncoder().encode(String(content));
107
- }
108
-
109
- // Calculate SHA-384 hash
110
- const hashBuffer = await crypto.subtle.digest("SHA-384", buffer);
111
-
112
- // Convert to base64
113
- const hashBase64 = btoa(
114
- Array.from(new Uint8Array(hashBuffer))
115
- .map((byte) => String.fromCharCode(byte))
116
- .join(""),
117
- );
118
-
119
- return `sha384-${hashBase64}`;
120
- }
121
-
122
- /**
123
- * Extracts import specifiers from JavaScript/TypeScript code
124
- *
125
- * @param code - The source code to analyze
126
- * @returns An array of import specifiers found in the code
127
- */
128
- interface ParsedImports {
129
- absolute: string[];
130
- relative: string[];
131
- }
132
-
133
- /**
134
- * Extracts import specifiers from JavaScript/TypeScript code, separating absolute and relative imports
135
- *
136
- * @param code - The source code to analyze
137
- * @returns Object containing absolute and relative imports found in the code
138
- */
139
- async function extractImportsFromCode(code: string): Promise<ParsedImports> {
140
- try {
141
- // Initialize the lexer if it hasn't been initialized yet
142
- await init;
143
-
144
- const [imports] = parse(code);
145
-
146
- const allImports: string[] = [];
147
-
148
- for (const imp of imports) {
149
- // d === -1 means it's a static import (not dynamic, not import.meta)
150
- if (imp.d === -1) {
151
- // For static imports, use the name field when available
152
- // or extract from the source string
153
- const importSpecifier = imp.n || code.substring(imp.s, imp.e);
154
-
155
- // Clean up the import string if needed (remove quotes)
156
- const cleanedImport =
157
- typeof importSpecifier === "string"
158
- ? importSpecifier.replace(/['"\`]/g, "")
159
- : "";
160
-
161
- // Only include if it's not an absolute URL
162
- if (cleanedImport && !cleanedImport.startsWith("http")) {
163
- allImports.push(cleanedImport);
164
- }
165
- }
166
- }
167
-
168
- debug("Processed imports:", allImports);
169
-
170
- // Separate into absolute and relative imports
171
- const absolute: string[] = [];
172
- const relative: string[] = [];
173
-
174
- for (const imp of allImports) {
175
- if (
176
- imp.startsWith("/") ||
177
- imp.startsWith("./") ||
178
- imp.startsWith("../")
179
- ) {
180
- relative.push(imp);
181
- } else if (!imp.startsWith("/")) {
182
- // Skip absolute file paths
183
- absolute.push(imp);
184
- }
185
- }
186
-
187
- return { absolute, relative };
188
- } catch (err) {
189
- console.error("Failed to parse imports:", err);
190
- return { absolute: [], relative: [] };
191
- }
192
- }
193
-
194
- /**
195
- * Results from analyzing modules including all dependencies and visited modules
196
- */
197
- interface AnalysisResult {
198
- /** Direct and indirect module dependencies (absolute imports) */
199
- dependencies: Set<string>;
200
- /** The content of the main module */
201
- content: string;
202
- /** The binary data of the main module */
203
- binary: ArrayBuffer;
204
- /** Map of all visited modules with their content and binary data */
205
- visitedModules: Map<string, { content: string; binary: ArrayBuffer }>;
206
- }
207
-
208
- /**
209
- * Fetches a module and recursively analyzes all its imports
210
- *
211
- * @param baseUrl - The base URL for resolving relative imports
212
- * @param url - The URL of the module to analyze
213
- * @param debug - Whether to output debug information
214
- * @param visited - Set of already visited URLs to prevent cycles
215
- * @param visitedModules - Map to track all visited modules with their content
216
- * @returns Object containing all dependencies, content, binary data, and visited modules
217
- */
218
- async function analyzeModuleGraph(
219
- baseUrl: string,
220
- url: string,
221
- visited: Set<string> = new Set(),
222
- visitedModules: Map<
223
- string,
224
- { content: string; binary: ArrayBuffer }
225
- > = new Map(),
226
- ): Promise<AnalysisResult> {
227
- // Avoid circular dependencies and skip the shared library's build manifest reference
228
- if (visited.has(url) || url.endsWith("/user-facing/build-manifest.js")) {
229
- return {
230
- dependencies: new Set(),
231
- content: "",
232
- binary: new ArrayBuffer(0),
233
- visitedModules,
234
- };
235
- }
236
-
237
- visited.add(url);
238
-
239
- try {
240
- debug(`Fetching module: ${url}`);
241
-
242
- // First fetch as ArrayBuffer for efficiency
243
- const response = await fetch(url);
244
-
245
- if (!response.ok) {
246
- throw new Error(
247
- `Failed to fetch ${url}: ${response.status} ${response.statusText}`,
248
- );
249
- }
250
-
251
- // Get binary data once
252
- const binary = await response.arrayBuffer();
253
-
254
- // Convert to text for import analysis
255
- const decoder = new TextDecoder();
256
- const code = decoder.decode(binary);
257
-
258
- // Store this module in the visitedModules map
259
- visitedModules.set(url, { content: code, binary });
260
-
261
- // Extract both absolute and relative imports
262
- const { absolute, relative } = await extractImportsFromCode(code);
263
-
264
- debug(
265
- `Found in ${url}:\n Absolute: ${absolute.join(", ")}\n Relative: ${relative.join(", ")}`,
266
- );
267
-
268
- // Initialize with the direct absolute dependencies
269
- const dependencies = new Set(absolute);
270
-
271
- // Recursively process relative imports
272
- for (const relativeImport of relative) {
273
- // Resolve relative path against the current URL
274
- const resolvedUrl = new URL(relativeImport, url).href;
275
-
276
- debug(
277
- `Resolving relative import ${relativeImport} from ${url} to ${resolvedUrl}`,
278
- );
279
-
280
- // Recursively analyze this dependency
281
- const { dependencies: nestedDeps } = await analyzeModuleGraph(
282
- baseUrl,
283
- resolvedUrl,
284
- visited,
285
- visitedModules,
286
- );
287
-
288
- // Add all nested dependencies to our set
289
- for (const dep of nestedDeps) {
290
- dependencies.add(dep);
291
- }
292
- }
293
-
294
- return { dependencies, content: code, binary, visitedModules };
295
- } catch (err) {
296
- console.error(`Error analyzing ${url}:`, err);
297
- return {
298
- dependencies: new Set(),
299
- content: "",
300
- binary: new ArrayBuffer(0),
301
- visitedModules,
302
- };
303
- }
304
- }
305
-
306
- /**
307
- * Fetches a remote module and extracts all its imports, including following relative imports
308
- *
309
- * @param url - The URL of the module to analyze
310
- * @param debug - Whether to output debug information
311
- * @returns Object containing all found dependencies, content, binary data, and all visited modules
312
- */
313
- async function analyzeRemoteModule(url: string): Promise<{
314
- dependencies: string[];
315
- content: string;
316
- binary: ArrayBuffer;
317
- allModules: Map<string, { content: string; binary: ArrayBuffer }>;
318
- neededExports: Map<string, Set<string>>;
319
- }> {
320
- try {
321
- debug(`Analyzing remote module: ${url}`);
322
- const baseUrl = new URL(url).href;
323
- const { dependencies, content, binary, visitedModules } =
324
- await analyzeModuleGraph(baseUrl, url);
325
-
326
- // First analyze the main module's needed exports
327
- const mainModuleNeededExports = await extractNeededExports(url, content);
328
-
329
- // Start with the main module's exports and merge in exports from dependencies
330
- const allNeededExports = new Map<string, Set<string>>(
331
- mainModuleNeededExports,
332
- );
333
-
334
- // Recursively analyze all related modules to find their imports
335
- // This is critical for handling transitive dependencies
336
- let analyzedModules = 0;
337
- for (const [moduleUrl, moduleData] of visitedModules.entries()) {
338
- if (moduleUrl === url) continue; // Skip the main module we already analyzed
339
-
340
- analyzedModules++;
341
- const moduleNeededExports = await extractNeededExports(
342
- moduleUrl,
343
- moduleData.content,
344
- );
345
-
346
- // Merge all needed exports into our map
347
- for (const [
348
- importedModule,
349
- exportNames,
350
- ] of moduleNeededExports.entries()) {
351
- if (!allNeededExports.has(importedModule)) {
352
- allNeededExports.set(importedModule, new Set());
353
- }
354
-
355
- // Add all export names from this module
356
- for (const name of exportNames) {
357
- allNeededExports.get(importedModule)?.add(name);
358
- }
359
- }
360
- }
361
-
362
- debug(
363
- `Analyzed ${analyzedModules} related modules for ${url}\n` +
364
- `Found ${dependencies.size} dependencies: ${Array.from(dependencies).join(", ")}`,
365
- );
366
-
367
- // Log all needed exports for debugging
368
- if (allNeededExports.size > 0) {
369
- debug(`Needed exports from dependencies:`);
370
- for (const [module, exports] of allNeededExports.entries()) {
371
- debug(` ${module}: ${[...exports].join(", ")}`);
372
- }
373
- } else {
374
- debug(`No dependency exports needed by ${url}`);
375
- }
376
-
377
- return {
378
- dependencies: Array.from(dependencies),
379
- content,
380
- binary,
381
- allModules: visitedModules,
382
- neededExports: allNeededExports,
383
- };
384
- } catch (err) {
385
- console.error(`Error analyzing remote module ${url}:`, err);
386
- return {
387
- dependencies: [],
388
- content: "",
389
- binary: new ArrayBuffer(0),
390
- allModules: new Map(),
391
- neededExports: new Map(),
392
- };
393
- }
394
- }
395
-
396
- /**
397
- * Extracts export specifiers used by a module and its dependencies
398
- * @param content - The source code to analyze
399
- * @returns Map of module names to their needed exports
400
- */
401
- async function extractNeededExports(
402
- moduleName: string,
403
- content: string,
404
- ): Promise<Map<string, Set<string>>> {
405
- try {
406
- // Initialize the lexer if it hasn't been initialized yet
407
- await init;
408
-
409
- const result = new Map<string, Set<string>>();
410
- const [imports] = parse(content);
411
-
412
- for (const imp of imports) {
413
- // Static imports
414
- if (imp.d === -1 && imp.n) {
415
- // Get the imported module name
416
- const importedModule = imp.n;
417
-
418
- // Check for namespace imports: import * as X from 'module'
419
- if (imp.ss === -1) {
420
- // This is a namespace import, meaning we need all exports
421
- if (!result.has(importedModule)) {
422
- result.set(importedModule, new Set(["*"]));
423
- }
424
- continue;
425
- }
426
-
427
- // Extract the specific imported names for named imports
428
- const importCode = content.substring(imp.ss, imp.se);
429
- const namedImportMatch = importCode.match(/\{([^}]*)\}/);
430
-
431
- if (namedImportMatch) {
432
- const namedImports = namedImportMatch[1]
433
- .split(",")
434
- .map((name) => name.trim().split(" as ")[0].trim())
435
- .filter(Boolean);
436
-
437
- if (!result.has(importedModule)) {
438
- result.set(importedModule, new Set());
439
- }
440
-
441
- for (const name of namedImports) {
442
- result.get(importedModule)?.add(name);
443
- }
444
- } else {
445
- // Default import: import X from 'module'
446
- if (!result.has(importedModule)) {
447
- result.set(importedModule, new Set(["default"]));
448
- } else {
449
- result.get(importedModule)?.add("default");
450
- }
451
- }
452
- }
453
- }
454
-
455
- debug(
456
- `Extracted needed exports for ${moduleName}: %O`,
457
- Object.fromEntries(
458
- Array.from(result.entries()).map(([key, value]) => [
459
- key,
460
- Array.from(value),
461
- ]),
462
- ),
463
- );
464
-
465
- return result;
466
- } catch (err) {
467
- console.error(`Error extracting needed exports for ${moduleName}:`, err);
468
- return new Map();
469
- }
470
- }
471
-
472
- const wellKnownPackages = new Map<string, string>([
473
- ["react", "https://esm.sh/react@18.2.0"],
474
- ["react-dom", "https://esm.sh/react-dom@18.2.0"],
475
- ["react/jsx-runtime", "https://esm.sh/react@18.2.0/jsx-runtime"],
476
- ["react/jsx-dev-runtime", "https://esm.sh/react@18.2.0/jsx-dev-runtime"],
477
- ["./user-facing/build-manifest.js", "/assets/build-manifest.js"],
478
- ["/assets/user-facing/build-manifest.js", "/assets/build-manifest.js"],
479
- ]);
480
-
481
- const packagesToExcludeFromCdnRedirect = new Set<string>([
482
- "./user-facing/build-manifest.js",
483
- "/assets/user-facing/build-manifest.js",
484
- ]);
485
-
486
- const react18CdnUrl = "https://esm.sh/react@18.2.0";
487
- const reactDom18CdnUrl = "https://esm.sh/react-dom@18.2.0";
488
-
489
- /**
490
- * Creates a Vite plugin that injects an import map into the HTML.
491
- * It analyzes remote modules to find their imports and maps them to local Vite modules.
492
- *
493
- * Features:
494
- * - Automatically discovers and maps imports in remote modules
495
- * - Efficiently fetches binary data once and reuses it for multiple purposes
496
- * - In production mode, adds integrity hashes and modulepreload directives
497
- * - In development mode, only adds the import map for faster iteration
498
- * - Ensures proper ordering of elements in head for correct module resolution
499
- *
500
- * @param options - The import map configuration options
501
- * @returns A Vite plugin that injects the import map into HTML
502
- */
503
- export async function superblocksCdnPlugin(
504
- options: ImportMapOptions,
505
- ): Promise<Plugin[]> {
506
- const {
507
- imports: initialImportMap = {},
508
- scopes: additionalScopes = {},
509
- cssImports = {},
510
- } = options;
511
-
512
- let mode: string;
513
- let config: ResolvedConfig;
514
-
515
- // Get the keys from the imports object to use as the ignored imports
516
- const imports = Object.keys(initialImportMap);
517
-
518
- // tracks imports of modules so we can map them to chunks later
519
- const moduleToImportsMap = new Map<string, Set<string>>();
520
- // maps imports to chunks so we can resolve them in the import map
521
- const importToChunkMap = new Map<string, string>();
522
- // tracks the dependencies of modules in the import map
523
- const importMapModulesDependencies: Map<string, string[]> = new Map();
524
- // tracks remote modules to preload with their content and integrity
525
- const modulesToPreload = new Map<string, PreloadModule>();
526
- // a map of imports required by CDN bundles, grouped by their import specifier
527
- const cdnDependencies: Map<string, Set<string>> = new Map();
528
- // a map tracking the chunks that CDN dependencies end up in, created in buildStart()
529
- const cdnDependencyChunks: Map<string, string> = new Map();
530
-
531
- // For each import, download the file and analyze its imports
532
- for (const [, importUrl] of Object.entries(initialImportMap)) {
533
- // Skip non-HTTP imports
534
- if (!importUrl.startsWith("http")) continue;
535
-
536
- // Add the main module to preload
537
- modulesToPreload.set(importUrl, { url: importUrl });
538
-
539
- // Extract the base URL (e.g., http://localhost:4173/)
540
- const urlObj = new URL(importUrl);
541
- const baseUrl = `${urlObj.protocol}//${urlObj.host}/`;
542
- debug(`Extracted base URL from %s: %s`, importUrl, baseUrl);
543
-
544
- // Analyze the remote module to find all dependencies (including recursive ones)
545
- const { dependencies, content, binary, allModules, neededExports } =
546
- await analyzeRemoteModule(importUrl);
547
-
548
- // Store the list of dependencies for this import
549
- importMapModulesDependencies.set(baseUrl, [
550
- ...(importMapModulesDependencies.get(baseUrl) ?? []),
551
- ...dependencies,
552
- ]);
553
-
554
- // Store the needed exports for each dependency
555
- for (const [dependency, exportNames] of neededExports) {
556
- if (
557
- dependency.startsWith("/") ||
558
- dependency.startsWith("./") ||
559
- dependency.startsWith("../")
560
- ) {
561
- continue;
562
- }
563
-
564
- if (!cdnDependencies.has(dependency)) {
565
- cdnDependencies.set(dependency, new Set());
566
- }
567
-
568
- // Add all needed exports from this CDN module
569
- for (const exportName of exportNames) {
570
- cdnDependencies.get(dependency)?.add(exportName);
571
- }
572
- }
573
-
574
- // Add module info to preload map for main module
575
- const modulePreloadData = modulesToPreload.get(importUrl);
576
- if (modulePreloadData) {
577
- modulePreloadData.content = content;
578
-
579
- // Don't calculate integrity for react 18 CDN modules
580
- if (
581
- !importUrl.startsWith(react18CdnUrl) &&
582
- !importUrl.startsWith(reactDom18CdnUrl)
583
- ) {
584
- modulePreloadData.integrity = await calculateIntegrity(binary);
585
- }
586
- }
587
-
588
- // Process all discovered modules and add them to the preload list
589
- for (const [moduleUrl, moduleData] of allModules) {
590
- // Don't add the main module again
591
- if (moduleUrl === importUrl) continue;
592
-
593
- // Only add modules from the same origin (relative modules)
594
- if (moduleUrl.startsWith(baseUrl)) {
595
- // Add the module to preload if not already added
596
- if (!modulesToPreload.has(moduleUrl)) {
597
- // Calculate the integrity hash for non-react 18 CDN modules
598
- let integrity: string | undefined;
599
- if (
600
- !moduleUrl.startsWith(react18CdnUrl) &&
601
- !moduleUrl.startsWith(reactDom18CdnUrl)
602
- ) {
603
- integrity = await calculateIntegrity(moduleData.binary);
604
- }
605
-
606
- modulesToPreload.set(moduleUrl, {
607
- url: moduleUrl,
608
- content: moduleData.content,
609
- integrity,
610
- });
611
- debug(
612
- `Added relative module for preload: %s with integrity %s`,
613
- moduleUrl,
614
- integrity,
615
- );
616
- }
617
- }
618
- }
619
- }
620
-
621
- return [
622
- reactTransformPlugin(),
623
-
624
- /**
625
- * When a CDN module imports symbols from a local package (e.g., h, Fragment
626
- * from preact), Vite/Rollup may tree-shake these exports if they're not used locally.
627
- * This causes runtime errors when the CDN module tries to use these missing exports.
628
- *
629
- * This plugin creates virtual entry chunks that import and re-export exactly
630
- * the symbols needed by CDN modules. By using emitFile with preserveSignature: "strict",
631
- * we ensure these specific exports are preserved without modifying the original chunks.
632
- */
633
- {
634
- name: "preserve-cdn-dependency-imports",
635
-
636
- // When a module is parsed that matches one of the dependencies of our CDN bundle,
637
- // create a virtual chunk for it to ensure its exports remain available
638
- moduleParsed(moduleInfo) {
639
- const match = [...cdnDependencyChunks.entries()].find(
640
- ([, id]) => id === moduleInfo.id,
641
- );
642
- if (!match) return;
643
-
644
- const [specifier] = match;
645
- const exportList = cdnDependencies.get(specifier);
646
-
647
- debug(
648
- `Emitting virtual chunk for ${specifier} with exports: ${
649
- exportList ? [...exportList].join(", ") : "none"
650
- }`,
651
- );
652
-
653
- // create a virtual chunk that Rollup treats as an entry point,
654
- // forcing exports to be preserved
655
- this.emitFile({
656
- type: "chunk",
657
- id: `virtual:preserve-exports/${specifier}`,
658
- // this option ensures that the modules' import/export signature remains unmodified
659
- preserveSignature: "strict",
660
- });
661
- },
662
-
663
- resolveId(id) {
664
- if (id.startsWith("virtual:preserve-exports")) {
665
- debug(`Resolving virtual export preservation module: ${id}`);
666
- // return the id unchanged to handle it in the load hook
667
- return id;
668
- }
669
-
670
- return null;
671
- },
672
-
673
- // Generate the content for our virtual export preservation modules
674
- load(id) {
675
- if (id.startsWith("virtual:preserve-exports/")) {
676
- const parts = id.split("/");
677
- const dependency = parts.slice(1).join("/");
678
- const exports = cdnDependencies.get(dependency);
679
-
680
- if (!exports || exports.size === 0) {
681
- debug(`No exports to preserve for ${dependency}`);
682
- return "";
683
- }
684
-
685
- const exportsList = Array.from(exports);
686
- debug(
687
- `Generating preservation module for ${dependency} with exports: ${exportsList.join(", ")}`,
688
- );
689
-
690
- return stripIndent`
691
- import { ${commaLists`${exportsList}`} } from "${dependency}";
692
- export { ${commaLists`${exportsList}`} };
693
- `;
694
- }
695
-
696
- return null;
697
- },
698
- },
699
-
700
- {
701
- name: "vite-plugin-superblocks-cdn",
702
-
703
- /**
704
- * Configure Vite to properly handle CDN modules
705
- * - Exclude CDN modules from optimization
706
- * - Configure build for proper chunking and export preservation
707
- */
708
- config(_config: UserConfig, { mode: configuredMode }: ConfigEnv) {
709
- mode = configuredMode;
710
- debug(`Plugin initializing in ${mode} mode`);
711
-
712
- // Exclude CDN imports from pre-bundling
713
- // This prevents Vite from trying to resolve and optimize modules
714
- // that will actually be loaded from CDN at runtime
715
- const optimizeDepsExclude = [
716
- ...(_config.optimizeDeps?.exclude || []),
717
- ...Object.keys(initialImportMap),
718
- ];
719
-
720
- debug(
721
- `Excluding CDN modules from optimization: ${optimizeDepsExclude.join(", ")}`,
722
- );
723
-
724
- return {
725
- ..._config,
726
- optimizeDeps: {
727
- ..._config.optimizeDeps,
728
- exclude: optimizeDepsExclude,
729
- },
730
- build: {
731
- ..._config.build,
732
- rollupOptions: {
733
- ..._config.build?.rollupOptions,
734
- output: {
735
- ..._config.build?.rollupOptions?.output,
736
- // Group all node_modules into a vendor chunk
737
- manualChunks: (source) =>
738
- source.includes("node_modules") ? "vendor" : null,
739
- },
740
- },
741
- },
742
- };
743
- },
744
-
745
- configResolved(_config) {
746
- config = _config;
747
- },
748
-
749
- async buildStart() {
750
- // create an internal resolver to avoid calling this.resolve(),
751
- // which would trigger most plugin hooks.
752
- const resolver = createIdResolver(config);
753
-
754
- // resolve all CDN dependencies to their local modules
755
- for (const [dep] of cdnDependencies) {
756
- if (cdnDependencyChunks.has(dep)) continue;
757
-
758
- const resolved = await resolver(
759
- this.environment as unknown as Environment,
760
- dep,
761
- );
762
- if (!resolved) continue;
763
-
764
- cdnDependencyChunks.set(dep, resolved);
765
- }
766
-
767
- debug("dependencies of CDN modules", cdnDependencyChunks);
768
- },
769
-
770
- resolveId(id, importer) {
771
- // Build a graph of module relationships for later chunk resolution
772
- // This is needed for mapping imports in the scopes section of the import map
773
- if (importer) {
774
- if (!moduleToImportsMap.has(importer)) {
775
- moduleToImportsMap.set(importer, new Set());
776
- }
777
- moduleToImportsMap.get(importer)?.add(id);
778
- }
779
-
780
- // Externalize modules specified in the import map
781
- // The cdn: prefix ensures they're properly resolved to the CDN URL
782
- if (imports.includes(id)) {
783
- const shouldRedirectToCdn = !packagesToExcludeFromCdnRedirect.has(id);
784
- debug(`Externalizing module for CDN resolution: ${id}`);
785
- return {
786
- id: `${shouldRedirectToCdn ? "cdn:" : ""}${id}`,
787
- external: true,
788
- };
789
- }
790
-
791
- return null;
792
- },
793
-
794
- /**
795
- * Handle loading of externalized CDN modules
796
- * These modules will be loaded from CDN at runtime, so we return empty content
797
- */
798
- async load(id: string) {
799
- debug("loading module", id);
800
-
801
- if (id.startsWith("cdn:")) {
802
- debug(`Providing empty module for CDN import: ${id}`);
803
- return "";
804
- }
805
-
806
- // returning null here will cause Vite to continue with default behavior
807
- return null;
808
- },
809
-
810
- // track which modules are bundled in which runtime chunk
811
- generateBundle(_, bundle) {
812
- const moduleToChunkMap = new Map();
813
-
814
- for (const [fileName, chunk] of Object.entries(bundle)) {
815
- if (chunk.type === "chunk" && chunk.modules) {
816
- for (const moduleId in chunk.modules) {
817
- moduleToChunkMap.set(moduleId, fileName);
818
- }
819
- }
820
- }
821
-
822
- // Map imports to chunks using our tracking data
823
- for (const [moduleId, importIds] of moduleToImportsMap) {
824
- const chunkName = moduleToChunkMap.get(moduleId);
825
- if (chunkName) {
826
- for (const importId of importIds) {
827
- importToChunkMap.set(importId, chunkName);
828
- }
829
- }
830
- }
831
-
832
- debug({ moduleToChunkMap, importToChunkMap });
833
- },
834
-
835
- async transformIndexHtml(html, { server }) {
836
- debug("transforming index.html");
837
-
838
- const isDevMode = !!server;
839
-
840
- // Create the base import map with explicit ordering for proper resolution
841
- // Important: Import maps require an "imports" object at the top level
842
- const importMap = {
843
- imports: {
844
- ...Object.fromEntries(
845
- Object.entries(initialImportMap).map(([module, url]) => {
846
- if (wellKnownPackages.has(module)) {
847
- return [module, wellKnownPackages.get(module)!];
848
- }
849
- return [`${isDevMode ? "/@id/" : ""}cdn:${module}`, url];
850
- }),
851
- ),
852
- },
853
- // Scopes apply to specific URL prefixes for more granular control
854
- scopes: { ...additionalScopes },
855
- };
856
-
857
- for (const [baseUrl, dependencies] of importMapModulesDependencies) {
858
- if (dependencies.length === 0) continue;
859
-
860
- // Create a scope for this remote URL if we found dependencies
861
- importMap.scopes = importMap.scopes || {};
862
- importMap.scopes[baseUrl] = importMap.scopes[baseUrl] || {};
863
-
864
- debug(
865
- `Creating scope for %s with %s dependencies`,
866
- baseUrl,
867
- dependencies.length,
868
- );
869
-
870
- const base = config.base ?? "/";
871
-
872
- for (const moduleName of dependencies) {
873
- let dependencyChunkUrl: string = "";
874
-
875
- try {
876
- if (isDevMode) {
877
- // this.resolve is not available in this plugin hook
878
- const mod =
879
- await server.moduleGraph.ensureEntryFromUrl(moduleName);
880
-
881
- if (mod) {
882
- // Need to get a properly resolved path for the browser
883
- // For import maps to work correctly, the URL must be resolvable by the browser
884
-
885
- // First try to get the resolved URL from Vite
886
- let resolvedChunkUrl = "";
887
-
888
- // Use the module's resolved URL or its resolved ID
889
- if (mod.id) {
890
- // mod.id contains the full absolute path to the file, change to relative
891
- resolvedChunkUrl = mod.id.replace(server.config.root, "");
892
- }
893
-
894
- // Get the asset URL as it would be served to the browser
895
- // For Vite to correctly resolve this module in the browser
896
- if (resolvedChunkUrl.startsWith("/")) {
897
- // For paths starting with /, they're already root-relative
898
- // Make sure we don't duplicate the base path
899
- dependencyChunkUrl = resolvedChunkUrl.startsWith(base)
900
- ? resolvedChunkUrl
901
- : `${base}${resolvedChunkUrl.slice(1)}`;
902
- } else if (resolvedChunkUrl.startsWith("http")) {
903
- // For absolute URLs, use them directly
904
- dependencyChunkUrl = resolvedChunkUrl;
905
- } else {
906
- // For other paths, prepend the base path
907
- dependencyChunkUrl = `${base}${resolvedChunkUrl}`;
908
- }
909
- }
910
- } else {
911
- // Production mode approach
912
- const chunkPath = importToChunkMap.get(moduleName);
913
- if (chunkPath) {
914
- dependencyChunkUrl = `${base}${chunkPath}`;
915
- } else if (wellKnownPackages.has(moduleName)) {
916
- dependencyChunkUrl = wellKnownPackages.get(moduleName)!;
917
- }
918
- }
919
-
920
- if (dependencyChunkUrl === "") {
921
- throw new Error("No module found for " + moduleName);
922
- }
923
- } catch (error) {
924
- console.warn(`Failed to resolve module ${moduleName}: %O`, error);
925
- } finally {
926
- if (dependencyChunkUrl) {
927
- debug(`Resolved %s to %s`, moduleName, dependencyChunkUrl);
928
- // Store in the import map scope
929
- importMap.scopes[baseUrl][moduleName] = dependencyChunkUrl;
930
- // Also add to main imports for external dependencies
931
- importMap.imports[moduleName] = dependencyChunkUrl;
932
- }
933
- }
934
- }
935
- }
936
-
937
- const importMapJson = JSON.stringify(importMap, null, 2);
938
-
939
- debug(`Final generated import map:\n${importMapJson}`);
940
-
941
- const script = `<script type="importmap">${importMapJson}</script>`;
942
-
943
- // Generate preload links with integrity when available
944
- const preloadLinks = Array.from(modulesToPreload.values())
945
- .map((module) => {
946
- const integrityAttr = module.integrity
947
- ? ` integrity="${module.integrity}" crossorigin="anonymous"`
948
- : "";
949
- return `<link rel="modulepreload" href="${module.url}"${integrityAttr}>`;
950
- })
951
- .join("\n");
952
-
953
- // Import map must come before preload links for proper module resolution
954
- const combinedContent = preloadLinks
955
- ? `${script}\n${preloadLinks}`
956
- : script;
957
-
958
- // Use the utility function to inject content after charset meta tag
959
- const modifiedHtml = injectAfterCharset(
960
- html,
961
- combinedContent,
962
- "Injecting import map and preload links after charset meta tag",
963
- );
964
-
965
- // Format the HTML and return
966
- return await prettier.format(modifiedHtml, {
967
- parser: "html",
968
- embeddedLanguageFormatting: "auto",
969
- });
970
- },
971
- } satisfies Plugin,
972
-
973
- /**
974
- * Injects CDN stylesheets into the HTML. CSS files are not supported in import maps,
975
- * which is why we need a separate solution for it.
976
- */
977
- {
978
- name: "inject-cdn-stylesheets",
979
- transform(code) {
980
- // Create a regex pattern for each CSS import to remove
981
- // This allows for multiple CSS files to be imported
982
- const cssImportPatterns = Object.keys(cssImports).map((key) => {
983
- // Create a regex that matches imports for this key
984
- // Escaping slashes and dots in the import path
985
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
986
- return new RegExp(`import ["'](\.\.\/)*(${escapedKey})["'];?`, "g");
987
- });
988
-
989
- // Apply all patterns to the code
990
- let transformedCode = code;
991
- for (const pattern of cssImportPatterns) {
992
- transformedCode = transformedCode.replace(pattern, "");
993
- }
994
-
995
- if (transformedCode !== code) {
996
- debug("Removed CSS imports from code");
997
- return transformedCode;
998
- }
999
-
1000
- return code;
1001
- },
1002
-
1003
- async transformIndexHtml(html) {
1004
- // Get cssImports from options, with a default empty object
1005
- const cssImportsMap = options.cssImports || {};
1006
-
1007
- // Generate link tags for each CSS import
1008
- const cssLinks = Object.values(cssImportsMap)
1009
- .map((url) => `<link rel="stylesheet" href="${url}">\n`)
1010
- .join("");
1011
-
1012
- if (!cssLinks) {
1013
- return await prettier.format(html, {
1014
- parser: "html",
1015
- embeddedLanguageFormatting: "auto",
1016
- });
1017
- }
1018
-
1019
- // Use the shared utility function to inject CSS links
1020
- const modifiedHtml = injectAfterCharset(
1021
- html,
1022
- cssLinks,
1023
- `Injecting CSS links: ${cssLinks}`,
1024
- );
1025
-
1026
- return await prettier.format(modifiedHtml, {
1027
- parser: "html",
1028
- embeddedLanguageFormatting: "auto",
1029
- });
1030
- },
1031
- },
1032
- ];
1033
- }