Skip to main content

omena_bridge/
style_resolution.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, VecDeque},
3    ffi::OsString,
4    fs,
5    path::{Component, Path, PathBuf},
6    sync::{Mutex, OnceLock},
7};
8
9use crate::bundler_config_alias::load_omena_bridge_workspace_bundler_path_alias_mappings;
10use omena_resolver::{
11    OmenaResolverBundlerPathAliasMappingV0, OmenaResolverStyleModuleConfirmationOptionsV0,
12    OmenaResolverStyleModuleDiskCandidateIdentityV0, OmenaResolverStylePackageManifestV0,
13    OmenaResolverTsconfigPathMappingV0,
14    collect_omena_resolver_style_module_source_candidates_with_path_mappings,
15    confirm_omena_resolver_style_module_candidate_with_options,
16    is_omena_resolver_indexable_style_module_path,
17    normalize_omena_resolver_style_module_source_for_routing,
18};
19use omena_sif::{
20    OmenaLifExportsV1, OmenaSifSourceSyntaxV1, OmenaSifStaticGeneratorInputV1, OmenaSifV1,
21    compute_omena_sif_leaf_hash_v1, generate_static_omena_lif_exports_v1,
22    generate_static_omena_sif_v1, read_omena_sif_json_v1, write_omena_canonical_json_bytes_v1,
23    write_omena_sif_json_v1,
24};
25use serde::Serialize;
26use serde_json::{Value, json};
27
28const WORKSPACE_PACKAGE_MANIFEST_SCAN_LIMIT: usize = 1024;
29const EXTERNAL_SIF_CACHE_SCHEMA_VERSION: &str = "0";
30const EXTERNAL_SIF_CACHE_PRODUCT: &str = "omena-bridge.external-sif-cache-shard";
31const EXTERNAL_SIF_CACHE_RELATIVE_DIR: &str = ".cache/omena/external-sif-v0";
32const EXTERNAL_SIF_CACHE_ENV_KILL_SWITCH: &str = "OMENA_BRIDGE_EXTERNAL_SIF_CACHE";
33const EXTERNAL_SIF_CACHE_MAX_MEMORY_ENTRIES: usize = 256;
34const EXTERNAL_SIF_CACHE_MAX_SHARDS: usize = 2048;
35const EXTERNAL_SIF_CACHE_MAX_TOTAL_BYTES: u64 = 256 * 1024 * 1024;
36const EXTERNAL_SIF_CACHE_MAX_SHARD_BYTES: u64 = 8 * 1024 * 1024;
37const WORKSPACE_STYLE_PATH_IDENTITY_SCAN_LIMIT: usize = 4096;
38const WORKSPACE_STYLE_PATH_IDENTITY_MAX_DEPTH: usize = 8;
39
40static EXTERNAL_SIF_MEMORY_CACHE: OnceLock<Mutex<BTreeMap<String, OmenaSifV1>>> = OnceLock::new();
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct OmenaBridgeStyleResolutionSummaryV0 {
45    pub schema_version: &'static str,
46    pub product: &'static str,
47    pub owner_crate: &'static str,
48    pub resolver_name: &'static str,
49    pub supported_specifier_kinds: Vec<&'static str>,
50    pub candidate_extensions: Vec<&'static str>,
51    pub request_path_policy: Vec<&'static str>,
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct OmenaBridgeStyleResolutionInputsV0 {
57    pub package_manifests: Vec<OmenaResolverStylePackageManifestV0>,
58    pub tsconfig_path_mappings: Vec<OmenaResolverTsconfigPathMappingV0>,
59    pub bundler_path_mappings: Vec<OmenaResolverBundlerPathAliasMappingV0>,
60    #[serde(skip_serializing_if = "Vec::is_empty")]
61    pub disk_style_path_identities: Vec<OmenaResolverStyleModuleDiskCandidateIdentityV0>,
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct OmenaBridgeExternalSifCacheContextV0 {
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub freshness_fingerprint: Option<String>,
69}
70
71pub fn summarize_omena_bridge_style_resolution_boundary() -> OmenaBridgeStyleResolutionSummaryV0 {
72    OmenaBridgeStyleResolutionSummaryV0 {
73        schema_version: "0",
74        product: "omena-bridge.style-resolution",
75        owner_crate: "omena-bridge",
76        resolver_name: "style-import-specifier-resolver",
77        supported_specifier_kinds: vec![
78            "relative",
79            "tsconfigPaths",
80            "jsconfigPaths",
81            "bundlerAliases",
82            "npmPackages",
83            "packageImports",
84        ],
85        candidate_extensions: vec!["scss", "sass", "css", "less"],
86        request_path_policy: vec![
87            "resolverConsumesSourceUriWorkspaceUriAndRawSpecifier",
88            "relativeSpecifierExpandsStyleModuleCandidates",
89            "pathAliasResolutionUsesNearestWorkspaceTsconfigOrJsconfig",
90            "pathAliasResolutionFollowsRelativeTsconfigExtends",
91            "bundlerAliasResolutionUsesLiteralViteWebpackConfig",
92            "packageSpecifierResolutionUsesOmenaResolver",
93            "fileUriOutputIsPercentEncoded",
94            "lspServerOwnsOnlyDocumentRoutingAndUriRangeMapping",
95        ],
96    }
97}
98
99pub fn resolve_omena_bridge_style_uri_for_specifier(
100    source_uri: &str,
101    workspace_folder_uri: Option<&str>,
102    specifier: &str,
103) -> Option<String> {
104    resolve_omena_bridge_style_uri_for_specifier_with_package_manifests(
105        source_uri,
106        workspace_folder_uri,
107        specifier,
108        &[],
109    )
110}
111
112pub fn resolve_omena_bridge_style_uri_for_specifier_with_package_manifests(
113    source_uri: &str,
114    workspace_folder_uri: Option<&str>,
115    specifier: &str,
116    configured_package_manifests: &[OmenaResolverStylePackageManifestV0],
117) -> Option<String> {
118    let source_path = normalize_path(file_uri_to_path(source_uri)?);
119    let workspace_path = workspace_folder_uri
120        .and_then(file_uri_to_path)
121        .map(normalize_path);
122    let package_manifests = merged_package_manifests_for_request(
123        source_path.parent(),
124        workspace_path.as_deref(),
125        specifier,
126        configured_package_manifests,
127    );
128    let inputs = OmenaBridgeStyleResolutionInputsV0 {
129        package_manifests,
130        tsconfig_path_mappings: tsconfig_path_mappings_for_workspace(workspace_path.as_deref())
131            .unwrap_or_default(),
132        bundler_path_mappings: load_omena_bridge_workspace_bundler_path_alias_mappings(
133            workspace_path.as_deref(),
134        ),
135        disk_style_path_identities: workspace_path
136            .as_deref()
137            .map(workspace_style_path_identities)
138            .unwrap_or_default(),
139    };
140    resolve_omena_bridge_style_uri_for_specifier_with_resolution_inputs(
141        source_uri,
142        workspace_folder_uri,
143        specifier,
144        &inputs,
145    )
146}
147
148pub fn resolve_omena_bridge_style_uri_for_specifier_with_resolution_inputs(
149    source_uri: &str,
150    _workspace_folder_uri: Option<&str>,
151    specifier: &str,
152    resolution_inputs: &OmenaBridgeStyleResolutionInputsV0,
153) -> Option<String> {
154    let source_path = normalize_path(file_uri_to_path(source_uri)?);
155    let source_path_text = source_path.to_string_lossy().to_string();
156    let routing_specifier = normalize_omena_resolver_style_module_source_for_routing(specifier);
157    let requires_existing_candidate = (package_name_from_specifier(routing_specifier).is_some()
158        || is_package_import_specifier(routing_specifier))
159        && !resolution_inputs
160            .tsconfig_path_mappings
161            .iter()
162            .any(|mapping| {
163                tsconfig_path_pattern_matches(mapping.pattern.as_str(), routing_specifier)
164            })
165        && !resolution_inputs
166            .bundler_path_mappings
167            .iter()
168            .any(|mapping| {
169                bundler_path_alias_pattern_matches(mapping.pattern.as_str(), routing_specifier)
170            });
171    let candidates = collect_omena_resolver_style_module_source_candidates_with_path_mappings(
172        source_path_text.as_str(),
173        specifier,
174        resolution_inputs.package_manifests.as_slice(),
175        resolution_inputs.bundler_path_mappings.as_slice(),
176        resolution_inputs.tsconfig_path_mappings.as_slice(),
177    );
178
179    style_uri_for_resolver_candidates(
180        candidates.as_slice(),
181        resolution_inputs.disk_style_path_identities.as_slice(),
182        requires_existing_candidate,
183    )
184}
185
186/// Bridges the resolver→generator hop in-process: takes a resolved external
187/// style module entry (the `file://` URI returned by
188/// `resolve_omena_bridge_style_uri_for_specifier*`, or a plain filesystem
189/// path) and produces an [`OmenaSifV1`] by reading the entry's source and
190/// running the static SIF generator.
191///
192/// The returned SIF's `canonical_url` matches the resolved entry's `file://`
193/// URI so the query layer can pair it against import targets. The CLI converts
194/// each result into an `OmenaQueryExternalSifInputV0` without a JSON round-trip.
195///
196/// Errors gracefully (never panics) when the path is unresolvable, missing, or
197/// unreadable.
198pub fn generate_omena_bridge_sif_for_resolved_style_path(
199    resolved_path: &str,
200) -> Result<OmenaSifV1, String> {
201    generate_omena_bridge_sif_for_resolved_style_path_with_cache_context(
202        resolved_path,
203        &OmenaBridgeExternalSifCacheContextV0::default(),
204    )
205}
206
207pub fn generate_omena_bridge_lif_exports_for_resolved_style_path(
208    resolved_path: &str,
209) -> Result<OmenaLifExportsV1, String> {
210    let raw_path = raw_resolved_style_entry_path(resolved_path)
211        .ok_or_else(|| format!("unresolvable style module entry path: {resolved_path}"))?;
212    let path = normalize_path(raw_path);
213    let source = fs::read_to_string(path.as_path()).map_err(|error| {
214        format!(
215            "failed to read resolved style module {}: {error}",
216            path.to_string_lossy()
217        )
218    })?;
219    let syntax = infer_omena_bridge_sif_source_syntax(path.as_path());
220    Ok(generate_static_omena_lif_exports_v1(
221        OmenaSifStaticGeneratorInputV1 {
222            canonical_url: path_to_file_uri(path.as_path()).as_str(),
223            source: source.as_str(),
224            syntax,
225        },
226    ))
227}
228
229pub fn generate_omena_bridge_sif_for_resolved_style_path_with_cache_context(
230    resolved_path: &str,
231    cache_context: &OmenaBridgeExternalSifCacheContextV0,
232) -> Result<OmenaSifV1, String> {
233    let raw_path = raw_resolved_style_entry_path(resolved_path)
234        .ok_or_else(|| format!("unresolvable style module entry path: {resolved_path}"))?;
235    let path = normalize_path(raw_path.clone());
236    let canonical_url = path_to_file_uri(path.as_path());
237    let source_bytes = fs::read(path.as_path()).map_err(|error| {
238        format!(
239            "failed to read resolved style module {}: {error}",
240            path.to_string_lossy()
241        )
242    })?;
243    let source_hash = compute_omena_sif_leaf_hash_v1(source_bytes.as_slice())
244        .as_str()
245        .to_string();
246    let resolved_base_dir = path
247        .parent()
248        .map(|base_dir| base_dir.to_string_lossy().to_string())
249        .unwrap_or_default();
250    let cache_key = external_sif_cache_key(
251        source_hash.as_str(),
252        resolved_base_dir.as_str(),
253        canonical_url.as_str(),
254        cache_context.freshness_fingerprint.as_deref(),
255    );
256    if !external_sif_cache_kill_switch_engaged() {
257        if let Some(sif) = load_external_sif_from_memory_cache(cache_key.as_str()) {
258            return Ok(sif);
259        }
260        if let Some(cache_dir) = external_sif_cache_dir_for_path(raw_path.as_path())
261            && let Some(sif) = load_external_sif_cache_shard(
262                cache_dir.as_path(),
263                cache_key.as_str(),
264                canonical_url.as_str(),
265                source_hash.as_str(),
266                resolved_base_dir.as_str(),
267            )
268        {
269            store_external_sif_in_memory_cache(cache_key.clone(), sif.clone());
270            return Ok(sif);
271        }
272    }
273    let source = String::from_utf8(source_bytes).map_err(|error| {
274        format!(
275            "failed to decode resolved style module {} as utf-8: {error}",
276            path.to_string_lossy()
277        )
278    })?;
279    let syntax = infer_omena_bridge_sif_source_syntax(path.as_path());
280    let sif = generate_static_omena_sif_v1(OmenaSifStaticGeneratorInputV1 {
281        canonical_url: canonical_url.as_str(),
282        source: source.as_str(),
283        syntax,
284    })
285    .map_err(|error| format!("failed to generate SIF for {canonical_url}: {error}"))?;
286    if !external_sif_cache_kill_switch_engaged() {
287        store_external_sif_in_memory_cache(cache_key.clone(), sif.clone());
288        if let Some(cache_dir) = external_sif_cache_dir_for_path(raw_path.as_path()) {
289            store_external_sif_cache_shard(
290                cache_dir.as_path(),
291                cache_key.as_str(),
292                canonical_url.as_str(),
293                source_hash.as_str(),
294                resolved_base_dir.as_str(),
295                &sif,
296            );
297        }
298    }
299    Ok(sif)
300}
301
302fn raw_resolved_style_entry_path(resolved_path: &str) -> Option<PathBuf> {
303    let path = if resolved_path.starts_with("file://") {
304        file_uri_to_path(resolved_path)?
305    } else if resolved_path.is_empty() {
306        return None;
307    } else {
308        PathBuf::from(resolved_path)
309    };
310    Some(normalize_path_lexical(path))
311}
312
313fn external_sif_cache_key(
314    source_hash: &str,
315    resolved_base_dir: &str,
316    canonical_url: &str,
317    freshness_fingerprint: Option<&str>,
318) -> String {
319    let input = json!({
320        "schemaVersion": EXTERNAL_SIF_CACHE_SCHEMA_VERSION,
321        "product": "omena-bridge.external-sif-cache-key",
322        "crateVersion": env!("CARGO_PKG_VERSION"),
323        "sourceHash": source_hash,
324        "resolvedBaseDir": resolved_base_dir,
325        "canonicalUrl": canonical_url,
326        "freshnessFingerprint": freshness_fingerprint,
327    });
328    write_omena_canonical_json_bytes_v1(&input)
329        .map(|bytes| {
330            compute_omena_sif_leaf_hash_v1(bytes.as_slice())
331                .as_str()
332                .to_string()
333        })
334        .unwrap_or_else(|_| {
335            compute_omena_sif_leaf_hash_v1(
336                format!(
337                    "{source_hash}\0{resolved_base_dir}\0{canonical_url}\0{}",
338                    freshness_fingerprint.unwrap_or("")
339                )
340                .as_bytes(),
341            )
342            .as_str()
343            .to_string()
344        })
345}
346
347fn load_external_sif_from_memory_cache(key: &str) -> Option<OmenaSifV1> {
348    EXTERNAL_SIF_MEMORY_CACHE
349        .get_or_init(|| Mutex::new(BTreeMap::new()))
350        .lock()
351        .ok()?
352        .get(key)
353        .cloned()
354}
355
356fn store_external_sif_in_memory_cache(key: String, sif: OmenaSifV1) {
357    let Ok(mut cache) = EXTERNAL_SIF_MEMORY_CACHE
358        .get_or_init(|| Mutex::new(BTreeMap::new()))
359        .lock()
360    else {
361        return;
362    };
363    cache.insert(key, sif);
364    while cache.len() > EXTERNAL_SIF_CACHE_MAX_MEMORY_ENTRIES {
365        let Some(first_key) = cache.keys().next().cloned() else {
366            break;
367        };
368        cache.remove(first_key.as_str());
369    }
370}
371
372fn external_sif_cache_dir_for_path(path: &Path) -> Option<PathBuf> {
373    external_sif_cache_workspace_root(path).map(|root| root.join(EXTERNAL_SIF_CACHE_RELATIVE_DIR))
374}
375
376fn external_sif_cache_workspace_root(path: &Path) -> Option<PathBuf> {
377    let mut current = path.parent();
378    while let Some(dir) = current {
379        if dir.file_name().and_then(|value| value.to_str()) == Some("node_modules") {
380            return dir.parent().map(Path::to_path_buf);
381        }
382        current = dir.parent();
383    }
384
385    let mut current = path.parent();
386    while let Some(dir) = current {
387        if dir.join("package.json").is_file() {
388            return Some(dir.to_path_buf());
389        }
390        current = dir.parent();
391    }
392
393    path.parent().map(Path::to_path_buf)
394}
395
396fn external_sif_cache_shard_file_path(dir: &Path, key: &str) -> Option<PathBuf> {
397    let hex = key.strip_prefix("blake3:")?;
398    if hex.is_empty() || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
399        return None;
400    }
401    Some(dir.join(format!("{hex}.json")))
402}
403
404fn load_external_sif_cache_shard(
405    dir: &Path,
406    key: &str,
407    canonical_url: &str,
408    source_hash: &str,
409    resolved_base_dir: &str,
410) -> Option<OmenaSifV1> {
411    let shard_path = external_sif_cache_shard_file_path(dir, key)?;
412    let metadata = fs::metadata(shard_path.as_path()).ok()?;
413    if !metadata.is_file() || metadata.len() > EXTERNAL_SIF_CACHE_MAX_SHARD_BYTES {
414        let _ = fs::remove_file(shard_path.as_path());
415        return None;
416    }
417    let bytes = fs::read(shard_path.as_path()).ok()?;
418    let shard = serde_json::from_slice::<Value>(bytes.as_slice()).ok()?;
419    if !external_sif_cache_shard_matches(&shard, key, canonical_url, source_hash, resolved_base_dir)
420    {
421        let _ = fs::remove_file(shard_path.as_path());
422        return None;
423    }
424    let sif_json = shard.get("sifJson")?.as_str()?;
425    read_omena_sif_json_v1(sif_json).ok()
426}
427
428fn external_sif_cache_shard_matches(
429    shard: &Value,
430    key: &str,
431    canonical_url: &str,
432    source_hash: &str,
433    resolved_base_dir: &str,
434) -> bool {
435    shard.get("schemaVersion").and_then(Value::as_str) == Some(EXTERNAL_SIF_CACHE_SCHEMA_VERSION)
436        && shard.get("product").and_then(Value::as_str) == Some(EXTERNAL_SIF_CACHE_PRODUCT)
437        && shard.get("key").and_then(Value::as_str) == Some(key)
438        && shard.get("canonicalUrl").and_then(Value::as_str) == Some(canonical_url)
439        && shard.get("sourceHash").and_then(Value::as_str) == Some(source_hash)
440        && shard.get("resolvedBaseDir").and_then(Value::as_str) == Some(resolved_base_dir)
441        && shard.get("payloadDigest").and_then(Value::as_str)
442            == shard
443                .get("sifJson")
444                .and_then(Value::as_str)
445                .map(|sif_json| compute_omena_sif_leaf_hash_v1(sif_json.as_bytes()))
446                .as_ref()
447                .map(|digest| digest.as_str())
448}
449
450fn store_external_sif_cache_shard(
451    dir: &Path,
452    key: &str,
453    canonical_url: &str,
454    source_hash: &str,
455    resolved_base_dir: &str,
456    sif: &OmenaSifV1,
457) {
458    let Ok(sif_json) = write_omena_sif_json_v1(sif) else {
459        return;
460    };
461    let payload_digest = compute_omena_sif_leaf_hash_v1(sif_json.as_bytes())
462        .as_str()
463        .to_string();
464    let shard = json!({
465        "schemaVersion": EXTERNAL_SIF_CACHE_SCHEMA_VERSION,
466        "product": EXTERNAL_SIF_CACHE_PRODUCT,
467        "key": key,
468        "canonicalUrl": canonical_url,
469        "sourceHash": source_hash,
470        "resolvedBaseDir": resolved_base_dir,
471        "payloadDigest": payload_digest,
472        "sifJson": sif_json,
473    });
474    let Ok(bytes) = write_omena_canonical_json_bytes_v1(&shard) else {
475        return;
476    };
477    if bytes.len() as u64 > EXTERNAL_SIF_CACHE_MAX_SHARD_BYTES {
478        return;
479    }
480    if write_external_sif_cache_shard_atomically(dir, key, bytes.as_slice()).is_ok() {
481        enforce_external_sif_cache_caps(dir);
482    }
483}
484
485fn write_external_sif_cache_shard_atomically(
486    dir: &Path,
487    key: &str,
488    bytes: &[u8],
489) -> std::io::Result<()> {
490    fs::create_dir_all(dir)?;
491    let final_path = external_sif_cache_shard_file_path(dir, key).ok_or_else(|| {
492        std::io::Error::new(
493            std::io::ErrorKind::InvalidInput,
494            "invalid external SIF cache key",
495        )
496    })?;
497    let temporary_path = final_path.with_extension(format!("tmp-{}", std::process::id()));
498    fs::write(temporary_path.as_path(), bytes)?;
499    let renamed = fs::rename(temporary_path.as_path(), final_path.as_path());
500    if renamed.is_err() {
501        let _ = fs::remove_file(temporary_path.as_path());
502        if final_path.is_file() {
503            return Ok(());
504        }
505    }
506    renamed
507}
508
509fn enforce_external_sif_cache_caps(dir: &Path) {
510    let Ok(entries) = fs::read_dir(dir) else {
511        return;
512    };
513    let mut shards = entries
514        .flatten()
515        .filter_map(|entry| {
516            let path = entry.path();
517            if path.extension().and_then(|extension| extension.to_str()) != Some("json") {
518                return None;
519            }
520            let metadata = entry.metadata().ok()?;
521            if !metadata.is_file() {
522                return None;
523            }
524            let modified = metadata.modified().ok()?;
525            Some((modified, metadata.len(), path))
526        })
527        .collect::<Vec<_>>();
528    shards.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.2.cmp(&right.2)));
529    let mut total_bytes = shards.iter().map(|(_, bytes, _)| *bytes).sum::<u64>();
530    let mut shard_count = shards.len();
531    for (_, bytes, path) in shards {
532        if shard_count <= 1
533            || (shard_count <= EXTERNAL_SIF_CACHE_MAX_SHARDS
534                && total_bytes <= EXTERNAL_SIF_CACHE_MAX_TOTAL_BYTES)
535        {
536            break;
537        }
538        if fs::remove_file(path.as_path()).is_ok() {
539            shard_count -= 1;
540            total_bytes = total_bytes.saturating_sub(bytes);
541        }
542    }
543}
544
545fn external_sif_cache_kill_switch_engaged() -> bool {
546    std::env::var(EXTERNAL_SIF_CACHE_ENV_KILL_SWITCH)
547        .is_ok_and(|value| value.eq_ignore_ascii_case("off") || value == "0" || value == "false")
548}
549
550#[cfg(test)]
551fn clear_external_sif_memory_cache_for_test() {
552    if let Some(cache) = EXTERNAL_SIF_MEMORY_CACHE.get()
553        && let Ok(mut cache) = cache.lock()
554    {
555        cache.clear();
556    }
557}
558
559fn infer_omena_bridge_sif_source_syntax(path: &Path) -> OmenaSifSourceSyntaxV1 {
560    match path
561        .extension()
562        .and_then(|extension| extension.to_str())
563        .map(str::to_ascii_lowercase)
564        .as_deref()
565    {
566        Some("css") => OmenaSifSourceSyntaxV1::Css,
567        Some("sass") => OmenaSifSourceSyntaxV1::Sass,
568        Some("less") => OmenaSifSourceSyntaxV1::Less,
569        _ => OmenaSifSourceSyntaxV1::Scss,
570    }
571}
572
573pub fn load_omena_bridge_workspace_style_resolution_inputs(
574    workspace_folder_uri: Option<&str>,
575    configured_package_manifests: &[OmenaResolverStylePackageManifestV0],
576) -> OmenaBridgeStyleResolutionInputsV0 {
577    let workspace_path = workspace_folder_uri
578        .and_then(file_uri_to_path)
579        .map(normalize_path);
580    load_omena_bridge_workspace_style_resolution_inputs_from_path(
581        workspace_path.as_deref(),
582        configured_package_manifests,
583    )
584}
585
586fn load_omena_bridge_workspace_style_resolution_inputs_from_path(
587    workspace_path: Option<&Path>,
588    configured_package_manifests: &[OmenaResolverStylePackageManifestV0],
589) -> OmenaBridgeStyleResolutionInputsV0 {
590    OmenaBridgeStyleResolutionInputsV0 {
591        package_manifests: merge_package_manifest_lists(
592            configured_package_manifests,
593            workspace_package_manifests(workspace_path).as_slice(),
594        ),
595        tsconfig_path_mappings: tsconfig_path_mappings_for_workspace(workspace_path)
596            .unwrap_or_default(),
597        bundler_path_mappings: load_omena_bridge_workspace_bundler_path_alias_mappings(
598            workspace_path,
599        ),
600        disk_style_path_identities: workspace_path
601            .map(workspace_style_path_identities)
602            .unwrap_or_default(),
603    }
604}
605
606fn workspace_style_path_identities(
607    workspace_path: &Path,
608) -> Vec<OmenaResolverStyleModuleDiskCandidateIdentityV0> {
609    let mut identities = Vec::new();
610    let mut queue = VecDeque::from([workspace_path.to_path_buf()]);
611    while let Some(dir) = queue.pop_front() {
612        if identities.len() >= WORKSPACE_STYLE_PATH_IDENTITY_SCAN_LIMIT {
613            break;
614        }
615        let Ok(entries) = fs::read_dir(dir.as_path()) else {
616            continue;
617        };
618        for entry in entries.flatten() {
619            if identities.len() >= WORKSPACE_STYLE_PATH_IDENTITY_SCAN_LIMIT {
620                break;
621            }
622            let path = entry.path();
623            let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
624                continue;
625            };
626            if path.is_dir() {
627                let relative_depth = path
628                    .strip_prefix(workspace_path)
629                    .ok()
630                    .map(|relative| relative.components().count())
631                    .unwrap_or(usize::MAX);
632                if relative_depth > WORKSPACE_STYLE_PATH_IDENTITY_MAX_DEPTH {
633                    continue;
634                }
635                if should_skip_style_identity_scan_dir(file_name) {
636                    continue;
637                }
638                queue.push_back(path);
639                continue;
640            }
641            if !is_indexable_style_path(path.as_path()) {
642                continue;
643            }
644            let Some(metadata_identity) = file_metadata_identity(path.as_path()) else {
645                continue;
646            };
647            identities.push(OmenaResolverStyleModuleDiskCandidateIdentityV0 {
648                style_path: normalize_path(path).to_string_lossy().to_string(),
649                metadata_identity,
650            });
651        }
652    }
653    identities.sort_by(|left, right| left.style_path.cmp(&right.style_path));
654    identities.dedup_by(|left, right| left.style_path == right.style_path);
655    identities
656}
657
658fn should_skip_style_identity_scan_dir(name: &str) -> bool {
659    matches!(
660        name,
661        ".git" | ".next" | ".nuxt" | ".svelte-kit" | "coverage" | "target"
662    )
663}
664
665fn file_metadata_identity(path: &Path) -> Option<String> {
666    let metadata = fs::symlink_metadata(path).ok()?;
667    let modified = metadata
668        .modified()
669        .ok()
670        .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
671        .map(|duration| format!("{}.{:09}", duration.as_secs(), duration.subsec_nanos()))
672        .unwrap_or_else(|| "unknownMtime".to_string());
673    let file_type = if metadata.file_type().is_symlink() {
674        "symlink"
675    } else if metadata.is_file() {
676        "file"
677    } else {
678        "other"
679    };
680    Some(format!("{file_type}|len{}|mtime{modified}", metadata.len()))
681}
682
683fn merge_package_manifest_lists(
684    primary: &[OmenaResolverStylePackageManifestV0],
685    secondary: &[OmenaResolverStylePackageManifestV0],
686) -> Vec<OmenaResolverStylePackageManifestV0> {
687    let mut manifests = primary.to_vec();
688    let mut seen = manifests
689        .iter()
690        .map(|manifest| manifest.package_json_path.clone())
691        .collect::<BTreeSet<_>>();
692    for manifest in secondary {
693        if seen.insert(manifest.package_json_path.clone()) {
694            manifests.push(manifest.clone());
695        }
696    }
697    manifests
698}
699
700fn merged_package_manifests_for_specifier(
701    source_dir: Option<&Path>,
702    specifier: &str,
703    configured_package_manifests: &[OmenaResolverStylePackageManifestV0],
704) -> Vec<OmenaResolverStylePackageManifestV0> {
705    merge_package_manifest_lists(
706        configured_package_manifests,
707        package_manifests_for_specifier(source_dir, specifier)
708            .unwrap_or_default()
709            .as_slice(),
710    )
711}
712
713fn merged_package_manifests_for_request(
714    source_dir: Option<&Path>,
715    workspace_path: Option<&Path>,
716    specifier: &str,
717    configured_package_manifests: &[OmenaResolverStylePackageManifestV0],
718) -> Vec<OmenaResolverStylePackageManifestV0> {
719    let source_manifests =
720        merged_package_manifests_for_specifier(source_dir, specifier, configured_package_manifests);
721    merge_package_manifest_lists(
722        source_manifests.as_slice(),
723        workspace_package_manifests(workspace_path).as_slice(),
724    )
725}
726
727fn tsconfig_path_mappings_for_workspace(
728    workspace_path: Option<&Path>,
729) -> Option<Vec<OmenaResolverTsconfigPathMappingV0>> {
730    let workspace_path = workspace_path?;
731    let mut mappings = Vec::new();
732    for config_path in [
733        workspace_path.join("tsconfig.json"),
734        workspace_path.join("jsconfig.json"),
735    ] {
736        mappings.extend(tsconfig_path_mappings_for_config(config_path.as_path()));
737    }
738    Some(mappings)
739}
740
741fn tsconfig_path_mappings_for_config(
742    config_path: &Path,
743) -> Vec<OmenaResolverTsconfigPathMappingV0> {
744    tsconfig_path_mappings_for_config_with_seen(config_path, &mut BTreeSet::new())
745}
746
747fn tsconfig_path_mappings_for_config_with_seen(
748    config_path: &Path,
749    seen: &mut BTreeSet<PathBuf>,
750) -> Vec<OmenaResolverTsconfigPathMappingV0> {
751    let normalized_config_path = normalize_path(config_path.to_path_buf());
752    if !seen.insert(normalized_config_path.clone()) {
753        return Vec::new();
754    }
755    let Some(config_text) = fs::read_to_string(config_path).ok() else {
756        return Vec::new();
757    };
758    let Some(config) = serde_json::from_str::<Value>(config_text.as_str()).ok() else {
759        return Vec::new();
760    };
761    let own_mappings = tsconfig_path_mappings_from_value(config_path, &config).unwrap_or_default();
762    if !own_mappings.is_empty() {
763        return own_mappings;
764    }
765    resolve_tsconfig_extends_path(config_path, &config)
766        .map(|extends_path| {
767            tsconfig_path_mappings_for_config_with_seen(extends_path.as_path(), seen)
768        })
769        .unwrap_or_default()
770}
771
772fn tsconfig_path_mappings_from_value(
773    config_path: &Path,
774    config: &Value,
775) -> Option<Vec<OmenaResolverTsconfigPathMappingV0>> {
776    let compiler_options = config.get("compilerOptions")?;
777    let paths = compiler_options.get("paths")?.as_object()?;
778    let config_dir = config_path.parent()?;
779    let base_url = compiler_options
780        .get("baseUrl")
781        .and_then(Value::as_str)
782        .unwrap_or(".");
783    let base_path = normalize_path(config_dir.join(base_url));
784    let mut mappings = Vec::new();
785    for (pattern, targets) in paths {
786        let Some(targets) = targets.as_array() else {
787            continue;
788        };
789        let target_patterns = targets
790            .iter()
791            .filter_map(Value::as_str)
792            .map(ToString::to_string)
793            .collect::<Vec<_>>();
794        if target_patterns.is_empty() {
795            continue;
796        }
797        mappings.push(OmenaResolverTsconfigPathMappingV0 {
798            base_path: base_path.to_string_lossy().to_string(),
799            pattern: pattern.to_string(),
800            target_patterns,
801        });
802    }
803    Some(mappings)
804}
805
806fn resolve_tsconfig_extends_path(config_path: &Path, config: &Value) -> Option<PathBuf> {
807    let extends = config.get("extends")?.as_str()?;
808    if !extends.starts_with('.') {
809        return None;
810    }
811    let config_dir = config_path.parent()?;
812    let raw_path = config_dir.join(extends);
813    tsconfig_extends_candidates(raw_path)
814        .into_iter()
815        .find(|candidate| candidate.exists())
816}
817
818fn tsconfig_extends_candidates(path: PathBuf) -> Vec<PathBuf> {
819    if path.extension().is_some() {
820        return vec![path];
821    }
822    vec![path.with_extension("json"), path.join("tsconfig.json")]
823}
824
825fn package_manifests_for_specifier(
826    source_dir: Option<&Path>,
827    specifier: &str,
828) -> Option<Vec<OmenaResolverStylePackageManifestV0>> {
829    if is_package_import_specifier(specifier) {
830        return Some(package_scope_manifests_for_source_dir(source_dir));
831    }
832    let package_name = package_name_from_specifier(specifier)?;
833    let mut manifests = Vec::new();
834    let mut seen = BTreeSet::new();
835    let mut current_dir = source_dir;
836    while let Some(dir) = current_dir {
837        let package_json_path = dir
838            .join("node_modules")
839            .join(package_name)
840            .join("package.json");
841        if seen.insert(package_json_path.clone())
842            && let Ok(package_json_source) = fs::read_to_string(package_json_path.as_path())
843        {
844            manifests.push(OmenaResolverStylePackageManifestV0 {
845                package_json_path: normalize_path(package_json_path)
846                    .to_string_lossy()
847                    .to_string(),
848                package_json_source,
849            });
850        }
851        current_dir = dir.parent();
852    }
853    Some(manifests)
854}
855
856fn package_scope_manifests_for_source_dir(
857    source_dir: Option<&Path>,
858) -> Vec<OmenaResolverStylePackageManifestV0> {
859    let mut manifests = Vec::new();
860    let mut current_dir = source_dir;
861    while let Some(dir) = current_dir {
862        push_workspace_package_manifest(dir.join("package.json"), &mut manifests);
863        current_dir = dir.parent();
864    }
865    manifests
866}
867
868fn workspace_package_manifests(
869    workspace_path: Option<&Path>,
870) -> Vec<OmenaResolverStylePackageManifestV0> {
871    let Some(workspace_path) = workspace_path else {
872        return Vec::new();
873    };
874    let mut manifests = Vec::new();
875    push_workspace_package_manifest(workspace_path.join("package.json"), &mut manifests);
876
877    let node_modules = workspace_path.join("node_modules");
878    let Ok(entries) = fs::read_dir(node_modules.as_path()) else {
879        return manifests;
880    };
881    for entry in entries.flatten() {
882        if manifests.len() >= WORKSPACE_PACKAGE_MANIFEST_SCAN_LIMIT {
883            break;
884        }
885        let path = entry.path();
886        let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
887            continue;
888        };
889        if file_name.starts_with('@') {
890            push_scoped_workspace_package_manifests(path.as_path(), &mut manifests);
891        } else {
892            push_workspace_package_manifest(path.join("package.json"), &mut manifests);
893        }
894    }
895    manifests.sort_by(|left, right| left.package_json_path.cmp(&right.package_json_path));
896    manifests.dedup_by(|left, right| left.package_json_path == right.package_json_path);
897    manifests
898}
899
900fn push_scoped_workspace_package_manifests(
901    scope_path: &Path,
902    manifests: &mut Vec<OmenaResolverStylePackageManifestV0>,
903) {
904    let Ok(entries) = fs::read_dir(scope_path) else {
905        return;
906    };
907    for entry in entries.flatten() {
908        if manifests.len() >= WORKSPACE_PACKAGE_MANIFEST_SCAN_LIMIT {
909            return;
910        }
911        push_workspace_package_manifest(entry.path().join("package.json"), manifests);
912    }
913}
914
915fn push_workspace_package_manifest(
916    package_json_path: PathBuf,
917    manifests: &mut Vec<OmenaResolverStylePackageManifestV0>,
918) {
919    if manifests.len() >= WORKSPACE_PACKAGE_MANIFEST_SCAN_LIMIT {
920        return;
921    }
922    let normalized_package_json_path = normalize_path(package_json_path);
923    let package_json_path_text = normalized_package_json_path.to_string_lossy().to_string();
924    if manifests
925        .iter()
926        .any(|manifest| manifest.package_json_path == package_json_path_text)
927    {
928        return;
929    }
930    let Ok(package_json_source) = fs::read_to_string(normalized_package_json_path.as_path()) else {
931        return;
932    };
933    manifests.push(OmenaResolverStylePackageManifestV0 {
934        package_json_path: package_json_path_text,
935        package_json_source,
936    });
937}
938
939fn package_name_from_specifier(specifier: &str) -> Option<&str> {
940    let specifier = specifier.strip_prefix("pkg:").unwrap_or(specifier);
941    if specifier.starts_with('.')
942        || specifier.starts_with('/')
943        || is_package_import_specifier(specifier)
944        || is_external_style_specifier(specifier)
945    {
946        return None;
947    }
948    if specifier.starts_with('@') {
949        let mut segments = specifier.splitn(3, '/');
950        let scope = segments.next()?;
951        let package = segments.next()?;
952        if scope.len() <= 1 || package.is_empty() {
953            return None;
954        }
955        return specifier.get(..scope.len() + 1 + package.len());
956    }
957    specifier.split('/').next().filter(|name| !name.is_empty())
958}
959
960fn is_package_import_specifier(specifier: &str) -> bool {
961    specifier
962        .strip_prefix("pkg:")
963        .unwrap_or(specifier)
964        .starts_with('#')
965}
966
967fn tsconfig_path_pattern_matches(pattern: &str, specifier: &str) -> bool {
968    if let Some((prefix, suffix)) = pattern.split_once('*') {
969        return !suffix.contains('*')
970            && specifier.starts_with(prefix)
971            && specifier.ends_with(suffix)
972            && specifier.len() >= prefix.len() + suffix.len();
973    }
974    pattern == specifier
975}
976
977fn bundler_path_alias_pattern_matches(pattern: &str, specifier: &str) -> bool {
978    if pattern.is_empty() {
979        return false;
980    }
981    if let Some(exact_pattern) = pattern.strip_suffix('$') {
982        return specifier == exact_pattern;
983    }
984    if pattern == specifier {
985        return true;
986    }
987    let prefix = if pattern.ends_with('/') {
988        pattern.to_string()
989    } else {
990        format!("{pattern}/")
991    };
992    specifier.starts_with(prefix.as_str())
993}
994
995fn is_external_style_specifier(specifier: &str) -> bool {
996    specifier.starts_with("sass:")
997        || specifier.starts_with("http://")
998        || specifier.starts_with("https://")
999}
1000
1001fn style_uri_for_resolver_candidates(
1002    candidates: &[String],
1003    disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
1004    requires_existing_candidate: bool,
1005) -> Option<String> {
1006    let empty_available = BTreeSet::new();
1007    let confirmation = confirm_omena_resolver_style_module_candidate_with_options(
1008        candidates,
1009        &empty_available,
1010        disk_style_path_identities,
1011        OmenaResolverStyleModuleConfirmationOptionsV0 {
1012            allow_disk_confirmation: true,
1013            allow_live_disk_confirmation: true,
1014            allow_unconfirmed_indexable_candidate: !requires_existing_candidate,
1015            ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
1016        },
1017    );
1018    confirmation
1019        .resolved_style_path
1020        .map(PathBuf::from)
1021        .map(|path| path_to_file_uri(normalize_path(path).as_path()))
1022}
1023
1024fn is_indexable_style_path(path: &Path) -> bool {
1025    is_omena_resolver_indexable_style_module_path(path.to_string_lossy().as_ref())
1026}
1027
1028fn file_uri_to_path(uri: &str) -> Option<PathBuf> {
1029    let raw_path = uri.strip_prefix("file://")?;
1030    Some(PathBuf::from(percent_decode_uri_path(raw_path)?))
1031}
1032
1033fn percent_decode_uri_path(raw_path: &str) -> Option<String> {
1034    let bytes = raw_path.as_bytes();
1035    let mut decoded = Vec::with_capacity(bytes.len());
1036    let mut index = 0usize;
1037    while index < bytes.len() {
1038        if bytes[index] == b'%' {
1039            let high = bytes.get(index + 1).and_then(|byte| hex_value(*byte))?;
1040            let low = bytes.get(index + 2).and_then(|byte| hex_value(*byte))?;
1041            decoded.push((high << 4) | low);
1042            index += 3;
1043        } else {
1044            decoded.push(bytes[index]);
1045            index += 1;
1046        }
1047    }
1048    String::from_utf8(decoded).ok()
1049}
1050
1051fn hex_value(byte: u8) -> Option<u8> {
1052    match byte {
1053        b'0'..=b'9' => Some(byte - b'0'),
1054        b'a'..=b'f' => Some(byte - b'a' + 10),
1055        b'A'..=b'F' => Some(byte - b'A' + 10),
1056        _ => None,
1057    }
1058}
1059
1060fn path_to_file_uri(path: &Path) -> String {
1061    let path = normalize_path(path.to_path_buf());
1062    format!(
1063        "file://{}",
1064        percent_encode_uri_path(path.to_string_lossy().as_ref())
1065    )
1066}
1067
1068fn percent_encode_uri_path(path: &str) -> String {
1069    let mut encoded = String::with_capacity(path.len());
1070    for byte in path.as_bytes() {
1071        match *byte {
1072            b'A'..=b'Z'
1073            | b'a'..=b'z'
1074            | b'0'..=b'9'
1075            | b'-'
1076            | b'.'
1077            | b'_'
1078            | b'~'
1079            | b'/'
1080            | b'@'
1081            | b':'
1082            | b'!'
1083            | b'$'
1084            | b'&'
1085            | b'\''
1086            | b'*'
1087            | b'+'
1088            | b','
1089            | b';'
1090            | b'=' => encoded.push(*byte as char),
1091            _ => encoded.push_str(format!("%{byte:02X}").as_str()),
1092        }
1093    }
1094    encoded
1095}
1096
1097fn normalize_path(path: PathBuf) -> PathBuf {
1098    if let Some(canonical) = canonicalize_existing_path_or_parent(path.as_path()) {
1099        return normalize_path_lexical(canonical);
1100    }
1101    normalize_path_lexical(path)
1102}
1103
1104fn canonicalize_existing_path_or_parent(path: &Path) -> Option<PathBuf> {
1105    if let Ok(canonical) = fs::canonicalize(path) {
1106        return Some(canonical);
1107    }
1108
1109    let mut current = path.to_path_buf();
1110    let mut suffix = Vec::<OsString>::new();
1111    while let Some(parent) = current.parent() {
1112        if let Some(file_name) = current.file_name() {
1113            suffix.push(file_name.to_os_string());
1114        }
1115        if let Ok(mut canonical_parent) = fs::canonicalize(parent) {
1116            for segment in suffix.iter().rev() {
1117                canonical_parent.push(segment);
1118            }
1119            return Some(canonical_parent);
1120        }
1121        current = parent.to_path_buf();
1122    }
1123    None
1124}
1125
1126fn normalize_path_lexical(path: PathBuf) -> PathBuf {
1127    let mut normalized = PathBuf::new();
1128    for component in path.components() {
1129        match component {
1130            Component::CurDir => {}
1131            Component::ParentDir => {
1132                normalized.pop();
1133            }
1134            Component::Normal(_) | Component::RootDir | Component::Prefix(_) => {
1135                normalized.push(component.as_os_str());
1136            }
1137        }
1138    }
1139    normalized
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144    use std::{fs, time::SystemTime};
1145
1146    use super::*;
1147
1148    #[test]
1149    fn resolves_relative_style_candidates() -> Result<(), Box<dyn std::error::Error>> {
1150        let root = temp_dir("omena_bridge_style_relative")?;
1151        let source = root.join("src/App.tsx");
1152        let style = root.join("src/Button.module.scss");
1153        fs::create_dir_all(
1154            source
1155                .parent()
1156                .ok_or_else(|| std::io::Error::other("parent"))?,
1157        )?;
1158        fs::write(&source, "")?;
1159        fs::write(&style, ".root {}")?;
1160
1161        let uri = resolve_omena_bridge_style_uri_for_specifier(
1162            path_to_file_uri(source.as_path()).as_str(),
1163            Some(path_to_file_uri(root.as_path()).as_str()),
1164            "./Button.module.scss",
1165        );
1166
1167        assert_eq!(
1168            uri.as_deref(),
1169            Some(path_to_file_uri(style.as_path()).as_str())
1170        );
1171        let _ = fs::remove_dir_all(root);
1172        Ok(())
1173    }
1174
1175    #[test]
1176    fn generates_sif_for_resolved_relative_style_module() -> Result<(), Box<dyn std::error::Error>>
1177    {
1178        let root = temp_dir("omena_bridge_sif_resolved")?;
1179        let source = root.join("src/App.tsx");
1180        let style = root.join("src/theme.scss");
1181        fs::create_dir_all(
1182            style
1183                .parent()
1184                .ok_or_else(|| std::io::Error::other("parent"))?,
1185        )?;
1186        fs::write(&source, "")?;
1187        fs::write(&style, "$brand: #0af;\n@mixin focus-ring {}\n")?;
1188
1189        let resolved = resolve_omena_bridge_style_uri_for_specifier(
1190            path_to_file_uri(source.as_path()).as_str(),
1191            Some(path_to_file_uri(root.as_path()).as_str()),
1192            "./theme.scss",
1193        )
1194        .ok_or_else(|| std::io::Error::other("resolution failed"))?;
1195
1196        let sif = generate_omena_bridge_sif_for_resolved_style_path(resolved.as_str())?;
1197
1198        assert_eq!(sif.canonical_url, resolved);
1199        assert_eq!(sif.source.syntax, OmenaSifSourceSyntaxV1::Scss);
1200        assert!(
1201            sif.exports
1202                .variables
1203                .iter()
1204                .any(|variable| variable.name == "$brand"),
1205            "expected $brand variable export, got {:?}",
1206            sif.exports.variables
1207        );
1208        assert!(
1209            sif.exports
1210                .mixins
1211                .iter()
1212                .any(|mixin| mixin.name == "focus-ring"),
1213            "expected focus-ring mixin export, got {:?}",
1214            sif.exports.mixins
1215        );
1216        // The produced SIF must round-trip through the exact JSON contract the
1217        // CLI's `read_external_sifs` consumes, proving it is a valid artifact.
1218        let json = omena_sif::write_omena_sif_json_v1(&sif)?;
1219        let parsed = omena_sif::read_omena_sif_json_v1(json.as_str())?;
1220        assert_eq!(parsed, sif);
1221        let _ = fs::remove_dir_all(root);
1222        Ok(())
1223    }
1224
1225    #[test]
1226    fn generates_lif_exports_for_resolved_less_specifier() -> Result<(), Box<dyn std::error::Error>>
1227    {
1228        let root = temp_dir("omena_bridge_lif_resolved_less")?;
1229        let source = root.join("src/App.tsx");
1230        let style = root.join("src/tokens.less");
1231        fs::create_dir_all(
1232            style
1233                .parent()
1234                .ok_or_else(|| std::io::Error::other("parent"))?,
1235        )?;
1236        fs::write(&source, "")?;
1237        fs::write(
1238            &style,
1239            "@brand: #fff;\n@tokens: { primary: @brand; @gap: 2px; };\n.button(@gap: 1rem) when (@gap > 0) { color: @brand; }\n",
1240        )?;
1241
1242        let resolved = resolve_omena_bridge_style_uri_for_specifier(
1243            path_to_file_uri(source.as_path()).as_str(),
1244            Some(path_to_file_uri(root.as_path()).as_str()),
1245            "./tokens.less",
1246        )
1247        .ok_or_else(|| std::io::Error::other("Less resolution failed"))?;
1248
1249        let exports = generate_omena_bridge_lif_exports_for_resolved_style_path(resolved.as_str())?;
1250
1251        assert_eq!(exports.less_variables[0].name, "@brand");
1252        assert_eq!(
1253            exports.less_variables[0].value_repr.as_deref(),
1254            Some("#fff")
1255        );
1256        assert_eq!(exports.less_mixins[0].name, ".button");
1257        assert!(exports.less_mixins[0].guarded);
1258        assert_eq!(exports.less_detached_rulesets[0].name, "@tokens");
1259        assert_eq!(
1260            exports.less_detached_rulesets[0].member_names,
1261            vec!["@gap", "primary"]
1262        );
1263        let _ = fs::remove_dir_all(root);
1264        Ok(())
1265    }
1266
1267    #[test]
1268    fn generates_sif_from_plain_resolved_path() -> Result<(), Box<dyn std::error::Error>> {
1269        let root = temp_dir("omena_bridge_sif_plain")?;
1270        let style = root.join("tokens.sass");
1271        fs::write(&style, "$gap: 8px\n")?;
1272
1273        let sif =
1274            generate_omena_bridge_sif_for_resolved_style_path(style.to_string_lossy().as_ref())?;
1275
1276        assert_eq!(sif.source.syntax, OmenaSifSourceSyntaxV1::Sass);
1277        let _ = fs::remove_dir_all(root);
1278        Ok(())
1279    }
1280
1281    #[test]
1282    fn generates_sif_with_less_source_syntax() -> Result<(), Box<dyn std::error::Error>> {
1283        let root = temp_dir("omena_bridge_sif_less")?;
1284        let style = root.join("tokens.less");
1285        fs::write(&style, "@gap: 8px;\n.button { margin: @gap; }\n")?;
1286
1287        let sif =
1288            generate_omena_bridge_sif_for_resolved_style_path(style.to_string_lossy().as_ref())?;
1289
1290        assert_eq!(sif.source.syntax, OmenaSifSourceSyntaxV1::Less);
1291        let json = omena_sif::write_omena_sif_json_v1(&sif)?;
1292        assert!(json.contains(r#""syntax":"less""#));
1293        let _ = fs::remove_dir_all(root);
1294        Ok(())
1295    }
1296
1297    #[test]
1298    fn generates_lif_exports_for_resolved_less_path() -> Result<(), Box<dyn std::error::Error>> {
1299        let root = temp_dir("omena_bridge_lif_less")?;
1300        let style = root.join("tokens.less");
1301        fs::write(
1302            &style,
1303            "@brand: red;\n@tokens: { primary: @brand; };\n.button(@gap: 1rem) { color: @brand; }\n",
1304        )?;
1305
1306        let exports = generate_omena_bridge_lif_exports_for_resolved_style_path(
1307            style.to_string_lossy().as_ref(),
1308        )?;
1309
1310        assert_eq!(exports.less_variables[0].name, "@brand");
1311        assert_eq!(exports.less_mixins[0].name, ".button");
1312        assert_eq!(exports.less_detached_rulesets[0].name, "@tokens");
1313        assert_eq!(
1314            exports.less_detached_rulesets[0].member_names,
1315            vec!["primary"]
1316        );
1317        let _ = fs::remove_dir_all(root);
1318        Ok(())
1319    }
1320
1321    #[test]
1322    fn external_sif_cache_key_is_base_dir_sensitive_and_serves_fresh_sif()
1323    -> Result<(), Box<dyn std::error::Error>> {
1324        let root = temp_dir("omena_bridge_external_sif_cache")?;
1325        let first_dir = root.join("node_modules/design-a");
1326        let second_dir = root.join("node_modules/design-b");
1327        fs::create_dir_all(first_dir.as_path())?;
1328        fs::create_dir_all(second_dir.as_path())?;
1329        fs::write(root.join("package.json"), r#"{"name":"workspace"}"#)?;
1330        let first_style = first_dir.join("tokens.scss");
1331        let second_style = second_dir.join("tokens.scss");
1332        let source = "$brand: #0af;\n";
1333        fs::write(first_style.as_path(), source)?;
1334        fs::write(second_style.as_path(), source)?;
1335
1336        let first_path = normalize_path(first_style.clone());
1337        let second_path = normalize_path(second_style.clone());
1338        let source_hash = compute_omena_sif_leaf_hash_v1(source.as_bytes())
1339            .as_str()
1340            .to_string();
1341        let first_base_dir = first_path
1342            .parent()
1343            .ok_or_else(|| std::io::Error::other("first parent"))?
1344            .to_string_lossy()
1345            .to_string();
1346        let second_base_dir = second_path
1347            .parent()
1348            .ok_or_else(|| std::io::Error::other("second parent"))?
1349            .to_string_lossy()
1350            .to_string();
1351        let first_key = external_sif_cache_key(
1352            source_hash.as_str(),
1353            first_base_dir.as_str(),
1354            path_to_file_uri(first_path.as_path()).as_str(),
1355            None,
1356        );
1357        let second_key = external_sif_cache_key(
1358            source_hash.as_str(),
1359            second_base_dir.as_str(),
1360            path_to_file_uri(second_path.as_path()).as_str(),
1361            None,
1362        );
1363        assert_ne!(
1364            first_key, second_key,
1365            "same bytes under different resolved bases must not share an external SIF cache key"
1366        );
1367        let old_fingerprint_key = external_sif_cache_key(
1368            source_hash.as_str(),
1369            first_base_dir.as_str(),
1370            path_to_file_uri(first_path.as_path()).as_str(),
1371            Some("lockfile:old"),
1372        );
1373        let new_fingerprint_key = external_sif_cache_key(
1374            source_hash.as_str(),
1375            first_base_dir.as_str(),
1376            path_to_file_uri(first_path.as_path()).as_str(),
1377            Some("lockfile:new"),
1378        );
1379        assert_ne!(
1380            old_fingerprint_key, new_fingerprint_key,
1381            "lockfile or package-manager freshness changes must invalidate external SIF cache keys"
1382        );
1383
1384        let first_uri = path_to_file_uri(first_style.as_path());
1385        let fresh = generate_omena_bridge_sif_for_resolved_style_path(first_uri.as_str())?;
1386        clear_external_sif_memory_cache_for_test();
1387        let cached = generate_omena_bridge_sif_for_resolved_style_path(first_uri.as_str())?;
1388        assert_eq!(cached, fresh);
1389        let cache_dir = external_sif_cache_dir_for_path(first_style.as_path())
1390            .ok_or_else(|| std::io::Error::other("cache dir"))?;
1391        assert!(
1392            cache_dir.read_dir()?.flatten().any(|entry| entry
1393                .path()
1394                .extension()
1395                .and_then(|ext| ext.to_str())
1396                == Some("json")),
1397            "expected a disk external SIF cache shard in {}",
1398            cache_dir.display()
1399        );
1400        let _ = fs::remove_dir_all(root);
1401        Ok(())
1402    }
1403
1404    #[test]
1405    fn errors_gracefully_for_missing_resolved_style_module() {
1406        let missing = std::env::temp_dir().join("omena_bridge_sif_missing/does-not-exist.scss");
1407        let result =
1408            generate_omena_bridge_sif_for_resolved_style_path(missing.to_string_lossy().as_ref());
1409        assert!(result.is_err(), "expected error for missing entry");
1410    }
1411
1412    #[test]
1413    fn errors_gracefully_for_empty_resolved_path() {
1414        let result = generate_omena_bridge_sif_for_resolved_style_path("");
1415        assert!(result.is_err(), "expected error for empty path");
1416    }
1417
1418    #[test]
1419    fn resolves_tsconfig_path_alias_style_candidates() -> Result<(), Box<dyn std::error::Error>> {
1420        let root = temp_dir("omena_bridge_style_alias")?;
1421        let source = root.join("src/App.tsx");
1422        let style = root.join("src/styles/Button.module.scss");
1423        fs::create_dir_all(
1424            style
1425                .parent()
1426                .ok_or_else(|| std::io::Error::other("parent"))?,
1427        )?;
1428        fs::write(&source, "")?;
1429        fs::write(&style, ".root {}")?;
1430        fs::write(
1431            root.join("tsconfig.json"),
1432            r#"{"compilerOptions":{"baseUrl":".","paths":{"@styles/*":["src/styles/*"]}}}"#,
1433        )?;
1434
1435        let uri = resolve_omena_bridge_style_uri_for_specifier(
1436            path_to_file_uri(source.as_path()).as_str(),
1437            Some(path_to_file_uri(root.as_path()).as_str()),
1438            "@styles/Button.module.scss",
1439        );
1440
1441        assert_eq!(
1442            uri.as_deref(),
1443            Some(path_to_file_uri(style.as_path()).as_str())
1444        );
1445        let _ = fs::remove_dir_all(root);
1446        Ok(())
1447    }
1448
1449    #[test]
1450    fn resolves_tsconfig_extends_path_alias_style_candidates()
1451    -> Result<(), Box<dyn std::error::Error>> {
1452        let root = temp_dir("omena_bridge_style_alias_extends")?;
1453        let source = root.join("src/App.tsx");
1454        let style = root.join("src/shared/Button.module.scss");
1455        let config_dir = root.join("config");
1456        fs::create_dir_all(
1457            style
1458                .parent()
1459                .ok_or_else(|| std::io::Error::other("parent"))?,
1460        )?;
1461        fs::create_dir_all(config_dir.as_path())?;
1462        fs::write(&source, "")?;
1463        fs::write(&style, ".root {}")?;
1464        fs::write(
1465            config_dir.join("base.json"),
1466            r#"{"compilerOptions":{"baseUrl":"..","paths":{"$shared/*":["src/shared/*"]}}}"#,
1467        )?;
1468        fs::write(root.join("tsconfig.json"), r#"{"extends":"./config/base"}"#)?;
1469
1470        let uri = resolve_omena_bridge_style_uri_for_specifier(
1471            path_to_file_uri(source.as_path()).as_str(),
1472            Some(path_to_file_uri(root.as_path()).as_str()),
1473            "$shared/Button.module.scss",
1474        );
1475
1476        assert_eq!(
1477            uri.as_deref(),
1478            Some(path_to_file_uri(style.as_path()).as_str())
1479        );
1480        let _ = fs::remove_dir_all(root);
1481        Ok(())
1482    }
1483
1484    #[test]
1485    fn tsconfig_extends_child_paths_override_parent_paths() -> Result<(), Box<dyn std::error::Error>>
1486    {
1487        let root = temp_dir("omena_bridge_style_alias_extends_override")?;
1488        let source = root.join("src/App.tsx");
1489        let parent_style = root.join("src/parent/Button.module.scss");
1490        let child_style = root.join("src/child/Button.module.scss");
1491        fs::create_dir_all(
1492            parent_style
1493                .parent()
1494                .ok_or_else(|| std::io::Error::other("parent"))?,
1495        )?;
1496        fs::create_dir_all(
1497            child_style
1498                .parent()
1499                .ok_or_else(|| std::io::Error::other("child"))?,
1500        )?;
1501        fs::write(&source, "")?;
1502        fs::write(&parent_style, ".root { color: red; }")?;
1503        fs::write(&child_style, ".root { color: green; }")?;
1504        fs::write(
1505            root.join("base.json"),
1506            r#"{"compilerOptions":{"baseUrl":".","paths":{"$shared/*":["src/parent/*"]}}}"#,
1507        )?;
1508        fs::write(
1509            root.join("tsconfig.json"),
1510            r#"{"extends":"./base.json","compilerOptions":{"baseUrl":".","paths":{"$shared/*":["src/child/*"]}}}"#,
1511        )?;
1512
1513        let uri = resolve_omena_bridge_style_uri_for_specifier(
1514            path_to_file_uri(source.as_path()).as_str(),
1515            Some(path_to_file_uri(root.as_path()).as_str()),
1516            "$shared/Button.module.scss",
1517        );
1518
1519        assert_eq!(
1520            uri.as_deref(),
1521            Some(path_to_file_uri(child_style.as_path()).as_str())
1522        );
1523        let _ = fs::remove_dir_all(root);
1524        Ok(())
1525    }
1526
1527    #[test]
1528    fn resolves_vite_bundler_alias_style_candidates() -> Result<(), Box<dyn std::error::Error>> {
1529        let root = temp_dir("omena_bridge_style_bundler_alias")?;
1530        let source = root.join("src/App.tsx");
1531        let style = root.join("src/styles/Button.module.scss");
1532        fs::create_dir_all(
1533            style
1534                .parent()
1535                .ok_or_else(|| std::io::Error::other("parent"))?,
1536        )?;
1537        fs::write(&source, "")?;
1538        fs::write(&style, ".root {}")?;
1539        fs::write(
1540            root.join("vite.config.ts"),
1541            r#"export default { resolve: { alias: { "@styles": "./src/styles" } } };"#,
1542        )?;
1543
1544        let uri = resolve_omena_bridge_style_uri_for_specifier(
1545            path_to_file_uri(source.as_path()).as_str(),
1546            Some(path_to_file_uri(root.as_path()).as_str()),
1547            "@styles/Button.module.scss",
1548        );
1549
1550        assert_eq!(
1551            uri.as_deref(),
1552            Some(path_to_file_uri(style.as_path()).as_str())
1553        );
1554        let _ = fs::remove_dir_all(root);
1555        Ok(())
1556    }
1557
1558    #[test]
1559    fn resolves_webpack_exact_bundler_alias_style_candidates()
1560    -> Result<(), Box<dyn std::error::Error>> {
1561        let root = temp_dir("omena_bridge_style_bundler_exact_alias")?;
1562        let source = root.join("src/App.tsx");
1563        let style = root.join("src/styles/index.module.scss");
1564        fs::create_dir_all(
1565            style
1566                .parent()
1567                .ok_or_else(|| std::io::Error::other("parent"))?,
1568        )?;
1569        fs::write(&source, "")?;
1570        fs::write(&style, ".root {}")?;
1571        fs::write(
1572            root.join("webpack.config.js"),
1573            r#"module.exports = { resolve: { alias: [{ find: "@theme$", replacement: "./src/styles/index.module.scss" }] } };"#,
1574        )?;
1575
1576        let exact_uri = resolve_omena_bridge_style_uri_for_specifier(
1577            path_to_file_uri(source.as_path()).as_str(),
1578            Some(path_to_file_uri(root.as_path()).as_str()),
1579            "@theme",
1580        );
1581        let prefix_uri = resolve_omena_bridge_style_uri_for_specifier(
1582            path_to_file_uri(source.as_path()).as_str(),
1583            Some(path_to_file_uri(root.as_path()).as_str()),
1584            "@theme/Button.module.scss",
1585        );
1586
1587        assert_eq!(
1588            exact_uri.as_deref(),
1589            Some(path_to_file_uri(style.as_path()).as_str())
1590        );
1591        assert!(prefix_uri.is_none());
1592        let _ = fs::remove_dir_all(root);
1593        Ok(())
1594    }
1595
1596    #[test]
1597    fn resolves_sass_style_candidates_without_legacy_language_filter()
1598    -> Result<(), Box<dyn std::error::Error>> {
1599        let root = temp_dir("omena_bridge_style_sass")?;
1600        let source = root.join("src/App.tsx");
1601        let style = root.join("src/Button.module.sass");
1602        fs::create_dir_all(
1603            source
1604                .parent()
1605                .ok_or_else(|| std::io::Error::other("parent"))?,
1606        )?;
1607        fs::write(&source, "")?;
1608        fs::write(&style, ".root\n  color: red\n")?;
1609
1610        let uri = resolve_omena_bridge_style_uri_for_specifier(
1611            path_to_file_uri(source.as_path()).as_str(),
1612            Some(path_to_file_uri(root.as_path()).as_str()),
1613            "./Button.module.sass",
1614        );
1615
1616        assert_eq!(
1617            uri.as_deref(),
1618            Some(path_to_file_uri(style.as_path()).as_str())
1619        );
1620        let _ = fs::remove_dir_all(root);
1621        Ok(())
1622    }
1623
1624    #[test]
1625    fn resolves_package_style_candidates_through_omena_resolver()
1626    -> Result<(), Box<dyn std::error::Error>> {
1627        let root = temp_dir("omena_bridge_style_package")?;
1628        let source = root.join("src/App.module.scss");
1629        let package_root = root.join("node_modules/@design/tokens");
1630        let style = package_root.join("src/index.scss");
1631        fs::create_dir_all(
1632            style
1633                .parent()
1634                .ok_or_else(|| std::io::Error::other("parent"))?,
1635        )?;
1636        fs::create_dir_all(
1637            source
1638                .parent()
1639                .ok_or_else(|| std::io::Error::other("source parent"))?,
1640        )?;
1641        fs::write(&source, "@use \"@design/tokens\";")?;
1642        fs::write(
1643            package_root.join("package.json"),
1644            r#"{"sass":"src/index.scss"}"#,
1645        )?;
1646        fs::write(&style, "$gap: 1rem;")?;
1647
1648        let uri = resolve_omena_bridge_style_uri_for_specifier(
1649            path_to_file_uri(source.as_path()).as_str(),
1650            Some(path_to_file_uri(root.as_path()).as_str()),
1651            "@design/tokens",
1652        );
1653
1654        assert_eq!(
1655            uri.as_deref(),
1656            Some(path_to_file_uri(style.as_path()).as_str())
1657        );
1658        let _ = fs::remove_dir_all(root);
1659        Ok(())
1660    }
1661
1662    #[test]
1663    fn resolves_sass_pkg_style_candidates_through_manifest_discovery()
1664    -> Result<(), Box<dyn std::error::Error>> {
1665        let root = temp_dir("omena_bridge_style_pkg_manifest")?;
1666        let source = root.join("src/App.module.scss");
1667        let package_root = root.join("node_modules/@design/tokens");
1668        let style = package_root.join("dist/theme.scss");
1669        fs::create_dir_all(
1670            style
1671                .parent()
1672                .ok_or_else(|| std::io::Error::other("style parent"))?,
1673        )?;
1674        fs::create_dir_all(
1675            source
1676                .parent()
1677                .ok_or_else(|| std::io::Error::other("source parent"))?,
1678        )?;
1679        fs::write(&source, "@use \"pkg:@design/tokens/theme\";")?;
1680        fs::write(
1681            package_root.join("package.json"),
1682            r#"{"exports":{"./theme":{"sass":"./dist/theme.scss"}}}"#,
1683        )?;
1684        fs::write(&style, "$gap: 1rem;")?;
1685
1686        let uri = resolve_omena_bridge_style_uri_for_specifier(
1687            path_to_file_uri(source.as_path()).as_str(),
1688            Some(path_to_file_uri(root.as_path()).as_str()),
1689            "pkg:@design/tokens/theme",
1690        );
1691
1692        assert_eq!(
1693            uri.as_deref(),
1694            Some(path_to_file_uri(style.as_path()).as_str())
1695        );
1696        let _ = fs::remove_dir_all(root);
1697        Ok(())
1698    }
1699
1700    #[test]
1701    fn resolves_package_import_style_candidates_through_workspace_manifests()
1702    -> Result<(), Box<dyn std::error::Error>> {
1703        let root = temp_dir("omena_bridge_style_package_import_manifest")?;
1704        let source = root.join("src/App.module.scss");
1705        let package_root = root.join("node_modules/@design/tokens");
1706        let style = package_root.join("dist/theme.scss");
1707        fs::create_dir_all(
1708            style
1709                .parent()
1710                .ok_or_else(|| std::io::Error::other("style parent"))?,
1711        )?;
1712        fs::create_dir_all(
1713            source
1714                .parent()
1715                .ok_or_else(|| std::io::Error::other("source parent"))?,
1716        )?;
1717        fs::write(&source, "@use \"#theme\" as tokens;")?;
1718        fs::write(
1719            root.join("package.json"),
1720            r##"{"imports":{"#theme":"@design/tokens/theme"}}"##,
1721        )?;
1722        fs::write(
1723            package_root.join("package.json"),
1724            r#"{"exports":{"./theme":{"sass":"./dist/theme.scss"}}}"#,
1725        )?;
1726        fs::write(&style, "$gap: 1rem;")?;
1727
1728        let uri = resolve_omena_bridge_style_uri_for_specifier(
1729            path_to_file_uri(source.as_path()).as_str(),
1730            Some(path_to_file_uri(root.as_path()).as_str()),
1731            "#theme",
1732        );
1733
1734        assert_eq!(
1735            uri.as_deref(),
1736            Some(path_to_file_uri(style.as_path()).as_str())
1737        );
1738        let _ = fs::remove_dir_all(root);
1739        Ok(())
1740    }
1741
1742    #[cfg(unix)]
1743    #[test]
1744    fn resolves_symlinked_package_style_candidates_to_canonical_uri()
1745    -> Result<(), Box<dyn std::error::Error>> {
1746        let root = temp_dir("omena_bridge_style_symlinked_package")?;
1747        let source = root.join("src/App.module.scss");
1748        let real_package = root.join(".pnpm/@design+tokens@1.0.0/node_modules/@design/tokens");
1749        let linked_scope = root.join("node_modules/@design");
1750        let linked_package = linked_scope.join("tokens");
1751        let style = real_package.join("src/index.scss");
1752        fs::create_dir_all(
1753            style
1754                .parent()
1755                .ok_or_else(|| std::io::Error::other("style parent"))?,
1756        )?;
1757        fs::create_dir_all(
1758            source
1759                .parent()
1760                .ok_or_else(|| std::io::Error::other("source parent"))?,
1761        )?;
1762        fs::create_dir_all(linked_scope.as_path())?;
1763        fs::write(&source, "@use \"@design/tokens\";")?;
1764        fs::write(
1765            real_package.join("package.json"),
1766            r#"{"sass":"src/index.scss"}"#,
1767        )?;
1768        fs::write(&style, "$gap: 1rem;")?;
1769        std::os::unix::fs::symlink(real_package.as_path(), linked_package.as_path())?;
1770
1771        let uri = resolve_omena_bridge_style_uri_for_specifier(
1772            path_to_file_uri(source.as_path()).as_str(),
1773            Some(path_to_file_uri(root.as_path()).as_str()),
1774            "@design/tokens",
1775        );
1776        let expected_uri = path_to_file_uri(fs::canonicalize(style)?.as_path());
1777
1778        assert_eq!(uri.as_deref(), Some(expected_uri.as_str()));
1779        let _ = fs::remove_dir_all(root);
1780        Ok(())
1781    }
1782
1783    #[test]
1784    fn does_not_fabricate_missing_package_style_candidates()
1785    -> Result<(), Box<dyn std::error::Error>> {
1786        let root = temp_dir("omena_bridge_style_missing_package")?;
1787        let source = root.join("src/App.tsx");
1788        fs::create_dir_all(
1789            source
1790                .parent()
1791                .ok_or_else(|| std::io::Error::other("parent"))?,
1792        )?;
1793        fs::write(&source, "")?;
1794
1795        let uri = resolve_omena_bridge_style_uri_for_specifier(
1796            path_to_file_uri(source.as_path()).as_str(),
1797            Some(path_to_file_uri(root.as_path()).as_str()),
1798            "@design/tokens",
1799        );
1800
1801        assert!(uri.is_none(), "{uri:?}");
1802        let _ = fs::remove_dir_all(root);
1803        Ok(())
1804    }
1805
1806    #[test]
1807    fn emits_percent_encoded_file_uris_for_route_group_paths()
1808    -> Result<(), Box<dyn std::error::Error>> {
1809        let root = temp_dir("omena_bridge_style_route_group")?;
1810        let source = root.join("app/(marketing)/page.tsx");
1811        let style = root.join("app/(marketing)/Card.module.scss");
1812        fs::create_dir_all(
1813            source
1814                .parent()
1815                .ok_or_else(|| std::io::Error::other("parent"))?,
1816        )?;
1817        fs::write(&source, "")?;
1818        fs::write(&style, ".card {}")?;
1819
1820        let uri = resolve_omena_bridge_style_uri_for_specifier(
1821            path_to_file_uri(source.as_path()).as_str(),
1822            Some(path_to_file_uri(root.as_path()).as_str()),
1823            "./Card.module.scss",
1824        )
1825        .ok_or_else(|| std::io::Error::other("route group style should resolve"))?;
1826
1827        assert!(uri.contains("%28marketing%29"), "{uri}");
1828        assert_eq!(uri, path_to_file_uri(style.as_path()));
1829        let _ = fs::remove_dir_all(root);
1830        Ok(())
1831    }
1832
1833    #[test]
1834    fn declares_bridge_owned_style_resolution_boundary() {
1835        let summary = summarize_omena_bridge_style_resolution_boundary();
1836
1837        assert_eq!(summary.product, "omena-bridge.style-resolution");
1838        assert_eq!(summary.owner_crate, "omena-bridge");
1839        assert!(summary.supported_specifier_kinds.contains(&"tsconfigPaths"));
1840        assert!(
1841            summary
1842                .supported_specifier_kinds
1843                .contains(&"bundlerAliases")
1844        );
1845        assert!(summary.supported_specifier_kinds.contains(&"npmPackages"));
1846        assert!(
1847            summary
1848                .request_path_policy
1849                .contains(&"pathAliasResolutionFollowsRelativeTsconfigExtends")
1850        );
1851        assert!(
1852            summary
1853                .request_path_policy
1854                .contains(&"bundlerAliasResolutionUsesLiteralViteWebpackConfig")
1855        );
1856        assert!(
1857            summary
1858                .request_path_policy
1859                .contains(&"lspServerOwnsOnlyDocumentRoutingAndUriRangeMapping")
1860        );
1861    }
1862
1863    #[test]
1864    fn resolves_nested_next_config_alias_style_candidates() -> Result<(), Box<dyn std::error::Error>>
1865    {
1866        let root = temp_dir("omena_bridge_style_next_nested")?;
1867        let app_dir = root.join("apps/web");
1868        let source = app_dir.join("src/App.tsx");
1869        let style = app_dir.join("src/styles/Button.module.scss");
1870        fs::create_dir_all(
1871            style
1872                .parent()
1873                .ok_or_else(|| std::io::Error::other("style parent"))?,
1874        )?;
1875        fs::write(&source, "")?;
1876        fs::write(&style, ".root {}")?;
1877        fs::write(
1878            app_dir.join("next.config.mjs"),
1879            r#"export default { resolve: { alias: { "@styles": "./src/styles" } } };"#,
1880        )?;
1881
1882        let uri = resolve_omena_bridge_style_uri_for_specifier(
1883            path_to_file_uri(source.as_path()).as_str(),
1884            Some(path_to_file_uri(root.as_path()).as_str()),
1885            "@styles/Button.module.scss",
1886        );
1887
1888        assert_eq!(
1889            uri.as_deref(),
1890            Some(path_to_file_uri(style.as_path()).as_str())
1891        );
1892        let _ = fs::remove_dir_all(root);
1893        Ok(())
1894    }
1895
1896    #[test]
1897    fn resolves_tilde_package_style_candidates() -> Result<(), Box<dyn std::error::Error>> {
1898        let root = temp_dir("omena_bridge_style_tilde_package")?;
1899        let source = root.join("src/App.module.scss");
1900        let package_root = root.join("node_modules/@scope/theme");
1901        let style = package_root.join("index.scss");
1902        fs::create_dir_all(
1903            source
1904                .parent()
1905                .ok_or_else(|| std::io::Error::other("source parent"))?,
1906        )?;
1907        fs::create_dir_all(package_root.as_path())?;
1908        fs::write(&source, "@use \"~@scope/theme\";")?;
1909        fs::write(
1910            package_root.join("package.json"),
1911            r#"{"sass":"./index.scss"}"#,
1912        )?;
1913        fs::write(&style, "$brand: red;")?;
1914
1915        let uri = resolve_omena_bridge_style_uri_for_specifier(
1916            path_to_file_uri(source.as_path()).as_str(),
1917            Some(path_to_file_uri(root.as_path()).as_str()),
1918            "~@scope/theme",
1919        );
1920
1921        assert_eq!(
1922            uri.as_deref(),
1923            Some(path_to_file_uri(style.as_path()).as_str())
1924        );
1925        let _ = fs::remove_dir_all(root);
1926        Ok(())
1927    }
1928
1929    fn temp_dir(prefix: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
1930        let suffix = SystemTime::now()
1931            .duration_since(SystemTime::UNIX_EPOCH)?
1932            .as_nanos();
1933        let path = std::env::temp_dir().join(format!("{prefix}_{suffix}"));
1934        fs::create_dir_all(path.as_path())?;
1935        Ok(path)
1936    }
1937}