Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/loadScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const defaultScriptUrl = 'https://cdn.unlayer.com/image-editor/embed.js';
// When reusing a host-injected tag we cannot know whether it already fired
// `error` (a dead tag never re-fires), so the wait is bounded instead of
// letting the promise hang forever.
const REUSED_TAG_TIMEOUT_MS = 30_000;
export const REUSED_TAG_TIMEOUT_MS = 30_000;

interface TrackedLoad {
promise: Promise<void>;
Expand Down Expand Up @@ -32,12 +32,18 @@ const findScriptTag = (scriptUrl: string): HTMLScriptElement | null => {
* host-injected tag, if it doesn't become ready within a bounded wait).
*/
export const loadScript = (
scriptUrl: string = defaultScriptUrl
scriptUrl: string = defaultScriptUrl,
reusedTagTimeoutMs: number = REUSED_TAG_TIMEOUT_MS
): Promise<void> => {
// The embed loader assigns window.ImageEditor synchronously while
// embed.js evaluates, so its presence means the script already ran
// (whether we injected it or the host page did).
if (window.ImageEditor) {
// Prefetch the versioned bundle, exactly as the load listener below
// does. Without this a page that injected embed.js itself pays a full
// extra round trip on the first createEditor. The embed loader caches
// its own promise, so a duplicate call is a no-op.
window.ImageEditor.load().catch(() => {});
return Promise.resolve();
}

Expand Down Expand Up @@ -99,7 +105,7 @@ export const loadScript = (
`Timed out waiting for an existing embed script tag: ${scriptUrl}`
)
);
}, REUSED_TAG_TIMEOUT_MS);
}, reusedTagTimeoutMs);
} else {
tag.src = scriptUrl;
document.head.appendChild(tag);
Expand Down
37 changes: 37 additions & 0 deletions test/loadScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,40 @@ it('resetLoader removes the global, the tag, and the cached promise', async () =
fire(scriptTags()[0], 'load');
await retry;
});

it('prefetches the bundle when the host page already installed the global', async () => {
// Without this the first createEditor on a host-injected page pays a full
// extra round trip that the injected path avoids.
const embed = mockEmbed();
window.ImageEditor = embed;

await loadScript();

expect(embed.load).toHaveBeenCalledTimes(1);
expect(scriptTags()).toHaveLength(0);
});

it('swallows a prefetch failure on the already-installed path', async () => {
const embed = mockEmbed();
vi.mocked(embed.load).mockRejectedValueOnce(new Error('bundle 404'));
window.ImageEditor = embed;

await expect(loadScript()).resolves.toBeUndefined();
});

it('accepts a custom reused-tag timeout', async () => {
vi.useFakeTimers();
try {
const hostTag = document.createElement('script');
hostTag.src = 'https://cdn.unlayer.com/image-editor/embed.js';
document.head.appendChild(hostTag);

const rejection = expect(
loadScript('https://cdn.unlayer.com/image-editor/embed.js', 5_000)
).rejects.toThrow(/Timed out/);
vi.advanceTimersByTime(5_000);
await rejection;
} finally {
vi.useRealTimers();
}
});