diff --git a/src/imo/imo-impl.ts b/src/imo/imo-impl.ts new file mode 100644 index 0000000..e52a227 --- /dev/null +++ b/src/imo/imo-impl.ts @@ -0,0 +1,246 @@ +/* +Tasks: + +1. Collect all import maps in the HEAD element. + - Validate as they are picked up. + - Discard invalid ones with warnings. +2. Merge the collected valid import maps. +3. Save the merged import map to session storage. +4. Apply active overrides from local storage to a copy of the merged import map. +5. Inject the resulting import map into the document. +6. Post the resulting import map to any Vite dev servers allowed in local storage +that are mentioned in the resulting import map, plus the page's origin if applicable. + +*/ +import { validate } from "@collagejs/importmap"; +import { skDiscoveredViteServers, skFinalImportMap, skImportMap, skOverrides, skViteDevServers } from "../shared/storage-keys.js"; +import type { HttpOrigin, ImoOverride, MergedImportMap, ValidatedImportMap } from "../private-types.js"; +import type { ImPostingOptions } from "../types.js"; +import pRetry from "p-retry"; +import { ensureImoController, getInvolvedViteServers, getStoredDevServers, isHttpOrigin, isViteServer } from "../shared/common.js"; +import { readImPostingOptions } from "../shared/options.js"; +import { ensureGlobalCollageJs } from "@collagejs/core"; +import { Logger } from "./Logger.js"; + +/** + * Searches for all overridable import maps in the document, validates them, and merges them into a + * single import map. + * + * Invalid overridable import maps are discarded as a whole before merging. + * + * The resulting import map is validated at the end of the merging process. + * @returns The resultant import map object and any validation errors that it may possess. + */ +function mergeAllImportMaps(): ValidatedImportMap { + const mergedImportMap: MergedImportMap = { + imports: {}, + scopes: {}, + integrity: {}, + }; + const importMapElements = document.head.querySelectorAll('script[type="overridable-importmap"]'); + logger.info(`Found ${importMapElements.length} import map source(s) in the document.`); + importMapElements.forEach((element) => { + try { + const importMap = JSON.parse(element.textContent || '{}'); + const validInfo = validate(importMap); + if (!validInfo.valid) { + logger.warn(`Invalid import map detected! ${validInfo.errors.length} error(s):`); + for (const e of validInfo.errors) { + logger.warn(` - ${e}`); + } + return; + } + if (importMap.imports && typeof importMap.imports === 'object') { + Object.assign(mergedImportMap.imports, importMap.imports); + } + if (importMap.scopes && typeof importMap.scopes === 'object') { + Object.entries(importMap.scopes).forEach(([scope, mappings]) => { + if (!mergedImportMap.scopes[scope]) { + mergedImportMap.scopes[scope] = {}; + } + Object.assign(mergedImportMap.scopes[scope], mappings); + }); + } + if (importMap.integrity && typeof importMap.integrity === 'object') { + Object.assign(mergedImportMap.integrity, importMap.integrity); + } + } catch (e) { + logger.warn(`Failed to parse import map: ${(e as Error).message}`); + logger.warn(`Discarding import map: ${element.textContent}`); + } + }); + const validInfo = validate(mergedImportMap); + return { importMap: mergedImportMap, errors: validInfo.errors }; +} +/** + * Applies any active overrides from local storage to the provided import map. + * @param importMap The merged import map object used as receptacle for overrides. + * @returns The resulting (overridden) import map object and its validation errors, if any. + */ +function applyImOverrides(importMap: MergedImportMap) { + importMap = structuredClone(importMap); + const configuredOverrides = JSON.parse(localStorage.getItem(skOverrides) || '[]') as ImoOverride[]; + configuredOverrides.forEach((override) => { + if (!override.active) { + logger.info(`Skipping inactive override for ${override.bareIdentifier} in scope "${override.scope ?? 'global'}".`); + return; + } + const destination = override.scope ? (importMap.scopes[override.scope] = importMap.scopes[override.scope] || {}) : importMap.imports; + destination[override.bareIdentifier] = override.replacement; + logger.info(`Applied override for "${override.bareIdentifier}"${override.scope ? ` in scope "${override.scope}"` : ''}: ${override.replacement}.`); + }); + const validInfo = validate(importMap); + if (!validInfo.valid) { + logger.warn(`Import map became invalid after applying overrides! (${validInfo.errors.length} error(s)):`); + for (const e of validInfo.errors) { + logger.warn(` - ${e}`); + } + } + return { importMap, errors: validInfo.errors }; +} +/** + * Discovers Vite servers from the provided import map and auto-allow option. + * @param importMap The merged import map object to search for Vite servers. + * @param autoAllowOption The auto-allow option for localhost. + * @returns A map of Vite server origins and their allow status. + */ +function discoverViteServers( + importMap: MergedImportMap, + autoAllowOption: Required['autoAllowLocalhost'] +): Map { + const servers = new Map(); + const allUrls = Object.values(importMap.imports); + // Also include the page's own URL at the top so it's the first receiving the import map. + allUrls.unshift(location.href); + Object.values(importMap.scopes).forEach((scopeMappings) => { + allUrls.push(...Object.values(scopeMappings)); + }); + allUrls.forEach((urlStr) => { + try { + const url = new URL(urlStr); + if (isHttpOrigin(url.origin)) { + servers.set(url.origin, isViteServer(url, autoAllowOption)); + } + else { + logger.info(`Ignoring non-HTTP(S) origin: ${url.origin}`); + } + } + catch { } // Ignore invalid URLs or the ones without a valid origin. + }); + return servers; +} +/** + * Saves the discovered Vite servers to local storage, merging with any existing entries. + * @param viteServers Discovered Vite servers. + */ +function saveDiscoveredViteServers(viteServers: Map) { + const result = [] as [string, boolean][]; + // Merge with existing list. + const existingServers = getStoredDevServers(); + for (const [origin, allowPostImportMap] of existingServers.entries()) { + result.push([origin, allowPostImportMap]); + } + for (const [origin, isVite] of viteServers.entries()) { + if (isVite && !existingServers.has(origin)) { + result.push([origin, true]); + } + } + localStorage.setItem(skViteDevServers, JSON.stringify(result)); + logger.info(`Saved discovered Vite servers to local storage: ${result.length} server(s).`); + sessionStorage.setItem(skDiscoveredViteServers, JSON.stringify([...viteServers.entries()])); + logger.info(`Saved discovered Vite servers to session storage: ${viteServers.size} server(s).`); +} +/** + * Injects the provided import map into the document's HEAD element as a script of type + * `"importmap"`. + * @param importMap Import map to inject to the page's HEAD element. + */ +function injectImportMap(importMap: MergedImportMap) { + const script = document.createElement('script'); + script.type = 'importmap'; + script.textContent = JSON.stringify(importMap, null, 2); + const currentScript = document.currentScript; + if (currentScript && currentScript.parentNode) { + currentScript.parentNode.insertBefore(script, currentScript); + } else { + document.head.appendChild(script); + } + logger.info("Injected final import map into the document."); +} +/** + * Posts the provided import map to all Vite servers that are allowed to receive it. + * @param importMap Import map to send. + * @param foundViteServers List of Vite servers that have been seen in the resultant import map. + * @param options Import map posting options. + */ +async function postImportMapToViteServers(importMap: MergedImportMap, foundViteServers: HttpOrigin[], options: Required) { + const postPromises = [] as Promise[]; + const body = JSON.stringify(importMap); + const [involvedViteServers, excludedViteServers] = getInvolvedViteServers(foundViteServers); + excludedViteServers.forEach((origin) => { + logger.info(`Vite server at ${origin} is not allowed to receive the import map. Skipping.`); + }); + let serverCount = 0; + const postImportMaps = (serverUrl: string) => { + postPromises.push(pRetry(async (attemptNumber) => { + const response = await fetch(`${serverUrl}${options.importMapEndpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body, + }); + if (response.ok) { + ++serverCount; + logger.info(`Successfully posted import map to Vite server at ${serverUrl}.`); + } + else { + logger.warn(`Attempt #${attemptNumber}: Failed to post import map to Vite server at ${serverUrl}: ${response.status} ${response.statusText}`); + } + }, { + ...options.retryOptions, + onFailedAttempt: (error) => { + if (error.retriesLeft > 0) { + logger.warn(`Attempt ${error.attemptNumber} to post import map to ${serverUrl} failed. There are ${error.retriesLeft} retries left.`); + } + else { + logger.error(`All attempts to post import map to Vite server at ${serverUrl} have failed.`); + } + logger.save(); + }, + })); + }; + involvedViteServers.forEach((serverUrl) => { + postImportMaps(serverUrl); + }); + await Promise.all(postPromises); + logger.info(`Import map posting complete. Posted successfully to ${serverCount} Vite server(s).`); +} + +export async function main() { + ensureGlobalCollageJs(); + ensureImoController(); + logger.info("@collagejs/imo started!"); + try { + const mergedImportMap = mergeAllImportMaps(); + sessionStorage.setItem(skImportMap, JSON.stringify(mergedImportMap)); + const finalImportMap = applyImOverrides(mergedImportMap.importMap); + sessionStorage.setItem(skFinalImportMap, JSON.stringify(finalImportMap)); + if (finalImportMap.errors.length > 0) { + logger.warn(`The resulting import map turned out invalid. Will not be injected or posted to Vite servers.`); + return; + } + injectImportMap(finalImportMap.importMap); + const options = await readImPostingOptions(); + const foundViteServers = await discoverViteServers(finalImportMap.importMap, options.autoAllowLocalhost); + saveDiscoveredViteServers(foundViteServers); + logger.save(); + await postImportMapToViteServers(finalImportMap.importMap, [...foundViteServers.keys()], options); + } + finally { + logger.info("@collagejs/imo completed."); + logger.save(); + } +} + +const logger = new Logger(); diff --git a/src/imo/imo.ts b/src/imo/imo.ts index aaf4a6a..cded028 100644 --- a/src/imo/imo.ts +++ b/src/imo/imo.ts @@ -1,212 +1,3 @@ -/* -Tasks: +import { main } from "./imo-impl.js"; -1. Collect all import maps in the HEAD element. - - Validate as they are picked up. - - Discard invalid ones with warnings. -2. Merge the collected valid import maps. -3. Save the merged import map to session storage. -4. Apply active overrides from local storage to a copy of the merged import map. -5. Inject the resulting import map into the document. -6. Post the resulting import map to any Vite dev servers allowed in local storage -that are mentioned in the resulting import map, plus the page's origin if applicable. - -*/ -import { validate } from "@collagejs/importmap"; -import { skFinalImportMap, skImportMap, skOverrides, skViteDevServers } from "../shared/storage-keys.js"; -import type { ImoOverride, MergedImportMap, ValidatedImportMap } from "../private-types.js"; -import type { ImPostingOptions } from "../types.js"; -import pRetry from "p-retry"; -import { ensureImoController, getStoredDevServers, isViteServer } from "../shared/common.js"; -import { readImPostingOptions } from "../shared/options.js"; -import { ensureGlobalCollageJs } from "@collagejs/core"; -import { Logger } from "./Logger.js"; - -function mergeAllImportMaps(): ValidatedImportMap { - const mergedImportMap = { - imports: {} as Record, - scopes: {} as Record>, - integrity: {} as Record, - }; - const importMapElements = document.head.querySelectorAll('script[type="overridable-importmap"]'); - logger.info(`Found ${importMapElements.length} import map source(s) in the document.`); - importMapElements.forEach((element) => { - try { - const importMap = JSON.parse(element.textContent || '{}'); - const validInfo = validate(importMap); - if (!validInfo.valid) { - logger.warn(`Invalid import map detected! ${validInfo.errors.length} error(s):`); - for (const e of validInfo.errors) { - logger.warn(` - ${e}`); - } - return; - } - if (importMap.imports && typeof importMap.imports === 'object') { - Object.assign(mergedImportMap.imports, importMap.imports); - } - if (importMap.scopes && typeof importMap.scopes === 'object') { - Object.entries(importMap.scopes).forEach(([scope, mappings]) => { - if (!mergedImportMap.scopes[scope]) { - mergedImportMap.scopes[scope] = {}; - } - Object.assign(mergedImportMap.scopes[scope], mappings); - }); - } - if (importMap.integrity && typeof importMap.integrity === 'object') { - Object.assign(mergedImportMap.integrity, importMap.integrity); - } - } catch (e) { - logger.warn(`Failed to parse import map: ${(e as Error).message}`); - logger.warn(`Discarding import map: ${element.textContent}`); - } - }); - const validInfo = validate(mergedImportMap); - return { importMap: mergedImportMap, errors: validInfo.errors }; -} - -function applyImOverrides(importMap: MergedImportMap) { - importMap = structuredClone(importMap); - const configuredOverrides = JSON.parse(localStorage.getItem(skOverrides) || '[]') as ImoOverride[]; - configuredOverrides.forEach((override) => { - if (!override.active) { - logger.info(`Skipping inactive override for ${override.bareIdentifier} in scope "${override.scope ?? 'global'}".`); - return; - } - const destination = override.scope ? (importMap.scopes[override.scope] = importMap.scopes[override.scope] || {}) : importMap.imports; - destination[override.bareIdentifier] = override.replacement; - logger.info(`Applied override for "${override.bareIdentifier}"${override.scope ? ` in scope "${override.scope}"` : ''}: ${override.replacement}.`); - }); - const validInfo = validate(importMap); - if (!validInfo.valid) { - logger.warn(`Import map after applying overrides is invalid! (${validInfo.errors.length} error(s)):`); - for (const e of validInfo.errors) { - logger.warn(` - ${e}`); - } - } - return { importMap, errors: validInfo.errors }; -} - -function discoverViteServers(importMap: MergedImportMap, autoAllowOption: Required['autoAllowLocalhost']): Map { - const servers = new Map(); - const allUrls = Object.values(importMap.imports); - // Also include the page's own URL at the top so it's the first receiving the import map. - allUrls.unshift(location.href); - Object.values(importMap.scopes).forEach((scopeMappings) => { - allUrls.push(...Object.values(scopeMappings)); - }); - allUrls.forEach((urlStr) => { - try { - const url = new URL(urlStr); - servers.set(url.origin, isViteServer(url, autoAllowOption)); - } - catch { } // Ignore invalid URLs or the ones without a valid origin. - }); - return servers; -} - -function saveDiscoveredViteServers(viteServers: Map) { - const result = [] as [string, boolean][]; - // Merge with existing list. - const existingServers = getStoredDevServers(); - for (const [origin, allowPostImportMap] of existingServers.entries()) { - result.push([origin, allowPostImportMap]); - } - for (const [origin, allowPostImportMap] of viteServers.entries()) { - if (!existingServers.has(origin)) { - result.push([origin, allowPostImportMap]); - } - } - localStorage.setItem(skViteDevServers, JSON.stringify(result)); - logger.info(`Saved discovered Vite servers to local storage: ${result.length} server(s).`); -} - -function injectImportMap(importMap: MergedImportMap) { - const script = document.createElement('script'); - script.type = 'importmap'; - script.textContent = JSON.stringify(importMap, null, 2); - // const currentScript = document.currentScript || document.querySelector('script[src*="imo"]') || document.scripts[document.scripts.length - 1]; - const currentScript = document.currentScript; - if (currentScript && currentScript.parentNode) { - currentScript.parentNode.insertBefore(script, currentScript); - } else { - document.head.appendChild(script); - } - logger.info("Injected final import map into the document."); -} - -async function postImportMapToViteServers(importMap: MergedImportMap, foundViteServers: string[], options: Required) { - const viteServers = getStoredDevServers(); - const postPromises = [] as Promise[]; - const body = JSON.stringify(importMap); - const involvedViteServers = new Set(foundViteServers ? Array.from(foundViteServers).filter((origin) => { - if (viteServers.get(origin)) { - return true; - } - logger.info(`Vite server at ${origin} is not allowed to receive the import map. Skipping.`); - return false; - }) : []); - let serverCount = 0; - const postImportMaps = (serverUrl: string) => { - postPromises.push(pRetry(async (attemptNumber) => { - const response = await fetch(`${serverUrl}${options.importMapEndpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body, - }); - if (response.ok) { - ++serverCount; - logger.info(`Successfully posted import map to Vite server at ${serverUrl}.`); - } - else { - logger.warn(`Attempt #${attemptNumber}: Failed to post import map to Vite server at ${serverUrl}: ${response.status} ${response.statusText}`); - } - }, { - ...options.retryOptions, - onFailedAttempt: (error) => { - if (error.retriesLeft > 0) { - logger.warn(`Attempt ${error.attemptNumber} to post import map to ${serverUrl} failed. There are ${error.retriesLeft} retries left.`); - } - else { - logger.error(`All attempts to post import map to Vite server at ${serverUrl} have failed.`); - } - logger.save(); - }, - })); - }; - involvedViteServers.forEach((serverUrl) => { - postImportMaps(serverUrl); - }); - await Promise.all(postPromises); - logger.info(`Import map posting complete. Posted successfully to ${serverCount} Vite server(s).`); -} - -async function main() { - ensureGlobalCollageJs(); - ensureImoController(); - logger.info("@collagejs/imo started!"); - try { - const mergedImportMap = mergeAllImportMaps(); - sessionStorage.setItem(skImportMap, JSON.stringify(mergedImportMap)); - const finalImportMap = applyImOverrides(mergedImportMap.importMap); - sessionStorage.setItem(skFinalImportMap, JSON.stringify(finalImportMap)); - if (finalImportMap.errors.length > 0) { - logger.warn(`The resulting import map turned out invalid. Will not be injected or posted to Vite servers.`); - return; - } - injectImportMap(finalImportMap.importMap); - const options = await readImPostingOptions(); - const foundViteServers = await discoverViteServers(finalImportMap.importMap, options.autoAllowLocalhost); - saveDiscoveredViteServers(foundViteServers); - logger.save(); - await postImportMapToViteServers(finalImportMap.importMap, [...foundViteServers.keys()], options); - } - finally { - logger.info("@collagejs/imo completed."); - logger.save(); - } -} - -const logger = new Logger(); main(); diff --git a/src/lib/Imo/CurrentImportMap.svelte b/src/lib/Imo/CurrentImportMap.svelte index 6269806..ab2f793 100644 --- a/src/lib/Imo/CurrentImportMap.svelte +++ b/src/lib/Imo/CurrentImportMap.svelte @@ -57,17 +57,17 @@ ]; function acceptOverrideChange(row: (typeof data)[number]) { - row.override.replacement = overrideInputs[row.id]?.value || ""; + row.override.replacement = overrideInputs[row.id]?.value.trim() || ""; row.override.active = true; row.editing = false; - overrideInputs[row.id].value = ""; + overrideInputs[row.id].value = row.override.replacement || ""; overrideInputs[row.id].history.push(row.override.replacement); } function startEdit(row: (typeof data)[number]) { row.editing = true; row.override.active = !!row.override.replacement || row.override.active; - overrideInputs[row.id].value = ""; + overrideInputs[row.id].value = row.override.replacement || ""; } diff --git a/src/lib/Imo/DeleteAndReload.svelte b/src/lib/Imo/DeleteAndReload.svelte new file mode 100644 index 0000000..b36bf9c --- /dev/null +++ b/src/lib/Imo/DeleteAndReload.svelte @@ -0,0 +1,130 @@ + + + +{#if showTimedMessage} + window?.location.reload()} + > + {#snippet children(timeLeft)} + + Some deletions failed. Restarting in {timeLeft} second{timeLeft === + 1 + ? "" + : "s"}... + +
+ + +
+ {/snippet} + {#snippet stopped()} + Some deletions failed. + + {/snippet} +
+{/if} diff --git a/src/lib/Imo/DialogContent.svelte b/src/lib/Imo/DialogContent.svelte new file mode 100644 index 0000000..82f5985 --- /dev/null +++ b/src/lib/Imo/DialogContent.svelte @@ -0,0 +1,52 @@ + + +

+ Manage your import map overrides using the tabs below. +

+ + {#snippet tab_current()} + Current + {/snippet} + {#snippet tab_original()} + Original + {/snippet} + {#snippet tab_viteServers()} + Dev Servers + {/snippet} + + + + + + + + + + + + diff --git a/src/lib/Imo/ImoDialog.svelte b/src/lib/Imo/ImoDialog.svelte index 6b23057..bf624e9 100644 --- a/src/lib/Imo/ImoDialog.svelte +++ b/src/lib/Imo/ImoDialog.svelte @@ -1,45 +1,40 @@ @@ -47,42 +42,19 @@ {#snippet title()}

Import Map Overrides

{/snippet} -

Manage your import map overrides using the tabs below.

- - {#snippet tab_current()} - Current - {/snippet} - {#snippet tab_original()} - Original - {/snippet} - {#snippet tab_viteServers()} - Dev Servers - {/snippet} - - - - - - - - - - + {#if reloadApp} + + {:else} + + {/if} {#snippet footer()} {#if pendingOverrides} - + {/if} - + {/snippet} - - diff --git a/src/lib/Imo/ServerActionProgress.svelte b/src/lib/Imo/ServerActionProgress.svelte new file mode 100644 index 0000000..3402333 --- /dev/null +++ b/src/lib/Imo/ServerActionProgress.svelte @@ -0,0 +1,82 @@ + + +
+

{title} Development Servers

+

+ {#if done} + All operations have completed. + {:else} + {text} import map on {servers.length} server{servers.length === 1 ? '' : 's'}... + {/if} +

+
+ {#each servers as server, index} +
{server.origin}
+
+ {#if results[index] === undefined} + + {:else if results[index].success} + + {:else} + + {results[index].error} + {/if} +
+ {/each} +
+
+ + diff --git a/src/lib/Imo/Spinner.svelte b/src/lib/Imo/Spinner.svelte new file mode 100644 index 0000000..b1e9faf --- /dev/null +++ b/src/lib/Imo/Spinner.svelte @@ -0,0 +1,51 @@ + + +
+
+
+
+
+ + diff --git a/src/lib/Imo/TimerMessage.svelte b/src/lib/Imo/TimerMessage.svelte new file mode 100644 index 0000000..05251fb --- /dev/null +++ b/src/lib/Imo/TimerMessage.svelte @@ -0,0 +1,42 @@ + + +
+ {#if timerStopped} + {@render stopped?.()} + {:else} + {@render children?.(timeLeft)} + {/if} +
diff --git a/src/private-types.ts b/src/private-types.ts index 69550d0..e263797 100644 --- a/src/private-types.ts +++ b/src/private-types.ts @@ -129,3 +129,7 @@ export type OverrideData = ImoEntry & { */ editing: boolean; }; +/** + * Defines a branded string type for HTTP origins. + */ +export type HttpOrigin = string & { __brand: 'HttpOrigin' }; diff --git a/src/shared/common.ts b/src/shared/common.ts index 58d6f3d..e094081 100644 --- a/src/shared/common.ts +++ b/src/shared/common.ts @@ -1,6 +1,7 @@ import { ImoController } from "../ImoController.js"; +import type { HttpOrigin } from "../private-types.js"; import type { ImPostingOptions } from "../types.js"; -import { skViteDevServers } from "./storage-keys.js"; +import { skDiscoveredViteServers, skViteDevServers } from "./storage-keys.js"; export const eventImoLogsUpdated = 'imo-logs-updated'; @@ -73,7 +74,16 @@ export function isViteServer(url: URL, allowedType: Required[' export function getStoredDevServers(): Map { return new Map(JSON.parse(localStorage.getItem(skViteDevServers) || '[]')); } - +/** + * Reads from session storage and returns the list of potential Vite servers that have been discovered. + * + * The list is stored in storage as an array of `[origin, boolean]` pairs, where the Boolean value indicates + * whether the server is truly a Vite server or not by this package's detection logic. + * @returns A map object containing the discovered Vite dev servers, or an empty map object. + */ +export function getStoredDiscoveredDevServers() { + return new Map(JSON.parse(sessionStorage.getItem(skDiscoveredViteServers) || '[]')); +} /** * Ensures that the `CollageJs.Imo` controller is initialized and available. If it doesn't exist, it is created. */ @@ -83,3 +93,36 @@ export function ensureImoController(): void { CollageJs.Imo = new ImoController(); } } + +/** + * Gets the subset of Vite servers that are allowed to receive the import map. + * @param foundViteServers Vite servers spotted in the resultant import map. + * @returns A tuple with 2 sets: The first set contains the involved Vite servers that are allowed to receive the + * import map, and the second set contains the excluded Vite servers that are not allowed to receive the import map. + */ +export function getInvolvedViteServers(foundViteServers: Iterable) { + const viteServers = getStoredDevServers(); + const excludedViteServers = new Set(); + const involvedViteServers = new Set(foundViteServers ? Array.from(foundViteServers).filter((origin) => { + if (viteServers.get(origin)) { + return true; + } + excludedViteServers.add(origin); + return false; + }) : []); + return [involvedViteServers, excludedViteServers] as const; +} + +/** + * Tests whether a value represents an HTTP origin. + * @param value Value to test. + * @returns `true` if the value represents an HTTP origin, or `false` otherwise. + */ +export function isHttpOrigin(value: string): value is HttpOrigin { + try { + const url = new URL(value); + return (url.protocol === 'http:' || url.protocol === 'https:') && value === url.origin; + } catch { + return false; + } +} diff --git a/src/shared/storage-keys.ts b/src/shared/storage-keys.ts index aba170f..e819d37 100644 --- a/src/shared/storage-keys.ts +++ b/src/shared/storage-keys.ts @@ -59,6 +59,12 @@ export const skOverrideEntryHistoryPrefix = storageKey("oeh:"); */ export const skViteDevServers = storageKey("vds"); +/** + * Storage key for discovered Vite development servers. This is stored in session storage and is + * currently only used to request import map deletion from Vite servers before applying overrides. + */ +export const skDiscoveredViteServers = storageKey("disc-vds"); + /** * Deletes the specified data entry from storage. * @param entry Data entry to delete. @@ -96,6 +102,7 @@ export function deleteDataEntry(entry: DataEntry, name?: string): void { break; case 'vite-servers': localStorage.removeItem(skViteDevServers); + sessionStorage.removeItem(skDiscoveredViteServers); break; default: throw new Error(`Unknown data entry type: ${entry}`); diff --git a/vite.config.ts b/vite.config.ts index 4920ce4..a745173 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,8 @@ export default defineConfig({ projectId: 'cjs-imo', entryPoints: ['src/index.ts', 'src/const.ts', 'src/imo-ui.ts'], assetFileNames: 'assets/[name][extname]', + }, { + importMapTimeout: 200, }), viteTest(), viteIife({