Commit 0db91fa2 authored by Hiroshige Hayashizaki's avatar Hiroshige Hayashizaki Committed by Commit Bot

[Import Maps] implement "Packages" via trailing slashes

This CL also implements "most-specific wins" rule.
https://github.com/WICG/import-maps/issues/102

Bug: 928149
Change-Id: I484266086bbe244de8b43ceeddacc8552307b7f4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1475049
Commit-Queue: Hiroshige Hayashizaki <hiroshige@chromium.org>
Reviewed-by: default avatarKouhei Ueno <kouhei@chromium.org>
Cr-Commit-Position: refs/heads/master@{#638532}
parent 85aee6c6
...@@ -205,6 +205,57 @@ ImportMap* ImportMap::Create(const Modulator& modulator_for_built_in_modules, ...@@ -205,6 +205,57 @@ ImportMap* ImportMap::Create(const Modulator& modulator_for_built_in_modules,
modules_map); modules_map);
} }
base::Optional<ImportMap::MatchResult> ImportMap::MatchExact(
const ParsedSpecifier& parsed_specifier) const {
const String key = parsed_specifier.GetImportMapKeyString();
MatchResult exact = imports_.find(key);
if (exact != imports_.end())
return exact;
return base::nullopt;
}
base::Optional<ImportMap::MatchResult> ImportMap::MatchPrefix(
const ParsedSpecifier& parsed_specifier) const {
// Do not perform prefix match for non-bare specifiers.
if (parsed_specifier.GetType() != ParsedSpecifier::Type::kBare)
return base::nullopt;
const String key = parsed_specifier.GetImportMapKeyString();
// Prefix match, i.e. "Packages" via trailing slashes.
// https://github.com/WICG/import-maps#packages-via-trailing-slashes
//
// TODO(hiroshige): optimize this if necessary. See
// https://github.com/WICG/import-maps/issues/73#issuecomment-439327758
// for some candidate implementations.
// "most-specific wins", i.e. when there are multiple matching keys,
// choose the longest.
// https://github.com/WICG/import-maps/issues/102
base::Optional<MatchResult> best_match;
for (auto it = imports_.begin(); it != imports_.end(); ++it) {
if (!it->key.EndsWith('/'))
continue;
if (!key.StartsWith(it->key))
continue;
if (best_match && it->key.length() < (*best_match)->key.length())
continue;
best_match = it;
}
return best_match;
}
base::Optional<ImportMap::MatchResult> ImportMap::Match(
const ParsedSpecifier& parsed_specifier) const {
if (auto exact = MatchExact(parsed_specifier))
return exact;
return MatchPrefix(parsed_specifier);
}
ImportMap::ImportMap(const Modulator& modulator_for_built_in_modules, ImportMap::ImportMap(const Modulator& modulator_for_built_in_modules,
const HashMap<String, Vector<KURL>>& imports) const HashMap<String, Vector<KURL>>& imports)
: imports_(imports), : imports_(imports),
...@@ -214,25 +265,32 @@ base::Optional<KURL> ImportMap::Resolve(const ParsedSpecifier& parsed_specifier, ...@@ -214,25 +265,32 @@ base::Optional<KURL> ImportMap::Resolve(const ParsedSpecifier& parsed_specifier,
String* debug_message) const { String* debug_message) const {
DCHECK(debug_message); DCHECK(debug_message);
const String key = parsed_specifier.GetImportMapKeyString(); const String key = parsed_specifier.GetImportMapKeyString();
auto it = imports_.find(key);
if (it == imports_.end()) { base::Optional<MatchResult> maybe_matched = Match(parsed_specifier);
if (!maybe_matched) {
*debug_message = "Import Map: \"" + key + *debug_message = "Import Map: \"" + key +
"\" matches with no entries and thus is not mapped."; "\" matches with no entries and thus is not mapped.";
return base::nullopt; return base::nullopt;
} }
for (const auto& candidate_url : it->value) { MatchResult& matched = *maybe_matched;
const String postfix = key.Substring(matched->key.length());
for (const KURL& value : matched->value) {
const KURL complete_url = postfix.IsEmpty() ? value : KURL(value, postfix);
if (blink::layered_api::ResolveFetchingURL(*modulator_for_built_in_modules_, if (blink::layered_api::ResolveFetchingURL(*modulator_for_built_in_modules_,
candidate_url) complete_url)
.IsValid()) { .IsValid()) {
*debug_message = "Import Map: \"" + key + "\" matches with \"" + it->key + *debug_message = "Import Map: \"" + key + "\" matches with \"" +
"\" and is mapped to " + candidate_url.ElidedString(); matched->key + "\" and is mapped to " +
return candidate_url; complete_url.ElidedString();
return complete_url;
} }
} }
*debug_message = "Import Map: \"" + key + "\" matches with \"" + it->key + *debug_message = "Import Map: \"" + key + "\" matches with \"" +
"\" but fails to be mapped (no viable URLs)"; matched->key + "\" but fails to be mapped (no viable URLs)";
return NullURL(); return NullURL();
} }
......
...@@ -41,6 +41,11 @@ class ImportMap final : public GarbageCollectedFinalized<ImportMap> { ...@@ -41,6 +41,11 @@ class ImportMap final : public GarbageCollectedFinalized<ImportMap> {
void Trace(Visitor*); void Trace(Visitor*);
private: private:
using MatchResult = HashMap<String, Vector<KURL>>::const_iterator;
base::Optional<MatchResult> Match(const ParsedSpecifier&) const;
base::Optional<MatchResult> MatchExact(const ParsedSpecifier&) const;
base::Optional<MatchResult> MatchPrefix(const ParsedSpecifier&) const;
HashMap<String, Vector<KURL>> imports_; HashMap<String, Vector<KURL>> imports_;
Member<const Modulator> modulator_for_built_in_modules_; Member<const Modulator> modulator_for_built_in_modules_;
}; };
......
...@@ -7,7 +7,7 @@ FAIL Unmapped / should fail for absolute non-fetch-scheme URLs assert_throws: fu ...@@ -7,7 +7,7 @@ FAIL Unmapped / should fail for absolute non-fetch-scheme URLs assert_throws: fu
FAIL Unmapped / should fail for strings not parseable as absolute URLs and not starting with ./ ../ or / assert_throws: function "() => resolveUnderTest('https://ex ample.org/')" did not throw FAIL Unmapped / should fail for strings not parseable as absolute URLs and not starting with ./ ../ or / assert_throws: function "() => resolveUnderTest('https://ex ample.org/')" did not throw
PASS Mapped using the "imports" key only (no scopes) / should fail when the mapping is to an empty array PASS Mapped using the "imports" key only (no scopes) / should fail when the mapping is to an empty array
PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should work for package main modules PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should work for package main modules
FAIL Mapped using the "imports" key only (no scopes) / Package-like scenarios / should work for package submodules Failed to resolve module specifier moment/foo: Relative references must start with either "/", "./", or "../". PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should work for package submodules
PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should work for package names that end in a slash by just passing through PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should work for package names that end in a slash by just passing through
PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should still fail for package modules that are not declared PASS Mapped using the "imports" key only (no scopes) / Package-like scenarios / should still fail for package modules that are not declared
PASS Mapped using the "imports" key only (no scopes) / Tricky specifiers / should work for explicitly-mapped specifiers that happen to have a slash PASS Mapped using the "imports" key only (no scopes) / Tricky specifiers / should work for explicitly-mapped specifiers that happen to have a slash
...@@ -18,5 +18,6 @@ PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / sho ...@@ -18,5 +18,6 @@ PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / sho
PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / should fail for URLs that remap to empty arrays PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / should fail for URLs that remap to empty arrays
PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / should remap URLs that are just composed from / and . PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / should remap URLs that are just composed from / and .
PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / should use the last entry's address when URL-like specifiers parse to the same absolute URL PASS Mapped using the "imports" key only (no scopes) / URL-like specifiers / should use the last entry's address when URL-like specifiers parse to the same absolute URL
PASS Mapped using the "imports" key only (no scopes) / overlapping entries with trailing slashes / most-specific wins
Harness: the test ran to completion. Harness: the test ran to completion.
'use strict'; 'use strict';
// Imported from:
// https://github.com/WICG/import-maps/blob/master/reference-implementation/__tests__/resolving.js
// TODO: Upstream local changes.
const { URL } = require('url'); const { URL } = require('url');
const { parseFromString } = require('../lib/parser.js'); const { parseFromString } = require('../lib/parser.js');
const { resolve } = require('../lib/resolver.js'); const { resolve } = require('../lib/resolver.js');
...@@ -203,4 +208,23 @@ describe('Mapped using the "imports" key only (no scopes)', () => { ...@@ -203,4 +208,23 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
expect(resolveUnderTest('/test')).toMatchURL('https://example.com/lib/test2.mjs'); expect(resolveUnderTest('/test')).toMatchURL('https://example.com/lib/test2.mjs');
}); });
}); });
describe('overlapping entries with trailing slashes', () => {
const resolveUnderTest = makeResolveUnderTest(`{
"imports": {
"a": "/1",
"a/": "/2/",
"a/b": "/3",
"a/b/": "/4/"
}
}`);
it('most-specific wins', () => {
expect(resolveUnderTest('a')).toMatchURL('https://example.com/1');
expect(resolveUnderTest('a/')).toMatchURL('https://example.com/2/');
expect(resolveUnderTest('a/b')).toMatchURL('https://example.com/3');
expect(resolveUnderTest('a/b/')).toMatchURL('https://example.com/4/');
expect(resolveUnderTest('a/b/c')).toMatchURL('https://example.com/4/c');
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment