islands-runtime 0.1.0

The shared WASM runtime for islands.rs: reactive Signal/Scope/effect primitives and idempotent island mounting, emitted as islands_core.{js,wasm}.
Documentation
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
//! Client-side navigation for islands.rs (the `nav` feature).
//!
//! Same-origin `<a>` clicks are intercepted and resolved against an in-memory
//! route to bundle manifest; the destination bundle is `import()`ed (and its
//! WASM instantiated via `await default()`), the destination HTML is fetched
//! with the `X-Islands-Nav` header and morphed into the live document, and
//! island lifecycle is driven precisely through idiomorph's callbacks — all
//! without a full document reload.
//!
//! This module implements the **normative Navigation execution contract** from
//! `.omc/plans/client-navigation.md`. The orchestration is single-sourced: a
//! nav runs contract steps 1-7 in [`navigate_to`], and **mounting happens
//! exactly once**, at step 6, inside [`lifecycle::activate_suspense_then_mount`].
//!
//! Submodules:
//!   - [`opt_out`]: the pure click opt-out predicate (AC-V2), natively tested.
//!   - [`dom`]: impure DOM adapters (reads a live click into a pure `LinkClick`).
//!   - [`manifest`]: the route to bundle manifest schema + fetch + cache.
//!   - [`lifecycle`]: morph options bound to island lifecycle + the single mount.
//!   - [`history`]: scroll restoration, pushState, focus, popstate, bfcache.
//!
//! Prefetch (Phase 3f) and View Transitions (Phase 3g) are deliberately NOT
//! here: the morph+mount of steps 4-6 is isolated in [`perform_morph_and_mount`]
//! so a view transition can wrap exactly that callback, and the fetch/import
//! helpers are split out so a prefetch commit can substitute a cached
//! speculation for a fresh fetch.

mod dom;
mod history;
mod lifecycle;
mod manifest;
mod opt_out;
mod prefetch;
mod view_transition;

use std::cell::RefCell;

use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::{spawn_local, JsFuture};

use crate::nav::history::ScrollPosition;

thread_local! {
    /// Set once nav has been initialized so [`init`] is idempotent (a bfcache
    /// `pageshow` or a duplicate call does not re-attach listeners).
    static INITIALIZED: RefCell<bool> = const { RefCell::new(false) };

    /// The `AbortController` of the in-flight nav fetch, if any. A new accepted
    /// nav aborts the prior one so the latest nav wins (AC-V10).
    static ACTIVE_FETCH_CONTROLLER: RefCell<Option<web_sys::AbortController>> =
        const { RefCell::new(None) };

    /// Monotonic generation token bumped at the start of every accepted nav.
    /// Each nav captures the value it started; after every `await` it re-checks
    /// that its generation is still current and bails if a newer nav has begun.
    /// This is what "the morph is keyed to the latest nav — no interleaved
    /// morphs" (AC-V10) means even when an unabortable `import()` interleaves:
    /// the superseded nav resolves into a no-op rather than morphing stale HTML.
    static NAV_GENERATION: RefCell<u32> = const { RefCell::new(0) };
}

/// Begin a new nav: bump the generation, abort any prior in-flight fetch, and
/// return the new generation token the caller threads through its `await`s.
fn begin_nav_generation() -> u32 {
    abort_active_fetch();
    NAV_GENERATION.with(|generation| {
        let next = generation.borrow().wrapping_add(1);
        *generation.borrow_mut() = next;
        next
    })
}

/// Whether `generation` is still the current nav (no newer nav has started).
fn is_current_generation(generation: u32) -> bool {
    NAV_GENERATION.with(|current| *current.borrow() == generation)
}

/// Initialize client navigation: attach the capture-phase click interceptor and
/// the popstate listener, set manual scroll restoration, and attach the warm-tier
/// prefetch listeners. The nav manifest is fetched lazily on the first navigation,
/// not here. Idempotent — safe to call from the runtime start path on every page
/// load; only the first call wires anything up.
///
/// Called automatically by the runtime when the `nav` feature is enabled (see
/// `crate::init_nav`). Re-invocation (including after a bfcache restore) is a
/// no-op so listeners are never doubled.
pub fn init() {
    let already = INITIALIZED.with(|flag| {
        if *flag.borrow() {
            return true;
        }
        *flag.borrow_mut() = true;
        false
    });
    if already {
        return;
    }

    if let Err(error) = wire_up() {
        web_sys::console::error_2(
            &JsValue::from_str("islands nav: init failed:"),
            &error,
        );
    }
}

/// Attach all nav-global listeners and prime the manifest. Separated from [`init`]
/// so the idempotency guard stays tiny and the error handling is in one place.
fn wire_up() -> Result<(), JsValue> {
    history::set_manual_scroll_restoration()?;
    attach_click_interceptor()?;
    attach_popstate_listener()?;
    // Nav-global warm-tier singletons (hover/focus + viewport observer).
    prefetch::attach_warm_listeners()?;

    // The nav manifest is fetched lazily on the first actual navigation
    // (`resolve_entry` → `manifest::ensure_cached`), not eagerly here. An app that
    // never morph-navigates — or never wires a nav manifest — therefore issues NO
    // request for `/static/nav-manifest.json` on page load. A missing manifest is
    // treated as "nav not configured" (a 404 caches an empty manifest), so it
    // never surfaces as a failed request or a console error in DevTools.
    Ok(())
}

/// Programmatically navigate to `url` (a same-origin path), as if a link to it
/// were clicked. Runs the same contract as an intercepted click. On any failure
/// it falls back to a full document load so the user always reaches the page.
///
/// Re-exported by the bindings + umbrella crates (task #9) as the public
/// programmatic entry point.
pub fn navigate(url: &str) {
    let url_owned = url.to_owned();
    // Bump the generation (and abort any prior fetch) synchronously, before the
    // first await, so a rapid second click always supersedes the first.
    let generation = begin_nav_generation();
    spawn_local(async move {
        if let Err(error) = navigate_to(&url_owned, generation).await {
            // A superseded nav returns Ok(()) (no-op); only a real failure of
            // the current nav reaches here and falls back to a full load.
            if is_current_generation(generation) {
                web_sys::console::error_2(
                    &JsValue::from_str("islands nav: navigation failed, falling back to full load:"),
                    &error,
                );
                fall_back_to_full_load(&url_owned);
            }
        }
    });
}

/// Capture-phase `click` listener on `document`: resolve the nearest `<a>`, apply
/// the AC-V2 opt-out rules, and for an accepted click prevent the default
/// navigation and run the nav. Capture phase so we see the click before any
/// island handler can stop propagation.
fn attach_click_interceptor() -> Result<(), JsValue> {
    let document = dom::document()?;
    let handler = Closure::<dyn FnMut(web_sys::Event)>::new(move |event: web_sys::Event| {
        if let Err(error) = handle_click(&event) {
            web_sys::console::error_2(&JsValue::from_str("islands nav: click handler error:"), &error);
        }
    });
    document.add_event_listener_with_callback_and_bool(
        "click",
        handler.as_ref().unchecked_ref(),
        true,
    )?;
    // Nav-global singleton: never cleaned up (lives for the document's life).
    handler.forget();
    Ok(())
}

/// Decide whether `event` is an interceptable link click and, if so, take it
/// over. Pure decision via [`opt_out::should_intercept`]; everything else is
/// left to the browser.
fn handle_click(event: &web_sys::Event) -> Result<(), JsValue> {
    let mouse_event = match event.dyn_ref::<web_sys::MouseEvent>() {
        Some(mouse_event) => mouse_event,
        None => return Ok(()),
    };
    // Already prevented by something upstream — respect that.
    if mouse_event.default_prevented() {
        return Ok(());
    }
    let target = match event.target() {
        Some(target) => target,
        None => return Ok(()),
    };
    let anchor = match dom::closest_anchor(&target) {
        Some(anchor) => anchor,
        None => return Ok(()),
    };

    let link = dom::link_click_from(mouse_event, &anchor, dom::document_origin()?);
    if !opt_out::should_intercept(&link) {
        return Ok(());
    }

    // Accepted: take over this click.
    event.prevent_default();
    let url = anchor.href();

    // Same-href click (resolved URL identical to the current location): a morph
    // would push a duplicate history entry + redundantly fetch the page already
    // shown. Both URLs are resolved by the same parser, so an exact match is
    // reliable; preventDefault already suppressed the browser reload, so just
    // no-op. (A pure-hash same-page link never reaches here — it opted out via
    // is_pure_fragment.)
    if let Ok(current) = dom::window().and_then(|window| window.location().href()) {
        if url == current {
            return Ok(());
        }
    }

    navigate(&url);
    Ok(())
}

/// Listen for `popstate` (back/forward) and morph to the target entry, restoring
/// its stored scroll. A bfcache `pageshow` does NOT reach here, so a restore
/// never triggers a morph.
fn attach_popstate_listener() -> Result<(), JsValue> {
    let window = dom::window()?;
    let handler = Closure::<dyn FnMut(web_sys::PopStateEvent)>::new(
        move |event: web_sys::PopStateEvent| {
            handle_popstate(&event);
        },
    );
    window.add_event_listener_with_callback("popstate", handler.as_ref().unchecked_ref())?;
    handler.forget();
    Ok(())
}

/// Handle a `popstate`: morph to the current `location` and restore the scroll
/// position recorded in the target entry's state.
///
/// Only history entries we created (tagged via [`history::is_nav_state`]) are
/// morph-navigated. A foreign or `null` state — an entry not pushed by this nav
/// layer, e.g. the document's initial entry or one created by other code — is
/// NOT morphed; it falls back to a full document load so the page is always
/// correct (the defensive AC-V11 path). This keeps the morph keyed to entries
/// whose markup the nav layer is responsible for.
fn handle_popstate(event: &web_sys::PopStateEvent) {
    let state = event.state();
    let url = match dom::window().and_then(|window| window.location().href()) {
        Ok(url) => url,
        Err(_) => return,
    };

    // Foreign / null state: not one of ours — full-load rather than morph.
    if !history::is_nav_state(&state) {
        fall_back_to_full_load(&url);
        return;
    }

    // Our entry: restore the scroll position recorded when we pushed it.
    let target_scroll = history::scroll_from_state(&state);
    let generation = begin_nav_generation();
    spawn_local(async move {
        if let Err(error) = navigate_via_popstate(&url, target_scroll, generation).await {
            if is_current_generation(generation) {
                web_sys::console::error_2(
                    &JsValue::from_str("islands nav: popstate navigation failed, full load:"),
                    &error,
                );
                fall_back_to_full_load(&url);
            }
        }
    });
}

/// The core forward-nav contract (steps 1-7) for an accepted click / programmatic
/// `navigate`. A failure returns `Err` so the caller falls back to a full load.
///
/// `generation` is captured by the caller via [`begin_nav_generation`] (which
/// also aborts any prior in-flight fetch). After each `await` we re-check it: a
/// newer nav having begun makes this one resolve into a no-op (AC-V10 — the
/// morph is keyed to the latest nav, so a superseded — possibly unabortable —
/// `import()` never morphs stale HTML).
async fn navigate_to(url: &str, generation: u32) -> Result<(), JsValue> {
    // Step 1: resolve URL → manifest entry (lazy cold-miss fetch). An unresolvable
    // route (no manifest configured, or route absent) is not an error — fall back
    // to a full document load silently so the user always reaches the page.
    let entry = match resolve_entry(url).await? {
        Some(entry) => entry,
        None => {
            if is_current_generation(generation) {
                fall_back_to_full_load(url);
            }
            return Ok(());
        }
    };
    if !is_current_generation(generation) {
        return Ok(());
    }

    // Step 2: load the destination bundle and INSTANTIATE its WASM. Awaiting the
    // default export runs wasm-bindgen init() → #[wasm_bindgen(start)] →
    // register_island. A bare import() would NOT instantiate.
    load_and_instantiate_bundle(&entry.js).await?;
    if !is_current_generation(generation) {
        return Ok(());
    }

    // Step 3: obtain the destination HTML (X-Islands-Nav) — consuming a warm
    // speculation if one is cached, else fetching fresh.
    let html = obtain_destination_html(url).await?;
    if !is_current_generation(generation) {
        return Ok(());
    }

    // Step 7a (before the morph): capture outgoing scroll + pushState.
    history::capture_scroll_and_push(url)?;

    // Steps 4-6: morph + Suspense activation + the single mount, wrapped in a
    // view transition when one is available + motion is allowed (else plain).
    view_transition::run_morph_with_optional_transition(move || perform_morph_and_mount(&html))
        .await?;

    // Steps 7b + 7c (after the morph paints): forward nav resets scroll to top,
    // then focus <main>.
    history::restore_scroll_next_frame(ScrollPosition::default())?;
    history::focus_main()?;

    clear_active_controller();
    Ok(())
}

/// The popstate variant: same steps 1-6, but scroll is RESTORED to `target` (not
/// reset) and there is no pushState (the browser already moved the entry).
async fn navigate_via_popstate(
    url: &str,
    target: ScrollPosition,
    generation: u32,
) -> Result<(), JsValue> {
    let entry = match resolve_entry(url).await? {
        Some(entry) => entry,
        None => {
            if is_current_generation(generation) {
                fall_back_to_full_load(url);
            }
            return Ok(());
        }
    };
    if !is_current_generation(generation) {
        return Ok(());
    }
    load_and_instantiate_bundle(&entry.js).await?;
    if !is_current_generation(generation) {
        return Ok(());
    }
    let html = obtain_destination_html(url).await?;
    if !is_current_generation(generation) {
        return Ok(());
    }

    // popstate morphs are VT-wrapped under the identical guards as forward nav.
    view_transition::run_morph_with_optional_transition(move || perform_morph_and_mount(&html))
        .await?;

    // Back/forward restores the prior scroll position; no focus reset (the user
    // is returning to a place they have seen).
    history::restore_scroll_next_frame(target)?;
    clear_active_controller();
    Ok(())
}

/// Contract steps 4-6 in one place: parse the fetched HTML, morph the live
/// document into it (island lifecycle via the bound callbacks), activate any
/// streamed-Suspense leftovers, then `mount_all()` exactly once.
///
/// Isolated as the unit a View Transition (Phase 3g) will wrap — it is fully
/// synchronous (morph and mount are sync), so wrapping it in a synchronous
/// `startViewTransition` callback captures island first-paint into the snapshot.
fn perform_morph_and_mount(html: &str) -> Result<(), JsValue> {
    let new_document = parse_html_document(html)?;
    let new_root = new_document
        .document_element()
        .ok_or_else(|| JsValue::from_str("fetched document has no documentElement"))?;
    let live_root = dom::document()?
        .document_element()
        .ok_or_else(|| JsValue::from_str("live document has no documentElement"))?;

    // Step 4: morph (HeadStyle::Merge; island subtrees skipped; departing
    // islands unmounted; new-doc markers stripped of client state).
    islands_morph::morph(
        live_root.as_ref(),
        new_root.as_ref(),
        lifecycle::nav_morph_options(),
    )?;

    // Steps 5 + 6: activate Suspense slots (self-gating), then the single mount.
    lifecycle::activate_suspense_then_mount()
}

/// Resolve `url`'s pathname to a manifest entry. The manifest is fetched lazily on
/// first use (cold-miss blocking fetch, contract step 1).
///
/// Returns `Ok(None)` when the route is not resolvable — either no manifest is
/// configured (a 404 caches an empty manifest) or the route is simply absent. The
/// caller treats `None` as "not a morph-nav route" and falls back to a full
/// document load silently, rather than logging a non-nav route as an error.
async fn resolve_entry(url: &str) -> Result<Option<manifest::NavEntry>, JsValue> {
    let pathname = pathname_of(url)?;
    manifest::ensure_cached().await?;
    Ok(manifest::resolved_entry(&pathname))
}

/// Contract step 2: `const m = await import(jsUrl); await m.default();`.
///
/// Awaiting the default export runs the page bundle's wasm-bindgen `init()`,
/// which instantiates the page WASM and runs its `#[wasm_bindgen(start)]`
/// (registering the island). A bare `import()` returns the module exports but
/// does NOT instantiate the WASM, so the await is load-bearing. On revisit the
/// module is import-cached and `start()` does not re-run; the island stays
/// registered.
async fn load_and_instantiate_bundle(js_url: &str) -> Result<(), JsValue> {
    let module = JsFuture::from(dynamic_import(js_url)).await?;
    let default_export = js_sys::Reflect::get(&module, &JsValue::from_str("default"))?;
    let default_function = default_export
        .dyn_ref::<js_sys::Function>()
        .ok_or_else(|| JsValue::from_str("page bundle has no default export function"))?;
    let init_result = default_function.call0(&module)?;
    // wasm-bindgen's default export returns a Promise; await it to finish
    // instantiation. If it is not a Promise (already-instantiated edge), the
    // await of a non-Promise resolves immediately.
    if let Ok(promise) = init_result.dyn_into::<js_sys::Promise>() {
        JsFuture::from(promise).await?;
    }
    Ok(())
}

/// Obtain the destination HTML, consuming a warm-tier speculation when one is
/// cached (Phase 3f commit) and otherwise fetching fresh.
///
/// The consume is **synchronous** and happens before any `await`: it removes the
/// speculation from the cache and adopts its `AbortController` as the active
/// controller in one run-to-completion step, so the TTL evictor and the
/// active-nav abort can never observe a half-consumed entry (AC-V10 abort
/// scope). The promoted fetch is then owned by the active nav, not the cache.
async fn obtain_destination_html(url: &str) -> Result<String, JsValue> {
    // Synchronous consume + controller adoption (no await in this block).
    let parked_body = prefetch::consume_speculation(url).map(|speculation| {
        let body_text_promise = speculation.body_text_promise();
        // The active nav now owns the fetch's lifetime; install its controller
        // as the active one (begin_nav_generation already aborted any prior).
        set_active_controller(speculation.into_controller());
        body_text_promise
    });

    if let Some(body_text_promise) = parked_body {
        // Commit hit: await the already-in-flight body — no duplicate fetch.
        let text_value = JsFuture::from(body_text_promise).await?;
        return text_value
            .as_string()
            .ok_or_else(|| JsValue::from_str("prefetched body was not a string"));
    }

    // Miss: fresh fetch under a new active controller.
    let controller = install_active_controller()?;
    fetch_destination_html(url, &controller).await
}

/// Fetch the destination HTML with the `X-Islands-Nav` header so the server can
/// render streaming routes inline (AC-V13). Uses `controller`'s signal so a
/// superseding nav can abort this fetch (AC-V10). A non-2xx response is an error
/// (→ full-load fallback, AC-V11).
async fn fetch_destination_html(
    url: &str,
    controller: &web_sys::AbortController,
) -> Result<String, JsValue> {
    let headers = web_sys::Headers::new()?;
    headers.set(manifest::NAV_HEADER_NAME, manifest::NAV_HEADER_VALUE)?;

    let options = web_sys::RequestInit::new();
    options.set_headers(&headers);
    options.set_signal(Some(&controller.signal()));

    let request = web_sys::Request::new_with_str_and_init(url, &options)?;
    let window = dom::window()?;
    let response_value = JsFuture::from(window.fetch_with_request(&request)).await?;
    let response: web_sys::Response = response_value.dyn_into()?;
    if !response.ok() {
        return Err(JsValue::from_str(&format!(
            "destination fetch failed: HTTP {}",
            response.status()
        )));
    }
    let text_value = JsFuture::from(response.text()?).await?;
    text_value
        .as_string()
        .ok_or_else(|| JsValue::from_str("destination body was not a string"))
}

/// Parse fetched HTML into a `Document` via `DOMParser` (contract step 3).
fn parse_html_document(html: &str) -> Result<web_sys::Document, JsValue> {
    let parser = web_sys::DomParser::new()?;
    parser.parse_from_string(html, web_sys::SupportedType::TextHtml)
}

/// Install a fresh `AbortController` as the active one for this nav's fetch.
///
/// Any prior in-flight fetch was already aborted by [`begin_nav_generation`] at
/// the start of this nav (AC-V10: the latest nav wins), so this only stores the
/// new controller. Returns it for the fetch's signal.
fn install_active_controller() -> Result<web_sys::AbortController, JsValue> {
    let controller = web_sys::AbortController::new()?;
    set_active_controller(controller.clone());
    Ok(controller)
}

/// Adopt an existing `AbortController` (e.g. one promoted out of the speculation
/// cache on commit) as the active controller, so subsequent active-nav aborts
/// and the completion clear target it.
fn set_active_controller(controller: web_sys::AbortController) {
    ACTIVE_FETCH_CONTROLLER.with(|slot| *slot.borrow_mut() = Some(controller));
}

/// Abort the in-flight nav fetch, if one is outstanding.
fn abort_active_fetch() {
    ACTIVE_FETCH_CONTROLLER.with(|slot| {
        if let Some(controller) = slot.borrow_mut().take() {
            controller.abort();
        }
    });
}

/// Clear the active controller slot once a nav has completed without being
/// superseded (so a later abort does not target a finished fetch).
fn clear_active_controller() {
    ACTIVE_FETCH_CONTROLLER.with(|slot| *slot.borrow_mut() = None);
}

/// AC-V11 fallback: a full document load to `url`. The console.error is logged
/// by the caller BEFORE this runs, per the contract's observability note.
fn fall_back_to_full_load(url: &str) {
    if let Ok(window) = dom::window() {
        let _ = window.location().assign(url);
    }
}

/// Extract the pathname from a (possibly absolute) URL string using the `URL`
/// API against the current document base.
fn pathname_of(url: &str) -> Result<String, JsValue> {
    let base = dom::window()?.location().href()?;
    let parsed = web_sys::Url::new_with_base(url, &base)?;
    Ok(parsed.pathname())
}

/// `import(specifier)` as a `Promise`, reached through a tiny JS glue extern
/// because dynamic `import` is a syntactic form with no `web_sys` binding. The
/// specifier is a manifest URL (already content-hashed in `--release`); it is
/// passed verbatim and never assembled from basenames, so xtask's `--release`
/// rewrite has nothing to catch here.
fn dynamic_import(specifier: &str) -> js_sys::Promise {
    dynamic_import_glue(specifier)
}

#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
export function __islands_dynamic_import(specifier) { return import(specifier); }
"#)]
extern "C" {
    #[wasm_bindgen(js_name = __islands_dynamic_import)]
    fn dynamic_import_glue(specifier: &str) -> js_sys::Promise;
}