mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 12:51:57 +00:00
feat(resolver): implement fully specified (#687)
This is for turning off ESM's forced extension functionality.
This commit is contained in:
parent
702d5b0120
commit
d21307827d
6 changed files with 148 additions and 42 deletions
|
|
@ -20,7 +20,7 @@
|
|||
| ✅ | extensions | [".js", ".json", ".node"] | A list of extensions which should be tried for files |
|
||||
| ✅ | fallback | [] | Same as `alias`, but only used if default resolving fails |
|
||||
| ✅ | fileSystem | | The file system which should be used |
|
||||
| | fullySpecified | false | Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests) |
|
||||
| ✅ | fullySpecified | false | Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests) |
|
||||
| | mainFields | ["main"] | A list of main fields in description files |
|
||||
| ✅ | mainFiles | ["index"] | A list of main files in directories |
|
||||
| ✅ | modules | ["node_modules"] | A list of directories to resolve modules from, can be absolute path or folder name |
|
||||
|
|
@ -51,7 +51,7 @@ Crossed out test files are irrelevant.
|
|||
- [x] extensions.test.js
|
||||
- [x] fallback.test.js (need to fix a todo)
|
||||
- [x] ~forEachBail.test.js~
|
||||
- [ ] fullSpecified.test.js
|
||||
- [x] fullSpecified.test.js
|
||||
- [ ] getPaths.test.js
|
||||
- [x] identifier.test.js (see unit test in `crates/oxc_resolver/src/request.rs`)
|
||||
- [x] importsField.test.js
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ pub use crate::{
|
|||
resolution::Resolution,
|
||||
};
|
||||
|
||||
type ResolveState = Result<Option<CacheValue>, ResolveError>;
|
||||
|
||||
/// Resolver with the current operating system as the file system
|
||||
pub type Resolver = ResolverGeneric<FileSystemOs>;
|
||||
|
||||
|
|
@ -53,6 +51,13 @@ pub struct ResolverGeneric<Fs> {
|
|||
cache: Cache<Fs>,
|
||||
}
|
||||
|
||||
type ResolveState = Result<Option<CacheValue>, ResolveError>;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct ResolveContext {
|
||||
fully_specified: bool,
|
||||
}
|
||||
|
||||
impl<Fs: FileSystem> Default for ResolverGeneric<Fs> {
|
||||
fn default() -> Self {
|
||||
Self::new(ResolveOptions::default())
|
||||
|
|
@ -80,13 +85,14 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
) -> Result<Resolution, ResolveError> {
|
||||
let path = path.as_ref();
|
||||
let request = Request::parse(request).map_err(ResolveError::Request)?;
|
||||
let ctx = ResolveContext { fully_specified: self.options.fully_specified };
|
||||
let cache_value = self.cache.value(path);
|
||||
let cache_value = if let Some(path) =
|
||||
self.load_alias(&cache_value, request.path.as_str(), &self.options.alias)?
|
||||
{
|
||||
path
|
||||
} else {
|
||||
let result = self.require(&cache_value, &request);
|
||||
let result = self.require(&cache_value, &request, ctx);
|
||||
if result.as_ref().is_err_and(ResolveError::is_not_found) {
|
||||
if let Some(path) =
|
||||
self.load_alias(&cache_value, request.path.as_str(), &self.options.fallback)?
|
||||
|
|
@ -114,6 +120,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
&self,
|
||||
cache_value: &CacheValue,
|
||||
request: &Request,
|
||||
ctx: ResolveContext,
|
||||
) -> Result<CacheValue, ResolveError> {
|
||||
let path = match request.path {
|
||||
// 1. If X is a core module,
|
||||
|
|
@ -123,15 +130,15 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// a. set Y to be the file system root
|
||||
RequestPath::Absolute(absolute_path) => {
|
||||
if !self.options.prefer_relative && self.options.prefer_absolute {
|
||||
if let Ok(path) = self.package_resolve(cache_value, absolute_path) {
|
||||
if let Ok(path) = self.package_resolve(cache_value, absolute_path, ctx) {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
self.load_roots(cache_value, absolute_path)
|
||||
self.load_roots(cache_value, absolute_path, ctx)
|
||||
}
|
||||
// 3. If X begins with './' or '/' or '../'
|
||||
RequestPath::Relative(relative_path) => {
|
||||
self.require_relative(cache_value, relative_path)
|
||||
self.require_relative(cache_value, relative_path, ctx)
|
||||
}
|
||||
// 4. If X begins with '#'
|
||||
RequestPath::Hash(specifier) => {
|
||||
|
|
@ -143,11 +150,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
|
||||
RequestPath::Bare(bare_specifier) => {
|
||||
if self.options.prefer_relative {
|
||||
if let Ok(path) = self.require_relative(cache_value, bare_specifier) {
|
||||
if let Ok(path) = self.require_relative(cache_value, bare_specifier, ctx) {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
self.package_resolve(cache_value, bare_specifier)
|
||||
self.package_resolve(cache_value, bare_specifier, ctx)
|
||||
}
|
||||
}?;
|
||||
|
||||
|
|
@ -164,17 +171,18 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
&self,
|
||||
cache_value: &CacheValue,
|
||||
request: &str,
|
||||
ctx: ResolveContext,
|
||||
) -> Result<CacheValue, ResolveError> {
|
||||
let path = cache_value.path().normalize_with(request);
|
||||
let cache_value = self.cache.value(&path);
|
||||
// a. LOAD_AS_FILE(Y + X)
|
||||
if !request.ends_with('/') {
|
||||
if let Some(path) = self.load_as_file(&cache_value)? {
|
||||
if let Some(path) = self.load_as_file(&cache_value, ctx)? {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
// b. LOAD_AS_DIRECTORY(Y + X)
|
||||
if let Some(path) = self.load_as_directory(&cache_value)? {
|
||||
if let Some(path) = self.load_as_directory(&cache_value, ctx)? {
|
||||
return Ok(path);
|
||||
}
|
||||
// c. THROW "not found"
|
||||
|
|
@ -186,40 +194,54 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
&self,
|
||||
cache_value: &CacheValue,
|
||||
request: &str,
|
||||
ctx: ResolveContext,
|
||||
) -> Result<CacheValue, ResolveError> {
|
||||
let (_, subpath) = Self::parse_package_specifier(request);
|
||||
let mut ctx = ctx;
|
||||
if subpath.is_empty() {
|
||||
ctx.fully_specified = false;
|
||||
}
|
||||
|
||||
let dirname = self.cache.dirname(cache_value);
|
||||
// 5. LOAD_PACKAGE_SELF(X, dirname(Y))
|
||||
if let Some(path) = self.load_package_self(dirname, request)? {
|
||||
return Ok(path);
|
||||
}
|
||||
// 6. LOAD_NODE_MODULES(X, dirname(Y))
|
||||
if let Some(path) = self.load_node_modules(dirname, request)? {
|
||||
if let Some(path) = self.load_node_modules(dirname, request, ctx)? {
|
||||
return Ok(path);
|
||||
}
|
||||
// 7. THROW "not found"
|
||||
Err(ResolveError::NotFound(cache_value.to_path_buf().into_boxed_path()))
|
||||
}
|
||||
|
||||
fn load_as_file(&self, cache_value: &CacheValue) -> ResolveState {
|
||||
fn load_as_file(&self, cache_value: &CacheValue, ctx: ResolveContext) -> ResolveState {
|
||||
// enhanced-resolve feature: extension_alias
|
||||
if let Some(path) = self.load_extension_alias(cache_value)? {
|
||||
if let Some(path) = self.load_extension_alias(cache_value, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
// 1. If X is a file, load X as its file extension format. STOP
|
||||
// let cache_value = self.cache.cache_value(&path);
|
||||
if let Some(path) = self.load_alias_or_file(cache_value)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
// 2. If X.js is a file, load X.js as JavaScript text. STOP
|
||||
// 3. If X.json is a file, parse X.json to a JavaScript Object. STOP
|
||||
// 4. If X.node is a file, load X.node as binary addon. STOP
|
||||
if let Some(path) = self.load_extensions(cache_value, &self.options.extensions)? {
|
||||
if let Some(path) = self.load_extensions(cache_value, &self.options.extensions, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn load_extensions(&self, cache_value: &CacheValue, extensions: &[String]) -> ResolveState {
|
||||
fn load_extensions(
|
||||
&self,
|
||||
cache_value: &CacheValue,
|
||||
extensions: &[String],
|
||||
ctx: ResolveContext,
|
||||
) -> ResolveState {
|
||||
if ctx.fully_specified {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut path_with_extension = cache_value.path().to_path_buf();
|
||||
for extension in extensions {
|
||||
path_with_extension.set_extension(extension);
|
||||
|
|
@ -239,7 +261,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_index(&self, cache_value: &CacheValue) -> ResolveState {
|
||||
fn load_index(&self, cache_value: &CacheValue, ctx: ResolveContext) -> ResolveState {
|
||||
for main_file in &self.options.main_files {
|
||||
let main_path = cache_value.path().join(main_file);
|
||||
let cache_value = self.cache.value(&main_path);
|
||||
|
|
@ -251,7 +273,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
|
||||
// 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
|
||||
// 3. If X/index.node is a file, load X/index.node as binary addon. STOP
|
||||
if let Some(path) = self.load_extensions(&cache_value, &self.options.extensions)? {
|
||||
if let Some(path) = self.load_extensions(&cache_value, &self.options.extensions, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +293,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
fn load_as_directory(&self, cache_value: &CacheValue) -> ResolveState {
|
||||
fn load_as_directory(&self, cache_value: &CacheValue, ctx: ResolveContext) -> ResolveState {
|
||||
// TODO: Only package.json is supported, so warn about having other values
|
||||
// Checking for empty files is needed for omitting checks on package.json
|
||||
// 1. If X/package.json is a file,
|
||||
|
|
@ -284,28 +306,29 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
let main_field_path = cache_value.path().normalize_with(main_field);
|
||||
// d. LOAD_AS_FILE(M)
|
||||
let cache_value = self.cache.value(&main_field_path);
|
||||
if let Some(path) = self.load_as_file(&cache_value)? {
|
||||
if let Some(path) = self.load_as_file(&cache_value, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
// e. LOAD_INDEX(M)
|
||||
if let Some(path) = self.load_index(&cache_value)? {
|
||||
if let Some(path) = self.load_index(&cache_value, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
// f. LOAD_INDEX(X) DEPRECATED
|
||||
// g. THROW "not found"
|
||||
return Err(ResolveError::NotFound(main_field_path.into_boxed_path()));
|
||||
}
|
||||
|
||||
if let Some(path) = self.load_index(cache_value)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. LOAD_INDEX(X)
|
||||
self.load_index(cache_value)
|
||||
self.load_index(cache_value, ctx)
|
||||
}
|
||||
|
||||
fn load_node_modules(&self, cache_value: &CacheValue, request: &str) -> ResolveState {
|
||||
fn load_node_modules(
|
||||
&self,
|
||||
cache_value: &CacheValue,
|
||||
request: &str,
|
||||
ctx: ResolveContext,
|
||||
) -> ResolveState {
|
||||
// 1. let DIRS = NODE_MODULES_PATHS(START)
|
||||
// Use a buffer to reduce total memory allocation.
|
||||
let mut node_module_path = cache_value.path().to_path_buf();
|
||||
|
|
@ -323,13 +346,13 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
let cache_value = self.cache.value(&node_module_file);
|
||||
// b. LOAD_AS_FILE(DIR/X)
|
||||
if !request.ends_with('/') {
|
||||
if let Some(path) = self.load_as_file(&cache_value)? {
|
||||
if let Some(path) = self.load_as_file(&cache_value, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
// c. LOAD_AS_DIRECTORY(DIR/X)
|
||||
if cache_value.is_dir(&self.cache.fs) {
|
||||
if let Some(path) = self.load_as_directory(&cache_value)? {
|
||||
if let Some(path) = self.load_as_directory(&cache_value, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
|
@ -458,7 +481,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
debug_assert!(package_json.path.file_name().is_some_and(|x| x == "package.json"));
|
||||
// TODO: Do we need to pass query and fragment?
|
||||
let cache_value = self.cache.value(package_json.path.parent().unwrap());
|
||||
return self.require(&cache_value, &request).map(Some);
|
||||
let ctx = ResolveContext::default();
|
||||
return self.require(&cache_value, &request, ctx).map(Some);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
|
|
@ -478,7 +502,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
};
|
||||
let new_request =
|
||||
Request::parse(&new_request).map_err(ResolveError::Request)?;
|
||||
match self.require(cache_value, &new_request) {
|
||||
let ctx = ResolveContext::default();
|
||||
match self.require(cache_value, &new_request, ctx) {
|
||||
Err(ResolveError::NotFound(_)) => { /* noop */ }
|
||||
Ok(path) => return Ok(Some(path)),
|
||||
Err(err) => return Err(err),
|
||||
|
|
@ -505,14 +530,14 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
/// # Errors
|
||||
///
|
||||
/// * [ResolveError::ExtensionAlias]: When all of the aliased extensions are not found
|
||||
fn load_extension_alias(&self, cache_value: &CacheValue) -> ResolveState {
|
||||
fn load_extension_alias(&self, cache_value: &CacheValue, ctx: ResolveContext) -> ResolveState {
|
||||
let Some(path_extension) = cache_value.path().extension() else { return Ok(None) };
|
||||
let Some((_, extensions)) =
|
||||
self.options.extension_alias.iter().find(|(ext, _)| OsStr::new(ext) == path_extension)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(path) = self.load_extensions(cache_value, extensions)? {
|
||||
if let Some(path) = self.load_extensions(cache_value, extensions, ctx)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
Err(ResolveError::ExtensionAlias)
|
||||
|
|
@ -522,15 +547,18 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
&self,
|
||||
cache_value: &CacheValue,
|
||||
request: &str,
|
||||
ctx: ResolveContext,
|
||||
) -> Result<CacheValue, ResolveError> {
|
||||
debug_assert!(request.starts_with('/'));
|
||||
if self.options.roots.is_empty() {
|
||||
let cache_value = self.cache.value(Path::new("/"));
|
||||
return self.package_resolve(&cache_value, request);
|
||||
return self.package_resolve(&cache_value, request, ctx);
|
||||
}
|
||||
for root in &self.options.roots {
|
||||
let cache_value = self.cache.value(root);
|
||||
if let Ok(path) = self.require_relative(&cache_value, request.trim_start_matches('/')) {
|
||||
if let Ok(path) =
|
||||
self.require_relative(&cache_value, request.trim_start_matches('/'), ctx)
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
|
@ -797,7 +825,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
normalize_string_target(target_key, target, pattern_match, package_url)?;
|
||||
let package_url = self.cache.value(package_url);
|
||||
// // 3. Return PACKAGE_RESOLVE(target, packageURL + "/").
|
||||
return self.package_resolve(&package_url, &target).map(Some);
|
||||
return self
|
||||
.package_resolve(&package_url, &target, ResolveContext::default())
|
||||
.map(Some);
|
||||
}
|
||||
|
||||
// 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ pub struct ResolveOptions {
|
|||
/// Default `[]`
|
||||
pub fallback: Alias,
|
||||
|
||||
/// Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests)
|
||||
/// Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests).
|
||||
///
|
||||
/// See also webpack configuration [resolve.fullySpecified](https://webpack.js.org/configuration/module/#resolvefullyspecified)
|
||||
///
|
||||
/// Default `false`
|
||||
pub fully_specified: bool,
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ fn exports_not_browser_field2() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fullSpecified"]
|
||||
#[ignore = "fully_specified"]
|
||||
// should resolve extension without fullySpecified
|
||||
fn extension_without_fully_specified() {
|
||||
let f2 = super::fixture().join("exports-field2");
|
||||
|
|
@ -181,7 +181,7 @@ fn extension_alias_3() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[ignore = "fully_specified"]
|
||||
fn extension_alias_throw_error() {
|
||||
let f = super::fixture().join("exports-field-and-extension-alias");
|
||||
|
||||
|
|
@ -195,8 +195,9 @@ fn extension_alias_throw_error() {
|
|||
|
||||
#[rustfmt::skip]
|
||||
let fail = [
|
||||
// These two test cases are exactly the same in enhanced-resolve
|
||||
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/test/exportsField.test.js#L2976-L3024
|
||||
("should throw error with the `extensionAlias` option", f.clone(), "pkg/string.js", ResolveError::PackagePathNotExported("node_modules/pkg/dist/string.js".to_string())),
|
||||
// They are exactly the same in enhanced-resolve
|
||||
("should throw error with the `extensionAlias` option #2", f, "pkg/string.js", ResolveError::PackagePathNotExported("node_modules/pkg/dist/string.js".to_string())),
|
||||
];
|
||||
|
||||
|
|
|
|||
72
crates/oxc_resolver/src/tests/full_specified.rs
Normal file
72
crates/oxc_resolver/src/tests/full_specified.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
//! https://github.com/webpack/enhanced-resolve/blob/main/test/fullSpecified.test.js
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{AliasValue, ResolveOptions, ResolverGeneric};
|
||||
|
||||
use super::memory_fs::MemoryFS;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
|
||||
fn test() {
|
||||
use crate::Resolution;
|
||||
|
||||
let file_system = MemoryFS::new(&[
|
||||
("/a/node_modules/package1/index.js", ""),
|
||||
("/a/node_modules/package1/file.js", ""),
|
||||
("/a/node_modules/package2/package.json", r#"{"main":"a"}"#),
|
||||
("/a/node_modules/package2/a.js", ""),
|
||||
("/a/node_modules/package3/package.json", r#"{"main":"dir"}"#),
|
||||
("/a/node_modules/package3/dir/index.js", ""),
|
||||
("/a/node_modules/package4/package.json", r#"{"browser":{"./a.js":"./b"}}"#),
|
||||
("/a/node_modules/package4/a.js", ""),
|
||||
("/a/node_modules/package4/b.js", ""),
|
||||
("/a/abc.js", ""),
|
||||
("/a/dir/index.js", ""),
|
||||
("/a/index.js", ""),
|
||||
]);
|
||||
|
||||
let options = ResolveOptions {
|
||||
alias: vec![
|
||||
("alias1".into(), vec![AliasValue::Path("/a/abc".into())]),
|
||||
("alias2".into(), vec![AliasValue::Path("/a".into())]),
|
||||
],
|
||||
alias_fields: vec!["browser".into()],
|
||||
fully_specified: true,
|
||||
..ResolveOptions::default()
|
||||
};
|
||||
|
||||
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(options, file_system);
|
||||
|
||||
let failing_resolves = [
|
||||
("no extensions", "./abc"),
|
||||
("no extensions (absolute)", "/a/abc"),
|
||||
("no extensions in packages", "package1/file"),
|
||||
("no directories", "."),
|
||||
("no directories 2", "./"),
|
||||
("no directories in packages", "package3/dir"),
|
||||
("no extensions in packages 2", "package3/a"),
|
||||
];
|
||||
|
||||
for (comment, request) in failing_resolves {
|
||||
let resolution = resolver.resolve("/a", request);
|
||||
assert!(resolution.is_err(), "{comment} {request}");
|
||||
}
|
||||
|
||||
let successful_resolves = [
|
||||
("fully relative", "./abc.js", "/a/abc.js"),
|
||||
("fully absolute", "/a/abc.js", "/a/abc.js"),
|
||||
("fully relative in package", "package1/file.js", "/a/node_modules/package1/file.js"),
|
||||
("extensions in mainFiles", "package1", "/a/node_modules/package1/index.js"),
|
||||
("extensions in mainFields", "package2", "/a/node_modules/package2/a.js"),
|
||||
("extensions in alias", "alias1", "/a/abc.js"),
|
||||
("directories in alias", "alias2", "/a/index.js"),
|
||||
("directories in packages", "package3", "/a/node_modules/package3/dir/index.js"),
|
||||
("extensions in aliasFields", "package4/a.js", "/a/node_modules/package4/b.js"),
|
||||
];
|
||||
|
||||
for (comment, request, expected) in successful_resolves {
|
||||
let resolution = resolver.resolve("/a", request).map(Resolution::full_path);
|
||||
assert_eq!(resolution, Ok(PathBuf::from(expected)), "{comment} {request}");
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ mod exports_field;
|
|||
mod extension_alias;
|
||||
mod extensions;
|
||||
mod fallback;
|
||||
mod full_specified;
|
||||
mod imports_field;
|
||||
mod incorrect_description_file;
|
||||
mod memory_fs;
|
||||
|
|
|
|||
Loading…
Reference in a new issue