///|
pub(all) struct ModuleDependency {
from : String
path : String
} derive(ToJson)
///|
pub extend ModuleDependency with ToJson::{to_json}
///|
pub(all) struct ProjectOutput {
entry : String
compilation : Compilation
dependencies : Array[ModuleDependency]
watched_files : Array[String]
cache_hit : Bool
}
///|
/// Owned virtual files and completed per-entry results. Module evaluation is
/// isolated per entry, including @use configuration. No cross-entry globals.
pub struct Project {
priv mut files : Map[String, String]
priv cache : Map[String, ProjectOutput]
}
///|
pub fn Project::new(files : Map[String, String]) -> Project raise ParseError {
{ files: checked_files(files), cache: Map([]), }
}
///|
pub fn Project::source(
self : Project,
entry : String,
) -> String raise ParseError {
match self.files.get(clean_path(entry)) {
Some(source) => source
None => raise Invalid("entry file not found")
}
}
///|
fn ProjectOutput::snapshot(self : ProjectOutput, hit : Bool) -> ProjectOutput {
{
..self,
cache_hit: hit,
compilation: {
..self.compilation,
loaded_files: self.compilation.loaded_files.copy(),
diagnostics: self.compilation.diagnostics.copy(),
},
dependencies: self.dependencies.copy(),
watched_files: self.watched_files.copy(),
}
}
///|
/// Reuses only a completed result of this exact virtual snapshot. Cache is
/// bounded to 16 entries / 4M CSS UTF-16 units; deterministic key-order eviction.
/// Large outputs are returned uncached; evaluation limits still apply.
pub fn Project::compile(
self : Project,
entry : String,
) -> ProjectOutput raise ParseError {
let entry = clean_path(entry)
if self.cache.get(entry) is Some(result) {
return result.snapshot(true)
}
let result = compile_tracked(entry, self.files)
if result.compilation.css.length() <= 4000000 {
let keys = self.cache.keys().to_array()
keys.sort()
let mut size = result.compilation.css.length()
for _, item in self.cache {
size += item.compilation.css.length()
}
for key in keys {
if self.cache.length() < 16 && size <= 4000000 {
break
}
size -= self.cache[key].compilation.css.length()
self.cache.remove(key)
}
self.cache[entry] = result
}
result.snapshot(false)
}
///|
/// Atomically apply a file delta after canonical path and total-size validation.
/// None deletes a file. Return invalidated cached entry paths, sorted. Absent
/// resolution candidates are tracked, so additions can invalidate results too.
/// Invalid edits leave both the virtual files and caches unchanged.
pub fn Project::apply(
self : Project,
updates : Map[String, String?],
) -> Array[String] raise ParseError {
if updates.length() > 256 {
raise Invalid("update count limit")
}
let next = self.files.copy()
let seen : Map[String, Bool] = Map([])
let changed : Map[String, Bool] = Map([])
for raw, value in updates {
let path = clean_path(raw)
if !path.has_suffix(".scss") || seen.contains(path) {
raise Invalid("invalid or duplicate canonical update path")
}
seen[path] = true
if next.get(path) != value {
changed[path] = true
}
match value {
Some(source) => next[path] = source
None => next.remove(path)
}
}
let checked = checked_files(next)
let invalidated : Array[String] = []
for entry, result in self.cache {
if result.watched_files.any(path => changed.contains(path)) {
invalidated.push(entry)
}
}
// No state changes occur before all edits and constraints have been checked.
self.files = checked
for entry in invalidated {
self.cache.remove(entry)
}
invalidated.sort()
invalidated
}