Skip to main content

fallow_graph/resolve/
types.rs

1//! Type definitions and constants for import resolution.
2
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5
6use oxc_resolver::Resolver;
7use rustc_hash::{FxHashMap, FxHashSet};
8use serde_json::Value;
9
10use fallow_types::discover::FileId;
11
12/// Result of resolving an import specifier.
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub enum ResolveResult {
15    /// Resolved to a file within the project.
16    InternalModule(FileId),
17    /// Resolved from a CommonJS `require()` to a file within the project.
18    CommonJsInternalModule(FileId),
19    /// Resolved to a project file through a framework convention auto-import.
20    SyntheticAutoImport(FileId),
21    /// Resolved to a workspace or self package source file while preserving
22    /// dependency usage for package accounting.
23    InternalPackageModule {
24        /// Internal source file reached by the package map.
25        file_id: FileId,
26        /// Package name that was used in the import specifier.
27        package_name: String,
28    },
29    /// Resolved from a CommonJS `require()` to workspace or self-package source.
30    CommonJsInternalPackageModule {
31        /// Internal source file reached by the package map.
32        file_id: FileId,
33        /// Package name used in the require specifier.
34        package_name: String,
35    },
36    /// Resolved to a file outside the project (`node_modules`, `.json`, etc.).
37    ExternalFile(PathBuf),
38    /// Bare specifier — an npm package.
39    NpmPackage(String),
40    /// Bare npm package referenced through CommonJS `require()`.
41    CommonJsNpmPackage(String),
42    /// Could not resolve.
43    Unresolvable(String),
44}
45
46impl ResolveResult {
47    /// Return the target file for any project-internal result.
48    #[must_use]
49    pub const fn internal_file_id(&self) -> Option<FileId> {
50        match self {
51            Self::InternalModule(file_id)
52            | Self::CommonJsInternalModule(file_id)
53            | Self::SyntheticAutoImport(file_id)
54            | Self::InternalPackageModule { file_id, .. }
55            | Self::CommonJsInternalPackageModule { file_id, .. } => Some(*file_id),
56            Self::ExternalFile(_)
57            | Self::NpmPackage(_)
58            | Self::CommonJsNpmPackage(_)
59            | Self::Unresolvable(_) => None,
60        }
61    }
62
63    /// Return whether this edge was synthesized from framework auto-import conventions.
64    #[must_use]
65    pub const fn is_synthetic_auto_import(&self) -> bool {
66        matches!(self, Self::SyntheticAutoImport(_))
67    }
68
69    /// Return whether this edge originated from CommonJS `require()`.
70    #[must_use]
71    pub const fn is_commonjs_require(&self) -> bool {
72        matches!(
73            self,
74            Self::CommonJsInternalModule(_)
75                | Self::CommonJsInternalPackageModule { .. }
76                | Self::CommonJsNpmPackage(_)
77        )
78    }
79
80    /// Return whether this target is an unresolved bare package edge.
81    #[must_use]
82    pub const fn is_bare_package(&self) -> bool {
83        matches!(self, Self::NpmPackage(_) | Self::CommonJsNpmPackage(_))
84    }
85
86    /// Retain CommonJS provenance on targets that can participate in upgrades.
87    #[must_use]
88    pub fn into_commonjs_require(self) -> Self {
89        match self {
90            Self::InternalModule(file_id) => Self::CommonJsInternalModule(file_id),
91            Self::InternalPackageModule {
92                file_id,
93                package_name,
94            } => Self::CommonJsInternalPackageModule {
95                file_id,
96                package_name,
97            },
98            Self::NpmPackage(package_name) => Self::CommonJsNpmPackage(package_name),
99            other => other,
100        }
101    }
102
103    /// Remove CommonJS provenance while preserving the resolved destination.
104    #[must_use]
105    pub fn into_es_module(self) -> Self {
106        match self {
107            Self::CommonJsInternalModule(file_id) => Self::InternalModule(file_id),
108            Self::CommonJsInternalPackageModule {
109                file_id,
110                package_name,
111            } => Self::InternalPackageModule {
112                file_id,
113                package_name,
114            },
115            Self::CommonJsNpmPackage(package_name) => Self::NpmPackage(package_name),
116            other => other,
117        }
118    }
119
120    /// Return the package name that should receive dependency usage credit.
121    #[must_use]
122    pub fn package_usage_name(&self) -> Option<&str> {
123        match self {
124            Self::InternalPackageModule { package_name, .. }
125            | Self::CommonJsInternalPackageModule { package_name, .. }
126            | Self::NpmPackage(package_name)
127            | Self::CommonJsNpmPackage(package_name) => Some(package_name),
128            Self::InternalModule(_)
129            | Self::CommonJsInternalModule(_)
130            | Self::SyntheticAutoImport(_)
131            | Self::ExternalFile(_)
132            | Self::Unresolvable(_) => None,
133        }
134    }
135}
136
137/// Resolver output for one extracted project.
138#[derive(Debug, Default)]
139pub struct ResolvedProject {
140    /// Modules with every ordinary import and re-export resolved.
141    pub modules: Vec<ResolvedModule>,
142    /// Proven test-time replacements whose targets resolve inside the project.
143    pub replaced_module_targets: Vec<ResolvedReplacedModuleTarget>,
144}
145
146/// One project-internal module replacement resolved from its declaring source file.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct ResolvedReplacedModuleTarget {
149    /// File that declares the replacement fact.
150    pub source_file: FileId,
151    /// Project file replaced for this source's test-root traversal.
152    pub target_file: FileId,
153}
154
155/// A resolved import with its target.
156#[derive(Debug, Clone)]
157pub struct ResolvedImport {
158    /// The original import information.
159    pub info: fallow_types::extract::ImportInfo,
160    /// Where the import resolved to.
161    pub target: ResolveResult,
162}
163
164/// A resolved re-export with its target.
165#[derive(Debug, Clone)]
166pub struct ResolvedReExport {
167    /// The original re-export information.
168    pub info: fallow_types::extract::ReExportInfo,
169    /// Where the re-export source resolved to.
170    pub target: ResolveResult,
171}
172
173/// Any source-bearing module edge that resolves one literal specifier.
174pub enum ResolvedSourceEdge<'a> {
175    /// Static or literal dynamic import edge.
176    Import(&'a ResolvedImport),
177    /// Re-export source edge.
178    ReExport(&'a ResolvedReExport),
179}
180
181impl<'a> ResolvedSourceEdge<'a> {
182    /// Return the original source specifier.
183    #[must_use]
184    pub fn source_specifier(&self) -> &'a str {
185        match self {
186            Self::Import(import) => &import.info.source,
187            Self::ReExport(re_export) => &re_export.info.source,
188        }
189    }
190
191    /// Return the resolved target.
192    #[must_use]
193    pub const fn target(&self) -> &'a ResolveResult {
194        match self {
195            Self::Import(import) => &import.target,
196            Self::ReExport(re_export) => &re_export.target,
197        }
198    }
199
200    /// Return whether this edge is type-only.
201    #[must_use]
202    pub const fn is_type_only(&self) -> bool {
203        match self {
204            Self::Import(import) => import.info.is_type_only,
205            Self::ReExport(re_export) => re_export.info.is_type_only,
206        }
207    }
208
209    /// Return the span of the full import or re-export declaration.
210    #[must_use]
211    pub const fn span(&self) -> oxc_span::Span {
212        match self {
213            Self::Import(import) => import.info.span,
214            Self::ReExport(re_export) => re_export.info.span,
215        }
216    }
217
218    /// Return the source literal span when the extractor has one.
219    #[must_use]
220    pub const fn source_span(&self) -> oxc_span::Span {
221        match self {
222            Self::Import(import) => import.info.source_span,
223            Self::ReExport(re_export) => re_export.info.source_span,
224        }
225    }
226
227    /// Return the span of the enclosing statement.
228    ///
229    /// Imports do not record a statement span, so their per-binding
230    /// declaration span is the closest available statement anchor.
231    #[must_use]
232    pub const fn statement_span(&self) -> oxc_span::Span {
233        match self {
234            Self::Import(import) => import.info.span,
235            Self::ReExport(re_export) => re_export.info.statement_span,
236        }
237    }
238}
239
240/// Fully resolved module with all imports mapped to targets.
241///
242/// The `Arc<[T]>` fields are refcounted views of the same allocations owned by
243/// the source [`fallow_types::extract::ModuleInfo`], not copies; see the note
244/// on that struct.
245#[derive(Debug)]
246pub struct ResolvedModule {
247    /// Unique file identifier.
248    pub file_id: FileId,
249    /// Absolute path to the module file.
250    pub path: PathBuf,
251    /// All export declarations in this module.
252    pub exports: Arc<[fallow_types::extract::ExportInfo]>,
253    /// All re-exports with resolved targets.
254    pub re_exports: Vec<ResolvedReExport>,
255    /// All static imports with resolved targets.
256    pub resolved_imports: Vec<ResolvedImport>,
257    /// All dynamic imports with resolved targets.
258    pub resolved_dynamic_imports: Vec<ResolvedImport>,
259    /// Dynamic import patterns matched against discovered files.
260    pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
261    /// Static member accesses (e.g., `Status.Active`).
262    pub member_accesses: Arc<[fallow_types::extract::MemberAccess]>,
263    /// Typed semantic facts produced by extraction for cross-layer analysis.
264    pub semantic_facts: Arc<[fallow_types::extract::SemanticFact]>,
265    /// Identifiers used as whole objects (Object.values, for..in, spread, etc.).
266    pub whole_object_uses: Arc<[String]>,
267    /// Whether this module uses `CommonJS` exports.
268    pub has_cjs_exports: bool,
269    /// Whether this module declares at least one Angular `@Component({
270    /// templateUrl: ... })` decorator. Mirrors `ModuleInfo.has_angular_component_template_url`;
271    /// see that field for the contract this gate enforces.
272    pub has_angular_component_template_url: bool,
273    /// Local names of import bindings that are never referenced in this file.
274    pub unused_import_bindings: FxHashSet<String>,
275    /// Local import bindings referenced from type positions.
276    pub type_referenced_import_bindings: Vec<String>,
277    /// Local import bindings referenced from runtime/value positions.
278    pub value_referenced_import_bindings: Vec<String>,
279    /// Namespace-import aliases re-exported through an object literal.
280    /// See `fallow_types::extract::NamespaceObjectAlias` for the shape.
281    pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
282    /// Exported free-function factories that provably return one class instance.
283    /// See `fallow_types::extract::FactoryReturnExport` and issue #1441 (Part A).
284    pub exported_factory_returns: Arc<[fallow_types::extract::FactoryReturnExport]>,
285    /// Object-literal factory-return shapes (`export function createUi() {
286    /// return { orders: factory.ordersPage } }`), threaded from `ModuleInfo` for
287    /// the analyze-layer member-crediting join. See issue #1858.
288    pub exported_factory_return_object_shapes:
289        Arc<[fallow_types::extract::FactoryReturnObjectShapeExport]>,
290    /// Named-type property types declared by this module's top-level interfaces
291    /// and type-literal aliases. See `fallow_types::extract::TypeMemberTypeEntry`
292    /// and issue #1785.
293    pub type_member_types: Arc<[fallow_types::extract::TypeMemberTypeEntry]>,
294}
295
296impl Default for ResolvedModule {
297    fn default() -> Self {
298        Self {
299            file_id: FileId(0),
300            path: PathBuf::new(),
301            exports: Arc::default(),
302            re_exports: vec![],
303            resolved_imports: vec![],
304            resolved_dynamic_imports: vec![],
305            resolved_dynamic_patterns: vec![],
306            member_accesses: Arc::default(),
307            semantic_facts: Arc::default(),
308            whole_object_uses: Arc::default(),
309            has_cjs_exports: false,
310            has_angular_component_template_url: false,
311            unused_import_bindings: FxHashSet::default(),
312            type_referenced_import_bindings: vec![],
313            value_referenced_import_bindings: vec![],
314            namespace_object_aliases: vec![],
315            exported_factory_returns: Arc::default(),
316            exported_factory_return_object_shapes: Arc::default(),
317            type_member_types: Arc::default(),
318        }
319    }
320}
321
322impl ResolvedModule {
323    /// Iterate over all concrete resolved imports in source order buckets.
324    ///
325    /// Includes static `import`/`require` edges and literal dynamic `import()`
326    /// edges. Dynamic import patterns are intentionally excluded because they
327    /// resolve to sets of files rather than single import specifiers.
328    pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
329        self.resolved_imports
330            .iter()
331            .chain(self.resolved_dynamic_imports.iter())
332    }
333
334    /// Iterate over every literal source edge that has one resolved target.
335    ///
336    /// Includes static imports, literal dynamic imports, and re-export sources.
337    /// Dynamic import patterns are excluded because they resolve to sets of
338    /// files rather than single import specifiers.
339    pub fn all_resolved_source_edges(&self) -> impl Iterator<Item = ResolvedSourceEdge<'_>> {
340        self.resolved_imports
341            .iter()
342            .map(ResolvedSourceEdge::Import)
343            .chain(
344                self.resolved_dynamic_imports
345                    .iter()
346                    .map(ResolvedSourceEdge::Import),
347            )
348            .chain(self.re_exports.iter().map(ResolvedSourceEdge::ReExport))
349    }
350}
351
352/// Shared context for resolving import specifiers.
353///
354/// Groups the immutable lookup tables and caches that are shared across all
355/// `resolve_specifier` calls within a single `resolve_all_imports` invocation.
356pub(super) struct ResolveContext<'a> {
357    /// The oxc_resolver instance (configured once, shared across threads).
358    pub resolver: &'a Resolver,
359    /// CSS-only resolver with package.json `sass` and `style` conditions enabled.
360    /// Used only for stylesheet package subpaths so JS/TS imports do not
361    /// accidentally prefer CSS export branches.
362    pub style_resolver: &'a Resolver,
363    /// Ordered extension list used by the resolver.
364    pub extensions: &'a [String],
365    /// Canonical path → FileId lookup (raw paths when root is canonical).
366    pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
367    /// Raw (non-canonical) path → FileId lookup.
368    pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
369    /// Workspace name → canonical root path.
370    pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
371    /// Package manifests for the root package and workspace packages.
372    pub package_manifests: &'a [PackageManifestInfo],
373    /// Whether any package scope declared a Deno import map; false skips map
374    /// lookup per import.
375    pub has_deno_import_maps: bool,
376    /// Ordered package condition names matching the resolver configuration.
377    pub condition_names: &'a [String],
378    /// Plugin-provided path aliases (prefix, replacement).
379    pub path_aliases: &'a [(String, String)],
380    /// Absolute directories to search when resolving bare SCSS/Sass
381    /// `@import` / `@use` specifiers. Populated from Angular's
382    /// `stylePreprocessorOptions.includePaths` and equivalent settings.
383    pub scss_include_paths: &'a [PathBuf],
384    /// Static directory URL mappings from framework config.
385    /// Each tuple is `(absolute_source_dir, normalized_url_mount)`.
386    pub static_dir_mappings: &'a [(PathBuf, String)],
387    /// Project root directory.
388    pub root: &'a Path,
389    /// Lazy canonical path → FileId fallback for intra-project symlinks.
390    /// Only initialized on first miss when root is canonical. `None` when
391    /// path_to_id already uses canonical paths (root is not canonical).
392    pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
393    /// Dedup set for broken-tsconfig warnings. Emits one `tracing::warn!`
394    /// per unique error message instead of spamming the log with one
395    /// warning per affected file. Shared across all parallel resolver
396    /// threads via `Mutex`. Empty and unused when no tsconfig errors occur.
397    pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
398    /// Per-analysis cache for local tsconfig discovery and JSON parsing.
399    /// Import resolution calls these fallbacks for every unresolved or
400    /// tsconfig-poisoned specifier, so keeping it session-local avoids
401    /// repeated filesystem work without risking stale data across runs.
402    pub tsconfig_cache: &'a TsconfigCache,
403    /// Per-analysis cache of `dunce::canonicalize` results keyed by resolved
404    /// path. Every import resolving to a `node_modules` / output-dir / symlinked
405    /// target is realpath'd during classification, and the same package path is
406    /// re-canonicalized for every file that imports the package. The result is a
407    /// pure function of the path's on-disk state (constant within a run), so the
408    /// cache is session-local for watch-mode safety.
409    pub canonicalize_cache: &'a CanonicalizeCache,
410}
411
412/// Session-local cache of `dunce::canonicalize` results keyed by input path.
413#[derive(Default)]
414pub(super) struct CanonicalizeCache {
415    map: Mutex<FxHashMap<PathBuf, Option<PathBuf>>>,
416}
417
418impl CanonicalizeCache {
419    /// Return the cached `dunce::canonicalize(path)` outcome, computing it on
420    /// first miss. `None` (a path that fails to canonicalize) is cached too so a
421    /// repeated probe of the same missing path does not re-issue the syscall.
422    pub fn get(&self, path: &Path) -> Option<PathBuf> {
423        if let Ok(cache) = self.map.lock()
424            && let Some(value) = cache.get(path)
425        {
426            return value.clone();
427        }
428        let value = dunce::canonicalize(path).ok();
429        if let Ok(mut cache) = self.map.lock() {
430            cache.insert(path.to_path_buf(), value.clone());
431        }
432        value
433    }
434}
435
436/// Session-local cache for tsconfig helper lookups used during import resolution.
437#[derive(Default)]
438pub(super) struct TsconfigCache {
439    json: Mutex<FxHashMap<PathBuf, Option<Value>>>,
440    chains: Mutex<FxHashMap<PathBuf, Vec<PathBuf>>>,
441}
442
443impl TsconfigCache {
444    /// Return a cached parsed tsconfig JSON value, loading it on first miss.
445    pub fn json(&self, path: &Path, load: impl FnOnce(&Path) -> Option<Value>) -> Option<Value> {
446        if let Ok(cache) = self.json.lock()
447            && let Some(value) = cache.get(path)
448        {
449            return value.clone();
450        }
451
452        let value = load(path);
453        if let Ok(mut cache) = self.json.lock() {
454            cache.insert(path.to_path_buf(), value.clone());
455        }
456        value
457    }
458
459    /// Return the cached tsconfig chain for a source file, if one exists.
460    pub fn chain(&self, from_file: &Path) -> Option<Vec<PathBuf>> {
461        self.chains
462            .lock()
463            .ok()
464            .and_then(|cache| cache.get(from_file).cloned())
465    }
466
467    /// Store the computed tsconfig chain for a source file.
468    pub fn store_chain(&self, from_file: &Path, chain: Vec<PathBuf>) {
469        if let Ok(mut cache) = self.chains.lock() {
470            cache.insert(from_file.to_path_buf(), chain);
471        }
472    }
473}
474
475/// Package manifest data used by source fallbacks.
476#[derive(Debug, Clone)]
477pub(super) struct PackageManifestInfo {
478    /// Package root path as discovered from the workspace tree.
479    pub root: PathBuf,
480    /// Canonical package root path for node_modules symlink comparisons.
481    pub canonical_root: PathBuf,
482    /// Parsed package name.
483    pub name: Option<String>,
484    /// Parsed package.json fields.
485    pub package_json: fallow_config::PackageJson,
486    /// Effective Deno import map for this package scope.
487    pub deno_import_map: Vec<DenoImportMapEntry>,
488}
489
490/// One effective Deno import-map entry with the directory that declared it.
491#[derive(Debug, Clone)]
492pub(super) struct DenoImportMapEntry {
493    pub key: String,
494    pub target: String,
495    pub declaring_dir: PathBuf,
496}
497
498/// Thread-safe lazy canonical path index, built on first access.
499pub(super) struct CanonicalFallback<'a> {
500    files: &'a [fallow_types::discover::DiscoveredFile],
501    map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
502}
503
504impl<'a> CanonicalFallback<'a> {
505    pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
506        Self {
507            files,
508            map: std::sync::OnceLock::new(),
509        }
510    }
511
512    /// Look up a canonical path, lazily building the index on first call.
513    pub fn get(&self, canonical: &Path) -> Option<FileId> {
514        let map = self.map.get_or_init(|| {
515            tracing::debug!(
516                "intra-project symlinks detected, building canonical path index ({} files)",
517                self.files.len()
518            );
519            self.files
520                .iter()
521                .filter_map(|f| {
522                    dunce::canonicalize(&f.path)
523                        .ok()
524                        .map(|canonical| (canonical, f.id))
525                })
526                .collect()
527        });
528        map.get(canonical).copied()
529    }
530}
531
532#[cfg(all(test, not(miri)))]
533mod tests {
534    use super::*;
535    use fallow_types::discover::DiscoveredFile;
536
537    #[test]
538    fn canonical_fallback_returns_none_for_empty_files() {
539        let files: Vec<DiscoveredFile> = vec![];
540        let fallback = CanonicalFallback::new(&files);
541        assert!(fallback.get(Path::new("/nonexistent")).is_none());
542    }
543
544    #[test]
545    fn canonical_fallback_finds_existing_file() {
546        let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
547        let _ = std::fs::create_dir_all(&temp);
548        let test_file = temp.join("test.ts");
549        std::fs::write(&test_file, "").unwrap();
550
551        let files = vec![DiscoveredFile {
552            id: FileId(42),
553            path: test_file.clone(),
554            size_bytes: 0,
555        }];
556        let fallback = CanonicalFallback::new(&files);
557
558        let canonical = dunce::canonicalize(&test_file).unwrap();
559        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
560
561        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
562
563        let _ = std::fs::remove_dir_all(&temp);
564    }
565
566    #[test]
567    fn canonical_fallback_returns_none_for_missing_path() {
568        let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
569        let _ = std::fs::create_dir_all(&temp);
570        let test_file = temp.join("exists.ts");
571        std::fs::write(&test_file, "").unwrap();
572
573        let files = vec![DiscoveredFile {
574            id: FileId(1),
575            path: test_file,
576            size_bytes: 0,
577        }];
578        let fallback = CanonicalFallback::new(&files);
579        assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
580
581        let _ = std::fs::remove_dir_all(&temp);
582    }
583
584    #[test]
585    fn commonjs_provenance_wraps_internal_and_bare_package_targets() {
586        assert!(matches!(
587            ResolveResult::InternalModule(FileId(4)).into_commonjs_require(),
588            ResolveResult::CommonJsInternalModule(FileId(4))
589        ));
590        assert!(matches!(
591            ResolveResult::InternalPackageModule {
592                file_id: FileId(5),
593                package_name: "pkg".to_string(),
594            }
595            .into_commonjs_require(),
596            ResolveResult::CommonJsInternalPackageModule {
597                file_id: FileId(5),
598                package_name,
599            } if package_name == "pkg"
600        ));
601        assert!(matches!(
602            ResolveResult::NpmPackage("pkg".to_string()).into_commonjs_require(),
603            ResolveResult::CommonJsNpmPackage(package_name) if package_name == "pkg"
604        ));
605    }
606}
607
608/// Known output directory names that may appear in exports map targets.
609/// When an exports map points to `./dist/utils.js`, we try replacing these
610/// prefixes with `src/` (the conventional source directory) to find the tracked
611/// source file.
612pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
613
614/// Source extensions to try when mapping a built output file back to source.
615pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
616
617/// React Native platform extension prefixes.
618/// Metro resolves platform-specific files (e.g., `./foo` -> `./foo.web.tsx` on web).
619pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];