feat: dynamic ESM import in preload without context isolation (#48489)
Dynamic ESM import in non-context-isolated preload Extend `HostImportModuleWithPhaseDynamically`'s routing to support Node.js import resolution in non-context-isolated preloads through `v8_host_defined_options` length check. The length of host defined options is distinct between Blink and Node.js and we can use it to determine which resolver to use. Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Fedor Indutny <indutny@signal.org>
This commit is contained in:
parent
4a2f733d0a
commit
d0db2ec333
5 changed files with 148 additions and 35 deletions
|
|
@ -32,7 +32,7 @@ This table gives a general overview of where ESM is supported and which ESM load
|
||||||
| Main | Node.js | N/A | <ul><li> [You must use `await` generously before the app's `ready` event](#you-must-use-await-generously-before-the-apps-ready-event) </li></ul> |
|
| Main | Node.js | N/A | <ul><li> [You must use `await` generously before the app's `ready` event](#you-must-use-await-generously-before-the-apps-ready-event) </li></ul> |
|
||||||
| Renderer (Sandboxed) | Chromium | Unsupported | <ul><li> [Sandboxed preload scripts can't use ESM imports](#sandboxed-preload-scripts-cant-use-esm-imports) </li></ul> |
|
| Renderer (Sandboxed) | Chromium | Unsupported | <ul><li> [Sandboxed preload scripts can't use ESM imports](#sandboxed-preload-scripts-cant-use-esm-imports) </li></ul> |
|
||||||
| Renderer (Unsandboxed & Context Isolated) | Chromium | Node.js | <ul><li> [Unsandboxed ESM preload scripts will run after page load on pages with no content](#unsandboxed-esm-preload-scripts-will-run-after-page-load-on-pages-with-no-content) </li> <li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li></ul> |
|
| Renderer (Unsandboxed & Context Isolated) | Chromium | Node.js | <ul><li> [Unsandboxed ESM preload scripts will run after page load on pages with no content](#unsandboxed-esm-preload-scripts-will-run-after-page-load-on-pages-with-no-content) </li> <li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li></ul> |
|
||||||
| Renderer (Unsandboxed & Non Context Isolated) | Chromium | Node.js | <ul><li>[Unsandboxed ESM preload scripts will run after page load on pages with no content](#unsandboxed-esm-preload-scripts-will-run-after-page-load-on-pages-with-no-content)</li><li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li><li>[ESM preload scripts must be context isolated to use dynamic Node.js ESM imports](#esm-preload-scripts-must-be-context-isolated-to-use-dynamic-nodejs-esm-imports)</li></ul> |
|
| Renderer (Unsandboxed & Non Context Isolated) | Chromium | Node.js | <ul><li>[Unsandboxed ESM preload scripts will run after page load on pages with no content](#unsandboxed-esm-preload-scripts-will-run-after-page-load-on-pages-with-no-content)</li><li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li></ul> |
|
||||||
|
|
||||||
## Main process
|
## Main process
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,3 +137,4 @@ fix_add_macos_memory_query_fallback_to_avoid_crash.patch
|
||||||
fix_resolve_dynamic_background_material_update_issue_on_windows_11.patch
|
fix_resolve_dynamic_background_material_update_issue_on_windows_11.patch
|
||||||
feat_add_support_for_embedder_snapshot_validation.patch
|
feat_add_support_for_embedder_snapshot_validation.patch
|
||||||
band-aid_over_an_issue_with_using_deprecated_nsopenpanel_api.patch
|
band-aid_over_an_issue_with_using_deprecated_nsopenpanel_api.patch
|
||||||
|
expose_referrerscriptinfo_hostdefinedoptionsindex.patch
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Fedor Indutny <indutny@signal.org>
|
||||||
|
Date: Wed, 24 Sep 2025 10:08:48 -0700
|
||||||
|
Subject: Expose ReferrerScriptInfo::HostDefinedOptionsIndex
|
||||||
|
|
||||||
|
In `shell/common/node_bindings.cc`'s
|
||||||
|
`HostImportModuleWithPhaseDynamically` we route dynamic imports to
|
||||||
|
either Node.js's or Blink's resolver based on presence of Node.js
|
||||||
|
environment, process type, etc. Exporting `HostDefinedOptionsIndex`
|
||||||
|
allows us to route based on the size of `v8_host_defined_options` data
|
||||||
|
which enables us to support dynamic imports in non-context-isolated
|
||||||
|
preload scripts.
|
||||||
|
|
||||||
|
diff --git a/third_party/blink/renderer/bindings/core/v8/referrer_script_info.cc b/third_party/blink/renderer/bindings/core/v8/referrer_script_info.cc
|
||||||
|
index 1b797783987255622735047bd78ca0e8bb635d5e..b209c736bb80c186ed51999af1dac0a1d50fc232 100644
|
||||||
|
--- a/third_party/blink/renderer/bindings/core/v8/referrer_script_info.cc
|
||||||
|
+++ b/third_party/blink/renderer/bindings/core/v8/referrer_script_info.cc
|
||||||
|
@@ -12,15 +12,6 @@ namespace blink {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
-enum HostDefinedOptionsIndex : size_t {
|
||||||
|
- kBaseURL,
|
||||||
|
- kCredentialsMode,
|
||||||
|
- kNonce,
|
||||||
|
- kParserState,
|
||||||
|
- kReferrerPolicy,
|
||||||
|
- kLength
|
||||||
|
-};
|
||||||
|
-
|
||||||
|
// Omit storing base URL if it is same as ScriptOrigin::ResourceName().
|
||||||
|
// Note: This improves chance of getting into a fast path in
|
||||||
|
// ReferrerScriptInfo::ToV8HostDefinedOptions.
|
||||||
|
diff --git a/third_party/blink/renderer/bindings/core/v8/referrer_script_info.h b/third_party/blink/renderer/bindings/core/v8/referrer_script_info.h
|
||||||
|
index 0119624a028bec3e53e4e402938a98fe6def1483..743865839448748fe00e3e7d5027587cb65393c9 100644
|
||||||
|
--- a/third_party/blink/renderer/bindings/core/v8/referrer_script_info.h
|
||||||
|
+++ b/third_party/blink/renderer/bindings/core/v8/referrer_script_info.h
|
||||||
|
@@ -23,6 +23,15 @@ class CORE_EXPORT ReferrerScriptInfo {
|
||||||
|
STACK_ALLOCATED();
|
||||||
|
|
||||||
|
public:
|
||||||
|
+ enum HostDefinedOptionsIndex : size_t {
|
||||||
|
+ kBaseURL,
|
||||||
|
+ kCredentialsMode,
|
||||||
|
+ kNonce,
|
||||||
|
+ kParserState,
|
||||||
|
+ kReferrerPolicy,
|
||||||
|
+ kLength
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
ReferrerScriptInfo() {}
|
||||||
|
ReferrerScriptInfo(const KURL& base_url,
|
||||||
|
network::mojom::CredentialsMode credentials_mode,
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
#include "base/trace_event/trace_event.h"
|
#include "base/trace_event/trace_event.h"
|
||||||
#include "chrome/common/chrome_version.h"
|
#include "chrome/common/chrome_version.h"
|
||||||
#include "content/public/common/content_paths.h"
|
#include "content/public/common/content_paths.h"
|
||||||
|
#include "content/public/renderer/render_frame.h"
|
||||||
#include "electron/buildflags/buildflags.h"
|
#include "electron/buildflags/buildflags.h"
|
||||||
#include "electron/electron_version.h"
|
#include "electron/electron_version.h"
|
||||||
#include "electron/fuses.h"
|
#include "electron/fuses.h"
|
||||||
|
|
@ -41,7 +42,9 @@
|
||||||
#include "shell/common/node_util.h"
|
#include "shell/common/node_util.h"
|
||||||
#include "shell/common/process_util.h"
|
#include "shell/common/process_util.h"
|
||||||
#include "shell/common/world_ids.h"
|
#include "shell/common/world_ids.h"
|
||||||
|
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
|
||||||
#include "third_party/blink/public/web/web_local_frame.h"
|
#include "third_party/blink/public/web/web_local_frame.h"
|
||||||
|
#include "third_party/blink/renderer/bindings/core/v8/referrer_script_info.h" // nogncheck
|
||||||
#include "third_party/blink/renderer/bindings/core/v8/v8_initializer.h" // nogncheck
|
#include "third_party/blink/renderer/bindings/core/v8/v8_initializer.h" // nogncheck
|
||||||
#include "third_party/electron_node/src/debug_utils.h"
|
#include "third_party/electron_node/src/debug_utils.h"
|
||||||
#include "third_party/electron_node/src/module_wrap.h"
|
#include "third_party/electron_node/src/module_wrap.h"
|
||||||
|
|
@ -211,6 +214,61 @@ bool AllowWasmCodeGenerationCallback(v8::Local<v8::Context> context,
|
||||||
return node::AllowWasmCodeGenerationCallback(context, source);
|
return node::AllowWasmCodeGenerationCallback(context, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ESMHandlerPlatform {
|
||||||
|
kNone,
|
||||||
|
kNodeJS,
|
||||||
|
kBlink,
|
||||||
|
};
|
||||||
|
|
||||||
|
static ESMHandlerPlatform SelectESMHandlerPlatform(
|
||||||
|
v8::Local<v8::Context> context,
|
||||||
|
v8::Local<v8::Data> raw_host_defined_options) {
|
||||||
|
if (node::Environment::GetCurrent(context) == nullptr) {
|
||||||
|
if (electron::IsBrowserProcess() || electron::IsUtilityProcess())
|
||||||
|
return ESMHandlerPlatform::kNone;
|
||||||
|
|
||||||
|
return ESMHandlerPlatform::kBlink;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!electron::IsRendererProcess())
|
||||||
|
return ESMHandlerPlatform::kNodeJS;
|
||||||
|
|
||||||
|
blink::WebLocalFrame* frame = blink::WebLocalFrame::FrameForContext(context);
|
||||||
|
|
||||||
|
if (frame == nullptr)
|
||||||
|
return ESMHandlerPlatform::kBlink;
|
||||||
|
|
||||||
|
auto prefs = content::RenderFrame::FromWebFrame(frame)->GetBlinkPreferences();
|
||||||
|
|
||||||
|
// If we're running with contextIsolation enabled in the renderer process,
|
||||||
|
// fall back to Blink's logic when the frame is not in the isolated world.
|
||||||
|
if (prefs.context_isolation) {
|
||||||
|
return frame->GetScriptContextWorldId(context) ==
|
||||||
|
electron::WorldIDs::ISOLATED_WORLD_ID
|
||||||
|
? ESMHandlerPlatform::kNodeJS
|
||||||
|
: ESMHandlerPlatform::kBlink;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw_host_defined_options.IsEmpty() ||
|
||||||
|
!raw_host_defined_options->IsFixedArray()) {
|
||||||
|
return ESMHandlerPlatform::kBlink;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the routing is based on the `host_defined_options` length -
|
||||||
|
// make sure that Node's host defined options are different from Blink's.
|
||||||
|
static_assert(
|
||||||
|
static_cast<size_t>(node::loader::HostDefinedOptions::kLength) !=
|
||||||
|
blink::ReferrerScriptInfo::HostDefinedOptionsIndex::kLength);
|
||||||
|
|
||||||
|
// Use Node.js resolver only if host options were created by it.
|
||||||
|
auto options = v8::Local<v8::FixedArray>::Cast(raw_host_defined_options);
|
||||||
|
if (options->Length() == node::loader::HostDefinedOptions::kLength) {
|
||||||
|
return ESMHandlerPlatform::kNodeJS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESMHandlerPlatform::kBlink;
|
||||||
|
}
|
||||||
|
|
||||||
v8::MaybeLocal<v8::Promise> HostImportModuleWithPhaseDynamically(
|
v8::MaybeLocal<v8::Promise> HostImportModuleWithPhaseDynamically(
|
||||||
v8::Local<v8::Context> context,
|
v8::Local<v8::Context> context,
|
||||||
v8::Local<v8::Data> v8_host_defined_options,
|
v8::Local<v8::Data> v8_host_defined_options,
|
||||||
|
|
@ -218,33 +276,22 @@ v8::MaybeLocal<v8::Promise> HostImportModuleWithPhaseDynamically(
|
||||||
v8::Local<v8::String> v8_specifier,
|
v8::Local<v8::String> v8_specifier,
|
||||||
v8::ModuleImportPhase import_phase,
|
v8::ModuleImportPhase import_phase,
|
||||||
v8::Local<v8::FixedArray> v8_import_attributes) {
|
v8::Local<v8::FixedArray> v8_import_attributes) {
|
||||||
if (node::Environment::GetCurrent(context) == nullptr) {
|
switch (SelectESMHandlerPlatform(context, v8_host_defined_options)) {
|
||||||
if (electron::IsBrowserProcess() || electron::IsUtilityProcess())
|
case ESMHandlerPlatform::kBlink:
|
||||||
return {};
|
|
||||||
return blink::V8Initializer::HostImportModuleWithPhaseDynamically(
|
|
||||||
context, v8_host_defined_options, v8_referrer_resource_url,
|
|
||||||
v8_specifier, import_phase, v8_import_attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're running with contextIsolation enabled in the renderer process,
|
|
||||||
// fall back to Blink's logic.
|
|
||||||
if (electron::IsRendererProcess()) {
|
|
||||||
blink::WebLocalFrame* frame =
|
|
||||||
blink::WebLocalFrame::FrameForContext(context);
|
|
||||||
if (!frame || frame->GetScriptContextWorldId(context) !=
|
|
||||||
electron::WorldIDs::ISOLATED_WORLD_ID) {
|
|
||||||
return blink::V8Initializer::HostImportModuleWithPhaseDynamically(
|
return blink::V8Initializer::HostImportModuleWithPhaseDynamically(
|
||||||
context, v8_host_defined_options, v8_referrer_resource_url,
|
context, v8_host_defined_options, v8_referrer_resource_url,
|
||||||
v8_specifier, import_phase, v8_import_attributes);
|
v8_specifier, import_phase, v8_import_attributes);
|
||||||
}
|
case ESMHandlerPlatform::kNodeJS:
|
||||||
|
// TODO: Switch to node::loader::ImportModuleDynamicallyWithPhase
|
||||||
|
// once we land the Node.js version that has it in upstream.
|
||||||
|
CHECK(import_phase == v8::ModuleImportPhase::kEvaluation);
|
||||||
|
return node::loader::ImportModuleDynamically(
|
||||||
|
context, v8_host_defined_options, v8_referrer_resource_url,
|
||||||
|
v8_specifier, v8_import_attributes);
|
||||||
|
case ESMHandlerPlatform::kNone:
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Switch to node::loader::ImportModuleDynamicallyWithPhase
|
|
||||||
// once we land the Node.js version that has it in upstream.
|
|
||||||
CHECK(import_phase == v8::ModuleImportPhase::kEvaluation);
|
|
||||||
return node::loader::ImportModuleDynamically(
|
|
||||||
context, v8_host_defined_options, v8_referrer_resource_url, v8_specifier,
|
|
||||||
v8_import_attributes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v8::MaybeLocal<v8::Promise> HostImportModuleDynamically(
|
v8::MaybeLocal<v8::Promise> HostImportModuleDynamically(
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,17 @@ describe('esm', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('nodeIntegration', () => {
|
describe('nodeIntegration', () => {
|
||||||
|
let badFilePath = '';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
badFilePath = path.resolve(path.resolve(os.tmpdir(), 'bad-file.badjs'));
|
||||||
|
await fs.promises.writeFile(badFilePath, 'const foo = "bar";');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fs.promises.unlink(badFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
it('should support an esm entrypoint', async () => {
|
it('should support an esm entrypoint', async () => {
|
||||||
const [webContents] = await loadWindowWithPreload('import { resolve } from "path"; window.resolvePath = resolve;', {
|
const [webContents] = await loadWindowWithPreload('import { resolve } from "path"; window.resolvePath = resolve;', {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
|
@ -189,6 +200,18 @@ describe('esm', () => {
|
||||||
expect(error?.message).to.include('Failed to fetch dynamically imported module');
|
expect(error?.message).to.include('Failed to fetch dynamically imported module');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use Node.js ESM dynamic loader in the preload', async () => {
|
||||||
|
const [, preloadError] = await loadWindowWithPreload(`await import(${JSON.stringify((pathToFileURL(badFilePath)))})`, {
|
||||||
|
nodeIntegration: true,
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preloadError).to.not.equal(null);
|
||||||
|
// This is a node.js specific error message
|
||||||
|
expect(preloadError!.toString()).to.include('Unknown file extension');
|
||||||
|
});
|
||||||
|
|
||||||
it('should use import.meta callback handling from Node.js for Node.js modules', async () => {
|
it('should use import.meta callback handling from Node.js for Node.js modules', async () => {
|
||||||
const result = await runFixture(path.resolve(fixturePath, 'import-meta'));
|
const result = await runFixture(path.resolve(fixturePath, 'import-meta'));
|
||||||
expect(result.code).to.equal(0);
|
expect(result.code).to.equal(0);
|
||||||
|
|
@ -196,17 +219,6 @@ describe('esm', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with context isolation', () => {
|
describe('with context isolation', () => {
|
||||||
let badFilePath = '';
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
badFilePath = path.resolve(path.resolve(os.tmpdir(), 'bad-file.badjs'));
|
|
||||||
await fs.promises.writeFile(badFilePath, 'const foo = "bar";');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await fs.promises.unlink(badFilePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use Node.js ESM dynamic loader in the isolated context', async () => {
|
it('should use Node.js ESM dynamic loader in the isolated context', async () => {
|
||||||
const [, preloadError] = await loadWindowWithPreload(`await import(${JSON.stringify((pathToFileURL(badFilePath)))})`, {
|
const [, preloadError] = await loadWindowWithPreload(`await import(${JSON.stringify((pathToFileURL(badFilePath)))})`, {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue