vite-plugin-cross-origin-storage 1.3.11 → 1.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,25 @@
1
1
  # vite-plugin-cross-origin-storage
2
2
 
3
- A Vite plugin to cache and load static assets (chunks) using the [Cross-Origin Storage (COS) API](https://github.com/WICG/cross-origin-storage).
3
+ A Vite plugin to cache and load static assets (chunks) using the
4
+ [Cross-Origin Storage (COS) API](https://github.com/WICG/cross-origin-storage).
4
5
 
5
- This plugin progressively enhances your application by attempting to load vendor chunks and other assets from a shared Cross-Origin Storage, reducing bandwidth usage and improving load times across different sites that share common dependencies.
6
+ This plugin progressively enhances your application by attempting to load vendor
7
+ chunks and other assets from a shared Cross-Origin Storage, reducing bandwidth
8
+ usage and improving load times across different sites that share common
9
+ dependencies.
6
10
 
7
11
  ## Features
8
12
 
9
- - **Automatic Import Rewriting**: Rewrites static imports to use COS-loaded Blob URLs when available.
10
- - **Network Fallback**: Gracefully falls back to standard network requests if COS is unavailable or the asset is missing.
11
- - **Smart Caching**: Automatically stores fetched assets into COS for future use.
12
- - **Configurable**: Easily include or exclude specific chunks using glob patterns.
13
- - **Runtime Loader**: Injects a lightweight loader to handle COS interactions transparently.
13
+ - **Automatic Import Rewriting**: Rewrites static imports to use COS-loaded Blob
14
+ URLs when available.
15
+ - **Network Fallback**: Gracefully falls back to standard network requests if
16
+ COS is unavailable or the asset is missing.
17
+ - **Smart Caching**: Automatically stores fetched assets into COS for future
18
+ use.
19
+ - **Configurable**: Easily include or exclude specific chunks using glob
20
+ patterns.
21
+ - **Runtime Loader**: Injects a lightweight loader to handle COS interactions
22
+ transparently.
14
23
 
15
24
  ## Installation
16
25
 
@@ -38,14 +47,16 @@ export default defineConfig({
38
47
 
39
48
  ## Configuration
40
49
 
41
- | Option | Type | Default | Description |
42
- | :--- | :--- | :--- | :--- |
43
- | `include` | `string \| RegExp \| Array` | `['**/*']` | Pattern to include chunks to be managed by COS. |
44
- | `exclude` | `string \| RegExp \| Array` | `undefined` | Pattern to exclude chunks from being managed. |
50
+ | Option | Type | Default | Description |
51
+ | :-------- | :-------------------------- | :---------- | :---------------------------------------------- |
52
+ | `include` | `string \| RegExp \| Array` | `['**/*']` | Pattern to include chunks to be managed by COS. |
53
+ | `exclude` | `string \| RegExp \| Array` | `undefined` | Pattern to exclude chunks from being managed. |
45
54
 
46
55
  ## Recipe: Granular Vendor Splitting
47
56
 
48
- To maximize caching benefits, it is recommended to split your `node_modules` dependencies into separate chunks. This ensures that updates to one package (e.g., `react`) do not invalidate the cache for others (e.g., `lodash`).
57
+ To maximize caching benefits, it is recommended to split your `node_modules`
58
+ dependencies into separate chunks. This ensures that updates to one package
59
+ (e.g., `react`) do not invalidate the cache for others (e.g., `lodash`).
49
60
 
50
61
  Add the following `manualChunks` configuration to your `vite.config.ts`:
51
62
 
@@ -64,7 +75,9 @@ export default defineConfig({
64
75
  // e.g. "node_modules/react/..." -> "vendor-react"
65
76
  // e.g. "node_modules/@scope/pkg/..." -> "vendor-scope-pkg"
66
77
  const parts = id.split('node_modules/')[1].split('/');
67
- const packageName = parts[0].startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
78
+ const packageName = parts[0].startsWith('@')
79
+ ? `${parts[0]}/${parts[1]}`
80
+ : parts[0];
68
81
  return `vendor-${packageName.replace('@', '').replace('/', '-')}`;
69
82
  }
70
83
  },
@@ -83,21 +96,30 @@ export default defineConfig({
83
96
  ## How It Works
84
97
 
85
98
  1. **Build Time**:
86
- - The plugin analyzes your bundle and identifies chunks matching the `include` pattern.
99
+ - The plugin analyzes your bundle and identifies chunks matching the
100
+ `include` pattern.
87
101
  - It generates a stable hash for each managed chunk.
88
- - It rewrites imports in your code to look for a global variable (e.g., `window.__COS_CHUNK_...`) containing the Blob URL of the chunk, falling back to the relative network path if the variable is unset.
89
- - It disables the default `<script type="module" src="...">` entry point in your `index.html` and injects a custom `loader.js`.
102
+ - It rewrites imports in your code to look for a global variable (e.g.,
103
+ `window.__COS_CHUNK_...`) containing the Blob URL of the chunk, falling
104
+ back to the relative network path if the variable is unset.
105
+ - It disables the default `<script type="module" src="...">` entry point in
106
+ your `index.html` and injects a custom `loader.js`.
90
107
 
91
108
  2. **Runtime**:
92
109
  - The injected loader checks for `navigator.crossOriginStorage`.
93
- - If supported, it requests the file handle for each managed chunk using its hash.
94
- - **Cache Hit**: If found, it creates a Blob URL and assigns it to the corresponding global variable.
95
- - **Cache Miss**: If not found, it fetches the file from the network, stores it in COS, and then creates the Blob URL.
96
- - Finally, the loader imports your application's entry point, which now seamlessly uses the cached assets.
110
+ - If supported, it requests the file handle for each managed chunk using its
111
+ hash.
112
+ - **Cache Hit**: If found, it creates a Blob URL and assigns it to the
113
+ corresponding global variable.
114
+ - **Cache Miss**: If not found, it fetches the file from the network, stores
115
+ it in COS, and then creates the Blob URL.
116
+ - Finally, the loader imports your application's entry point, which now
117
+ seamlessly uses the cached assets.
97
118
 
98
119
  ## Requirements
99
120
 
100
- - A browser with `Cross-Origin Storage` support (or a [browser extension](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih)).
121
+ - A browser with `Cross-Origin Storage` support (or a
122
+ [browser extension](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih)).
101
123
 
102
124
  ## License
103
125
 
package/dist/index.js CHANGED
@@ -1651,7 +1651,9 @@ function cosPlugin(options = {}) {
1651
1651
  mainChunk = chunk;
1652
1652
  } else {
1653
1653
  const res = filter(fileName);
1654
- console.log(`COS Plugin: [FILTER] ${fileName} -> ${res ? "INCLUDE" : "SKIP"}`);
1654
+ console.log(
1655
+ `COS Plugin: [FILTER] ${fileName} -> ${res ? "INCLUDE" : "SKIP"}`
1656
+ );
1655
1657
  if (res) {
1656
1658
  managedChunks[fileName] = chunk;
1657
1659
  }
@@ -1662,7 +1664,9 @@ function cosPlugin(options = {}) {
1662
1664
  }
1663
1665
  }
1664
1666
  if (mainChunk) {
1665
- const allChunks = Object.values(bundle).filter((c) => c.type === "chunk");
1667
+ const allChunks = Object.values(bundle).filter(
1668
+ (c) => c.type === "chunk"
1669
+ );
1666
1670
  const managedChunkNames = new Set(Object.keys(managedChunks));
1667
1671
  const managedChunkInfo = {};
1668
1672
  for (const fileName in managedChunks) {
@@ -1682,26 +1686,25 @@ function cosPlugin(options = {}) {
1682
1686
  if (isTargetManaged || isDepManaged) {
1683
1687
  let relPath = path.relative(importerDir, depFileName);
1684
1688
  if (!relPath.startsWith(".")) relPath = "./" + relPath;
1685
- const escapedRelPath = relPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1689
+ const escapedRelPath = relPath.replace(
1690
+ /[.*+?^${}()|[\]\\]/g,
1691
+ "\\$&"
1692
+ );
1686
1693
  const bareSpecifier = depFileName;
1687
- const staticPattern = `import\\s*(?:(?:\\{\\s*([^}]+)\\s*\\}|\\*\\s+as\\s+([^\\s]+)|([^\\s\\{\\}]+))\\s*from\\s*)?['"]${escapedRelPath}['"];?`;
1694
+ const staticPattern = `(import|export)\\b\\s*((?:(?!\\bimport\\b|\\bexport\\b)[\\s\\S])*?\\bfrom\\b\\s*)?['"]${escapedRelPath}['"]\\s*;?`;
1688
1695
  const staticRegex = new RegExp(staticPattern, "g");
1689
- targetChunk.code = targetChunk.code.replace(staticRegex, (_match, named, namespace, defaultImport) => {
1690
- if (named) return `import {${named}} from "${bareSpecifier}";`;
1691
- if (namespace) return `import * as ${namespace} from "${bareSpecifier}";`;
1692
- if (defaultImport) return `import ${defaultImport} from "${bareSpecifier}";`;
1693
- return `import "${bareSpecifier}";`;
1694
- });
1696
+ targetChunk.code = targetChunk.code.replace(
1697
+ staticRegex,
1698
+ (match, keyword, fromPart) => {
1699
+ return `${keyword}${fromPart ? " " + fromPart : " "}"${bareSpecifier}";`;
1700
+ }
1701
+ );
1695
1702
  const dynamicPattern = `import\\s*\\(\\s*['"]${escapedRelPath}['"]\\s*\\)`;
1696
1703
  const dynamicRegex = new RegExp(dynamicPattern, "g");
1697
- targetChunk.code = targetChunk.code.replace(dynamicRegex, () => `import("${bareSpecifier}")`);
1698
- const exportPattern = `export\\s*(?:(?:\\{\\s*([^}]+)\\s*\\}|\\*\\s*(?:as\\s+([^\\s]+))?))\\s*from\\s*['"]${escapedRelPath}['"];?`;
1699
- const exportRegex = new RegExp(exportPattern, "g");
1700
- targetChunk.code = targetChunk.code.replace(exportRegex, (_match, named, namespace) => {
1701
- if (named) return `export {${named}} from "${bareSpecifier}";`;
1702
- if (namespace) return `export * as ${namespace} from "${bareSpecifier}";`;
1703
- return `export * from "${bareSpecifier}";`;
1704
- });
1704
+ targetChunk.code = targetChunk.code.replace(
1705
+ dynamicRegex,
1706
+ () => `import("${bareSpecifier}")`
1707
+ );
1705
1708
  }
1706
1709
  }
1707
1710
  }
@@ -1725,7 +1728,10 @@ function cosPlugin(options = {}) {
1725
1728
  if (htmlAsset) {
1726
1729
  try {
1727
1730
  let loaderCode = fs.readFileSync(loaderPath, "utf-8");
1728
- loaderCode = loaderCode.replace("__COS_MANIFEST__", JSON.stringify(manifest));
1731
+ loaderCode = loaderCode.replace(
1732
+ "__COS_MANIFEST__",
1733
+ JSON.stringify(manifest)
1734
+ );
1729
1735
  let htmlSource = htmlAsset.source;
1730
1736
  htmlSource = htmlSource.replace(
1731
1737
  /<link\s+[^>]*rel=["']modulepreload["'][^>]*>/gi,
package/dist/loader.js CHANGED
@@ -1,5 +1,5 @@
1
1
  (async function () {
2
- await new Promise(resolve => setTimeout(resolve, 100));
2
+ await new Promise((resolve) => setTimeout(resolve, 100));
3
3
 
4
4
  const isCOSAvailable = 'crossOriginStorage' in navigator;
5
5
  console.log('COS Loader: isCOSAvailable =', isCOSAvailable);
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  // Identify managed chunks (anything with a hash)
18
- const chunksToLoad = Object.values(manifest).filter(item => item.hash);
18
+ const chunksToLoad = Object.values(manifest).filter((item) => item.hash);
19
19
 
20
20
  async function getBlobFromCOS(hash) {
21
21
  if (!isCOSAvailable) return null;
@@ -27,7 +27,8 @@
27
27
  return await handles[0].getFile();
28
28
  }
29
29
  } catch (err) {
30
- if (err.name !== 'NotFoundError') console.error('COS Loader: Error checking COS', err);
30
+ if (err.name !== 'NotFoundError')
31
+ console.error('COS Loader: Error checking COS', err);
31
32
  }
32
33
  return null;
33
34
  }
@@ -58,48 +59,69 @@
58
59
  // Longest prefix wins in Import Maps, so specific managed entries below take precedence.
59
60
  // We assume all chunks are in the same relative directory as the managed ones.
60
61
  const firstChunk = chunksToLoad[0];
61
- const assetsDir = firstChunk.fileName.substring(0, firstChunk.fileName.lastIndexOf('/') + 1);
62
- const assetsUrl = firstChunk.file.substring(0, firstChunk.file.lastIndexOf('/') + 1);
62
+ const assetsDir = firstChunk.fileName.substring(
63
+ 0,
64
+ firstChunk.fileName.lastIndexOf('/') + 1
65
+ );
66
+ const assetsUrl = firstChunk.file.substring(
67
+ 0,
68
+ firstChunk.file.lastIndexOf('/') + 1
69
+ );
63
70
  if (assetsDir && assetsUrl) {
64
71
  importMap.imports[assetsDir] = assetsUrl;
65
72
  }
66
73
 
67
- await Promise.all(chunksToLoad.map(async (chunk) => {
68
- let url = null;
74
+ await Promise.all(
75
+ chunksToLoad.map(async (chunk) => {
76
+ let url = null;
69
77
 
70
- const cosBlob = await getBlobFromCOS(chunk.hash);
71
- if (cosBlob) {
72
- console.log(`COS Loader: Loaded ${chunk.file} from COS!`);
73
- url = URL.createObjectURL(new Blob([cosBlob], { type: 'application/javascript' }));
74
- } else {
75
- console.log(`COS Loader: ${chunk.file} not in COS, fetching...`);
76
- try {
77
- const response = await fetch(chunk.file);
78
- if (response.ok) {
79
- const blob = await response.blob();
80
- url = URL.createObjectURL(new Blob([blob], { type: 'application/javascript' }));
81
- // Store in COS for next time
82
- storeBlobInCOS(blob, chunk.hash);
83
- } else {
84
- console.error(`COS Loader: Fetch failed with status ${response.status}`);
78
+ const cosBlob = await getBlobFromCOS(chunk.hash);
79
+ if (cosBlob) {
80
+ console.log(`COS Loader: Loaded ${chunk.file} from COS!`);
81
+ url = URL.createObjectURL(
82
+ new Blob([cosBlob], { type: 'text/javascript' })
83
+ );
84
+ } else {
85
+ console.log(`COS Loader: ${chunk.file} not in COS, fetching...`);
86
+ try {
87
+ const response = await fetch(chunk.file);
88
+ if (response.ok) {
89
+ const blob = await response.blob();
90
+ url = URL.createObjectURL(
91
+ new Blob([blob], { type: 'text/javascript' })
92
+ );
93
+ // Store in COS for next time
94
+ storeBlobInCOS(blob, chunk.hash);
95
+ } else {
96
+ console.error(
97
+ `COS Loader: Fetch failed with status ${response.status}`
98
+ );
99
+ }
100
+ } catch (e) {
101
+ console.error(
102
+ `COS Loader: Network fetch failed for ${chunk.file}`,
103
+ e
104
+ );
85
105
  }
86
- } catch (e) {
87
- console.error(`COS Loader: Network fetch failed for ${chunk.file}`, e);
88
106
  }
89
- }
90
107
 
91
- if (url) {
92
- // Map the bare specifier and absolute path directly to the Blob URL.
93
- // This avoids the indirection of a Data URL shim and handles cycles naturally.
94
- importMap.imports[chunk.fileName] = url;
95
- importMap.imports[chunk.file] = url;
108
+ if (url) {
109
+ // Use a Data URL shim to decouple the import graph.
110
+ // This ensures that the circular dependency between React and React-DOM
111
+ // doesn't cause a lockout/deadlock during the module graph instantiation.
112
+ const shim = `export * from "${url}";${chunk.hasDefault ? `export { default } from "${url}";` : ''}`;
113
+ const shimUrl = `data:text/javascript;base64,${btoa(shim)}`;
114
+
115
+ importMap.imports[chunk.fileName] = shimUrl;
116
+ importMap.imports[chunk.file] = shimUrl;
96
117
 
97
- // Also set global if anyone still needs it (legacy)
98
- if (chunk.globalVar) {
99
- window[chunk.globalVar] = url;
118
+ // Also set global if anyone still needs it (legacy)
119
+ if (chunk.globalVar) {
120
+ window[chunk.globalVar] = url;
121
+ }
100
122
  }
101
- }
102
- }));
123
+ })
124
+ );
103
125
 
104
126
  // Inject the importmap
105
127
  if (Object.keys(importMap.imports).length > 0) {
@@ -113,6 +135,8 @@
113
135
  // Start App
114
136
  try {
115
137
  console.log('COS Loader: Starting app...');
138
+ // Ensure the importmap is registered before importing
139
+ await new Promise((resolve) => setTimeout(resolve, 0));
116
140
  await import(mainEntry.file);
117
141
  } catch (err) {
118
142
  console.error('COS Loader: Failed to start app', err);
package/loader.js CHANGED
@@ -1,5 +1,5 @@
1
1
  (async function () {
2
- await new Promise(resolve => setTimeout(resolve, 100));
2
+ await new Promise((resolve) => setTimeout(resolve, 100));
3
3
 
4
4
  const isCOSAvailable = 'crossOriginStorage' in navigator;
5
5
  console.log('COS Loader: isCOSAvailable =', isCOSAvailable);
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  // Identify managed chunks (anything with a hash)
18
- const chunksToLoad = Object.values(manifest).filter(item => item.hash);
18
+ const chunksToLoad = Object.values(manifest).filter((item) => item.hash);
19
19
 
20
20
  async function getBlobFromCOS(hash) {
21
21
  if (!isCOSAvailable) return null;
@@ -27,7 +27,8 @@
27
27
  return await handles[0].getFile();
28
28
  }
29
29
  } catch (err) {
30
- if (err.name !== 'NotFoundError') console.error('COS Loader: Error checking COS', err);
30
+ if (err.name !== 'NotFoundError')
31
+ console.error('COS Loader: Error checking COS', err);
31
32
  }
32
33
  return null;
33
34
  }
@@ -58,48 +59,69 @@
58
59
  // Longest prefix wins in Import Maps, so specific managed entries below take precedence.
59
60
  // We assume all chunks are in the same relative directory as the managed ones.
60
61
  const firstChunk = chunksToLoad[0];
61
- const assetsDir = firstChunk.fileName.substring(0, firstChunk.fileName.lastIndexOf('/') + 1);
62
- const assetsUrl = firstChunk.file.substring(0, firstChunk.file.lastIndexOf('/') + 1);
62
+ const assetsDir = firstChunk.fileName.substring(
63
+ 0,
64
+ firstChunk.fileName.lastIndexOf('/') + 1
65
+ );
66
+ const assetsUrl = firstChunk.file.substring(
67
+ 0,
68
+ firstChunk.file.lastIndexOf('/') + 1
69
+ );
63
70
  if (assetsDir && assetsUrl) {
64
71
  importMap.imports[assetsDir] = assetsUrl;
65
72
  }
66
73
 
67
- await Promise.all(chunksToLoad.map(async (chunk) => {
68
- let url = null;
74
+ await Promise.all(
75
+ chunksToLoad.map(async (chunk) => {
76
+ let url = null;
69
77
 
70
- const cosBlob = await getBlobFromCOS(chunk.hash);
71
- if (cosBlob) {
72
- console.log(`COS Loader: Loaded ${chunk.file} from COS!`);
73
- url = URL.createObjectURL(new Blob([cosBlob], { type: 'application/javascript' }));
74
- } else {
75
- console.log(`COS Loader: ${chunk.file} not in COS, fetching...`);
76
- try {
77
- const response = await fetch(chunk.file);
78
- if (response.ok) {
79
- const blob = await response.blob();
80
- url = URL.createObjectURL(new Blob([blob], { type: 'application/javascript' }));
81
- // Store in COS for next time
82
- storeBlobInCOS(blob, chunk.hash);
83
- } else {
84
- console.error(`COS Loader: Fetch failed with status ${response.status}`);
78
+ const cosBlob = await getBlobFromCOS(chunk.hash);
79
+ if (cosBlob) {
80
+ console.log(`COS Loader: Loaded ${chunk.file} from COS!`);
81
+ url = URL.createObjectURL(
82
+ new Blob([cosBlob], { type: 'text/javascript' })
83
+ );
84
+ } else {
85
+ console.log(`COS Loader: ${chunk.file} not in COS, fetching...`);
86
+ try {
87
+ const response = await fetch(chunk.file);
88
+ if (response.ok) {
89
+ const blob = await response.blob();
90
+ url = URL.createObjectURL(
91
+ new Blob([blob], { type: 'text/javascript' })
92
+ );
93
+ // Store in COS for next time
94
+ storeBlobInCOS(blob, chunk.hash);
95
+ } else {
96
+ console.error(
97
+ `COS Loader: Fetch failed with status ${response.status}`
98
+ );
99
+ }
100
+ } catch (e) {
101
+ console.error(
102
+ `COS Loader: Network fetch failed for ${chunk.file}`,
103
+ e
104
+ );
85
105
  }
86
- } catch (e) {
87
- console.error(`COS Loader: Network fetch failed for ${chunk.file}`, e);
88
106
  }
89
- }
90
107
 
91
- if (url) {
92
- // Map the bare specifier and absolute path directly to the Blob URL.
93
- // This avoids the indirection of a Data URL shim and handles cycles naturally.
94
- importMap.imports[chunk.fileName] = url;
95
- importMap.imports[chunk.file] = url;
108
+ if (url) {
109
+ // Use a Data URL shim to decouple the import graph.
110
+ // This ensures that the circular dependency between React and React-DOM
111
+ // doesn't cause a lockout/deadlock during the module graph instantiation.
112
+ const shim = `export * from "${url}";${chunk.hasDefault ? `export { default } from "${url}";` : ''}`;
113
+ const shimUrl = `data:text/javascript;base64,${btoa(shim)}`;
114
+
115
+ importMap.imports[chunk.fileName] = shimUrl;
116
+ importMap.imports[chunk.file] = shimUrl;
96
117
 
97
- // Also set global if anyone still needs it (legacy)
98
- if (chunk.globalVar) {
99
- window[chunk.globalVar] = url;
118
+ // Also set global if anyone still needs it (legacy)
119
+ if (chunk.globalVar) {
120
+ window[chunk.globalVar] = url;
121
+ }
100
122
  }
101
- }
102
- }));
123
+ })
124
+ );
103
125
 
104
126
  // Inject the importmap
105
127
  if (Object.keys(importMap.imports).length > 0) {
@@ -113,6 +135,8 @@
113
135
  // Start App
114
136
  try {
115
137
  console.log('COS Loader: Starting app...');
138
+ // Ensure the importmap is registered before importing
139
+ await new Promise((resolve) => setTimeout(resolve, 0));
116
140
  await import(mainEntry.file);
117
141
  } catch (err) {
118
142
  console.error('COS Loader: Failed to start app', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-cross-origin-storage",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "Vite plugin to load chunks from Cross-Origin Storage",
5
5
  "keywords": [
6
6
  "vite",