Skip to content

feat(preview): native-module & multi-page (MPA) preview extension points - #62

Open
Huxpro wants to merge 2 commits into
mainfrom
claude/go-web-native-mpa-1lwopq
Open

feat(preview): native-module & multi-page (MPA) preview extension points#62
Huxpro wants to merge 2 commits into
mainfrom
claude/go-web-native-mpa-1lwopq

Conversation

@Huxpro

@Huxpro Huxpro commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

The web preview renders a single <lynx-view> (components/web-iframe.tsx) with no way to register a native-modules bridge, inject per-card globalProps, or stack cards. So any example whose bundle calls a native module fails at runtime (Native module … is not registered), and multi-page (MPA) examples can't be demonstrated — cross-page navigation has no second card to go to.

This PR adds two opt-in, product-agnostic extension points. go-web hard-codes no framework module names or URL scheme; it only provides generic hooks. When unset, the preview is byte-for-byte identical to today.

web-core API confirmed (pinned @lynx-js/web-core@0.20.3)

<lynx-view> (LynxViewElement) exposes: onNativeModulesCall (setter; runtime caches calls made before assignment), nativeModulesMap (module-name → ESM url, read at worker init), napiModulesMap / onNapiModulesCall, and globalProps / initData. Crucially, #render() reads nativeModulesMap/globalProps/initData asynchronously inside a queueMicrotask after await-ing the iframe realm — so assigning them from the element ref (exactly the path the existing browserConfig init already uses) lands before web-core consumes them. Types are derived from LynxViewElement so they stay correct across web-core versions.

Level A — native environment (required)

New GoConfig.previewNativeEnv and per-instance nativeEnv prop (shallow-merged, per-instance wins) forward a generic env to the previewed view:

Field Purpose
onNativeModulesCall receive NativeModule calls (late assignment safe — web-core caches)
nativeModulesMap module-name → ESM url definition (applied before start)
napiModulesMap / onNapiModulesCall napi equivalents (advanced)
globalProps / initData Cloneable or (entryName) => Cloneable factory

Level B — pluggable preview runtime

New GoConfig.PreviewRuntime replaces just the inner card renderer — go-web keeps owning the tab bar, QR, code browser, fit/scaling, and SSG path. The component receives every previewable entry (with absolute web URLs) plus the resolved native env.

A/B design tradeoff (evaluated both, recommending B1)

One hook covers both MPA shapes:

  • B1 — in-process card stack (✅ recommended default). Keep a stack of <lynx-view> cards in React state; push on navigate, pop on back. Lower cards stay mounted (stable React key) so their heap and state survive the round-trip. No cross-origin/postMessage handshake, type-safe composition, reuses go-web scaling.
  • B2 — iframe runtime (escape hatch). A PreviewRuntime that renders <iframe src={runtimeUrl}> and passes entries via query/postMessage. An iframe is a real nested browsing context, so window.history/navigation are naturally scoped to it — ideal for an embedder that already has a full-page "web shell". B2 is simply one implementation of the same PreviewRuntime hook, so go-web ships a single generic slot rather than two APIs.

Recommendation: B1 as the built-in default (simpler, in-process, satisfies the acceptance test via an in-memory stack); B2 documented as the escape hatch for embedders with an existing full-page shell.

Demo + tests

  • example/src/mpa/ — a runnable B1 prototype (StackedPreviewRuntime.tsx + framework-agnostic card-stack.ts + demo-side nav-intent.ts) and demo-native-env.ts (Level A: onNativeModulesCall + nativeModulesMap + a globalProps factory). Wired into the example app behind an opt-in Preview toggle (Default / Native / MPA) — default is unchanged.
  • Vitest (new) with 19 tests:
    • Level A — resolveCloneableInput, mergePreviewNativeEnv, and applyPreviewNativeEnv against a faithful <lynx-view> fake: worker-init values set before start, idempotency, and a native call (incl. one cached before handler assignment) delivered to the embedder's handler.
    • Level B — cardStackReducer + resolveDemoNavIntent: opening a second card and back returns to the first with its state intact (root card identity preserved).

Compatibility & checks

  • Backwards compatible; opt-in; default preview identical to today (verified: nativeEnv unset ⇒ no-op; PreviewRuntime unset ⇒ built-in WebIframe). SSG path preserved.
  • Local: pnpm format:check ✅ · pnpm typecheck ✅ · pnpm test ✅ (19) · example tsc --noEmit ✅ · pnpm build ✅. Added a Unit Test CI job.

Public API additions

GoConfig.previewNativeEnv, GoConfig.PreviewRuntime, GoProps.nativeEnv; exported PreviewNativeEnv, CloneableInput, Cloneable, NativeModulesCall/Map, NapiModulesCall/Map, PreviewRuntimeComponent/Props/Entry, and helpers resolveCloneableInput / mergePreviewNativeEnv / applyPreviewNativeEnv.

🤖 Generated with Claude Code


Generated by Claude Code

The web preview rendered one `<lynx-view>` with no way to register a
native-modules bridge, inject per-card globalProps, or stack cards, so
examples that call native modules failed and multi-page (MPA) examples
could not be demonstrated. Add two opt-in, product-agnostic hooks.

Level A — native environment. `GoConfig.previewNativeEnv` and the
per-instance `nativeEnv` prop forward a generic environment
(`onNativeModulesCall`, `nativeModulesMap`, `napiModulesMap`,
`onNapiModulesCall`, and static-or-factory `globalProps`/`initData`) to
the previewed view. Values web-core reads at worker init are assigned
from the element ref (the same path `browserConfig` already uses), which
lands before web-core's async start reads them.

Level B — pluggable preview runtime. `GoConfig.PreviewRuntime` replaces
just the inner card renderer (go-web keeps owning tabs/QR/code-browser/
scaling/SSG), receiving every previewable entry plus the resolved native
environment. One hook covers both MPA shapes: an in-process card stack
(recommended) or an embedder iframe runtime. A runnable B1 prototype
lives in example/src/mpa/ with a demo native env, wired behind an opt-in
Preview toggle.

Both are backwards compatible: when unset, the preview is identical to
before, and go-web hard-codes no framework module names or URL scheme.
Adds Vitest with tests for the native-env wiring and the card-stack
navigation, plus a CI test job and README docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnnSHyr2e9qXzWHZSCSMVx
Copilot AI review requested due to automatic review settings July 13, 2026 13:42
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 67dce58

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@lynx-js/go-web Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploy Preview for lynx-go-web ready!

Name Link
🔨 Latest commit 67dce58
🔍 Latest deploy log https://app.netlify.com/projects/lynx-go-web/deploys/6a54fed980e04b0008824960
😎 Deploy Preview https://deploy-preview-62--lynx-go-web.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in extension points to the go-web live preview so embedders can (A) provide a native-module environment to <lynx-view> and (B) swap the single-card web preview renderer for a pluggable runtime that can support multi-page (MPA) flows (e.g., stacked cards). This is paired with a demo implementation and new unit tests to validate the core logic paths.

Changes:

  • Introduces Level A native-env plumbing (previewNativeEnv + per-instance nativeEnv) and helper utilities to apply/merge/resolve cloneable inputs.
  • Introduces Level B PreviewRuntime slot and corresponding entry/prop types to enable custom (e.g., stacked) preview renderers.
  • Adds Vitest + unit tests, updates CI to run unit tests, and adds an example MPA runtime + demo native env.

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
vitest.config.ts Adds Vitest configuration for pure-logic node-environment tests.
test/preview-native-env.test.ts Unit tests for resolving/merging/applying the preview native environment.
test/mpa-card-stack.test.ts Unit tests for card stack reducer and demo nav-intent mapping.
src/index.ts Exports new public API types/helpers for Level A and Level B.
src/example-preview/preview-runtime.ts Defines the Level B PreviewRuntime component contract and entry model.
src/example-preview/preview-native-env.ts Implements Level A types and helper functions (resolveCloneableInput, merge/apply).
src/example-preview/index.tsx Wires config + per-instance native env merging and builds entries for custom runtimes.
src/example-preview/components/web-iframe.tsx Applies Level A native env to <lynx-view> from the ref before worker init.
src/example-preview/components/index.tsx Switches between built-in WebIframe and custom PreviewRuntime rendering.
src/config.tsx Adds GoConfig.previewNativeEnv and GoConfig.PreviewRuntime extension points.
README.md Documents Level A/Level B usage and updates CI section to include unit tests.
package.json Adds pnpm test script and Vitest dev dependency.
pnpm-lock.yaml Locks Vitest + transitive dependencies.
example/src/mpa/StackedPreviewRuntime.tsx Demo B1 stacked-card preview runtime implementation.
example/src/mpa/nav-intent.ts Demo-only mapping from native calls to navigation intents.
example/src/mpa/card-stack.ts Pure reducer/state model for the stacked-card runtime.
example/src/main.tsx Adds example-app toggle to demo Default/Native/MPA preview modes.
example/src/demo-native-env.ts Demo Level A native env (nativeModulesMap, handler, globalProps factory).
.github/workflows/workflow-test.yml Adds a Unit Test job and gates builds on it.
.changeset/native-mpa-preview.md Changeset entry for the new minor feature/API additions.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +119 to +122
/**
* Imperatively apply a resolved native env to a `<lynx-view>` element for a
* given entry. Idempotent per element+env via the provided `applied` guard set.
*
Comment on lines +61 to +70
const handleRef = useCallback(
(el: LynxViewElement | null) => {
if (!el) return;
// Apply the resolved native env before the view starts (see
// applyPreviewNativeEnv for the ordering guarantee), tagging each card
// with its own id via globalProps so stacked containers stay distinct.
applyPreviewNativeEnv(el, nativeEnv, card.entryName, appliedRef.current);
},
[card.entryName, nativeEnv],
);
The Vitest suite validates go-web's wiring against a faithful <lynx-view>
fake. Add an opt-in Playwright harness (`pnpm test:browser`) that drives
the built example app in headless Chromium against the REAL @lynx-js/web-core
runtime and asserts:

- default preview still boots exactly one <lynx-view> (no regression);
- Level A reaches the real element (nativeModulesMap / onNativeModulesCall /
  globalProps) and it boots;
- Level B pushes a second real <lynx-view> on a native `open` call and Back
  returns to the original root element (same DOM node => state intact).

Kept out of CI (needs the example built + a Chromium binary). Documents the
E2E gap: literal acceptance of a native-calling example needs a fixture
bundle, which this harness slots into. README gains a Testing section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnnSHyr2e9qXzWHZSCSMVx
Copilot AI review requested due to automatic review settings July 13, 2026 15:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file


/**
* Imperatively apply a resolved native env to a `<lynx-view>` element for a
* given entry. Idempotent per element+env via the provided `applied` guard set.
Comment on lines +136 to +160
if (!env) return;
if (applied.has(el)) return;
applied.add(el);

// Worker-init values first, before any late handler assignment.
if (env.nativeModulesMap !== undefined) {
el.nativeModulesMap = env.nativeModulesMap;
}
if (env.napiModulesMap !== undefined) {
el.napiModulesMap = env.napiModulesMap;
}
const globalProps = resolveCloneableInput(env.globalProps, entryName);
if (globalProps !== undefined) {
el.globalProps = globalProps;
}
const initData = resolveCloneableInput(env.initData, entryName);
if (initData !== undefined) {
el.initData = initData;
}
if (env.onNativeModulesCall !== undefined) {
el.onNativeModulesCall = env.onNativeModulesCall;
}
if (env.onNapiModulesCall !== undefined) {
el.onNapiModulesCall = env.onNapiModulesCall;
}
Comment on lines 580 to +584
const handleLynxViewRef = useCallback((el: LynxView | null) => {
setLynxView(el);
if (!el) return;
// Apply Level-A native env before browserConfig. Both are consumed by
// web-core inside its async `#render()` microtask (after the iframe realm
Comment on lines +64 to +66
// Apply the resolved native env before the view starts (see
// applyPreviewNativeEnv for the ordering guarantee), tagging each card
// with its own id via globalProps so stacked containers stay distinct.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants