@@ -5,22 +5,36 @@ import type { dh as DhType } from '@deephaven/jsapi-types';
5
5
import { downloadFromURL, urlToDirectoryName } from './serverUtils.js';
6
6
import { polyfillWs } from './polyfillWs.js';
7
7
import { ensureDirectoriesExist, getDownloadPaths } from './fsUtils.js';
8
+ import { HttpError } from './errorUtils.js';
8
9
9
10
type NonEmptyArray<T> = [T, ...T[]];
10
11
12
+ const DH_CORE_MODULE = 'jsapi/dh-core.js' as const;
13
+ const DH_INTERNAL_MODULE = 'jsapi/dh-internal.js' as const;
14
+
11
15
/** Transform downloaded content */
12
16
export type PostDownloadTransform = (
13
17
serverPath: string,
14
18
content: string
15
19
) => string;
16
20
21
+ export type PostDownloadErrorTransform = (
22
+ serverPath: string,
23
+ error: unknown
24
+ ) => string;
25
+
17
26
export type LoadModuleOptions = {
18
27
serverUrl: URL;
19
28
serverPaths: NonEmptyArray<string>;
20
- download: boolean | PostDownloadTransform;
21
29
storageDir: string;
22
30
targetModuleType: 'esm' | 'cjs';
23
- };
31
+ } & (
32
+ | { download: false }
33
+ | {
34
+ download: true | PostDownloadTransform;
35
+ errorTransform?: PostDownloadErrorTransform;
36
+ }
37
+ );
24
38
25
39
/**
26
40
* Load a list of modules from a server.
@@ -32,38 +46,56 @@ export type LoadModuleOptions = {
32
46
* the modules will be downloaded and stored. If set to a `PostDownloadTransform`
33
47
* function, the downloaded content will be passed to the function and the result
34
48
* saved to disk.
49
+ * @param errorTransform Optional function to transform errors that occur during
50
+ * the download process. If not provided, errors will be thrown.
35
51
* @param storageDir The directory to store the downloaded modules.
36
52
* @param targetModuleType The type of module to load. Can be either 'esm' or 'cjs'.
37
53
* @returns The default export of the first module in `serverPaths`.
38
54
*/
39
- export async function loadModules<TMainModule>({
40
- serverUrl,
41
- serverPaths,
42
- download,
43
- storageDir,
44
- targetModuleType,
45
- }: LoadModuleOptions): Promise<TMainModule> {
55
+ export async function loadModules<TMainModule>(
56
+ options: LoadModuleOptions
57
+ ): Promise<TMainModule> {
46
58
polyfillWs();
47
59
60
+ const { serverUrl, serverPaths, storageDir, targetModuleType } = options;
61
+
48
62
const serverStorageDir = path.join(storageDir, urlToDirectoryName(serverUrl));
49
63
50
- if (download !== false) {
64
+ if (options. download !== false) {
51
65
ensureDirectoriesExist([serverStorageDir]);
52
66
67
+ // Handle rejected Promise from download
68
+ const handleRejected = (reason: unknown, i: number): string => {
69
+ if (typeof options.errorTransform === 'function') {
70
+ return options.errorTransform(serverPaths[i], reason);
71
+ }
72
+
73
+ throw reason;
74
+ };
75
+
76
+ // Handle resolved Promise from download
77
+ const handleResolved = (value: string, i: number): string => {
78
+ if (typeof options.download === 'function') {
79
+ return options.download(serverPaths[i], value);
80
+ }
81
+
82
+ return value;
83
+ };
84
+
53
85
// Download from server
54
86
const serverUrls = serverPaths.map(
55
87
serverPath => new URL(serverPath, serverUrl)
56
88
);
57
- let contents = await Promise.all(
89
+
90
+ const settledResults = await Promise.allSettled(
58
91
serverUrls.map(url => downloadFromURL(url))
59
92
);
60
93
61
- // Post-download transform
62
- if (typeof download === 'function') {
63
- contents = contents.map((content, i) =>
64
- download(serverPaths[i], content)
65
- );
66
- }
94
+ const contents: string[] = settledResults.map((result, i) =>
95
+ result.status === 'rejected'
96
+ ? handleRejected(result.reason, i)
97
+ : handleResolved(result.value, i)
98
+ );
67
99
68
100
// Write to disk
69
101
const downloadPaths = getDownloadPaths(serverStorageDir, serverPaths);
@@ -111,37 +143,60 @@ export async function loadDhModules({
111
143
globalThis.window = globalThis;
112
144
}
113
145
146
+ // If target module type is `cjs`, we need to transform the downloaded content
147
+ // by replaing some ESM specific syntax with CJS syntax.
148
+ const cjsDownloadTransform: PostDownloadTransform = (
149
+ serverPath: string,
150
+ content: string
151
+ ): string => {
152
+ if (serverPath === DH_CORE_MODULE) {
153
+ return content
154
+ .replace(
155
+ `import {dhinternal} from './dh-internal.js';`,
156
+ `const {dhinternal} = require("./dh-internal.js");`
157
+ )
158
+ .replace(`export default dh;`, `module.exports = dh;`);
159
+ }
160
+
161
+ if (serverPath === DH_INTERNAL_MODULE) {
162
+ return content.replace(
163
+ `export{__webpack_exports__dhinternal as dhinternal};`,
164
+ `module.exports={dhinternal:__webpack_exports__dhinternal};`
165
+ );
166
+ }
167
+
168
+ return content;
169
+ };
170
+
171
+ // `dh-internal.js` module is being removed from future versions of DH core,
172
+ // but there's not a great way for this library to know whether it's present
173
+ // or not. Treat 404s as empty content to make things compatible with both
174
+ // configurations.
175
+ const errorTransform: PostDownloadErrorTransform = (
176
+ serverPath: string,
177
+ error: unknown
178
+ ): string => {
179
+ if (
180
+ serverPath === DH_INTERNAL_MODULE &&
181
+ error instanceof HttpError &&
182
+ error.statusCode === 404
183
+ ) {
184
+ return '';
185
+ }
186
+
187
+ throw error;
188
+ };
189
+
114
190
const coreModule = await loadModules<
115
191
typeof DhType & { default?: typeof DhType }
116
192
>({
117
193
serverUrl,
118
- serverPaths: ['jsapi/dh-core.js', 'jsapi/dh-internal.js' ],
194
+ serverPaths: [DH_CORE_MODULE, DH_INTERNAL_MODULE ],
119
195
storageDir,
120
196
targetModuleType,
121
- download:
122
- targetModuleType === 'esm'
123
- ? // ESM does not need any transformation since the server modules are already ESM.
124
- true
125
- : // CJS needs a post-download transform to convert the ESM modules to CJS.
126
- (serverPath, content) => {
127
- if (serverPath === 'jsapi/dh-core.js') {
128
- return content
129
- .replace(
130
- `import {dhinternal} from './dh-internal.js';`,
131
- `const {dhinternal} = require("./dh-internal.js");`
132
- )
133
- .replace(`export default dh;`, `module.exports = dh;`);
134
- }
135
-
136
- if (serverPath === 'jsapi/dh-internal.js') {
137
- return content.replace(
138
- `export{__webpack_exports__dhinternal as dhinternal};`,
139
- `module.exports={dhinternal:__webpack_exports__dhinternal};`
140
- );
141
- }
142
-
143
- return content;
144
- },
197
+ // Download the module and transform it if the target module type is `cjs`.
198
+ download: targetModuleType === 'esm' ? true : cjsDownloadTransform,
199
+ errorTransform,
145
200
});
146
201
147
202
// ESM uses `default` export. CJS does not.
0 commit comments