Skip to content

Commit

Permalink
Implement import.meta.resolve()
Browse files Browse the repository at this point in the history
See whatwg/html#5572.

Intent to Ship: https://groups.google.com/a/chromium.org/g/blink-dev/c/ZVODFsnIf74

Fixed: 1296665
Change-Id: I63938700518941d0f65a2a1c7fd13910bd095261
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3456729
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Reviewed-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
Reviewed-by: Yuki Shiino <yukishiino@chromium.org>
Commit-Queue: Domenic Denicola <domenic@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1021529}
NOKEYCHECK=True
GitOrigin-RevId: b2f7928219b78bf917e8ae1355d3b43f3ca85014
  • Loading branch information
domenic authored and Copybara-Service committed Jul 7, 2022
1 parent 35fb183 commit 3314524
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 24 deletions.
10 changes: 9 additions & 1 deletion blink/renderer/bindings/core/v8/v8_initializer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -638,10 +638,18 @@ void HostGetImportMetaProperties(v8::Local<v8::Context> context,

ModuleImportMeta host_meta = modulator->HostGetImportMetaProperties(module);

// 3. Return <<Record { [[Key]]: "url", [[Value]]: urlString }>>. [spec text]
// 6. Return « Record { [[Key]]: "url", [[Value]]: urlString }, Record {
// [[Key]]: "resolve", [[Value]]: resolveFunction } ». [spec text]
v8::Local<v8::String> url_key = V8String(isolate, "url");
v8::Local<v8::String> url_value = V8String(isolate, host_meta.Url());

v8::Local<v8::String> resolve_key = V8String(isolate, "resolve");
v8::Local<v8::Function> resolve_value =
host_meta.MakeResolveV8Function(modulator);
resolve_value->SetName(resolve_key);

meta->CreateDataProperty(context, url_key, url_value).ToChecked();
meta->CreateDataProperty(context, resolve_key, resolve_value).ToChecked();
}

void InitializeV8Common(v8::Isolate* isolate) {
Expand Down
1 change: 1 addition & 0 deletions blink/renderer/core/script/build.gni
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ blink_core_sources_script = [
"modulator.h",
"modulator_impl_base.cc",
"modulator_impl_base.h",
"module_import_meta.cc",
"module_import_meta.h",
"module_map.cc",
"module_map.h",
Expand Down
52 changes: 52 additions & 0 deletions blink/renderer/core/script/module_import_meta.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/core/script/module_import_meta.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/core/script/modulator.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"

namespace blink {

const v8::Local<v8::Function> ModuleImportMeta::MakeResolveV8Function(
Modulator* modulator) const {
ScriptFunction* fn = MakeGarbageCollected<ScriptFunction>(
modulator->GetScriptState(),
MakeGarbageCollected<Resolve>(modulator, url_));
return fn->V8Function();
}

ScriptValue ModuleImportMeta::Resolve::Call(ScriptState* script_state,
ScriptValue value) {
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionContext::Context::kOperationInvoke,
"import.meta", "resolve");

const String specifier = NativeValueTraits<IDLString>::NativeValue(
script_state->GetIsolate(), value.V8Value(), exception_state);
if (exception_state.HadException()) {
return ScriptValue();
}

String failure_reason = "Unknown failure";
const KURL result = modulator_->ResolveModuleSpecifier(specifier, KURL(url_),
&failure_reason);

if (!result.IsValid()) {
exception_state.ThrowTypeError("Failed to resolve module specifier " +
specifier + ": " + failure_reason);
}

return ScriptValue::From(script_state, ToV8Traits<IDLString>::ToV8(
script_state, result.GetString())
.ToLocalChecked());
}

void ModuleImportMeta::Resolve::Trace(Visitor* visitor) const {
visitor->Trace(modulator_);
ScriptFunction::Callable::Trace(visitor);
}

} // namespace blink
25 changes: 25 additions & 0 deletions blink/renderer/core/script/module_import_meta.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_SCRIPT_MODULE_IMPORT_META_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_SCRIPT_MODULE_IMPORT_META_H_

#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "v8/include/v8.h"

namespace blink {

class Modulator;

// Represents import.meta data structure, which is the return value of
// https://html.spec.whatwg.org/C/#hostgetimportmetaproperties
class CORE_EXPORT ModuleImportMeta final {
Expand All @@ -20,7 +27,25 @@ class CORE_EXPORT ModuleImportMeta final {

const String& Url() const { return url_; }

// This will return a fresh function each time, so generally this should only
// be called once.
const v8::Local<v8::Function> MakeResolveV8Function(Modulator*) const;

private:
class Resolve final : public ScriptFunction::Callable {
public:
explicit Resolve(Modulator* modulator, String url)
: modulator_(modulator), url_(url) {}

ScriptValue Call(ScriptState*, ScriptValue) override;
int Length() const override { return 1; }
void Trace(Visitor*) const override;

private:
const Member<Modulator> modulator_;
const String url_;
};

const String url_;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// META: global=dedicatedworker-module,sharedworker-module,serviceworker-module

test(() => {
assert_equals(typeof import.meta, "object");
assert_not_equals(import.meta, null);
}, "import.meta is an object");

test(() => {
import.meta.newProperty = 1;
assert_true(Object.isExtensible(import.meta));
}, "import.meta is extensible");

test(() => {
for (const name of Reflect.ownKeys(import.meta)) {
const desc = Object.getOwnPropertyDescriptor(import.meta, name);
assert_equals(desc.writable, true);
assert_equals(desc.enumerable, true);
assert_equals(desc.configurable, true);
}
}, "import.meta's properties are writable, configurable, and enumerable");
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<!--
More extensive tests of import maps and import.meta.resolve() will be
located in the import maps test suite. This contains some basic tests plus
tests some tricky parts of the import.meta.resolve() algorithm around string
conversion which are only testable with import maps.
-->

<script type="importmap">
{
"imports": {
"bare": "https://example.com/",
"https://example.com/rewrite": "https://example.com/rewritten",

"1": "https://example.com/PASS-1",
"null": "https://example.com/PASS-null",
"undefined": "https://example.com/PASS-undefined",
"[object Object]": "https://example.com/PASS-object",

"./start": "./resources/export-1.mjs",
"./resources/export-1.mjs": "./resources/export-2.mjs"
}
}
</script>

<script type="module">
test(() => {
assert_equals(import.meta.resolve("bare"), "https://example.com/");
}, "import.meta.resolve() given an import mapped bare specifier");

test(() => {
assert_equals(import.meta.resolve("https://example.com/rewrite"), "https://example.com/rewritten");
}, "import.meta.resolve() given an import mapped URL-like specifier");

test(() => {
assert_equals(import.meta.resolve(), "https://example.com/PASS-undefined", "no-arg case");

assert_equals(import.meta.resolve(1), "https://example.com/PASS-1");
assert_equals(import.meta.resolve(null), "https://example.com/PASS-null");
assert_equals(import.meta.resolve(undefined), "https://example.com/PASS-undefined");

// Only toString() methods are consulted by ToString, not valueOf() ones.
// So this becomes "[object Object]".
assert_equals(import.meta.resolve({ valueOf() { return "./x"; } }), "https://example.com/PASS-object");
}, "Testing the ToString() step of import.meta.resolve() via import maps");

promise_test(async () => {
const one = (await import("./start")).default;
assert_equals(one, 1);

const two = (await import(import.meta.resolve("./start"))).default;
assert_equals(two, 2);
}, "import(import.meta.resolve(x)) can be different from import(x)");
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<iframe src="resources/store-import-meta.html"></iframe>

<script type="module">
import * as otherImportMeta from "./resources/export-import-meta.mjs";
setup({ explicit_done: true });

window.onload = () => {
test(() => {
assert_not_equals(frames[0].importMetaURL, import.meta.url,
"Precondition check: we've set things up so that the other script has a different import.meta.url");

const expected = (new URL("resources/x", location.href)).href;
assert_equals(frames[0].importMetaResolve("./x"), expected);
}, "import.meta.resolve resolves URLs relative to the import.meta.url, not relative to the active script when it is called: another global's inline script");

test(() => {
const otherFrameImportMetaResolve = frames[0].importMetaResolve;

document.querySelector("iframe").remove();

const expected = (new URL("resources/x", location.href)).href;
assert_equals(otherFrameImportMetaResolve("./x"), expected);
}, "import.meta.resolve still works if its global has been destroyed (by detaching the iframe)");

test(() => {
assert_not_equals(otherImportMeta.url, import.meta.url,
"Precondition check: we've set things up so that the other script has a different import.meta.url");

const expected = (new URL("resources/x", location.href)).href;
assert_equals(otherImportMeta.resolve("./x"), expected);
}, "import.meta.resolve resolves URLs relative to the import.meta.url, not relative to the active script when it is called: another module script");

done();
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// META: global=dedicatedworker-module,sharedworker-module,serviceworker-module

import { importMetaOnRootModule, importMetaOnDependentModule }
from "./import-meta-root.js";

test(() => {
assert_equals(typeof import.meta.resolve, "function");
assert_equals(import.meta.resolve.name, "resolve");
assert_equals(import.meta.resolve.length, 1);
assert_equals(Object.getPrototypeOf(import.meta.resolve), Function.prototype);
}, "import.meta.resolve is a function with the right properties");

test(() => {
assert_false(isConstructor(import.meta.resolve));

assert_throws_js(TypeError, () => new import.meta.resolve("./x"));
}, "import.meta.resolve is not a constructor");

test(() => {
// See also tests in ./import-meta-resolve-importmap.html.

assert_equals(import.meta.resolve({ toString() { return "./x"; } }), resolveURL("x"));
assert_throws_js(TypeError, () => import.meta.resolve(Symbol("./x")),
"symbol");
assert_throws_js(TypeError, () => import.meta.resolve(),
"no argument (which is treated like \"undefined\")");
}, "import.meta.resolve ToString()s its argument");

test(() => {
assert_equals(import.meta.resolve("./x"), resolveURL("x"),
"current module import.meta");
assert_equals(importMetaOnRootModule.resolve("./x"), resolveURL("x"),
"sibling module import.meta");
assert_equals(importMetaOnDependentModule.resolve("./x"), resolveURL("x"),
"dependency module import.meta");
}, "Relative URL-like specifier resolution");

test(() => {
assert_equals(import.meta.resolve("https://example.com/"), "https://example.com/",
"current module import.meta");
assert_equals(importMetaOnRootModule.resolve("https://example.com/"), "https://example.com/",
"sibling module import.meta");
assert_equals(importMetaOnDependentModule.resolve("https://example.com/"), "https://example.com/",
"dependency module import.meta");
}, "Absolute URL-like specifier resolution");

test(() => {
const invalidSpecifiers = [
"https://eggplant:b/c",
"pumpkins.js",
".tomato",
"..zuccini.mjs",
".\\yam.es"
];

for (const specifier of invalidSpecifiers) {
assert_throws_js(TypeError, () => import.meta.resolve(specifier), specifier);
}
}, "Invalid module specifiers");

test(() => {
const { resolve } = import.meta;
assert_equals(resolve("https://example.com/"), "https://example.com/", "current module import.meta");
}, "Works fine with no this value");

function resolveURL(urlRelativeToThisTest) {
return (new URL(urlRelativeToThisTest, location.href)).href;
}

function isConstructor(o) {
try {
new (new Proxy(o, { construct: () => ({}) }));
return true;
} catch {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,6 @@ test(() => {
base + "/import-meta-dependent.js");
}, "import.meta.url in a dependent external script");

test(() => {
assert_equals(typeof importMetaOnRootModule, "object");
assert_not_equals(importMetaOnRootModule, null);
}, "import.meta is an object");

test(() => {
importMetaOnRootModule.newProperty = 1;
assert_true(Object.isExtensible(importMetaOnRootModule));
}, "import.meta is extensible");

test(() => {
const names = new Set(Reflect.ownKeys(importMetaOnRootModule));
for (const name of names) {
var desc = Object.getOwnPropertyDescriptor(importMetaOnRootModule, name);
assert_equals(desc.writable, true);
assert_equals(desc.enumerable, true);
assert_equals(desc.configurable, true);
}
}, "import.meta's properties are writable, configurable, and enumerable");


import { importMetaOnRootModule as hashedImportMetaOnRootModule1,
importMetaOnDependentModule as hashedImportMetaOnDependentModule1 }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 2;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const url = import.meta.url;
export const resolve = import.meta.resolve;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!DOCTYPE html>
<script type="module">
window.importMetaURL = import.meta.url;
window.importMetaResolve = import.meta.resolve;
</script>
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
This is a testharness.js-based test.
PASS import.meta.url in a root external script
PASS import.meta.url in a dependent external script
PASS import.meta is an object
PASS import.meta is extensible
PASS import.meta's properties are writable, configurable, and enumerable
FAIL import.meta.url when importing the module with different fragments assert_equals: expected "http://web-platform.test:8001/html/semantics/scripting-1/the-script-element/module/import-meta/import-meta-root.js#1" but got "http://web-platform.test:8001/html/semantics/scripting-1/the-script-element/module/import-meta/import-meta-root.js"
PASS import.meta.url in a root inline script
PASS import.meta.url at top-level module DedicatedWorker
Expand Down

0 comments on commit 3314524

Please sign in to comment.