-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLoadScriptUpgrade.js
More file actions
315 lines (268 loc) Β· 10.6 KB
/
LoadScriptUpgrade.js
File metadata and controls
315 lines (268 loc) Β· 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/**
* Universal Script & Module Loader with ESM Support
* ================================================
* Loads scripts (classic or ESM modules) from URLs or local vault paths with caching.
*
* Features:
* - Classic script loading via <script> tags
* - ESM module loading via dynamic import()
* - URL caching in vault for offline access
* - Local vault path support
* - Global deduplication (prevents duplicate loads)
* - Idempotent with global checks
*
* Usage:
* ```js
* // Classic script with global check
* await loadScript(dc, 'https://unpkg.com/globe.gl', { globalName: 'Globe' });
*
* // ESM module from CDN
* const { compile } = await loadScript(dc, 'https://esm.sh/svelte@5/compiler?bundle', {
* type: 'module',
* globalName: 'SvelteCompiler'
* });
*
* // Local vault script
* await loadScript(dc, 'scripts/mylib.js');
* ```
*/
/**
* Loads a script or ESM module with caching and global deduplication.
*
* @param {object} dc - The Datacore context object (required for vault access).
* @param {string} src - The URL or local vault path of the script/module.
* @param {object} [options] - Configuration options.
* @param {string} [options.type='script'] - Load type: 'script' (classic) or 'module' (ESM).
* @param {string} [options.globalName] - Global variable name to check/store (for deduplication).
* @param {boolean} [options.cache=true] - Whether to cache URL resources in the vault.
* @param {Function} [options.onload] - Callback when script loads successfully.
* @param {Function} [options.onerror] - Callback on error.
* @returns {Promise<any>} Promise resolving with the module exports (for ESM) or script element (for classic).
*/
async function loadScript(dc, src, options = {}) {
const {
type = 'script',
globalName = null,
cache = true,
onload = null,
onerror = null
} = options;
// Validate dc context
if (!dc || !dc.app || !dc.app.vault || !dc.app.vault.adapter) {
const error = new Error("Datacore context 'dc' with vault adapter is required for loadScript.");
if (onerror) onerror(error);
throw error;
}
const adapter = dc.app.vault.adapter;
const cacheDir = dc.resolvePath("LOAD SCRIPT/data/cache/scripts");
const isUrl = /^https?:\/\//.test(src);
// --- GLOBAL DEDUPLICATION CHECK ---
if (globalName && window[globalName]) {
console.log(`[LoadScript] β ${globalName} already available (skipping load)`);
return type === 'module' ? window[globalName] : Promise.resolve();
}
// --- GLOBAL PROMISE TRACKING (prevent duplicate concurrent loads) ---
window.__scriptPromises = window.__scriptPromises || {};
const promiseKey = `${type}:${src}`;
if (window.__scriptPromises[promiseKey]) {
console.log(`[LoadScript] β³ ${src} already loading, reusing promise...`);
return window.__scriptPromises[promiseKey];
}
console.log(`[LoadScript] π₯ Loading ${type} from ${isUrl ? 'URL' : 'local'}: ${src}`);
// --- MAIN LOADING LOGIC ---
const loadPromise = (async () => {
try {
let scriptContent = null;
// Step 1: Fetch or read script content
if (isUrl) {
const safeFilename = src
.replace(/^https?:\/\//, '')
.replace(/[\/\\?%*:|"<>]/g, '_') + '.js';
const cachePath = `${cacheDir}/${safeFilename}`;
// Check cache first
if (cache && await adapter.exists(cachePath)) {
console.log(`[LoadScript] π¦ Loading from cache: ${cachePath}`);
try {
scriptContent = await adapter.read(cachePath);
} catch (readError) {
console.warn(`[LoadScript] β οΈ Cache read failed, refetching:`, readError);
}
}
// Fetch from network if not cached
if (scriptContent === null) {
console.log(`[LoadScript] π Fetching from network: ${src}`);
const response = await fetch(src);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
scriptContent = await response.text();
// Write to cache
if (cache) {
try {
if (!(await adapter.exists(cacheDir))) {
await adapter.mkdir(cacheDir);
}
console.log(`[LoadScript] πΎ Caching to: ${cachePath}`);
await adapter.write(cachePath, scriptContent);
} catch (writeError) {
console.warn(`[LoadScript] β οΈ Cache write failed:`, writeError);
}
}
}
} else {
// Local vault path
console.log(`[LoadScript] π Reading from vault: ${src}`);
if (!(await adapter.exists(src))) {
throw new Error(`Local file not found: ${src}`);
}
scriptContent = await adapter.read(src);
}
// Step 2: Execute based on type
let result;
if (type === 'module') {
// ESM MODULE LOADING
console.log(`[LoadScript] π Loading as ESM module...`);
// For ESM modules, we should directly import from the URL instead of using blob
// This allows the browser to properly resolve module imports
try {
let moduleExports;
if (isUrl) {
// For URLs, import directly - the CDN handles resolution
console.log(`[LoadScript] π¦ Importing from URL: ${src}`);
moduleExports = await import(src);
} else {
// For local files, we need to create a blob URL
console.log(`[LoadScript] π¦ Importing from blob...`);
const blob = new Blob([scriptContent], { type: 'application/javascript' });
const blobUrl = URL.createObjectURL(blob);
try {
moduleExports = await import(blobUrl);
} finally {
URL.revokeObjectURL(blobUrl);
}
}
console.log(`[LoadScript] β
Module loaded successfully`);
console.log(`[LoadScript] π Exports:`, Object.keys(moduleExports));
// Store in global if requested
if (globalName) {
window[globalName] = moduleExports;
console.log(`[LoadScript] π Stored as window.${globalName}`);
}
result = moduleExports;
} catch (importError) {
throw new Error(`Module import failed: ${importError.message}`);
}
} else {
// CLASSIC SCRIPT LOADING
console.log(`[LoadScript] π Loading as classic script...`);
const scriptElement = document.createElement('script');
// For inline scripts (textContent), onload doesn't fire
// We need to execute synchronously and resolve immediately
try {
scriptElement.textContent = scriptContent;
// Append to DOM to execute
document.body.appendChild(scriptElement);
// Script executes synchronously, so check immediately
console.log(`[LoadScript] β
Script executed successfully`);
// Check for global if specified
if (globalName) {
if (window[globalName]) {
console.log(`[LoadScript] π window.${globalName} available`);
} else {
console.warn(`[LoadScript] β οΈ Global "${globalName}" not found after load`);
}
}
result = scriptElement;
} catch (execError) {
console.error(`[LoadScript] β Script execution failed:`, execError);
if (scriptElement.parentNode) {
scriptElement.parentNode.removeChild(scriptElement);
}
throw new Error(`Script execution failed: ${execError.message}`);
}
}
// Success callback
if (onload) {
onload(result);
}
console.log(`[LoadScript] π Load complete: ${src}`);
return result;
} catch (error) {
console.error(`[LoadScript] π₯ Failed to load ${src}:`, error);
if (onerror) {
onerror(error);
}
throw error;
} finally {
// Clean up promise tracker
delete window.__scriptPromises[promiseKey];
}
})();
// Store promise for deduplication
window.__scriptPromises[promiseKey] = loadPromise;
return loadPromise;
}
/**
* Helper: Load multiple scripts/modules in sequence or parallel.
*
* @param {object} dc - Datacore context.
* @param {Array<{src: string, options?: object}>} scripts - Array of script configs.
* @param {boolean} [parallel=false] - Load in parallel (true) or sequence (false).
* @returns {Promise<Array>} Array of results.
*/
async function loadMultiple(dc, scripts, parallel = false) {
if (parallel) {
return Promise.all(scripts.map(({ src, options }) => loadScript(dc, src, options)));
} else {
const results = [];
for (const { src, options } of scripts) {
results.push(await loadScript(dc, src, options));
}
return results;
}
}
/**
* Fetches an image from a URL and caches it in the vault for offline access.
* On subsequent loads, it reads the image directly from the cache.
*
* @param {object} dc - The Datacore context object.
* @param {string} url - The URL of the image to fetch.
* @returns {Promise<string>} A promise that resolves with a local blob URL for the image.
*/
async function fetchAndCacheImage(dc, url) {
const cacheDir = dc.resolvePath("LOAD SCRIPT/data/cache/images");
const adapter = dc.app.vault.adapter;
const safeFilename = url.replace(/^https?:\/\//, '').replace(/[\/\\?%*:|"<>]/g, '_');
const cachePath = `${cacheDir}/${safeFilename}`;
// Check cache
if (await adapter.exists(cachePath)) {
console.log(`[ImageCache] Loading from cache: ${cachePath}`);
try {
const binaryData = await adapter.readBinary(cachePath);
const blob = new Blob([binaryData]);
return URL.createObjectURL(blob);
} catch (readError) {
console.warn(`[ImageCache] Cache read failed, re-fetching:`, readError);
}
}
// Fetch from network
console.log(`[ImageCache] Fetching: ${url}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
}
const blob = await response.blob();
// Write to cache
try {
const buffer = await blob.arrayBuffer();
if (!(await adapter.exists(cacheDir))) {
await adapter.mkdir(cacheDir);
}
console.log(`[ImageCache] Caching to: ${cachePath}`);
await adapter.writeBinary(cachePath, buffer);
} catch (writeError) {
console.warn(`[ImageCache] Cache write failed:`, writeError);
}
return URL.createObjectURL(blob);
}
return { loadScript, loadMultiple, fetchAndCacheImage };