vite-plugin-cross-origin-storage 1.3.10 → 1.3.12
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 +43 -21
- package/dist/index.js +28 -19
- package/dist/loader.js +55 -35
- package/loader.js +55 -35
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
- **
|
|
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
|
|
42
|
-
|
|
|
43
|
-
| `include` | `string \| RegExp \| Array` | `['**/*']`
|
|
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`
|
|
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('@')
|
|
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
|
|
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.,
|
|
89
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
- **Cache
|
|
96
|
-
|
|
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
|
|
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
|
@@ -1647,9 +1647,14 @@ function cosPlugin(options = {}) {
|
|
|
1647
1647
|
const chunk = bundle[fileName];
|
|
1648
1648
|
if (chunk.type === "chunk") {
|
|
1649
1649
|
if (chunk.isEntry) {
|
|
1650
|
+
console.log(`COS Plugin: [ENTRY] ${fileName}`);
|
|
1650
1651
|
mainChunk = chunk;
|
|
1651
1652
|
} else {
|
|
1652
|
-
|
|
1653
|
+
const res = filter(fileName);
|
|
1654
|
+
console.log(
|
|
1655
|
+
`COS Plugin: [FILTER] ${fileName} -> ${res ? "INCLUDE" : "SKIP"}`
|
|
1656
|
+
);
|
|
1657
|
+
if (res) {
|
|
1653
1658
|
managedChunks[fileName] = chunk;
|
|
1654
1659
|
}
|
|
1655
1660
|
}
|
|
@@ -1659,7 +1664,9 @@ function cosPlugin(options = {}) {
|
|
|
1659
1664
|
}
|
|
1660
1665
|
}
|
|
1661
1666
|
if (mainChunk) {
|
|
1662
|
-
const allChunks = Object.values(bundle).filter(
|
|
1667
|
+
const allChunks = Object.values(bundle).filter(
|
|
1668
|
+
(c) => c.type === "chunk"
|
|
1669
|
+
);
|
|
1663
1670
|
const managedChunkNames = new Set(Object.keys(managedChunks));
|
|
1664
1671
|
const managedChunkInfo = {};
|
|
1665
1672
|
for (const fileName in managedChunks) {
|
|
@@ -1679,26 +1686,25 @@ function cosPlugin(options = {}) {
|
|
|
1679
1686
|
if (isTargetManaged || isDepManaged) {
|
|
1680
1687
|
let relPath = path.relative(importerDir, depFileName);
|
|
1681
1688
|
if (!relPath.startsWith(".")) relPath = "./" + relPath;
|
|
1682
|
-
const escapedRelPath = relPath.replace(
|
|
1689
|
+
const escapedRelPath = relPath.replace(
|
|
1690
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
1691
|
+
"\\$&"
|
|
1692
|
+
);
|
|
1683
1693
|
const bareSpecifier = depFileName;
|
|
1684
|
-
const staticPattern = `import\\s*(?:(
|
|
1694
|
+
const staticPattern = `(import|export)\\b\\s*((?:(?!\\bimport\\b|\\bexport\\b)[\\s\\S])*?\\bfrom\\b\\s*)?['"]${escapedRelPath}['"]\\s*;?`;
|
|
1685
1695
|
const staticRegex = new RegExp(staticPattern, "g");
|
|
1686
|
-
targetChunk.code = targetChunk.code.replace(
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1696
|
+
targetChunk.code = targetChunk.code.replace(
|
|
1697
|
+
staticRegex,
|
|
1698
|
+
(match, keyword, fromPart) => {
|
|
1699
|
+
return `${keyword}${fromPart ? " " + fromPart : " "}"${bareSpecifier}";`;
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1692
1702
|
const dynamicPattern = `import\\s*\\(\\s*['"]${escapedRelPath}['"]\\s*\\)`;
|
|
1693
1703
|
const dynamicRegex = new RegExp(dynamicPattern, "g");
|
|
1694
|
-
targetChunk.code = targetChunk.code.replace(
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (named) return `export {${named}} from "${bareSpecifier}";`;
|
|
1699
|
-
if (namespace) return `export * as ${namespace} from "${bareSpecifier}";`;
|
|
1700
|
-
return `export * from "${bareSpecifier}";`;
|
|
1701
|
-
});
|
|
1704
|
+
targetChunk.code = targetChunk.code.replace(
|
|
1705
|
+
dynamicRegex,
|
|
1706
|
+
() => `import("${bareSpecifier}")`
|
|
1707
|
+
);
|
|
1702
1708
|
}
|
|
1703
1709
|
}
|
|
1704
1710
|
}
|
|
@@ -1722,7 +1728,10 @@ function cosPlugin(options = {}) {
|
|
|
1722
1728
|
if (htmlAsset) {
|
|
1723
1729
|
try {
|
|
1724
1730
|
let loaderCode = fs.readFileSync(loaderPath, "utf-8");
|
|
1725
|
-
loaderCode = loaderCode.replace(
|
|
1731
|
+
loaderCode = loaderCode.replace(
|
|
1732
|
+
"__COS_MANIFEST__",
|
|
1733
|
+
JSON.stringify(manifest)
|
|
1734
|
+
);
|
|
1726
1735
|
let htmlSource = htmlAsset.source;
|
|
1727
1736
|
htmlSource = htmlSource.replace(
|
|
1728
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')
|
|
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,65 @@
|
|
|
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(
|
|
62
|
-
|
|
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(
|
|
68
|
-
|
|
74
|
+
await Promise.all(
|
|
75
|
+
chunksToLoad.map(async (chunk) => {
|
|
76
|
+
let url = null;
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
if (url) {
|
|
109
|
+
// Map the bare specifier and absolute path directly to the Blob URL.
|
|
110
|
+
// This avoids the indirection of a Data URL shim and handles cycles naturally.
|
|
111
|
+
importMap.imports[chunk.fileName] = url;
|
|
112
|
+
importMap.imports[chunk.file] = url;
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
// Also set global if anyone still needs it (legacy)
|
|
115
|
+
if (chunk.globalVar) {
|
|
116
|
+
window[chunk.globalVar] = url;
|
|
117
|
+
}
|
|
100
118
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
119
|
+
})
|
|
120
|
+
);
|
|
103
121
|
|
|
104
122
|
// Inject the importmap
|
|
105
123
|
if (Object.keys(importMap.imports).length > 0) {
|
|
@@ -113,6 +131,8 @@
|
|
|
113
131
|
// Start App
|
|
114
132
|
try {
|
|
115
133
|
console.log('COS Loader: Starting app...');
|
|
134
|
+
// Ensure the importmap is registered before importing
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
116
136
|
await import(mainEntry.file);
|
|
117
137
|
} catch (err) {
|
|
118
138
|
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')
|
|
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,65 @@
|
|
|
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(
|
|
62
|
-
|
|
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(
|
|
68
|
-
|
|
74
|
+
await Promise.all(
|
|
75
|
+
chunksToLoad.map(async (chunk) => {
|
|
76
|
+
let url = null;
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
if (url) {
|
|
109
|
+
// Map the bare specifier and absolute path directly to the Blob URL.
|
|
110
|
+
// This avoids the indirection of a Data URL shim and handles cycles naturally.
|
|
111
|
+
importMap.imports[chunk.fileName] = url;
|
|
112
|
+
importMap.imports[chunk.file] = url;
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
// Also set global if anyone still needs it (legacy)
|
|
115
|
+
if (chunk.globalVar) {
|
|
116
|
+
window[chunk.globalVar] = url;
|
|
117
|
+
}
|
|
100
118
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
119
|
+
})
|
|
120
|
+
);
|
|
103
121
|
|
|
104
122
|
// Inject the importmap
|
|
105
123
|
if (Object.keys(importMap.imports).length > 0) {
|
|
@@ -113,6 +131,8 @@
|
|
|
113
131
|
// Start App
|
|
114
132
|
try {
|
|
115
133
|
console.log('COS Loader: Starting app...');
|
|
134
|
+
// Ensure the importmap is registered before importing
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
116
136
|
await import(mainEntry.file);
|
|
117
137
|
} catch (err) {
|
|
118
138
|
console.error('COS Loader: Failed to start app', err);
|