Skip to content

Mobile: Load plugin scripts directly from filesystem instead of passing through bridge#15095

Open
laurent22 wants to merge 3 commits intodevfrom
plugin_load_from_fs
Open

Mobile: Load plugin scripts directly from filesystem instead of passing through bridge#15095
laurent22 wants to merge 3 commits intodevfrom
plugin_load_from_fs

Conversation

@laurent22
Copy link
Copy Markdown
Owner

@laurent22 laurent22 commented Apr 14, 2026

Partially addresses #12301

Tested on desktop, Android and iOS

Summary

  • On native mobile, plugin scripts are now loaded directly from the filesystem by the WebView, instead of being read into JS memory and then passed across the React Native bridge as a string
  • On mobile, PluginService.loadPluginFromPath skips reading index.js entirely since it's no longer needed on the React Native side
  • The web platform is unaffected — it continues to pass the script text directly

Motivation

Plugin script bundles can be hundreds of KB. Previously, each enabled plugin's full JS bundle crossed the React Native bridge twice on every startup: once when read from disk (readFile), and again when injected into the WebView (injectJS). This contributed to a 2-3 second freeze on app startup.

Now the script content never enters the React Native JS thread at all — the WebView fetches it directly from disk via file:// URL.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 60edf0b3-824d-4413-a86e-a557e929f73c

📥 Commits

Reviewing files that changed from the base of the PR and between 20b5e02 and 04389e6.

📒 Files selected for processing (1)
  • packages/lib/services/plugins/PluginService.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lib/services/plugins/PluginService.ts

📝 Walkthrough

Walkthrough

Plugin script loading on mobile was changed to prefer passing a script file path and optionally the original script text. The background runner now asynchronously loads script text from file:// when no inline scriptText is provided before injecting it into the iframe/WebView.

Changes

Cohort / File(s) Summary
Runner & WebView
packages/app-mobile/components/plugins/PluginRunner.ts, packages/app-mobile/components/plugins/PluginRunnerWebView.tsx
PluginRunner.run now computes and passes a scriptFilePath (and also forwards plugin.scriptText); the WebView is allowed file access via allowFileAccessFromJs={true}.
Background page script loader
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.ts
runPlugin changed to async and now accepts scriptFilePath and optional scriptText. If scriptText is empty it fetches the script via XMLHttpRequest from file://${scriptFilePath} before injecting.
Plugin service path handling
packages/lib/services/plugins/PluginService.ts
loadPluginFromPath() delegates mobile platforms to pass empty scriptText (and checks for bundle existence) instead of reading index.js; non-mobile still reads the bundle contents and supplies scriptText.

Sequence Diagram(s)

sequenceDiagram
    participant PluginService
    participant PluginRunner
    participant WebView
    participant startStopPlugin as startStopPlugin.runPlugin()
    participant FileSystem as File System

    PluginService->>PluginRunner: loadPluginFromPath() / run(plugin)
    PluginRunner->>WebView: runPlugin(scriptFilePath, scriptText)
    WebView->>startStopPlugin: runPlugin(scriptFilePath, scriptText)
    alt scriptText provided
        startStopPlugin->>startStopPlugin: use scriptText directly
    else scriptText empty
        startStopPlugin->>FileSystem: GET file://{scriptFilePath} (XMLHttpRequest)
        FileSystem-->>startStopPlugin: respond with script content
        startStopPlugin->>startStopPlugin: use loaded script
    end
    startStopPlugin->>startStopPlugin: create iframe and inject script
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Pr Description Must Follow Guidelines ❌ Error The pull request description does not follow the required structure with explicit Problem, Solution, and Test Plan sections. Restructure the PR description with clearly-marked headers (## Problem, ## Solution, ## Test Plan) to organise the relevant information for easier review.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: loading plugin scripts directly from the filesystem on mobile instead of passing them through the React Native bridge.
Description check ✅ Passed The description is directly related to the changeset, providing clear motivation, implementation details, and testing information that all align with the file modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch plugin_load_from_fs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@laurent22 laurent22 added the v3.7 label Apr 14, 2026
@laurent22 laurent22 marked this pull request as ready for review April 14, 2026 11:55
@coderabbitai coderabbitai Bot added enhancement Feature requests and code enhancements mobile All mobile platforms plugins Anything related to Joplin's plugin system performance Performance issues labels Apr 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.ts`:
- Around line 29-50: Reserve a pending entry in loadedPlugins keyed by pluginId
before performing the async file load in runPlugin so concurrent stopPlugin or
duplicate runPlugin calls can detect and cancel a startup-in-progress;
specifically, when entering runPlugin (before awaiting the
XMLHttpRequest/promise that sets pluginScript) set loadedPlugins[pluginId] = {
status: 'starting', cancelled: false } (or another small sentinel object), then
after the XHR resolves check that loadedPlugins[pluginId]?.cancelled !== true
before continuing to fully initialize (and if cancelled, abort startup and
cleanup the entry); ensure stopPlugin checks for this sentinel and sets
cancelled = true (and removes the entry if necessary), and on any XHR error
remove the pending loadedPlugins entry to avoid leaking the slot.

In `@packages/app-mobile/components/plugins/PluginRunner.ts`:
- Around line 49-60: The injected call to pluginBackgroundPage.runPlugin is
fire-and-forget, so if loading the plugin script fails the WebView rejection is
unhandled and the host (PluginRunner.run) already marks plugin.running = true;
fix by adding a .catch in the injected JS around
pluginBackgroundPage.runPlugin(...) that posts an error back to the host
(include messageChannelId and plugin.id) via the WebView bridge (e.g.,
window.ReactNativeWebView.postMessage) and ensure PluginRunner.run listens for
that error message on the messageChannelId and clears plugin.running (or only
sets plugin.running = true after receiving a success/ready message). Update the
injected payload in PluginRunner (webviewRef.current.injectJS(...)) to include
this catch-and-report logic and update PluginRunner.run's message handling to
unmark running on error or mark running only on explicit success.

In `@packages/lib/services/plugins/PluginService.ts`:
- Around line 354-356: The upfront existence/readability check for the plugin
bundle was removed—restore a lightweight validation so mobile still verifies
`${distPath}/index.js` before proceeding: in the loadPluginFromPath() flow
(where shim.mobilePlatform() is used and fsDriver.readFile was called), add a
cheap fsDriver.exists or fsDriver.stat check for `${distPath}/index.js` when
shim.mobilePlatform() returns true and throw the same validation error used on
desktop if the file is missing/unreadable so installPlugin() and registration
keep the original failure semantics without reading the full file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9955bd50-81c1-40cc-bec1-9e422ff8df9f

📥 Commits

Reviewing files that changed from the base of the PR and between 222bb00 and 20b5e02.

📒 Files selected for processing (4)
  • packages/app-mobile/components/plugins/PluginRunner.ts
  • packages/app-mobile/components/plugins/PluginRunnerWebView.tsx
  • packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.ts
  • packages/lib/services/plugins/PluginService.ts

Comment thread packages/app-mobile/components/plugins/PluginRunner.ts
Comment thread packages/lib/services/plugins/PluginService.ts Outdated
@coderabbitai coderabbitai Bot removed enhancement Feature requests and code enhancements mobile All mobile platforms plugins Anything related to Joplin's plugin system performance Performance issues labels Apr 14, 2026
Comment on lines +51 to +52
// script text across the React Native bridge). On web, file:// URLs
// are blocked by CSP so we pass the script text directly.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

are blocked by CSP

Note: Rather than due to the CSP, this doesn't work on web because it uses a virtual file system to store plugins (because of browser sandboxing). Most files need to be read through fs-driver on web, rather than through native APIs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants